update: I added more example code and more clarification.
If PHP had array constraints/generics it would be so much easier to be sure the array items are all of the same type, public array<User> $users
.
So what are the options?
The array object way
abstract class TypedCollection extends ArrayObject {
abstract protected function getAllowedType(): string;
public static function fromArray(array $array): mixed
{
$class = static::class;
$instance = new $class();
foreach ($array as $item) {
$instance[] = $item;
}
return $instance;
}
public function __construct(object|array $array = [], int $flags = 0, string $iteratorClass = "ArrayIterator")
{
parent::__construct($array, $flags, $iteratorClass);
if(is_array($array)) {
foreach ($array as $item) {
$this->validate($item);
}
}
}
public function offsetSet($key, $value): void
{
$this->validate($value);
if (is_null($key)) {
$this->getIterator()->append($value);
} else {
$this->getIterator()->offsetSet($key, $value);
}
}
public function append(TypedCollection $value): self
{
foreach ($value as $item) {
$this[] = $item;
}
return $this;
}
public function toArray(): array
{
return $this->getArrayCopy();
}
private function validate($value) {
$allowedType = $this->getAllowedType();
if (!($value instanceof $allowedType)) {
throw new InvalidArgumentException(
"Value must be an instance of {$allowedType}"
);
}
}
}
The property hooks way
abstract class TypedCollection {
public array $arr = [] {
get => $this->arr;
set {
$this->validate($value);
$this->arr = $value;
}
}
public function __construct(array $arr)
{
$this->arr = $arr;
}
abstract protected function getAllowedType(): string;
private function validate($value) {
$allowedType = $this->getAllowedType();
if (!($value instanceof $allowedType)) {
throw new InvalidArgumentException(
"Value must be an instance of {$allowedType}"
);
}
}
}
Usage
class UserCollection extends Typedcollection {
protected function getAllowedType(): string
{
return User::class;
}
}
array object
$userCollection = new UserCollection([new User('me')]);
$userCollection[] = new User('you');
$userCollection = $userCollection->append(new UserCollection([new User('me2'), new User('you2')]);
$userArray = $userCollection->toArray();
property hooks
$userCollection = new UserCollection([new User('me')]);
$userCollection->arr = array_merge($userCollection->arr, [new User('you')]);
// no need for the two next lines of the array object code.
Pros and cons
Both will need more code if scalar need to be checked.
The array object way pro
- can add a single item to an array (single element validation)
The array object way con
- need for
toArray
method to use array functions (extra function call)
The property hooks way pro
- no need for an extra method to use array functions (no function call)
The property hooks way con
- not possible to add a single item to the array (multi element validation)
Conclusion
PhpStan can check code using generics. But this doesn't work at runtime.
Whatever option you choose it will be better than the array type hinting.
I find that the collection instantiation in the code makes it more readable than the anonymus []
.
Top comments (0)