web-dev-qa-db-ja.com

PHP 7のタイプヒンティング-オブジェクトの配列

たぶん何かを見逃しましたが、関数が引数を持っているか、たとえばUserオブジェクトの配列を返すように定義するオプションはありますか?

次のコードを検討してください。

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

関数findUserByAgeがユーザーの配列を返すように指示するオプションがPHP7にありますか?型ヒントが追加されたときにそれが可能になるはずですが、オブジェクトの配列の型ヒントに関する情報が見つからなかったため、おそらくPHP 7には含まれていません。含まれていない場合は、型ヒントが追加されたときに含まれなかった理由は何かありますか?

56

含まれていません。

含まれていない場合は、タイプヒントが追加されたときに含まれなかった理由がわかりますか?

現在の配列実装では、配列自体に型情報が含まれていないため、実行時にすべての配列要素をチェックする必要があります。

PHP 5.6については実際に既に提案されていますが、拒否されました: RFC "arrayof" -興味深いことに、無視できないパフォーマンスの問題が原因ではありませんが、正確に実装する必要があります。また、スカラー型のヒントがなければ不完全であるという異論もありました。ディスカッション全体に興味がある場合は、 メーリングリストアーカイブ内 をお読みください。

私見配列型のヒントは、型付き配列と一緒に最も多くの利点を提供します。それらが実装されるのを見てみたいです。

ですから、おそらく新しいRFCがあり、この議論を再開する時が来たのでしょう。


部分的な回避策:

ヒントの可変引数を入力して、署名を次のように書くことができます。

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

使用法:

findUserByAge(15, ...$userInput);

この呼び出しでは、引数$userInputは単一の変数に「アンパック」され、メソッド自体では配列$usersに「パック」されます。各アイテムは、Userタイプであることが検証されます。 $userInputはイテレータにすることもでき、配列に変換されます。

残念ながら、戻り値の型に対する同様の回避策はなく、最後の引数に対してのみ使用できます。

83

配列には混合値を含めることができるため、これは不可能です。

そのためにはオブジェクト/クラスを使用する必要があります。

独自のリスト配列(プライベート/保護属性)を管理するクラスを作成し、これが本当に必要な場合、この問題の回避策として他の値の追加を拒否できます。

ただし、特にあなたが正しくコメントした場合を除き、責任あるプログラマーが意図したパターンを破ることはありません。いずれにせよ、プログラムでエラーが発生すると認識されます。

説明:

たとえば、任意の配列を作成できます。

$myArray = array();

番号を追加します。

$myArray[] = 1;

文字列:

$myArray[] = "abc123";

およびオブジェクト

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

また、単純な配列、多次元スタック配列、および混合パターンを持つことができる連想配列も使用できることを忘れないでください。

パーサー/表記法を見つけて、すべてのバージョンが、配列のフォーマットを強制する式で機能するようにするのは、不可能になるまでかなり困難です。

一方の面ではクールですが、メダルのもう一方の面では、既存のコードの多くとPHPが提供しなければならない柔軟性に不可欠な配列内のデータを混合する能力を失います。

PHP 7で見逃したくない機能が混在しているため、配列の正確な内容を型ヒントにすることはできません。

8
Steini

Steiniが答えたことに追加します。

ObjectNを管理し、イテレータを実装するクラスObjectNIteratorを作成できます。 http://php.net/manual/en/class.iterator.php

MethodNから、読み込まれたObjectNIteratorを返すclassMethodMを呼び出し、ObjectNIteratorを予期するmethodOにこのデータを渡します。

public function methodO(ObjectNIterator $objectNCollection)

4
visualex

一般的な配列の型ヒントに関する一般的な回答を提供しています。

選択した回答のバリエーションを作成しました。主な違いは、パラメーターがチェック対象クラスの多くのインスタンスではなく配列であることです。

/**
 * @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タイプヒントエラーと同じ方法で処理されます。さらに、あなたはあなたの関数を使用する他のプログラマを混乱させず、常に少しハックを感じる配列を解凍する必要があります。

これがどのように機能するかを理解するには、プログラミングの経験が少し必要です。複数のパラメーターが必要な場合は、クロージャーの「use」セクションにいつでも追加できます。

Docコメントを使用して、タイプヒントを公開することもできます。

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

OOの例を次に示します。

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など)でも機能します

3
Stefmachine

コードベースには、コレクションの概念があります。これらは、ArrayObjectに基づくTypedArrayと呼ばれるクラスに基づいています。

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の型バリデーターもあると思います(最初はこれらのコレクションを使用していましたが、それらの柔軟性に関して問題がありました-私たちだけではなく、良いので、とにかく見てください!).

2