Cambio de firma de método para implementar clases en PHP

9

¿Existe algún trabajo decente en torno a la falta de genéricos de PHP que permitan la inspección de código estático para detectar la consistencia de tipo?

Tengo una clase abstracta, que quiero subclasificar y también imponer que uno de los métodos cambie de tomar un parámetro de un tipo, a tomar un parámetro que es una subclase de ese parámetro.

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

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

Esto no está permitido en PHP porque está cambiando la firma de métodos que no está permitido. Con los genéricos de estilo Java podrías hacer algo como:

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

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

Pero obviamente PHP no los admite.

Google para este problema, la gente sugiere usar instanceofpara verificar errores en tiempo de ejecución, por ejemplo

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

Pero eso solo funciona en tiempo de ejecución, no le permite inspeccionar su código en busca de errores utilizando análisis estático, entonces, ¿hay alguna forma sensata de manejar esto en PHP?

Un ejemplo más completo del problema es:

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

Debido a que paso solo un Itemto new ProcessedWoodItem($item)y espera un WoodItem como parámetro, la inspección del código sugiere que hay un error.

Danack
fuente
1
¿Por qué no usas interfaces? Puede usar la herencia de la interfaz para lograr lo que quiere, creo.
RibaldEddie
Porque las dos clases comparten el 80% de su código, que está en la clase abstracta. El uso de interfaces significaría duplicar el código o un gran refactor para mover el código compartido a otra clase que podría estar compuesta con las dos clases.
Danack
Estoy bastante seguro de que pueden usar ambos juntos.
RibaldEddie
Sí, pero no resuelve el problema de que i) los métodos compartidos necesitan usar la clase base de 'Elemento' ii) Los métodos específicos del tipo quieren usar una subclase "WoodItem" pero eso da un error como "Declaración of WoodProcessor :: bar () debe ser compatible con AbstractProcessor :: bar (Item $ item) "
Danack
No tengo tiempo para probar realmente el comportamiento, pero ¿qué sucede si insinúa la interfaz (cree un IItem y un IWoodItem y haga que el IWoodItem herede de IItem)? Luego, insinúe IItem en la firma de la función para la clase base e IWoodItem en el hijo. Tat podría funcionar. Tal vez no.
RibaldEddie

Respuestas:

3

Puede usar métodos sin argumentos, documentando los parámetros con doc-blocks en su lugar:

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

Sin embargo, no voy a decir que creo que es una buena idea.

Su problema es que tiene un contexto con un número variable de miembros; en lugar de tratar de forzarlos como argumentos, una idea mejor y más a prueba de futuro es introducir un tipo de contexto para llevar todos los argumentos posibles, de modo que el argumento La lista nunca necesita cambiar.

Al igual que:

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

Los métodos de fábrica estáticos son opcionales, por supuesto, pero podrían ser útiles si solo ciertas combinaciones específicas de miembros producen un contexto significativo. Si es así, es posible que también desee declararlo __construct()como protegido / privado.

mindplay.dk
fuente
Los aros que uno tiene que saltar para simular OOP en PHP.
Tulains Córdova
44
@ user61852 No digo esto para defender PHP (créeme) pero, pero la mayoría de los lenguajes no te permiten cambiar una firma de método heredada; históricamente, esto era posible en PHP, pero por varias razones decidí restringir (artificialmente) a los programadores de hacer esto, ya que causa problemas fundamentales con el idioma; este cambio se realizó para que el idioma esté más en línea con otros idiomas, por lo que no estoy seguro de con qué idioma se está comparando. El patrón demostrado en la segunda parte de mi respuesta es aplicable y útil en otros lenguajes como C # o Java también.
mindplay.dk