Ändern der Methodensignatur zum Implementieren von Klassen in PHP

9

Gibt es eine anständige Lösung für das Fehlen von Generika in PHP, die eine statische Codeprüfung ermöglichen, um die Typkonsistenz festzustellen?

Ich habe eine abstrakte Klasse, die ich unterordnen möchte, und erzwinge auch, dass sich eine der Methoden von der Verwendung eines Parameters eines Typs zur Verwendung eines Parameters ändert, der eine Unterklasse dieses Parameters ist.

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

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

Dies ist in PHP nicht zulässig, da die Methodensignatur geändert wird, was nicht zulässig ist. Mit Generika im Java-Stil können Sie Folgendes tun:

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

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

Aber offensichtlich unterstützt PHP diese nicht.

Google für dieses Problem schlagen die Leute vor instanceof, Fehler zur Laufzeit zu überprüfen, z

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

Dies funktioniert jedoch nur zur Laufzeit. Sie können Ihren Code nicht mithilfe einer statischen Analyse auf Fehler untersuchen. Gibt es also eine sinnvolle Möglichkeit, dies in PHP zu handhaben?

Ein vollständigeres Beispiel für das Problem ist:

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

Da ich nur ein Itemto übergebe new ProcessedWoodItem($item)und ein WoodItem als Parameter erwartet, deutet die Codeüberprüfung darauf hin, dass ein Fehler vorliegt.

Danack
quelle
1
Warum verwenden Sie keine Schnittstellen? Sie können die Schnittstellenvererbung verwenden, um das zu erreichen, was Sie wollen, glaube ich.
RibaldEddie
Weil die beiden Klassen 80% ihres Codes gemeinsam nutzen, der sich in der abstrakten Klasse befindet. Die Verwendung von Schnittstellen würde entweder das Duplizieren des Codes oder einen großen Refactor bedeuten, um den gemeinsam genutzten Code in eine andere Klasse zu verschieben, die aus den beiden Klassen zusammengesetzt werden könnte.
Danack
Ich bin mir ziemlich sicher, dass Sie beide zusammen verwenden können.
RibaldEddie
Ja - aber es behebt nicht das Problem, dass i) die gemeinsam genutzten Methoden die Basisklasse von 'Item' verwenden müssen ii) die typspezifischen Methoden eine Unterklasse "WoodItem" verwenden möchten, dies jedoch einen Fehler wie "Declaration" ergibt von WoodProcessor :: bar () muss mit AbstractProcessor :: bar (Item $ item) kompatibel sein "
Danack
Ich habe keine Zeit, das Verhalten tatsächlich zu testen, aber was ist, wenn Sie die Schnittstelle angedeutet haben (erstellen Sie ein IItem und ein IWoodItem und lassen Sie das IWoodItem von IItem erben)? Geben Sie dann IItem in der Funktionssignatur für die Basisklasse und IWoodItem im untergeordneten Element an. Tat könnte funktionieren. Möglicherweise nicht.
RibaldEddie

Antworten:

3

Sie können Methoden ohne Argumente verwenden und die Parameter stattdessen mit doc-Blöcken dokumentieren:

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

Ich werde nicht sagen, dass ich das für eine gute Idee halte.

Ihr Problem ist, dass Sie einen Kontext mit einer variablen Anzahl von Mitgliedern haben - anstatt zu versuchen, diese als Argumente durchzusetzen, ist es eine bessere und zukunftssicherere Idee, einen Kontexttyp einzuführen, der alle möglichen Argumente enthält, so dass das Argument Liste muss sich nie ändern.

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

Statische Factory-Methoden sind natürlich optional - könnten aber nützlich sein, wenn nur bestimmte bestimmte Kombinationen von Elementen einen aussagekräftigen Kontext ergeben. In diesem Fall möchten Sie möglicherweise auch __construct()als geschützt / privat deklarieren .

mindplay.dk
quelle
Die Reifen, durch die man springen muss, um OOP in PHP zu simulieren.
Tulains Córdova
4
@ user61852 Ich sage dies nicht, um PHP zu verteidigen (glauben Sie mir), aber in den meisten Sprachen können Sie eine geerbte Methodensignatur nicht ändern - historisch war dies in PHP möglich, aber aus verschiedenen Gründen wurde beschlossen, Programmierer (künstlich) einzuschränken davon, da es grundlegende Probleme mit der Sprache verursacht; Diese Änderung wurde vorgenommen, um die Sprache besser mit anderen Sprachen in Einklang zu bringen. Daher bin ich mir nicht sicher, mit welcher Sprache Sie vergleichen. Das im zweiten Teil meiner Antwort gezeigte Muster ist auch in anderen Sprachen wie C # oder Java anwendbar und nützlich.
mindplay.dk