diff --git a/src/Traits/HasTypeCheck.php b/src/Traits/HasTypeCheck.php new file mode 100644 index 0000000..f2c20bc --- /dev/null +++ b/src/Traits/HasTypeCheck.php @@ -0,0 +1,49 @@ + + * @package ComplexHeart\Domain\Model\Traits + */ +trait HasTypeCheck +{ + /** + * Assert that the given value type match the required validType. + * + * @param mixed $value + * @param string $validType + * + * @return bool + */ + protected function isValueTypeValid($value, string $validType): bool + { + if ($validType === 'mixed') { + return true; + } + + $primitives = ['integer', 'boolean', 'float', 'string', 'array', 'object', 'callable']; + $validation = in_array($validType, $primitives) + ? fn($value): bool => gettype($value) === $validType + : fn($value): bool => $value instanceof $validType; + + return $validation($value); + } + + /** + * Assert that the given value type NOT match the required validType. + * + * @param mixed $value + * @param string $validType + * + * @return bool + */ + protected function isValueTypeNotValid($value, string $validType): bool + { + return !$this->isValueTypeValid($value, $validType); + } +} \ No newline at end of file diff --git a/src/TypedCollection.php b/src/TypedCollection.php index 4d0414d..99880c6 100644 --- a/src/TypedCollection.php +++ b/src/TypedCollection.php @@ -4,9 +4,10 @@ namespace ComplexHeart\Domain\Model; -use Illuminate\Support\Collection; use ComplexHeart\Domain\Model\Exceptions\InvariantViolation; +use ComplexHeart\Domain\Model\Traits\HasTypeCheck; use ComplexHeart\Domain\Model\Traits\HasInvariants; +use Illuminate\Support\Collection; /** * Class TypedCollection @@ -17,6 +18,7 @@ class TypedCollection extends Collection { use HasInvariants; + use HasTypeCheck; /** * The type of each key in the collection. @@ -44,58 +46,164 @@ public function __construct(array $items = []) } /** - * Invariant: All items must be of the same type. + * Assert that the key type is compliant with the collection definition. * - * - If $typeOf is primitive check the type with gettype(). - * - If $typeOf is a class, check if the item is an instance of it. + * @param mixed $key * - * @return bool * @throws InvariantViolation */ - protected function invariantItemsMustMatchTheRequiredType(): bool + protected function checkKeyType($key): void { - if ($this->valueType !== 'mixed') { - $primitives = ['integer', 'boolean', 'float', 'string', 'array', 'object', 'callable']; - $check = in_array($this->valueType, $primitives) - ? fn($value): bool => gettype($value) !== $this->valueType - : fn($value): bool => !($value instanceof $this->valueType); - - foreach ($this->items as $item) { - if ($check($item)) { - throw new InvariantViolation("All items must be type of {$this->valueType}"); - } - } + $supported = ['string', 'integer']; + if (!in_array($this->keyType, $supported)) { + throw new InvariantViolation( + "Unsupported key type $this->keyType, must be one of ".implode(', ', $supported) + ); } - return true; + if ($this->isValueTypeNotValid($key, $this->keyType)) { + throw new InvariantViolation("All keys in the collection must be type of $this->keyType"); + } } /** - * Invariant: Check the collection keys to match the required type. + * Assert that the item type is compliant with the collection definition. + * + * @param mixed $item * - * Supported types: + * @throws InvariantViolation + */ + protected function checkValueType($item): void + { + if ($this->isValueTypeNotValid($item, $this->valueType)) { + throw new InvariantViolation("All items in the collection must be type of $this->valueType"); + } + } + + /** + * Check the keys and values of the collection to match the required type. + * + * Supported types for keys: * - string * - integer * + * Values can have any type: + * - If $type is primitive check the type with gettype(). + * - If $type is a class, check if the item is an instance of it. + * * @return bool * @throws InvariantViolation */ - protected function invariantKeysMustMatchTheRequiredType(): bool + protected function invariantKeysAndValuesMustMatchTheRequiredType(): bool { - if ($this->keyType !== 'mixed') { - $supported = ['string', 'integer']; - if (!in_array($this->keyType, $supported)) { - throw new InvariantViolation( - "Unsupported key type, must be one of ".implode(', ', $supported) - ); + if ($this->keyType === 'mixed' && $this->valueType === 'mixed') { + return true; + } + + foreach ($this->items as $key => $item) { + if ($this->keyType !== 'mixed') { + $this->checkKeyType($key); } - foreach ($this->items as $index => $item) { - if (gettype($index) !== $this->keyType) { - throw new InvariantViolation("All keys must be type of {$this->keyType}"); - } + if ($this->valueType !== 'mixed') { + $this->checkValueType($item); } } + return true; } + + /** + * Push one or more items onto the end of the collection. + * + * @param mixed $values [optional] + * + * @return static + * @throws InvariantViolation + */ + public function push(...$values) + { + foreach ($values as $value) { + $this->checkValueType($value); + } + + return parent::push(...$values); + } + + /** + * Offset to set. + * + * @param mixed $key + * @param mixed $value + * + * @throws InvariantViolation + */ + public function offsetSet($key, $value) + { + if ($this->keyType !== 'mixed') { + $this->checkKeyType($key); + } + + $this->checkValueType($value); + + parent::offsetSet($key, $value); + } + + /** + * Push an item onto the beginning of the collection. + * + * @param mixed $value + * @param null $key + * + * @return static + * @throws InvariantViolation + */ + public function prepend($value, $key = null) + { + if ($this->keyType !== 'mixed') { + $this->checkKeyType($key); + } + + $this->checkValueType($value); + + return parent::prepend($value, $key); + } + + /** + * Add an item to the collection. + * + * @param mixed $item + * + * @return static + * @throws InvariantViolation + */ + public function add($item) + { + $this->checkValueType($item); + + return parent::add($item); + } + + /** + * Get the values of a given key. + * + * @param string|array|int|null $value + * @param string|null $key + * + * @return Collection + */ + public function pluck($value, $key = null) + { + return $this->toBase()->pluck($value, $key); + } + + /** + * Get the keys of the collection items. + * + * @return Collection + */ + public function keys(): Collection + { + return $this->toBase()->keys(); + } } diff --git a/tests/TypedCollectionTest.php b/tests/TypedCollectionTest.php index 5baf3a3..8ff7d82 100644 --- a/tests/TypedCollectionTest.php +++ b/tests/TypedCollectionTest.php @@ -5,40 +5,130 @@ use ComplexHeart\Domain\Model\TypedCollection; use ComplexHeart\Domain\Model\Exceptions\InvariantViolation; -test('Successfully instantiate a TypedCollection.', function () { +test('Instantiate a TypedCollection[mixed].', function () { + $c = new TypedCollection(['a', 1, 1.2, new stdClass()]); + + expect($c)->toBeInstanceOf(TypedCollection::class); +})->group('Unit'); + +test('Instantiate a TypedCollection[int, string].', function () { $c = new class (['foo', 'bar']) extends TypedCollection { protected string $valueType = 'string'; }; expect($c)->toBeInstanceOf(TypedCollection::class); -} -); +})->group('Unit'); + +test('Instantiate a TypedCollection[string, string].', function () { + $c = new class (['one' => 'foo', 'two' => 'bar']) extends TypedCollection { + protected string $keyType = 'string'; + protected string $valueType = 'string'; + }; + + expect($c)->toBeInstanceOf(TypedCollection::class); +})->group('Unit'); + +test('Add a new item to TypedCollection[string, string]', function () { + $c = new class (['one' => 'foo', 'two' => 'bar']) extends TypedCollection { + protected string $keyType = 'string'; + protected string $valueType = 'string'; + }; + + $c['three'] = 'foobar'; + + expect($c)->toHaveCount(3); +})->group('Unit'); + +test('Push a new item into the TypedCollection[int, string]', function () { + $c = new class (['foo', 'bar']) extends TypedCollection { + protected string $valueType = 'string'; + }; + + $c[] = 'foobar'; + $c->push('other', 'another'); + $c->add('last one'); + + expect($c)->toHaveCount(6); +})->group('Unit'); + +test('Prepend a new item into the TypedCollection[string, string]', function () { + $c = new class (['one' => 'foo', 'two' => 'bar']) extends TypedCollection { + protected string $keyType = 'string'; + protected string $valueType = 'string'; + }; + + $c->prepend('last', 'last'); + + expect($c) + ->toHaveCount(3) + ->toMatchArray(['one' => 'foo', 'two' => 'bar', 'last' => 'last']); +})->group('Unit'); + +test('Pluck attribute from item in TypeCollection[array]', function () { + $items = [ + ['name' => 'Vicent', 'surname' => 'Vega'], + ['name' => 'Jules', 'surname' => 'Winnfield'] + ]; + + $c = new class ($items) extends TypedCollection { + protected string $valueType = 'array'; + }; + + $names = $c->pluck('name')->all(); + expect($names)->toMatchArray(['Vicent', 'Jules']); + + $names = $c->pluck('name', 'surname')->all(); + expect($names)->toMatchArray(['Vega' => 'Vicent', 'Winnfield' => 'Jules']); +})->group('Unit'); + +test('Return the collection keys.', function () { + $c = new class (['one' => 'foo', 'two' => 'bar']) extends TypedCollection { + protected string $keyType = 'string'; + protected string $valueType = 'string'; + }; + + $keys = $c->keys()->all(); + expect($keys)->toMatchArray(['one', 'two']); +})->group('Unit'); test('Fail with wrong primitive value item type.', function () { new class ([1, '2']) extends TypedCollection { protected string $valueType = 'integer'; }; -} -)->throws(InvariantViolation::class); +}) + ->throws(InvariantViolation::class) + ->group('Unit'); test('Fail with wrong class value item types.', function () { new class ([new stdClass(), '2']) extends TypedCollection { protected string $valueType = stdClass::class; }; -} -)->throws(InvariantViolation::class); +}) + ->throws(InvariantViolation::class) + ->group('Unit'); test('Fail due to unsupported key type.', function () { new class (['foo', 'bar']) extends TypedCollection { protected string $keyType = 'boolean'; }; -} -)->throws(InvariantViolation::class); +}) + ->throws(InvariantViolation::class) + ->group('Unit'); test('Fail due to wrong key type.', function () { new class (['foo', 'bar']) extends TypedCollection { protected string $keyType = 'string'; }; -} -)->throws(InvariantViolation::class); +}) + ->throws(InvariantViolation::class) + ->group('Unit'); + +test('Fail adding item with wrong key type.', function () { + $c = new class ([]) extends TypedCollection { + protected string $keyType = 'string'; + }; + $c[] = 'wrong'; +}) + ->throws(InvariantViolation::class) + ->group('Unit'); \ No newline at end of file