Type hinting in PHP 7 - array of objects

后端 未结 7 1880
迷失自我
迷失自我 2020-12-08 03:36

Maybe I missed something but is there any option to define that function should have argument or return for example array of User objects?

Consider the following cod

7条回答
  •  南笙
    南笙 (楼主)
    2020-12-08 04:09

    In our codebase, we have the concept of collections. These are based upon a class called TypedArray which is based upon ArrayObject.

    class ArrayObject extends \ArrayObject
    {
        /**
         * Clone a collection by cloning all items.
         */
        public function __clone()
        {
            foreach ($this as $key => $value) {
                $this[$key] = is_object($value) ? clone $value : $value;
            }
        }
    
        /**
         * Inserting the provided element at the index. If index is negative, it will be calculated from the end of the Array Object
         *
         * @param int $index
         * @param mixed $element
         */
        public function insert(int $index, $element)
        {
            $data = $this->getArrayCopy();
            if ($index < 0) {
                $index = $this->count() + $index;
            }
    
            $data = array_merge(array_slice($data, 0, $index, true), [$element], array_slice($data, $index, null, true));
            $this->exchangeArray($data);
        }
    
        /**
         * Remove a portion of the array and optionally replace it with something else.
         *
         * @see array_splice()
         *
         * @param int $offset
         * @param int|null $length
         * @param null $replacement
         *
         * @return static
         */
        public function splice(int $offset, int $length = null, $replacement = null)
        {
            $data = $this->getArrayCopy();
    
            // A null $length AND a null $replacement is not the same as supplying null to the call.
            if (is_null($length) && is_null($replacement)) {
                $result = array_splice($data, $offset);
            } else {
                $result = array_splice($data, $offset, $length, $replacement);
            }
            $this->exchangeArray($data);
    
            return new static($result);
        }
    
        /**
         * Adding a new value at the beginning of the collection
         *
         * @param mixed $value
         *
         * @return int Returns the new number of elements in the Array
         */
        public function unshift($value): int
        {
            $data = $this->getArrayCopy();
            $result = array_unshift($data, $value);
            $this->exchangeArray($data);
    
            return $result;
        }
    
        /**
         * Extract a slice of the array.
         *
         * @see array_slice()
         *
         * @param int $offset
         * @param int|null $length
         * @param bool $preserveKeys
         *
         * @return static
         */
        public function slice(int $offset, int $length = null, bool $preserveKeys = false)
        {
            return new static(array_slice($this->getArrayCopy(), $offset, $length, $preserveKeys));
        }
    
        /**
         * Sort an array.
         *
         * @see sort()
         *
         * @param int $sortFlags
         *
         * @return bool
         */
        public function sort($sortFlags = SORT_REGULAR)
        {
            $data = $this->getArrayCopy();
            $result = sort($data, $sortFlags);
            $this->exchangeArray($data);
    
            return $result;
        }
    
        /**
         * Apply a user supplied function to every member of an array
         *
         * @see array_walk
         *
         * @param callable $callback
         * @param mixed|null $userData
         *
         * @return bool Returns true on success, otherwise false
         *
         * @see array_walk()
         */
        public function walk($callback, $userData = null)
        {
            $data = $this->getArrayCopy();
            $result = array_walk($data, $callback, $userData);
            $this->exchangeArray($data);
    
            return $result;
        }
    
        /**
         * Chunks the object into ArrayObject containing
         *
         * @param int $size
         * @param bool $preserveKeys
         *
         * @return ArrayObject
         */
        public function chunk(int $size, bool $preserveKeys = false): ArrayObject
        {
            $data = $this->getArrayCopy();
            $result = array_chunk($data, $size, $preserveKeys);
    
            return new ArrayObject($result);
        }
    
        /**
         * @see array_column
         *
         * @param mixed $columnKey
         *
         * @return array
         */
        public function column($columnKey): array
        {
            $data = $this->getArrayCopy();
            $result = array_column($data, $columnKey);
    
            return $result;
        }
    
        /**
         * @param callable $mapper Will be called as $mapper(mixed $item)
         *
         * @return ArrayObject A collection of the results of $mapper(mixed $item)
         */
        public function map(callable $mapper): ArrayObject
        {
            $data = $this->getArrayCopy();
            $result = array_map($mapper, $data);
    
            return new self($result);
        }
    
        /**
         * Applies the callback function $callable to each item in the collection.
         *
         * @param callable $callable
         */
        public function each(callable $callable)
        {
            foreach ($this as &$item) {
                $callable($item);
            }
            unset($item);
        }
    
        /**
         * Returns the item in the collection at $index.
         *
         * @param int $index
         *
         * @return mixed
         *
         * @throws InvalidArgumentException
         * @throws OutOfRangeException
         */
        public function at(int $index)
        {
            $this->validateIndex($index);
    
            return $this[$index];
        }
    
        /**
         * Validates a number to be used as an index
         *
         * @param int $index The number to be validated as an index
         *
         * @throws OutOfRangeException
         * @throws InvalidArgumentException
         */
        private function validateIndex(int $index)
        {
            $exists = $this->indexExists($index);
    
            if (!$exists) {
                throw new OutOfRangeException('Index out of bounds of collection');
            }
        }
    
        /**
         * Returns true if $index is within the collection's range and returns false
         * if it is not.
         *
         * @param int $index
         *
         * @return bool
         *
         * @throws InvalidArgumentException
         */
        public function indexExists(int $index)
        {
            if ($index < 0) {
                throw new InvalidArgumentException('Index must be a non-negative integer');
            }
    
            return $index < $this->count();
        }
    
        /**
         * Finding the first element in the Array, for which $callback returns true
         *
         * @param callable $callback
         *
         * @return mixed Element Found in the Array or null
         */
        public function find(callable $callback)
        {
            foreach ($this as $element) {
                if ($callback($element)) {
                    return $element;
                }
            }
    
            return null;
        }
    
        /**
         * Filtering the array by retrieving only these elements for which callback returns true
         *
         * @param callable $callback
         * @param int $flag Use ARRAY_FILTER_USE_KEY to pass key as the only argument to $callback instead of value.
         *                  Use ARRAY_FILTER_USE_BOTH pass both value and key as arguments to $callback instead of value.
         *
         * @return static
         *
         * @see array_filter
         */
        public function filter(callable $callback, int $flag = 0)
        {
            $data = $this->getArrayCopy();
            $result = array_filter($data, $callback, $flag);
    
            return new static($result);
        }
    
        /**
         * Reset the array pointer to the first element and return the element.
         *
         * @return mixed
         *
         * @throws \OutOfBoundsException
         */
        public function first()
        {
            if ($this->count() === 0) {
                throw new \OutOfBoundsException('Cannot get first element of empty Collection');
            }
    
            return reset($this);
        }
    
        /**
         * Reset the array pointer to the last element and return the element.
         *
         * @return mixed
         *
         * @throws \OutOfBoundsException
         */
        public function last()
        {
            if ($this->count() === 0) {
                throw new \OutOfBoundsException('Cannot get last element of empty Collection');
            }
    
            return end($this);
        }
    
        /**
         * Apply a user supplied function to every member of an array
         *
         * @see array_reverse
         *
         * @param bool $preserveKeys
         *
         * @return static
         */
        public function reverse(bool $preserveKeys = false)
        {
            return new static(array_reverse($this->getArrayCopy(), $preserveKeys));
        }
    
        public function keys(): array
        {
            return array_keys($this->getArrayCopy());
        }
    
        /**
         * Use a user supplied callback to reduce the array to a single member and return it.
         *
         * @param callable $callback
         * @param mixed|null $initial
         *
         * @return mixed
         */
        public function reduce(callable $callback, $initial = null)
        {
            return array_reduce($this->getArrayCopy(), $callback, $initial);
        }
    }
    

    and

    /**
     * Class TypedArray
     *
     * This is a typed array
     *
     * By enforcing the type, you can guarantee that the content is safe to simply iterate and call methods on.
     */
    abstract class AbstractTypedArray extends ArrayObject
    {
        use TypeValidator;
    
        /**
         * Define the class that will be used for all items in the array.
         * To be defined in each sub-class.
         */
        const ARRAY_TYPE = null;
    
        /**
         * Array Type
         *
         * Once set, this ArrayObject will only accept instances of that type.
         *
         * @var string $arrayType
         */
        private $arrayType = null;
    
        /**
         * Constructor
         *
         * Store the required array type prior to parental construction.
         *
         * @param mixed[] $input Any data to preset the array to.
         * @param int $flags The flags to control the behaviour of the ArrayObject.
         * @param string $iteratorClass Specify the class that will be used for iteration of the ArrayObject object. ArrayIterator is the default class used.
         *
         * @throws InvalidArgumentException
         */
        public function __construct($input = [], $flags = 0, $iteratorClass = ArrayIterator::class)
        {
            // ARRAY_TYPE must be defined.
            if (empty(static::ARRAY_TYPE)) {
                throw new \RuntimeException(
                    sprintf(
                        '%s::ARRAY_TYPE must be set to an allowable type.',
                        get_called_class()
                    )
                );
            }
    
            // Validate that the ARRAY_TYPE is appropriate.
            try {
                $this->arrayType = $this->determineType(static::ARRAY_TYPE);
            } catch (\Collections\Exceptions\InvalidArgumentException $e) {
                throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
            }
    
            // Validate that the input is an array or an object with an Traversable interface.
            if (!(is_array($input) || (is_object($input) && in_array(Traversable::class, class_implements($input))))) {
                throw new InvalidArgumentException('$input must be an array or an object that implements \Traversable.');
            }
    
            // Create an empty array.
            parent::__construct([], $flags, $iteratorClass);
    
            // Append each item so to validate it's type.
            foreach ($input as $key => $value) {
                $this[$key] = $value;
            }
        }
    
        /**
         * Adding a new value at the beginning of the collection
         *
         * @param mixed $value
         *
         * @return int Returns the new number of elements in the Array
         *
         * @throws InvalidArgumentException
         */
        public function unshift($value): int
        {
            try {
                $this->validateItem($value, $this->arrayType);
            } catch (\Collections\Exceptions\InvalidArgumentException $e) {
                throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
            }
    
            return parent::unshift($value);
        }
    
        /**
         * Check the type and then store the value.
         *
         * @param mixed $offset The offset to store the value at or null to append the value.
         * @param mixed $value The value to store.
         *
         * @throws InvalidArgumentException
         */
        public function offsetSet($offset, $value)
        {
            try {
                $this->validateItem($value, $this->arrayType);
            } catch (\Collections\Exceptions\InvalidArgumentException $e) {
                throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
            }
    
            parent::offsetSet($offset, $value);
        }
    
        /**
         * Sort an array, taking into account objects being able to represent their sortable value.
         *
         * {@inheritdoc}
         */
        public function sort($sortFlags = SORT_REGULAR)
        {
            if (!in_array(SortableInterface::class, class_implements($this->arrayType))) {
                throw new \RuntimeException(
                    sprintf(
                        "Cannot sort an array of '%s' as that class does not implement '%s'.",
                        $this->arrayType,
                        SortableInterface::class
                    )
                );
            }
            // Get the data from
            $originalData = $this->getArrayCopy();
            $sortableData = array_map(
                function (SortableInterface $item) {
                    return $item->getSortValue();
                },
                $originalData
            );
    
            $result = asort($sortableData, $sortFlags);
    
            $order = array_keys($sortableData);
            uksort(
                $originalData,
                function ($key1, $key2) use ($order) {
                    return array_search($key1, $order) <=> array_search($key2, $order);
                }
            );
    
            $this->exchangeArray($originalData);
    
            return $result;
        }
    
        /**
         * {@inheritdoc}
         */
        public function filter(callable $callback, int $flag = 0)
        {
            if ($flag == ARRAY_FILTER_USE_KEY) {
                throw new InvalidArgumentException('Cannot filter solely by key. Use ARRAY_FILTER_USE_BOTH and amend your callback to receive $value and $key.');
            }
    
            return parent::filter($callback, $flag);
        }
    }
    

    An example use.

    class PaymentChannelCollection extends AbstractTypedArray
    {
        const ARRAY_TYPE = PaymentChannel::class;
    }
    

    You can now typehint with PaymentChannelCollection and be sure you've got a collection of PaymentChannels (for example).

    Some of the code may call exceptions in our namespace. I think there's a type validator too from danielgsims/php-collections (we initially used those collections but had issues around the flexibility of them - they're good, just not for us - so maybe take a look at them anyway!).

提交回复
热议问题