在 PHP 中键入提示 7 - 对象数组

Type hinting in PHP 7 - array of objects

也许我遗漏了一些东西,但是是否有任何选项可以定义该函数应该有参数或 return 例如用户对象数组?

考虑以下代码:

<?php

class User
{
    protected $name;

    protected $age;

    /**
     * User constructor.
     *
     * @param $name
     */
    public function __construct(string $name, int $age)
    {
        $this->name = $name;
        $this->age = $age;
    }

    /**
     * @return mixed
     */
    public function getName() : string
    {
        return $this->name;
    }

    public function getAge() : int
    {
        return $this->age;
    }
}

function findUserByAge(int $age, array $users) : array
{
    $result = [];
    foreach ($users as $user) {
        if ($user->getAge() == $age) {
            if ($user->getName() == 'John') {
                // complicated code here
                $result[] = $user->getName(); // bug
            } else {
                $result[] = $user;
            }
        }
    }

    return $result;
}

$users = [
    new User('John', 15),
    new User('Daniel', 25),
    new User('Michael', 15),
];

$matches = findUserByAge(15, $users);

foreach ($matches as $user) {
    echo $user->getName() . ' '.$user->getAge() . "\n";
}

PHP7 中是否有任何选项可以告诉函数 findUserByAge 应该 return 用户数组?我希望在添加类型提示时它应该是可能的,但我没有找到任何关于对象数组类型提示的信息,所以它可能不包含在 PHP 中 7. 如果不包含,你有任何提示添加类型提示时为什么不包括它的线索?

因为数组可以包含混合值,所以这是不可能的。

您必须为此目的使用对象/class。

您可以创建一个 class 来管理自己的列表数组(private/protected 属性)并拒绝添加其他值作为此问题的解决方法(如果确实需要的话)。

然而,没有负责任的程序员永远不会破坏预期的模式,尤其是如果您正确地注释它的话。无论如何都会在程序出现错误时被识别。

解释:

例如,您可以创建任何数组:

$myArray = array();

并添加一个数字:

$myArray[] = 1;

一个字符串:

$myArray[] = "abc123";

和一个对象

$myArray[] = new MyClass("some parameter", "and one more");

另外不要忘记,您可以有一个简单的数组、一个多维堆叠数组以及可以具有混合模式的关联数组。

很难找到一个 parser/nottation 来使所有版本都使用强制数组格式的表达式,我认为。

一方面会很酷,但另一方面你会失去一些在数组中混合数据的能力,这对很多现有代码和灵活性至关重要 PHP提供。

由于我们不想在 PHP 7 中遗漏混合内容,因此无法对数组的确切内容进行类型提示,因为您可以将其放入任何内容。

不包括在内。

If it's not included, do you have Any clue why it was not included when type hinting was added?

使用当前的数组实现,需要在运行时检查所有数组元素,因为数组本身不包含类型信息。

实际上已经为 PHP 5.6 提出但被拒绝:RFC "arrayof" - interestingly not because of performance issues which turned out to be neglible, but because there was no agreement in how exactly it should be implemented. There was also the objection that it is incomplete without scalar type hints. If you are interested in the whole discussion, read it in the mailing list archive.

恕我直言,数组类型提示将与类型化数组一起提供最大的好处,我很乐意看到它们的实现。

所以也许是时候制定新的 RFC 并重新开始讨论了。


部分解决方法:

您可以键入提示可变参数,从而将签名写为

function findUserByAge(int $age, User ...$users) : array

用法:

findUserByAge(15, ...$userInput);

在此调用中,参数 $userInput 将 "unpacked" 转换为单个变量,并在方法本身中 "packed" 返回数组 $users。每个项目都被验证为 User 类型。 $userInput也可以是迭代器,会转为数组。

遗憾的是,return 类型没有类似的解决方法,您只能将其用于最后一个参数。

补充 Steini 的回答。

您可以创建一个 class ObjectNIterator 来管理您的 ObjectN 并实现一个迭代器:http://php.net/manual/en/class.iterator.php

从 methodN 调用返回填充的 ObjectNIterator 的 classMethodM,然后将此数据传递给需要 ObjectNIterator 的 methodO:

public function methodO(ObjectNIterator $objectNCollection)

在我们的代码库中,我们有集合的概念。这些基于称为 TypedArray 的 class,它基于 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);
    }
}

/**
 * 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);
    }
}

使用示例。

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

您现在可以使用 PaymentChannelCollection 进行类型提示,并确保您有一组 PaymentChannels(例如)。

某些代码可能会在我们的命名空间中调用异常。我认为 danielgsims/php-collections 也有一个类型验证器(我们最初使用这些集合,但在它们的灵活性方面存在问题 - 它们很好,只是不适合我们 - 所以也许还是看看它们吧!)。 =14=]

我给出了一个关于数组类型提示的通用答案。

我对所选答案进行了修改。主要区别在于参数是一个数组而不是检查的 class.

的许多实例
/**
 * @param $_foos Foo[]
 */
function doFoo(array $_foos)
{return (function(Foo ...$_foos){

    // Do whatever you want with the $_foos array

})(...$_foos);}

看起来有点模糊,但很容易理解。不是总是在每次调用时手动解包数组,而是调用函数内的闭包,并将解包的数组作为参数。

function doFoo(array $_foos)
{
    return (function(Foo ...$_foos){ // Closure

    // Do whatever you want with the $_foos array

    })(...$_foos); //Main function's parameter $_foos unpacked
}

我觉得这很酷,因为您可以像使用具有 ArrayOfType 参数的任何其他语言函数一样使用该函数。另外,错误的处理方式与 PHP 类型提示错误的其余部分相同。此外,您不会让其他程序员感到困惑,他们将使用您的函数并且不得不解压缩他们的数组,这总是让人觉得有点 hacky。

您确实需要一些编程经验才能理解其工作原理。如果您需要多个参数,您可以随时将它们添加到闭包的 'use' 部分。

您还可以使用文档注释来公开类型提示。

/**
 * @param $_foos Foo[] <- An array of type Foo
 */

这是一个面向对象的例子:

class Foo{}

class NotFoo{}

class Bar{
    /**
     * @param $_foos Foo[]
     */
    public function doFoo(array $_foos, $_param2)
    {return (function(Foo ...$_foos) use($_param2){

        return $_param2;

    })(...$_foos);}
}

$myBar = new Bar();
$arrayOfFoo = array(new Foo(), new Foo(), new Foo());
$notArrayOfFoo = array(new Foo(), new NotFoo(), new Foo());

echo $myBar->doFoo($arrayOfFoo, 'Success');
// Success

echo $myBar->doFoo($notArrayOfFoo, 'Success');
// Uncaught TypeError: Argument 2 passed to Bar::{closure}() must be an instance of Foo, instance of NotFoo given...

注意:这也适用于非对象类型(int、string 等)

一个相当简单的方法是创建你自己的数组类型,它与 PHP 的内置函数一起工作,例如 foreach、count、unset、indexing 等。这是一个例子:

class DataRowCollection implements \ArrayAccess, \Iterator, \Countable
{
    private $rows = array();
    private $idx = 0;

    public function __construct()
    {
    }

    // ArrayAccess interface

    // Used when adding or updating an array value
    public function offsetSet($offset, $value)
    {
        if ($offset === null)
        {
            $this->rows[] = $value;
        }
        else
        {
            $this->rows[$offset] = $value;
        }
    }

    // Used when isset() is called
    public function offsetExists($offset)
    {
        return isset($this->rows[$offset]);
    }

    // Used when unset() is called
    public function offsetUnset($offset)
    {
        unset($this->rows[$offset]);
    }

    // Used to retrieve a value using indexing
    public function offsetGet($offset)
    {
        return $this->rows[$offset];
    }

    // Iterator interface

    public function rewind()
    {
        $this->idx = 0;
    }

    public function valid()
    {
        return $this->idx < count($this->rows);
    }

    public function current()
    {
        return $this->rows[$this->idx];
    }

    public function key()
    {
        return $this->idx;
    }

    public function next()
    {
        $this->idx++;
    }

    // Countable interface

    public function count()
    {
        return count($this->rows);
    }
}

用法示例:

$data = new DataRowCollection(); // = array();
$data[] = new DataRow("person");
$data[] = new DataRow("animal");

它的工作方式与传统数组一样,但它的类型却如您所愿。非常简单有效。

目前无法在对象数组的函数签名上定义它。但是你可以在函数文档中定义它。如果您传递混合值,它不会生成 PHP error/warning,但大多数 IDE 会给出提示。这是示例:

/**
 * @param int $age
 * @param User[] $users
 * @return User[]
 */
function findUserByAge(int $age, array $users) : array {
    $results = [];
    //
    //
    return $result;
}