PHP 中的强类型数组和列表

使用键入列表提高 PHP 编程速度

我们的目标

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
深夜节目安排
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.

PHP 数组的问题

PHP 数组是一种神奇、简单易用的数据结构. 它们可以存储元素列表,也可以用作哈希映射式数据结构. PHP 甚至可以将列表转换为地图,再将地图转换回列表.

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

PHP 数组没有强类型的值. Most modern programming languages have a feature called "generics" or in C++ "templates" that restricts an array or list to a specific type. 这样做有多个目的. It allows the programmer to know 正是 what type and shape the data is in the list. 它可以确保意外数据不会进入列表. 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. 是用户 ID 列表吗?也许是用户对象?也许是用户名列表,也许是用户 ID 到用户名的映射. 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.

理念

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

秘诀在于 ArrayAccess 和 Iterator 接口. Any class implementing these interfaces can be used 正是 like any array object in PHP. 这意味着你可以拥有一个实现了 ArrayAccess 和 Iterator 的 list 类,它可以在数组可以使用的任何地方使用.

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

这很好,但这又如何帮助我们解决类型化数组的问题呢?

实施魔法

当我们填写 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 正是 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. 精彩!

全面实施

请在自己的代码中尝试一下. 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.