Stark typisierte Arrays und Listen in PHP

Verbessern Sie Ihre PHP-Programmiergeschwindigkeit mit Hilfe von Typed Lists

Was wir erreichen wollen

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
Programmierungssitzungen zu später Stunde
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.

Das Problem mit PHP-Arrays

PHP-Arrays sind fantastische, einfach zu verwendende Datenstrukturen. Sie können Listen von Elementen speichern oder als Hashmap-ähnliche Datenstrukturen verwendet werden.. PHP kann sogar eine Liste in eine Karte und eine Karte zurück in eine Liste konvertieren.

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

Das einzige, was PHP-Arrays nicht haben, sind stark typisierte Werte. Most modern programming languages have a feature called "generics" or in C++ "templates" that restricts an array or list to a specific type. Dies dient mehreren Zwecken. It allows the programmer to know genau what type and shape the data is in the list. Sie stellt sicher, dass keine unerwarteten Daten in die Liste gelangen.. 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. Ist es eine Liste von Benutzerkennungen? Vielleicht Benutzerobjekte? Vielleicht ist es eine Liste von Benutzernamen, oder vielleicht ist es eine Zuordnung von Benutzerkennungen zu Benutzernamen. 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.

The Idea

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

Das Geheimnis sind die Schnittstellen ArrayAccess und Iterator. Any class implementing these interfaces can be used genau like any array object in PHP. Das bedeutet, dass Sie eine Listenklasse haben können, die ArrayAccess und Iterator implementiert und überall verwendet werden kann, wo ein Array verwendet werden kann.

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

Das ist großartig, aber wie hilft uns das, das Problem der typisierten Arrays zu lösen?

Der Umsetzungszauber

Die Magie kommt ins Spiel, wenn wir die Implementierung für Typed_List ausfüllen. 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];
  }
}?>

Beachte, dass der Rückgabetyp beider Methoden der Typ User ist. That is how the PHP Interpreter and your IDE knows genau 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. Glänzend!

Vollständige Umsetzung

Probieren Sie dies in Ihrem eigenen Code aus. 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.