10

Is there any decent work around to PHP's lack of Generics that allow static code inspection to detect type consistency?

I have an abstract class, that I want to sub-class and also enforce that one of the methods changes from taking a parameter of one type, to taking a parameter which is a sub-class of that parameter.

abstract class AbstractProcessor {
    abstract function processItem(Item $item);
}

class WoodProcessor extends AbstractProcessor {
    function processItem(WoodItem $item){}
}

This is not allowed in PHP because it's changing the methods signature which is not allowed. With Java style generics you could do something like:

abstract class AbstractProcessor<T> {
    abstract function processItem(T $item);
}

class WoodProcessor extends AbstractProcessor<WoodItem> {
    function processItem(WoodItem $item);
}

But obviously PHP doesn't support those.

Google for this problem, people suggest using instanceof to check errors at run-time e.g.

class WoodProcessor extends AbstractProcessor {
    function processItem(Item $item){
        if (!($item instanceof WoodItem)) {
            throw new \InvalidArgumentException(
                "item of class ".get_class($item)." is not a WoodItem");
        } 
    }
}

But that only works at runtime, it doesn't allow you to inspect your code for errors using static analysis - so is there any sensible way of handling this in PHP?

A more complete example of the problem is:

class StoneItem extends Item{}
class WoodItem extends Item{}

class WoodProcessedItem extends ProcessedItem {
    function __construct(WoodItem $woodItem){}
}

class StoneProcessedItem extends ProcessedItem{
    function __construct(StoneItem $stoneItem){}
}

abstract class AbstractProcessor {
    abstract function processItem(Item $item);

    function processAndBoxItem(Box $box, Item $item) {
       $processedItem = $this->processItem($item);
       $box->insertItem($item);
    }

    //Lots of other functions that can call processItem
}

class WoodProcessor extends AbstractProcessor {
    function processItem(Item $item) {
        return new ProcessedWoodItem($item); //This has an inspection error
    }
}

class StoneProcessor extends AbstractProcessor {
    function processItem(Item $item) {
        return new ProcessedStoneItem($item);//This has an inspection error
    }
}

Because I'm passing in just an Item to new ProcessedWoodItem($item) and it expects a WoodItem as the parameter, the code inspection suggests there is an error.

  • Why are you not using Interfaces? You can use interface inheritance to achieve what you want, I believe. – RibaldEddie Feb 5 '14 at 6:48
  • Because the two classes share 80% of their code, which is in the abstract class. Using interfaces would mean either duplicating the code, or a large refactor to move the shared code into another class that could be composited with the two classes. – Danack Feb 5 '14 at 14:58
  • I'm pretty sure you can use both together. – RibaldEddie Feb 5 '14 at 15:36
  • Yes - but it doesn't address the problem that i) the shared methods need to use the base class of 'Item' ii) The type specific methods want to use a sub-class "WoodItem" but that gives an error like "Declaration of WoodProcessor::bar() must be compatible with AbstractProcessor::bar(Item $item)" – Danack Feb 5 '14 at 15:47
  • I don't have time to actually test the behavior but what if you hinted the interface (create an IItem and an IWoodItem and have the IWoodItem inherit from IItem)? Then hint IItem in the function signature for the base class and IWoodItem in the child. Tat might work. Might not. – RibaldEddie Feb 5 '14 at 17:20
4

You can use methods with no arguments, documenting the parameters with doc-blocks instead:

<?php

class Foo
{
    /**
     * @param string $world
     */
    public function hello()
    {
        list($world) = func_get_args();

        echo "Hello, {$world}\n";
    }
}

class Bar extends Foo
{
    /**
     * @param string $greeting
     * @param string $world
     */
    public function hello()
    {
        list($greeting, $world) = func_get_args();

        echo "{$greeting}, {$world}\n";
    }
}

$foo = new Foo();
$foo->hello('World');

$bar = new Bar();
$bar->hello('Bonjour', 'World');

I'm not going to say I think this is a good idea though.

Your problem is, you have a context with a variable number of members - rather than trying to force those through as arguments, a better and more future-proof idea is to introduce a context-type to carry all possible arguments, so that the argument list never needs to change.

Like so:

<?php

class HelloContext
{
    /** @var string */
    public $greeting;

    /** @var string */
    public $world;

    public static function create($world)
    {
        $context = new self;

        $context->world = $world;

        return $context;
    }

    public static function createWithGreeting($greeting, $world)
    {
        $context = new self;

        $context->greeting = $greeting;
        $context->world = $world;

        return $context;
    }
}

class Foo
{
    public function hello(HelloContext $context)
    {
        echo "Hello, {$context->world}\n";
    }
}

class Bar extends Foo
{
    public function hello(HelloContext $context)
    {
        echo "{$context->greeting}, {$context->world}\n";
    }
}

$foo = new Foo();
$foo->hello(HelloContext::create('World'));

$bar = new Bar();
$bar->hello(HelloContext::createWithGreeting('Bonjour', 'World'));

Static factory methods are optional of course - but could be useful, if only certain specific combinations of members produce a meaningful context. If so, you may wish to declare __construct() as protected/private as well.

  • The hoops one has to jump through to simulate OOP in PHP. – Tulains Córdova Feb 23 '15 at 13:09
  • 4
    @user61852 I'm not saying this to defend PHP (believe me) but, but most languages would not let you change an inherited method-signature - historically, this was possible in PHP, but for various reasons decided to (artificially) restrict programmers from doing this, as it causes fundamental problems with the language; this change was made to bring the language more in line with other languages, so I'm not sure which language you're comparing to. The pattern demonstrated in the second part of my answer is applicable and useful in other languages like C# or Java as well. – mindplay.dk Feb 26 '15 at 14:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.