web-dev-qa-db-fra.com

PHP a-t-il une réponse à Java génériques de classe de style?

Lors de la construction d'un framework MVC en PHP j'ai rencontré un problème qui pourrait être résolu facilement en utilisant Java génériques de style. Une classe Controller abstraite pourrait ressembler à ceci:

abstract class Controller {

abstract public function addModel(Model $model);

Il peut y avoir un cas où une sous-classe de classe Controller ne devrait accepter qu'une sous-classe de Model. Par exemple, ExtendedController ne doit accepter ReOrderableModel que dans la méthode addModel car il fournit une méthode reOrder () à laquelle ExtendedController doit avoir accès:

class ExtendedController extends Controller {

public function addModel(ReOrderableModel $model) {

Dans PHP la signature de la méthode héritée doit être exactement la même pour que l'indicateur de type ne puisse pas être changé en une classe différente, même si la classe hérite du type de classe indiqué dans la superclasse. Dans Java Je ferais simplement ceci:

abstract class Controller<T> {

abstract public addModel(T model);


class ExtendedController extends Controller<ReOrderableModel> {

public addModel(ReOrderableModel model) {

Mais il n'y a pas de support générique en PHP. Existe-t-il une solution qui adhérerait toujours aux principes OOP?

Edit Je suis conscient que PHP ne nécessite pas d'indication de type du tout mais c'est peut-être une mauvaise POO. Premièrement, il n'est pas évident de l'interface (la signature de la méthode) ce type d'objet doit être accepté. Donc, si un autre développeur souhaite utiliser la méthode, il doit être évident que les objets de type X sont nécessaires sans qu'ils aient à parcourir l'implémentation (corps de la méthode) qui est une mauvaise encapsulation et casse le principe de masquage des informations. Deuxièmement, parce qu'il n'y a pas de sécurité de type, la méthode peut accepter n'importe quelle variable non valide, ce qui signifie que la vérification manuelle du type et le lancement d'exceptions sont nécessaires partout!

38
Jonathan

Cela semble fonctionner pour moi (bien qu'il lance un avertissement strict) avec le cas de test suivant:

class PassMeIn
{

}

class PassMeInSubClass extends PassMeIn
{

}

class ClassProcessor
{
    public function processClass (PassMeIn $class)
    {
        var_dump (get_class ($class));
    }
}

class ClassProcessorSubClass extends ClassProcessor 
{
    public function processClass (PassMeInSubClass $class)
    {
        parent::processClass ($class);
    }
}

$a  = new PassMeIn;
$b  = new PassMeInSubClass;
$c  = new ClassProcessor;
$d  = new ClassProcessorSubClass;

$c -> processClass ($a);
$c -> processClass ($b);
$d -> processClass ($b);

Si l'avertissement strict est quelque chose que vous ne voulez vraiment pas, vous pouvez le contourner comme ceci.

class ClassProcessor
{
    public function processClass (PassMeIn $class)
    {
        var_dump (get_class ($class));
    }
}

class ClassProcessorSubClass extends ClassProcessor 
{
    public function processClass (PassMeIn $class)
    {
        if ($class instanceof PassMeInSubClass)
        {
            parent::processClass ($class);
        }
        else
        {
            throw new InvalidArgumentException;
        }
    }
}

$a  = new PassMeIn;
$b  = new PassMeInSubClass;
$c  = new ClassProcessor;
$d  = new ClassProcessorSubClass;

$c -> processClass ($a);
$c -> processClass ($b);
$d -> processClass ($b);
$d -> processClass ($a);

Une chose que vous devez garder à l'esprit cependant, ce n'est strictement pas la meilleure pratique en termes OOP. Si une superclasse peut accepter des objets d'une classe particulière comme argument de méthode, alors toutes ses sous-classes devraient également pouvoir d'accepter également les objets de cette classe. Empêcher les sous-classes de traiter les classes que la superclasse peut accepter signifie que vous ne pouvez pas utiliser la sous-classe à la place de la superclasse et être sûr à 100% que cela fonctionnera dans tous les cas. La pratique pertinente est connue comme Liskov Substitution Principle et il indique que, entre autres choses, le type d'arguments de méthode ne peut que s'affaiblir dans les sous-classes et le type de valeurs de retour ne peut que devenir plus fort (l'entrée ne peut que devenir plus générale, la sortie ne peut qu'être plus spécifique).

C'est un problème très frustrant, et je l'ai moi-même essayé plusieurs fois, donc si l'ignorer dans un cas particulier est la meilleure chose à faire, je vous suggère de l'ignorer. Mais n'en prenez pas l'habitude ou votre code commencera à développer toutes sortes d'interdépendances subtiles qui seront un cauchemar à déboguer (les tests unitaires ne les attraperont pas parce que les unités individuelles se comporteront comme prévu, c'est l'interaction entre elles où se situe le problème). Si vous l'ignorez, commentez le code pour en informer les autres et qu'il s'agit d'un choix de conception délibéré.

12
GordonM

Quel que soit le monde Java inventé, il n'est pas toujours nécessaire d'avoir raison. Je pense avoir détecté une violation du principe de substitution Liskov ici, et PHP a raison de s'en plaindre) en mode E_STRICT:

Citez Wikipedia: "Si S est un sous-type de T, alors les objets de type T dans un programme peuvent être remplacés par des objets de type S sans altérer aucune des propriétés souhaitables de ce programme."

T est votre contrôleur. S est votre ExtendedController. Vous devriez pouvoir utiliser le ExtendedController partout où le contrôleur fonctionne sans rien casser. Changer le typehint sur la méthode addModel () casse les choses, car à chaque endroit qui a passé un objet de type Model, le typehint empêchera désormais de passer le même objet s'il n'est pas accidentellement un ReOrderableModel.

Comment y échapper?

Votre ExtendedController peut laisser le typehint tel quel et vérifier ensuite s'il a obtenu une instance de ReOrderableModel ou non. Cela contourne les plaintes PHP, mais cela casse toujours les choses en termes de substitution Liskov.

Une meilleure façon est de créer une nouvelle méthode addReOrderableModel() conçue pour injecter des objets ReOrderableModel dans ExtendedController. Cette méthode peut avoir le typehint dont vous avez besoin et peut simplement appeler en interne addModel() pour mettre le modèle en place là où il est attendu.

Si vous souhaitez qu'un ExtendedController soit utilisé au lieu d'un Controller en tant que paramètre, vous savez que votre méthode pour ajouter ReOrderableModel est présente et peut être utilisée. Vous déclarez explicitement que le contrôleur ne rentrera pas dans ce cas. Chaque méthode qui s'attend à ce qu'un Controller soit passé ne s'attendra pas à ce que addReOrderableModel() existe et n'essayera jamais de l'appeler. Chaque méthode qui attend ExtendedController a le droit d'appeler cette méthode, car elle doit être présente.

class ExtendedController extends Controller
{
  public function addReOrderableModel(ReOrderableModel $model)
  {
    return $this->addModel($model);
  }
}
9
Sven

Ma solution de contournement est la suivante:

/**
 * Generic list logic and an abstract type validator method.
 */    
abstract class AbstractList {
    protected $elements;

    public function __construct() {
        $this->elements = array();
    }

    public function add($element) {
        $this->validateType($element);
        $this->elements[] = $element;
    }

    public function get($index) {
        if ($index >= sizeof($this->elements)) {
            throw new OutOfBoundsException();
        }
        return $this->elements[$index];
    }

    public function size() {
        return sizeof($this->elements);
    }

    public function remove($element) {
        validateType($element);
        for ($i = 0; $i < sizeof($this->elements); $i++) {
            if ($this->elements[$i] == $element) {
               unset($this->elements[$i]);
            }
        }
    }

    protected abstract function validateType($element);
}


/**
 * Extends the abstract list with the type-specific validation
 */
class MyTypeList extends AbstractList {
    protected function validateType($element) {
        if (!($element instanceof MyType)) {
            throw new InvalidArgumentException("Parameter must be MyType instance");
        }
    }
}

/**
 * Just an example class as a subject to validation.
 */
class MyType {
    // blahblahblah
}


function proofOfConcept(AbstractList $lst) {
    $lst->add(new MyType());
    $lst->add("wrong type"); // Should throw IAE
}

proofOfConcept(new MyTypeList());

Bien que cela diffère toujours des génériques Java, il minimise à peu près le code supplémentaire nécessaire pour imiter le comportement.

En outre, c'est un peu plus de code que certains exemples donnés par d'autres, mais - au moins pour moi - il semble être plus propre (et plus similaire à l'homologue Java) que la plupart d'entre eux) .

J'espère que certains d'entre vous le trouveront utile.

Toute amélioration par rapport à cette conception est la bienvenue!

8
Powerslave

J'ai déjà vécu le même type de problème auparavant. Et j'ai utilisé quelque chose comme ça pour y faire face.

Class Myclass {

    $objectParent = "MyMainParent"; //Define the interface or abstract class or the main parent class here
    public function method($classObject) {
        if(!$classObject instanceof $this -> objectParent) { //check 
             throw new Exception("Invalid Class Identified");
        }
        // Carry on with the function
    }

}
3
Starx

Vous pouvez envisager de passer à Hack et HHVM. Il est développé par Facebook et entièrement compatible avec PHP. Vous pouvez décider d'utiliser <?php ou <?hh

Il prend en charge ce que vous voulez:

http://docs.hhvm.com/manual/en/hack.generics.php

Je sais que ce n'est pas PHP. Mais il est compatible avec lui et améliore également considérablement vos performances.

3

Vous pouvez le faire de manière sale en passant le type comme deuxième argument du constructeur

<?php class Collection implements IteratorAggregate{
      private $type;
      private $container;
      public function __construct(array $collection, $type='Object'){
          $this->type = $type;
          foreach($collection as $value){
             if(!($value instanceof $this->type)){
                 throw new RuntimeException('bad type for your collection');
             }  
          }
          $this->container = new \ArrayObject($collection);
      }
      public function getIterator(){
         return $this->container->getIterator();
      }
    }
2
artragis

Pour fournir un niveau élevé d'analyse de code statique, de typage strict et de convivialité, j'ai trouvé cette solution: https://Gist.github.com/rickhub/aa6cb712990041480b11d5624a60b53b

/**
 * Class GenericCollection
 */
class GenericCollection implements \IteratorAggregate, \ArrayAccess{
    /**
     * @var string
     */
    private $type;

    /**
     * @var array
     */
    private $items = [];

    /**
     * GenericCollection constructor.
     *
     * @param string $type
     */
    public function __construct(string $type){
        $this->type = $type;
    }

    /**
     * @param $item
     *
     * @return bool
     */
    protected function checkType($item): bool{
        $type = $this->getType();
        return $item instanceof $type;
    }

    /**
     * @return string
     */
    public function getType(): string{
        return $this->type;
    }

    /**
     * @param string $type
     *
     * @return bool
     */
    public function isType(string $type): bool{
        return $this->type === $type;
    }

    #region IteratorAggregate

    /**
     * @return \Traversable|$type
     */
    public function getIterator(): \Traversable{
        return new \ArrayIterator($this->items);
    }

    #endregion

    #region ArrayAccess

    /**
     * @param mixed $offset
     *
     * @return bool
     */
    public function offsetExists($offset){
        return isset($this->items[$offset]);
    }

    /**
     * @param mixed $offset
     *
     * @return mixed|null
     */
    public function offsetGet($offset){
        return isset($this->items[$offset]) ? $this->items[$offset] : null;
    }

    /**
     * @param mixed $offset
     * @param mixed $item
     */
    public function offsetSet($offset, $item){
        if(!$this->checkType($item)){
            throw new \InvalidArgumentException('invalid type');
        }
        $offset !== null ? $this->items[$offset] = $item : $this->items[] = $item;
    }

    /**
     * @param mixed $offset
     */
    public function offsetUnset($offset){
        unset($this->items[$offset]);
    }

    #endregion
}


/**
 * Class Item
 */
class Item{
    /**
     * @var int
     */
    public $id = null;

    /**
     * @var string
     */
    public $data = null;

    /**
     * Item constructor.
     *
     * @param int    $id
     * @param string $data
     */
    public function __construct(int $id, string $data){
        $this->id = $id;
        $this->data = $data;
    }
}


/**
 * Class ItemCollection
 */
class ItemCollection extends GenericCollection{
    /**
     * ItemCollection constructor.
     */
    public function __construct(){
        parent::__construct(Item::class);
    }

    /**
     * @return \Traversable|Item[]
     */
    public function getIterator(): \Traversable{
        return parent::getIterator();
    }
}


/**
 * Class ExampleService
 */
class ExampleService{
    /**
     * @var ItemCollection
     */
    private $items = null;

    /**
     * SomeService constructor.
     *
     * @param ItemCollection $items
     */
    public function __construct(ItemCollection $items){
        $this->items = $items;
    }

    /**
     * @return void
     */
    public function list(){
        foreach($this->items as $item){
            echo $item->data;
        }
    }
}


/**
 * Usage
 */
$collection = new ItemCollection;
$collection[] = new Item(1, 'foo');
$collection[] = new Item(2, 'bar');
$collection[] = new Item(3, 'foobar');

$collection[] = 42; // InvalidArgumentException: invalid type

$service = new ExampleService($collection);
$service->list();

Même si quelque chose comme ça se sentait tellement mieux:

class ExampleService{
    public function __construct(Collection<Item> $items){
        // ..
    }
}

J'espère que les génériques entreront dans PHP bientôt.

2
rckd