Upon building an MVC framework in PHP I ran into a problem which could be solved easily using Java style generics. An abstract Controller class might look something like this:
abstract class Controller { abstract public function addModel(Model $model);
There may be a case where a subclass of class Controller should only accept a subclass of Model. For example ExtendedController should only accept ReOrderableModel into the addModel method because it provides a reOrder() method that ExtendedController needs to have access to:
class ExtendedController extends Controller { public function addModel(ReOrderableModel $model) {
In PHP the inherited method signature has to be exactly the same so the type hint cannot be changed to a different class, even if the class inherits the class type hinted in the superclass. In java I would simply do this:
abstract class Controller<T> { abstract public addModel(T model); class ExtendedController extends Controller<ReOrderableModel> { public addModel(ReOrderableModel model) {
But there is no generics support in PHP. Is there any solution which would still adhere to OOP principles?
Edit I am aware that PHP does not require type hinting at all but it is perhaps bad OOP. Firstly it is not obvious from the interface (the method signature) what kind of objects should be accepted. So if another developer wanted to use the method it should be obvious that objects of type X are required without them having to look through the implementation (method body) which is bad encapsulation and breaks the information hiding principle. Secondly because there's no type safety the method can accept any invalid variable which means manual type checking and exception throwing is needed all over the place!
Generics are an important step towards PHP maturing as a gradually-typed language - a direction in which PHP is already moving, with the addition of scalar type-hints, and return type-hints, in PHP 7.
In a nutshell, generics enable types (classes and interfaces) to be parameters when defining classes, interfaces and methods. Much like the more familiar formal parameters used in method declarations, type parameters provide a way for you to re-use the same code with different inputs.
Generics were added to Java to ensure type safety. And to ensure that generics won't cause overhead at runtime, the compiler applies a process called type erasure on generics at compile time. Type erasure removes all type parameters and replaces them with their bounds or with Object if the type parameter is unbounded.
The actual type arguments of a generic type are. reference types, wildcards, or. parameterized types (i.e. instantiations of other generic types).
It appears to work for me (though it does throw a Strict warning) with the following test case:
class PassMeIn { } class PassMeInSubClass extends PassMeIn { } class ClassProcessor { public function processClass (PassMeIn $class) { var_dump (get_class ($class)); } } class ClassProcessorSubClass extends ClassProcessor { public function processClass (PassMeInSubClass $class) { parent::processClass ($class); } } $a = new PassMeIn; $b = new PassMeInSubClass; $c = new ClassProcessor; $d = new ClassProcessorSubClass; $c -> processClass ($a); $c -> processClass ($b); $d -> processClass ($b);
If the strict warning is something you really don't want, you can work around it like this.
class ClassProcessor { public function processClass (PassMeIn $class) { var_dump (get_class ($class)); } } class ClassProcessorSubClass extends ClassProcessor { public function processClass (PassMeIn $class) { if ($class instanceof PassMeInSubClass) { parent::processClass ($class); } else { throw new InvalidArgumentException; } } } $a = new PassMeIn; $b = new PassMeInSubClass; $c = new ClassProcessor; $d = new ClassProcessorSubClass; $c -> processClass ($a); $c -> processClass ($b); $d -> processClass ($b); $d -> processClass ($a);
One thing you should bear in mind though, this is strictly not best practice in OOP terms. If a superclass can accept objects of a particular class as a method argument then all its subclasses should also be able of accepting objects of that class as well. Preventing subclasses from processing classes that the superclass can accept means you can't use the subclass in place of the superclass and be 100% confident that it will work in all cases. The relevant practice is known as the Liskov Substitution Principle and it states that, amongst other things, the type of method arguments can only get weaker in subclasses and the type of return values can only get stronger (input can only get more general, output can only get more specific).
It's a very frustrating issue, and I've brushed up against it plenty of times myself, so if ignoring it in a particular case is the best thing to do then I'd suggest that you ignore it. But don't make a habit of it or your code will start to develop all kinds of subtle interdependencies that will be a nightmare to debug (unit testing won't catch them because the individual units will behave as expected, it's the interaction between them where the issue lies). If you do ignore it, then comment the code to let others know about it and that it's a deliberate design choice.
Whatever the Java world invented need not be always right. I think I detected a violation of the Liskov substitution principle here, and PHP is right in complaining about it in E_STRICT mode:
Cite Wikipedia: "If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program."
T is your Controller. S is your ExtendedController. You should be able to use the ExtendedController in every place where the Controller works without breaking anything. Changing the typehint on the addModel() method breaks things, because in every place that passed an object of type Model, the typehint will now prevent passing the same object if it isn't accidentally a ReOrderableModel.
How to escape this?
Your ExtendedController can leave the typehint as is and check afterwards whether he got an instance of ReOrderableModel or not. This circumvents the PHP complaints, but it still breaks things in terms of the Liskov substitution.
A better way is to create a new method addReOrderableModel()
designed to inject ReOrderableModel objects into the ExtendedController. This method can have the typehint you need, and can internally just call addModel()
to put the model in place where it is expected.
If you require an ExtendedController to be used instead of a Controller as parameter, you know that your method for adding ReOrderableModel is present and can be used. You explicitly declare that the Controller will not fit in this case. Every method that expects a Controller to be passed will not expect addReOrderableModel()
to exist and never attempt to call it. Every method that expects ExtendedController has the right to call this method, because it must be there.
class ExtendedController extends Controller { public function addReOrderableModel(ReOrderableModel $model) { return $this->addModel($model); } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With