Modification de la signature de méthode pour implémenter des classes en PHP

9

Existe-t-il un travail décent autour du manque de PHP de génériques qui permettent l'inspection statique du code pour détecter la cohérence des types?

J'ai une classe abstraite, que je veux sous-classer et imposer également que l'une des méthodes passe de la prise d'un paramètre d'un type à la prise d'un paramètre qui est une sous-classe de ce paramètre.

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

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

Ceci n'est pas autorisé en PHP car il modifie la signature des méthodes, ce qui n'est pas autorisé. Avec les génériques de style Java, vous pourriez faire quelque chose comme:

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

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

Mais évidemment, PHP ne les prend pas en charge.

Google pour ce problème, les gens suggèrent d'utiliser instanceofpour vérifier les erreurs au moment de l'exécution, par exemple

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

Mais cela ne fonctionne qu'au moment de l'exécution, cela ne vous permet pas d'inspecter votre code à la recherche d'erreurs à l'aide de l'analyse statique - existe-t-il donc un moyen sensé de gérer cela en PHP?

Un exemple plus complet du problème est:

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

Parce que je passe juste un Itemto new ProcessedWoodItem($item)et qu'il attend un WoodItem comme paramètre, l'inspection du code suggère qu'il y a une erreur.

Danack
la source
1
Pourquoi n'utilisez-vous pas les interfaces? Vous pouvez utiliser l'héritage d'interface pour réaliser ce que vous voulez, je crois.
RibaldEddie
Parce que les deux classes partagent 80% de leur code, qui est dans la classe abstraite. L'utilisation d'interfaces signifierait soit la duplication du code, soit un grand refactor pour déplacer le code partagé dans une autre classe qui pourrait être composée avec les deux classes.
Danack
Je suis sûr que vous pouvez utiliser les deux ensemble.
RibaldEddie
Oui - mais cela ne résout pas le problème que i) les méthodes partagées doivent utiliser la classe de base de 'Item' ii) Les méthodes spécifiques au type veulent utiliser une sous-classe "WoodItem" mais cela donne une erreur comme "Declaration" de WoodProcessor :: bar () doit être compatible avec AbstractProcessor :: bar (Item $ item) "
Danack
Je n'ai pas le temps de tester le comportement, mais que se passe-t-il si vous faites allusion à l'interface (créez un IItem et un IWoodItem et faites hériter le IWoodItem de IItem)? Indiquez ensuite IItem dans la signature de fonction pour la classe de base et IWoodItem dans l'enfant. Tat pourrait fonctionner. Peut-être pas.
RibaldEddie

Réponses:

3

Vous pouvez utiliser des méthodes sans arguments, en documentant les paramètres avec des blocs doc à la place:

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

Je ne vais pas dire que je pense que c'est une bonne idée.

Votre problème est que vous avez un contexte avec un nombre variable de membres - plutôt que d'essayer de les forcer en tant qu'arguments, une idée meilleure et plus à l'épreuve du temps consiste à introduire un type de contexte pour transporter tous les arguments possibles, de sorte que l'argument la liste n'a jamais besoin de changer.

Ainsi:

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

Les méthodes de fabrique statique sont bien sûr facultatives - mais pourraient être utiles, si seulement certaines combinaisons spécifiques de membres produisent un contexte significatif. Si c'est le cas, vous pouvez également déclarer __construct()comme protégé / privé.

mindplay.dk
la source
Les cerceaux que l'on doit franchir pour simuler la POO en PHP.
Tulains Córdova
4
@ user61852 Je ne dis pas cela pour défendre PHP (croyez-moi) mais, mais la plupart des langages ne vous permettraient pas de modifier une signature de méthode héritée - historiquement, cela était possible en PHP, mais pour diverses raisons, j'ai décidé de restreindre (artificiellement) les programmeurs de le faire, car cela cause des problèmes fondamentaux avec la langue; cette modification a été apportée pour aligner la langue sur les autres langues, donc je ne sais pas à quelle langue vous vous comparez. Le modèle démontré dans la deuxième partie de ma réponse est également applicable et utile dans d'autres langages comme C # ou Java.
mindplay.dk