Alterando a assinatura do método para implementar classes em PHP

9

Existe algum trabalho decente sobre a falta de genéricos do PHP que permita que a inspeção de código estático detecte a consistência do tipo?

Eu tenho uma classe abstrata, que quero subclassificar e também impor que um dos métodos mude de um parâmetro de um tipo para um parâmetro que é uma subclasse desse parâmetro.

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

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

Isso não é permitido no PHP porque está alterando a assinatura dos métodos, o que não é permitido. Com os genéricos de estilo Java, você pode fazer algo como:

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

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

Mas obviamente o PHP não suporta isso.

Google para este problema, as pessoas sugerem usar instanceofpara verificar erros em tempo de execução, por exemplo

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

Mas isso só funciona em tempo de execução, não permite que você inspecione seu código quanto a erros usando análise estática - então existe alguma maneira sensata de lidar com isso no PHP?

Um exemplo mais completo do problema é:

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

Como estou passando apenas um Itempara new ProcessedWoodItem($item)e ele espera um WoodItem como parâmetro, a inspeção do código sugere que há um erro.

Danack
fonte
11
Por que você não está usando interfaces? Você pode usar a herança da interface para conseguir o que deseja, acredito.
precisa saber é o seguinte
Porque as duas classes compartilham 80% de seu código, que está na classe abstrata. Usar interfaces significaria duplicar o código ou um grande refator para mover o código compartilhado para outra classe que pudesse ser composta pelas duas classes.
1155 Danack
Tenho certeza que você pode usar os dois juntos.
precisa saber é o seguinte
Sim - mas não resolve o problema de que i) os métodos compartilhados precisam usar a classe base de 'Item' ii) Os métodos específicos de tipo desejam usar uma subclasse "WoodItem", mas isso gera um erro como "Declaração do WoodProcessor :: bar () deve ser compatível com AbstractProcessor :: bar (Item $ item) "
Danack 5/14/14
Não tenho tempo para realmente testar o comportamento, mas e se você sugerisse a interface (crie um IItem e um IWoodItem e tenha o IWoodItem herdado do IItem)? Em seguida, insira IItem na assinatura da função para a classe base e IWoodItem no filho. Tat pode funcionar. Talvez não.
precisa saber é o seguinte

Respostas:

3

Você pode usar métodos sem argumentos, documentando os parâmetros com doc-blocks:

<?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');

Não vou dizer que acho que é uma boa ideia.

Seu problema é que você tem um contexto com um número variável de membros - em vez de tentar forçá-los como argumentos, uma idéia melhor e mais preparada para o futuro é introduzir um tipo de contexto para transportar todos os argumentos possíveis, para que o argumento lista nunca precisa mudar.

Igual a:

<?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'));

Os métodos estáticos de fábrica são opcionais, é claro - mas podem ser úteis, se apenas certas combinações específicas de membros produzirem um contexto significativo. Nesse caso, você também pode declarar __construct()como protegido / privado.

mindplay.dk
fonte
Os aros que você precisa pular para simular OOP em PHP.
Tulains Córdova
4
@ user61852 Não estou dizendo isso para defender o PHP (acredite em mim), mas a maioria das linguagens não permite alterar uma assinatura de método herdada - historicamente, isso era possível no PHP, mas por várias razões decidiu (artificialmente) restringir os programadores de fazer isso, pois causa problemas fundamentais com a linguagem; essa alteração foi feita para alinhar o idioma a outros idiomas; portanto, não sei com qual idioma você está se comparando. O padrão demonstrado na segunda parte da minha resposta é aplicável e útil em outras linguagens como C # ou Java também.
mindplay.dk