BlueMan
BlueMan

Reputation: 707

PHP - override function with different number of parameters

I'm extending a class, but in some scenarios I'm overriding a method. Sometimes in 2 parameters, sometimes in 3, sometimes without parameters.

Unfortunately I'm getting a PHP warning.

My minimum verifiable example: http://pastebin.com/6MqUX9Ui

<?php

class first {
    public function something($param1) {
        return 'first-'.$param1;
    }
}

class second extends first {
    public function something($param1, $param2) {
        return 'second params=('.$param1.','.$param2.')';
    }
}

// Strict standards: Declaration of second::something() should be compatible with that of first::something() in /home/szymon/webs/wildcard/www/source/public/override.php on line 13

$myClass = new Second();
var_dump( $myClass->something(123,456) );

I'm getting PHP error/warning/info: error screen

How can I prevent errors like this?

Upvotes: 27

Views: 26965

Answers (4)

MAChitgarha
MAChitgarha

Reputation: 4298

As of PHP 8.1, there's a cool hack to override a class's method with extra number of required arguments. You should use the new new in initializers feature. But how?

We define a class having a constructor always throwing a ArgumentCountError, and make it the default value of every extra required parameter (an improved version of @jose.serapicos's answer). Simple and cool!

Now let's see it in action. First, we define RequiredParam:

final class RequiredParameter extends \ArgumentCountError
{
    public function __construct()
    {
        // Nested hack
        throw $this;
    }
}

And then:

class Base
{
    public function something(string $baseParam): string
    {
        return $baseParam;
    }
}

class Derived extends Base
{
    public function something(
        string $baseParam,
        string|RequiredParameter $extraParam = new RequiredParameter(),
    ): string {
        return "$baseParam + $extraParam";
    }
}

This way, no one can bypass the extra parameters, because RequiredParameter is declared as final. It works for interfaces as well.

How Good or Bad is This?

One advantage is that it's a little more flexible than setting default parameters as null, as you can pass the constructor of RequiredParameter an arbitrary list of parameters and probably build a custom error message.

Another advantage is that it's handled less manually, and thus being more safe. You may forget about handling a null value, but RequiredParameter class handles things for you.

One major disadvantage of this method is that it breaks the rules. First and foremost, you must ask yourself why you would need this, because it breaks polymorphism in most cases. Use it with caution.

However, there are valid use cases for this, like extending parent class's method with the same name (if you cannot modify the parent, otherwise I recommend you to use traits instead), and using the child class as standalone (i.e. without the help of parent class's type).

Another disadvantage is that it requires you to use union types for each parameter. While the following workaround is possible, but it requires you to create more classes, which may hurt understandability of your code, as well as having little impact on maintainability and performance (based on your conditions). BTW, no hack comes for free.

Eliminating the use of Union Type

You could extend from or implement RequiredParameter the compatible type of the actual parameter to be able to remove the need for union type:

class BaseRequiredParameter extends Base
{
    public function __construct()
    {
        throw new \ArgumentCountError();
    }
}

class Derived extends Base
{
    public function something(
        string $baseParam,
        Base $extraParam = new BaseRequiredParameter()
    ): string {
        return "$baseParam + {$extraParam->something()}";
    }
}

It's also possible for strings, if you implement the Stringable interface (e.g. Throwable implements it by default). It doesn't work for some primitive types including bool, int, float, callable, array, etc., however, if you're interested, you're still able to use some alternatives like Closure or Traversable.

For making your life easier, you may want to define the constructor as a trait and use it (I'm aware of this answer, but in fact, this is a valid useful case for a constructor in a trait, at least IMO).

Upvotes: 3

jose.serapicos
jose.serapicos

Reputation: 738

you can redefine methods easily adding new arguments, it's only needs that the new arguments are optional (have a default value in your signature). See below:

class Parent
{
    protected function test($var1) {
        echo($var1);
    }
}

class Child extends Parent
{
    protected function test($var1, $var2 = null) {
        echo($var1);
        echo($var1);
    }
}

For more detail, check out the link: http://php.net/manual/en/language.oop5.abstract.php

Upvotes: 51

Damien Legros
Damien Legros

Reputation: 529

Another solution (a bit "dirtier") is to declare your methods with no argument at all, and in your methods to use the func_get_args() function to retrieve your arguments...

http://www.php.net/manual/en/function.func-get-args.php

Upvotes: 2

Ivan Yonkov
Ivan Yonkov

Reputation: 7034

Your interface/abstract class or the most parent class, should cotantin the maximum number of params a method could recieve, you can declare them explicitely to NULL, so if they are not given, no error will occur i.e.

Class A{
public function smth($param1, $param2='', $param3='')

Class B extends A {
public function smth($param1, $param2, $param3='')

Class C extends B {
public function smth($param1, $param2, $param3);

In this case, using the method smth() as an object of 'A' you will be obligated to use only one param ($param1), but using the same method as object 'B' you will be oblgiated to use 2 params ($param1, $param2) and instanciating it from C you have to give all the params

Upvotes: -1

Related Questions