I have an interface defined as follows:
interface MyInterface
{
public function setMyMethod(int $a);
public function getMyMethod(): int;
}
And I have a class that widens the parameter type:
class MyClass implements MyInterface
{
public function setMyMethod(int|string $a) {}
public function getMyMethod(): int;
}
In languages like Java or C#, I could use method overloading to define two methods named myMethod
: one with int $a
to conform to the interface and another with string $a
as a new implementation. Note that I am not widening the returning type, only how I set the value.
In PHP, method overloading is achieved by using union types, as I did above.
The PHP runtime accepts this approach (tested on versions 8.1, 8.2, and 8.3).
My question is: Is this acceptable? Am I violating any good practices in PHP by doing this?
I am unsure if this breaks the Liskov Substitution Principle. Classes expecting the MyInterface
implementation should still work correctly. However, if one part of an application uses MyClass
and is later replaced with another class that provides a "pure" implementation of MyInterface
, there is a risk of breaking the application.
From my perspective, since I can choose which implementation of the interface to use within my application, this should be fine. However, I want to ensure that I am not introducing any potential issues.
EDIT: I am adding my real case scenario.
I am exteding the PHP Psr7 Interface, an standard to do API request (https://www.php-fig.org/psr/psr-7/ and https://github.com/php-fig/http-message/)
The interface is:
interface RequestInterface extends MessageInterface
{
// ...
public function getMethod(): string;
public function withMethod(string $method): RequestInterface;
// ..
}
My implementation will do:
class Request implements RequestInterface
{
// ...
public function getMethod(): string;
public function withMethod(MethodEnum|string $method): RequestInterface;
// ..
}
The change is to allow something like this:
$request->withMethod(Method::Post);
instead of
$request->withMethod('POST');
-
According to wiki.php.net/rfc/union_types (the section titled "Weak Scalar Types"), the runtime might cast the parameter values. The casting rules are complex, so I would do some more research into this. I haven't written PHP in years, so my knowledge is a bit rusty.Greg Burghardt– Greg Burghardt01/10/2025 22:43:13Commented Jan 10 at 22:43
-
1Overloading is often overestimated. Why exactly your class needs "|string" piece? Is this about parsing string into int? If so, then this should be done outside, there's no reason for your class to take that responsibility.freakish– freakish01/11/2025 08:19:57Commented Jan 11 at 8:19
-
There is a use case. I am implementing the RequestInterface from Psr7 specification. The method: withMethod receives a case sentive string like POST, GET. Instead of use the string, I want to pass an Enum and then this enum is converted to string internally make it compatible with any other class. The enum reduces runtime errors because of unintended misspelling method. It is already implemented and working, I just want to know if it is OK in "a PHP way of code".Joao M– Joao M01/11/2025 14:38:28Commented Jan 11 at 14:38
-
@JoaoM that's the same case as in my previous comment. Make the class accept the enum only. Take care of parsing/conversion elsewhere. Again, there's no reason for the class to take this responsibility. That way you have cleanly separated components. As a bonus, you don't have to deal with this issue at all.freakish– freakish01/11/2025 17:48:41Commented Jan 11 at 17:48
-
Not a PHP dev, but can you also widen the return type? Because that opens the door to co/contravariance which is a fairly complicated subject matter.Flater– Flater01/13/2025 04:12:39Commented Jan 13 at 4:12
1 Answer 1
The Liskov Substitution Principle aims at ensuring that whenever a superclass object could be used, it could be replaced with a subtype object, yet still giving the same result.
This is done by looking at the preconditions, post conditions and invariants (and also the history constraint, but this is less relevant here):
- A precondition cannot be strenghthened. You comply apparently with this requirement: you widened the preconditions by allowing more possible parameter values than in the interface, yet all those accepted by the interface are also accepted by your class.
- A postcondition cannot be weakened: up to you to tell, but as long as the original preconditions are met (i.e. an int parameter is used) you should ensure the same post conditions (or stronger ones).
- the invariants shall not be weakened. In principle, if the parameter is not used to initialize some properties or if these properties must not guaranteed to be int, you're fine. If, on contrary, the weakening of parameter type would be propagated, for example via assignment to properties, then you might weaken the invariants, which would not be ok.
So in principle and at first sight your design is LSP compliant. But you have to cross-check the invariants, which are not visible in your code snippet.
-
I edit the question and added a real case scenario.Joao M– Joao M01/13/2025 19:44:48Commented Jan 13 at 19:44
Explore related questions
See similar questions with these tags.