Tableaux et listes fortement typés en PHP

Améliorez votre vitesse de programmation en PHP en utilisant les Listes Typées

Ce que nous allons accomplir

In this article I'm going to show you how you can have full auto complete support for array objects in PHP, allowing you to auto complete your way through your development tasks, improving your code, and reducing time and errors.

programming Console
Sessions de programmation tardives
Cory Marsh
Cory Marsh
Share:
Cory Marsh has over 20 years Internet security experience. He is a lead developer on the BitFire project and regularly releases PHP security and programming videos on BitFire's you tube channel.

Le problème des tableaux en PHP

Les tableaux PHP sont des structures de données fantastiques et simples à utiliser.. Ils peuvent stocker des listes d'éléments ou être utilisés comme des structures de données de type carte de hachage.. PHP peut même convertir une liste en carte et une carte en liste..

<?php
// array as list of numbers
$list_array[] = 1;
$list_array[] = 2;

// map as names to numbers
$map_array[1] = "one";
$map_array[2] = "two";

$values = array_values($map_array);
// prints "one" "two"
print_r($values);

// prints "one" "two"
$new_map = array_combine($list_array, $values);

// prints 1 => "one", 2 => "two"
print_r($values);
?>

La seule chose que les tableaux PHP n'ont pas, ce sont les valeurs fortement typées.. Most modern programming languages have a feature called "generics" or in C++ "templates" that restricts an array or list to a specific type. Cela sert plusieurs objectifs. It allows the programmer to know exactement what type and shape the data is in the list. Cela permet d'éviter que des données inattendues ne se glissent dans la liste. It allows the compiler to enforce these checks at compile time and reduces the load by removing these checks at runtime.

This means that when you are passed an array to a function or method say named "'users", you have no way of knowing what "users" is. S'agit-il d'une liste d'identifiants d'utilisateurs ? Peut-être des objets utilisateurs ? Il s'agit peut-être d'une liste de noms d'utilisateurs ou d'une carte des identifiants d'utilisateurs vers les noms d'utilisateurs.. PHP IDEs don't have type information about the array so you are forced to trace down the call stack and find what the original data is. Or maybe you just add a print statement or run the code through a debugger to find the type.

L'idée

While PHP does not support generics, you can create typed list that are supported by PHP >7.1 that can be used exactement like arrays but have associated type information for the PHP interpreter and your IDE.

Le secret réside dans les interfaces ArrayAccess et Iterator.. Any class implementing these interfaces can be used exactement like any array object in PHP. Cela signifie que vous pouvez avoir une classe de liste qui implémente ArrayAccess et Iterator et qui peut être utilisée partout où un tableau peut être utilisé..

<?php
class Typed_List implements ArrayAccess, Iterator { // ... }

$list = new Typed_List();
$list[] = "item 1";
$list[] = "item 2";

// prints "item 1" "item 2"
foreach ($list as $element) {
  echo "$element\n";
}
?>

C'est très bien, mais en quoi cela nous aide-t-il à résoudre le problème des tableaux typés ?

La magie de la mise en œuvre

La magie opère lorsque nous complétons l'implémentation de Typed_List. By filling in all required functions for our two interfaces and leaving the two getter methods offsetGet() and current() abstract we can specify the real return type in our concrete sub class and our IDE and PHP interpreter will now have full type information for the array.

<?php
class User {
  public string $name;
  public string $email;
}

class User_List extends Typed_List {

  // called when accessed like echo $list[$offset];
  public function offsetGet($offset) : User {
    return $this->list[$offset];
  }

  // called when accessed like foreach($list as $item) { // $item is type User }
  public function current() : User {
    return $this->list[$this->position];
  }
}?>

.. That is how the PHP Interpreter and your IDE knows exactement what the type of the array element is. Now when you iterate over your list of Users, your IDE will know that any element in the list is a User object and allow you to auto-complete $name and $email.

Mise en œuvre complète

Allez-y et essayez ceci dans votre propre code. I have included a stand alone class you can copy paste into your project to create your own Typed lists. Simply extend this class, implement the two abstract methods as shown and watch the IDE magic.

<?php
/**
 * a <generic> list. This is the base class for all typed lists 
 * Extend this abstract class and implement:
 * offsetGet() : Your_Return_Type and current() : Your_Return_Type
 * and then use your new subclass of type Your_Return_Type.
 * EG:
 * class User_List extends Typed_List {
 *   public function offsetGet($index) : User {
 *     return $this->protected_get($index);
 *   }
 *   public function current() : User {
 *     return $this->protected_get($this->_position);
 *   }
 * }
 */
abstract class Typed_List implements \ArrayAccess, \Iterator, \Countable, \SeekableIterator {

    protected string $_type = "mixed";
    protected $_position = 0;
    protected array $_list;

    private bool $_is_associated = false;
    private $_keys;

    
    public function __construct(array $list = []) {
        $this->_list = $list;
        $this->_type = $this->get_type();
    }

    /**
     * Example: echo $list[$index]
     * @param mixed $index index may be numeric or hash key
     * @return $this->_type cast this to your subclass type
     */
    #[\ReturnTypeWillChange]
    public abstract function offsetGet($index);

    /**
     * Example: foreach ($list as $key => $value)
     * @return mixed cast this to your subclass type at the current iterator index
     */
    #[\ReturnTypeWillChange]
    public abstract function current();


    /**
     * @return string the name of the type list supports or mixed
     */
    public abstract function get_type() : string;


    /**
     * return a new instance of the subclass with the given list
     * @param array $list 
     * @return static 
     */
    public static function of(array $list) : static {
        return new static($list);
    }

    /**
     * clone the current list into a new object
     * @return static new instance of subclass
     */
    #[\ReturnTypeWillChange]
    public function clone() {
        $new = new static();
        $new->_list = array_clone($this->_list);
        return $new;
    }

    /**
     * Example count($list);
     * @return int<0, \max> - the number of elements in the list
     */
    public function count(): int {
        return count($this->_list);
    }

    /**
     * SeekableIterator implementation
     * @param mixed $position - seek to this position in the list
     * @throws OutOfBoundsException - if the element does not exist
     */
    public function seek($position) : void {
        if (!isset($this->_list[$position])) {
            throw new OutOfBoundsException("invalid seek position ($position)");
        }
  
        $this->_position = $position;
    }

    /**
     * SeekableIterator implementation. seek internal pointer to the first element
     * @param mixed $position - seek to this position in the list
     */
    public function rewind() : void {
        if ($this->_is_associated) {
            $this->_keys = array_keys($this->_list);
            $this->_position = array_shift($this->_keys);
        } else {
            $this->_position = 0;
        }
    }

    /**
     * SeekableIterator implementation. equivalent of calling current()
     * @return mixed - the pointer to the current element
     */
    public function key() : mixed {
        return $this->_position;
    }

    /**
     * SeekableIterator implementation. equivalent of calling next()
     */
    public function next(): void {
        if ($this->_is_associated) {
            $this->_position = array_shift($this->_keys);
        } else {
            ++$this->_position;
        }
    }

    /**
     * SeekableIterator implementation. check if the current position is valid
     */
    public function valid() : bool {
        if (isset($this->_list[$this->_position])) {
            if ($this->_type != "mixed") {
                return $this->_list[$this->_position] instanceof $this->_type;
            }
            return true;
        }
        return false;
    }

    /**
     * Example: $list[1] = "data";  $list[] = "data2";
     * ArrayAccess implementation. set the value at a specific index
     * @throws 
     */
    public function offsetSet($index, $value) : void {
        // type checking
        if ($this->_type != "mixed") {
            if (! $value instanceof $this->_type) {
                $msg = get_class($this) . " only accepts objects of type \"" . $this->_type . "\", \"" . gettype($value) . "\" passed";
                throw new InvalidArgumentException($msg, 1);
            }
        }
        if (empty($index)) {
            $this->_list[] = $value;
        } else {
            $this->_is_associated = true;
            if ($index instanceof Hash_Code) {
                $this->_list[$index->hash_code()] = $value;
            } else {
                $this->_list[$index] = $value;
            }
        }
    }

    /**
     * unset($list[$value]);
     * ArrayAccess implementation. unset the value at a specific index
     */
    public function offsetUnset($index) : void {
        unset($this->_list[$index]);
    }

    /**
     * ArrayAccess implementation. check if the value at a specific index exist
     */
    public function offsetExists($index) : bool {
        return isset($this->_list[$index]);
    }

    /**
     * example $data = array_map($fn, $list->raw());
     * @return array - the internal array structure
     */
    public function &raw() : array {
        return $this->_list;
    }


    /**
     * sort the list
     * @return static - the current instance sorted
     */
    public function ksort(int $flags = SORT_REGULAR): static {
        ksort($this->_list, $flags);
        return $this;
    }

    /**
     * @return bool - true if the list is empty
     */
    public function empty() : bool {
        return empty($this->_list);
    }

    /**
     * helper method to be used by offsetGet() and current(), does bounds and key type checking
     * @param mixed $key 
     * @throws OutOfBoundsException - if the key is out of bounds
     */
    protected function protected_get($key) {
        if ($this->_is_associated) {
            if (isset($this->_list[$key])) {
                return $this->_list[$key];
            }
        }
        else {
            if ($key <= count($this->_list)) {
                return $this->_list[$key];
            }
        }

        throw new OutOfBoundsException("invalid key [$key]");
    }


   /**
     * filter the list using the given function 
     * @param callable $fn 
     * @return static
     */
    public function filter(callable $fn, bool $clone = false) {
        assert(fn_takes_x_args($fn, 1), last_assert_err() . " in " . get_class($this) . "->map()"); 
        assert(fn_arg_x_is_type($fn, 0, $this->_type), last_assert_err() . " in " . get_class($this) . "->map()");
        if ($clone) {
            return new static(array_filter(array_clone($this->_list), $fn));
        }
        $this->_list = array_filter($this->_list, $fn);
        return $this;
    }

    /**
     * json encoded version of the list
     * @return string json encoded version of first 5 elements
     */
    public function __toString() : string {
        return json_encode(array_slice($this->_list, 0, 5));
    }
}?>
Cory Marsh
Cory Marsh
Share:
Cory Marsh has over 20 years Internet security experience. He is a lead developer on the BitFire project and regularly releases PHP security and programming videos on BitFire's you tube channel.