25

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?

Joe Kennedy
  • 9,365
  • 7
  • 41
  • 55
BlueMan
  • 687
  • 2
  • 10
  • 23

4 Answers4

46

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

jose.serapicos
  • 580
  • 5
  • 10
2

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

Damien Legros
  • 519
  • 3
  • 7
2

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).

MAChitgarha
  • 3,728
  • 2
  • 33
  • 40
-1

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

Royal Bg
  • 6,988
  • 1
  • 18
  • 24
  • This is exactly the other way around. This script will lead to multiple warnings and errors as an object of type B does not behave like an object of type A which is a requirement. – fietserwin Jun 18 '20 at 14:08