|
| 1 | +<?php |
| 2 | + |
| 3 | +declare(strict_types=1); |
| 4 | + |
| 5 | +namespace Liip\MetadataParser\Metadata; |
| 6 | + |
| 7 | +use Doctrine\Common\Collections\Collection; |
| 8 | + |
| 9 | +/** |
| 10 | + * This property type can be merged with PropertyTypeClass<T>, provided that T is, inherits from, or is a parent class of $this->collectionClass |
| 11 | + * This property type can be merged with PropertyTypeIterable, if : |
| 12 | + * - we're not merging a plain array PropertyTypeIterable into a hashmap one, |
| 13 | + * - and the collection classes of each are either not present on both sides, or are the same, or parent-child of one another |
| 14 | + */ |
| 15 | +final class PropertyTypeIterable extends PropertyTypeArray |
| 16 | +{ |
| 17 | + /** |
| 18 | + * @var string |
| 19 | + */ |
| 20 | + private $collectionClass; |
| 21 | + |
| 22 | + /** |
| 23 | + * @param class-string<\Traversable>|null $collectionClass |
| 24 | + */ |
| 25 | + public function __construct(PropertyType $subType, bool $hashmap, bool $nullable, string $collectionClass = null) |
| 26 | + { |
| 27 | + parent::__construct($subType, $hashmap, $nullable, null != $collectionClass); |
| 28 | + |
| 29 | + $this->collectionClass = $collectionClass; |
| 30 | + } |
| 31 | + |
| 32 | + public function __toString(): string |
| 33 | + { |
| 34 | + if ($this->subType instanceof PropertyTypeUnknown) { |
| 35 | + return 'array'.($this->isCollection() ? '|\\'.$this->collectionClass : ''); |
| 36 | + } |
| 37 | + |
| 38 | + $array = $this->isHashmap() ? '[string]' : '[]'; |
| 39 | + if ($this->isCollection()) { |
| 40 | + $collectionType = $this->isHashmap() ? ', string' : ''; |
| 41 | + $array .= sprintf('|\\%s<%s%s>', $this->collectionClass, $this->subType, $collectionType); |
| 42 | + } |
| 43 | + |
| 44 | + return ((string) $this->subType).$array.AbstractPropertyType::__toString(); |
| 45 | + } |
| 46 | + |
| 47 | + public function getCollectionClass(): ?string |
| 48 | + { |
| 49 | + return $this->collectionClass; |
| 50 | + } |
| 51 | + |
| 52 | + public function isCollection(): bool |
| 53 | + { |
| 54 | + return null != $this->getCollectionClass(); |
| 55 | + } |
| 56 | + |
| 57 | + public function merge(PropertyType $other): PropertyType |
| 58 | + { |
| 59 | + $nullable = $this->isNullable() && $other->isNullable(); |
| 60 | + |
| 61 | + if ($other instanceof PropertyTypeUnknown) { |
| 62 | + return new self($this->subType, $this->isHashmap(), $nullable, $this->getCollectionClass()); |
| 63 | + } |
| 64 | + if ($this->isCollection() && (($other instanceof PropertyTypeClass) && is_a($other->getClassName(), Collection::class, true))) { |
| 65 | + return new self($this->getSubType(), $this->isHashmap(), $nullable, $this->findCommonCollectionClass($this->getCollectionClass(), $other->getClassName())); |
| 66 | + } |
| 67 | + if (!$other instanceof parent) { |
| 68 | + throw new \UnexpectedValueException(sprintf('Can\'t merge type %s with %s, they must be the same or unknown', self::class, \get_class($other))); |
| 69 | + } |
| 70 | + |
| 71 | + /* |
| 72 | + * We allow converting array to hashmap (but not the other way round). |
| 73 | + * |
| 74 | + * PHPDoc has no clear definition for hashmaps with string indexes, but JMS Serializer annotations do. |
| 75 | + */ |
| 76 | + if ($this->isHashmap() && !$other->isHashmap()) { |
| 77 | + throw new \UnexpectedValueException(sprintf('Can\'t merge type %s with %s, can\'t change hashmap into plain array', self::class, \get_class($other))); |
| 78 | + } |
| 79 | + |
| 80 | + if ($other->isCollection()) { |
| 81 | + $otherCollectionClass = ($other instanceof self) ? $other->getCollectionClass() : Collection::class; |
| 82 | + } else { |
| 83 | + $otherCollectionClass = null; |
| 84 | + } |
| 85 | + $hashmap = $this->isHashmap() || $other->isHashmap(); |
| 86 | + $commonClass = $this->findCommonCollectionClass($this->getCollectionClass(), $otherCollectionClass); |
| 87 | + |
| 88 | + if ($other->getSubType() instanceof PropertyTypeUnknown) { |
| 89 | + return new self($this->getSubType(), $hashmap, $nullable, $commonClass); |
| 90 | + } |
| 91 | + if ($this->getSubType() instanceof PropertyTypeUnknown) { |
| 92 | + return new self($other->getSubType(), $hashmap, $nullable, $commonClass); |
| 93 | + } |
| 94 | + |
| 95 | + return new self($this->getSubType()->merge($other->getSubType()), $hashmap, $nullable, $commonClass); |
| 96 | + } |
| 97 | + |
| 98 | + /** |
| 99 | + * Find the most derived class that doesn't deny both class hints, meaning the most derived |
| 100 | + * between left and right if one is a child of the other |
| 101 | + */ |
| 102 | + protected function findCommonCollectionClass(?string $left, ?string $right): ?string |
| 103 | + { |
| 104 | + if (null === $right) { |
| 105 | + return $left; |
| 106 | + } |
| 107 | + if (null === $left) { |
| 108 | + return $right; |
| 109 | + } |
| 110 | + |
| 111 | + if (is_a($left, $right, true)) { |
| 112 | + return $left; |
| 113 | + } |
| 114 | + if (is_a($right, $left, true)) { |
| 115 | + return $right; |
| 116 | + } |
| 117 | + |
| 118 | + throw new \UnexpectedValueException("Collection classes '{$left}' and '{$right}' do not match."); |
| 119 | + } |
| 120 | +} |
0 commit comments