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