*/ public readonly array $elements, public readonly bool $isUnsealed = false, public readonly ArrayType|VacantType|null $unsealedType = null, ) {} /** * @param array $elements */ public static function from( array $elements, bool $isUnsealed, Type|null $unsealedType = null, ): self { if ($unsealedType && ! $unsealedType instanceof ArrayType && ! $unsealedType instanceof VacantType) { throw new InvalidShapedArrayUnsealedType($unsealedType, ...$elements); } $sortedElements = []; $keys = []; foreach ($elements as $element) { $key = $element->key()->value(); if (in_array($key, $keys, true)) { throw new ShapedArrayElementDuplicatedKey((string)$key); } $keys[] = $key; $sortedElements[$key] = $element; } return new self($sortedElements, $isUnsealed, $unsealedType); } public function hasUnsealedType(): bool { return $this->unsealedType !== null; } public function unsealedType(): ArrayType|VacantType { assert($this->isUnsealed); return $this->unsealedType ?? ArrayType::native(); } public function accepts(mixed $value): bool { if (! is_array($value)) { return false; } $elements = []; foreach ($this->elements as $key => $element) { $elements[$key] = null; if (array_key_exists($key, $value) ? ! $element->type()->accepts($value[$key]) : ! $element->isOptional()) { return false; } } if ($this->isUnsealed) { return $this->unsealedType()->accepts(array_diff_key($value, $elements)); } return count($value) <= count($elements); } public function compiledAccept(ComplianceNode $node): ComplianceNode { $conditions = [ Node::functionCall('is_array', [$node]), ...array_map(function (ShapedArrayElement $element) use ($node) { $key = Node::value($element->key()->value()); return Node::ternary( condition: Node::functionCall('array_key_exists', [$key, $node]), ifTrue: $element->type()->compiledAccept($node->key($key)), ifFalse: Node::value($element->isOptional()), )->wrap(); }, array_values($this->elements)), ]; if ($this->isUnsealed) { $unsealedType = $this->unsealedType(); if ($unsealedType instanceof VacantType) { return Node::value(false); } $elementsKeys = array_flip(array_keys($this->elements)); $conditions[] = Node::functionCall(function_exists('array_all') ? 'array_all' : Polyfill::class . '::array_all', [ Node::functionCall('array_diff_key', [$node, Node::value($elementsKeys)]), Node::shortClosure( Node::logicalAnd( $unsealedType->keyType()->compiledAccept(Node::variable('key'))->wrap(), $unsealedType->subType()->compiledAccept(Node::variable('item'))->wrap(), ), )->witParameters( Node::parameterDeclaration('item', 'mixed'), Node::parameterDeclaration('key', 'mixed'), ), ]); } return Node::logicalAnd(...$conditions); } public function matches(Type $other): bool { if ($other instanceof MixedType) { return true; } if ($other instanceof UnionType) { return $other->isMatchedBy($this); } if ($other instanceof self) { foreach ($this->elements as $key => $element) { if (isset($other->elements[$key]) && $element->type()->matches($other->elements[$key]->type())) { continue; } if (! $element->isOptional()) { return false; } } if ($other->isUnsealed) { return $this->isUnsealed && $this->unsealedType()->matches($other->unsealedType()); } return true; } if ($other instanceof CompositeTraversableType) { $keyType = $other->keyType(); $subType = $other->subType(); foreach ($this->elements as $element) { if (! $element->key()->matches($keyType)) { return false; } if (! $element->type()->matches($subType)) { return false; } } if ($this->isUnsealed && ! $this->unsealedType()->matches($other)) { return false; } return true; } return false; } public function inferGenericsFrom(Type $other, Generics $generics): Generics { if (! $other instanceof self) { return $generics; } foreach ($this->elements as $key => $element) { if (! isset($other->elements[$key])) { continue; } $generics = $element->type()->inferGenericsFrom($other->elements[$key]->type(), $generics); } return $generics; } public function traverse(): array { $types = array_map(static fn (ShapedArrayElement $element) => $element->type(), array_values($this->elements)); if (isset($this->unsealedType)) { $types[] = $this->unsealedType; } return $types; } public function replace(callable $callback): Type { $elements = array_map( fn (ShapedArrayElement $element) => new ShapedArrayElement( $element->key(), $callback($element->type()), $element->isOptional(), $element->attributes(), ), $this->elements, ); $unsealedType = $this->unsealedType ? $callback($this->unsealedType) : null; return new self($elements, $this->isUnsealed, $unsealedType); } public function nativeType(): ArrayType { return ArrayType::native(); } public function dumpParts(): iterable { $elements = $this->elements; yield 'array{'; while ($element = array_shift($elements)) { $optional = $element->isOptional() ? '?' : ''; yield $element->key()->toString() . "$optional: "; yield $element->type(); if ($elements !== []) { yield ', '; } } yield '}'; } public function toString(): string { return $this->signature ??= $this->buildSignature(); } private function buildSignature(): string { $signature = 'array{'; $signature .= implode(', ', array_map(static fn (ShapedArrayElement $element) => $element->toString(), $this->elements)); if ($this->isUnsealed) { $signature .= ', ...'; if ($this->unsealedType) { $signature .= $this->unsealedType->toString(); } } $signature .= '}'; return $signature; } }