2

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');
asked Jan 10 at 22:17
5
  • 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. Commented Jan 10 at 22:43
  • 1
    Overloading 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. Commented 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". Commented 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. Commented 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. Commented Jan 13 at 4:12

1 Answer 1

2

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.

answered Jan 11 at 16:03
1
  • I edit the question and added a real case scenario. Commented Jan 13 at 19:44

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.