Changing method signature for implementing classes in PHP

classinheritancetype-safety

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.

Best Answer

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.