Does PHP have an answer to Java style class generics?

40,585

Solution 1

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.

Solution 2

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);
  }
}

Solution 3

My workaround is the following:

/**
 * Generic list logic and an abstract type validator method.
 */    
abstract class AbstractList {
    protected $elements;

    public function __construct() {
        $this->elements = array();
    }

    public function add($element) {
        $this->validateType($element);
        $this->elements[] = $element;
    }

    public function get($index) {
        if ($index >= sizeof($this->elements)) {
            throw new OutOfBoundsException();
        }
        return $this->elements[$index];
    }

    public function size() {
        return sizeof($this->elements);
    }

    public function remove($element) {
        validateType($element);
        for ($i = 0; $i < sizeof($this->elements); $i++) {
            if ($this->elements[$i] == $element) {
               unset($this->elements[$i]);
            }
        }
    }

    protected abstract function validateType($element);
}


/**
 * Extends the abstract list with the type-specific validation
 */
class MyTypeList extends AbstractList {
    protected function validateType($element) {
        if (!($element instanceof MyType)) {
            throw new InvalidArgumentException("Parameter must be MyType instance");
        }
    }
}

/**
 * Just an example class as a subject to validation.
 */
class MyType {
    // blahblahblah
}


function proofOfConcept(AbstractList $lst) {
    $lst->add(new MyType());
    $lst->add("wrong type"); // Should throw IAE
}

proofOfConcept(new MyTypeList());

Though this still differs from Java generics, it pretty much minimalizes the extra code needed for mimicking the behaviour.

Also, it is a bit more code than some examples given by others, but - at least to me - it seems to be more clean (and more simliar to the Java counterpart) than most of them.

I hope some of you will find it useful.

Any improvements over this design are welcome!

Solution 4

I did went through the same type of problem before. And I used something like this to tackle it.

Class Myclass {

    $objectParent = "MyMainParent"; //Define the interface or abstract class or the main parent class here
    public function method($classObject) {
        if(!$classObject instanceof $this -> objectParent) { //check 
             throw new Exception("Invalid Class Identified");
        }
        // Carry on with the function
    }

}

Solution 5

You can consider to switch to Hack and HHVM. It is developed by Facebook and full compatible to PHP. You can decide to use <?php or <?hh

It support that what you want:

http://docs.hhvm.com/manual/en/hack.generics.php

I know this is not PHP. But it is compatible with it, and also improves your performance dramatically.

Share:
40,585
Jonathan
Author by

Jonathan

Updated on July 09, 2022

Comments

  • Jonathan
    Jonathan almost 2 years

    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!

    • dynamic
      dynamic about 12 years
      in php you can pass any type object you want without having to worry
    • Jonathan
      Jonathan about 12 years
      I am aware of this already 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 only objects of type X should be used 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.
    • Benjamin Gruenbaum
      Benjamin Gruenbaum over 9 years
      It's also bad OOP because it violates the LSP, your ExtendedController specializes controller rather than extending it. If addModel on the parent can accept a Model requiring a subclass addModel to only accept a ReOderableModel is extremely bad oop and a violation of the LSP. It won't work in Java in any sensible way either. This is from 2 years ago so you probably know all this by now though...
    • BenMorel
      BenMorel about 7 years
      Note that there is an RFC for generics, currently in draft.
  • Jonathan
    Jonathan about 12 years
    You know what? I didn't even try it to see if it actually just worked haha. I simply read that it doesn't work and believed it. I guess I can use the @ symbol to just suppress warnings. Can you think of any reason why that would be a really bad idea? It's either that or your second solution below.
  • Jonathan
    Jonathan about 12 years
    Creative solution! I might do this in conjunction with the type hint. So it type checks the superclass and then does manual checking for the subclass.
  • Starx
    Starx about 12 years
    @jonathan, if you typecast a superclass then the object passed must be the object of that class. Which is exactly why, my solution does not use it, instead, checks the instance later on to verify it.
  • Jonathan
    Jonathan about 12 years
    This is an interesting solution! Like Starx's solution except the class is passed as a parameter nice.
  • Jonathan
    Jonathan about 12 years
    What I mean is that addModel(Model $model) will check if it is a Model but then in the method body I can manually check by doing if($model instanceof ReOrderableModel). ReOrderableModel extends Model so it would be allowed through the addModel(Model $model) method. Are you saying that this wouldn't work?
  • GordonM
    GordonM about 12 years
    Generally speaking, you really really want to avoid the @ operator, because it basically amounts to sweeping an error message under the rug. In this particular case it probably isn't going to lead to issues long-term, but you can't count on that because E_STRICT warnings are indications that the language may change at some point to no-longer support what you're doing. I'm not sure you could use it to suppress errors in a method declaration anyway. You could set an error_reporting value that excludes E_STRICT, though again that's just sweeping the problem under the carpet.
  • GordonM
    GordonM about 12 years
    PHP's manual has this to say on E_STRICT: In PHP 5 a new error level E_STRICT is available. As E_STRICT is not included within E_ALL you have to explicitly enable this kind of error level. Enabling E_STRICT during development has some benefits. STRICT messages will help you to use the latest and greatest suggested method of coding, for example warn you about using deprecated functions. So I'd say the best approach is to turn off E_STRICT on your production server, but leave it on on your dev/test machhine.
  • Sven
    Sven over 11 years
    I guess you should have added code to one of the SPL classes of PHP, like ArrayObject or SplDoublyLinkedList
  • Powerslave
    Powerslave over 11 years
    @Sven Well, yes. Also, it is obviously not a complete inteface that Java List<?> offers, but a subset that is most often used. It also can be further improved by introducing an AbstractValidatedList class that implements the validator, but calls into the children for an abstract getElementType() method that returns the class name. I'm slightly out of date with PHP as I've been working almost exclusively with J2EE over my last year. Thanks for pointing things out!
  • Sven
    Sven over 11 years
    The question is not about lists, I think. It's about inheritance in PHP.
  • Powerslave
    Powerslave over 11 years
    @Sven Well, the basic concept can be applied to other classes as you like. The main difference compared to Java generics is that you'll get runtime errors but no IDE will complain about type mismatch.
  • Sven
    Sven over 11 years
    If I understand correctly, Java generics are just a creative form of templating that is resolved into separate classes at compile time. You type your code once, create it as a template, and apply this template to several uses. PHP does not really support this approach, besides the fact that traits are essentially a compiler-resolved copy&paste solution for something that may be used here. I wouldn't recommend using traits, though.
  • Powerslave
    Powerslave over 11 years
    @Sven Generics classes aren't resolved into separate concrete classes at compile time, but are for the purpose of not having to use Object as subject type that would remove type safety from the code. Instead, in a templating-like manner, you can specify what actual type a particular instance should work with. They're resolved to the same class having a reference to the subject's class; practically wrapping it. This provides high code reusability in a strongly typed environment. Although PHP is not one of these, OOP itself is of typed nature, thus the need for such functionality might come up.
  • Powerslave
    Powerslave over 11 years
    @Sven Traits seem to me an awkward avoid/substitute code duplication. I'd rather use inheritance and composition though.
  • Powerslave
    Powerslave over 11 years
    @Sven Actually, it might be a good idea to take a look at generics if you're interested, as the bounds of SOF do not really allow for discussing them.
  • FantasticJamieBurns
    FantasticJamieBurns over 10 years
    The problem with this example is that one cannot easily see which type the list is constrained to (without reading the source code).
  • Powerslave
    Powerslave over 10 years
    @FantasticJamieBurns Your point is pretty much valid. However, I doubt one can come up with a suitable workaround without hacking with stuff like code generation and using eval. Of course, that might be refactored to an extent where is becomes acceptable, but that most probably wouldn't fit the bounds of SOF. Also, it'd require writing a complex framework for the purpose.
  • FantasticJamieBurns
    FantasticJamieBurns over 10 years
    @Powerslave See the solution posted by Sven below for the controller case. Your example is even more clear cut -- the AbstractList class should redefine the add/remove methods to be protected and named addObject/removeObject. The extending class should implement the add/remove methods with the type hint (if indeed they are required). The kind of code that would use a list anonymously doesn't need knowledge of the types (they are iterated anonymously). Consider IEnumerable in C#. In other words, if you know the type that should be added to a list, you know the type of the list in your hand.
  • Frederik Krautwald
    Frederik Krautwald almost 10 years
    +1 for detailing an aspect of the Liskov Substitution Principle.
  • jurchiks
    jurchiks almost 8 years
    It's actually not fully compatible with PHP. There's a lot of tiny things that aren't compatible, but collectively amount to a big difference.
  • Powerslave
    Powerslave almost 6 years
    @FantasticJamieBurns Well, yes and no. Regarding items type, mostly no, though. :) You could no problem have two different implementations of the list, covering the same type. In such cases, knowing the type is not the same as knowing the implementation. Also, you usually know the item type anyways, since there are hardly any reasons the deal with idkwtfs: If you're operating on a list of items, you want to do specific things to them that are covered by some concept the type of each instance extend/implement (if not, that's a code smell). I'd rather not iterate over a list of GodKnowsWhat.
  • Powerslave
    Powerslave almost 6 years
    (Yep, I know I just zombied, but I didn't get to reply earlier)