From 564494fef390dd82b1cecddb3db1322822746803 Mon Sep 17 00:00:00 2001 From: Tyler Ashton Date: Thu, 11 Jul 2024 13:49:17 +0200 Subject: [PATCH 1/3] Add a method to test if a Collection(Trait) has an object via php's in_array function, unit test it and update the docs. --- docs/collection-trait.md | 8 +++ src/CollectionTrait.php | 5 ++ tests/CollectionTest.php | 103 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) diff --git a/docs/collection-trait.md b/docs/collection-trait.md index 78abe9e..d328b0d 100644 --- a/docs/collection-trait.md +++ b/docs/collection-trait.md @@ -32,6 +32,14 @@ A fluent version of `append`. To do stuff like: $collection->add($item1)->add($item2); ``` +## has + +```php +public function has($value, $strict): bool; +``` + +This method will tell you if your collection contains the given value. Please note that this method encapsulates the php [in_array()](https://www.php.net/manual/en/function.in-array.php) method and as such can produce unexpected results when using loose checking. + ## unique ```php diff --git a/src/CollectionTrait.php b/src/CollectionTrait.php index 19a1b17..c7113f3 100644 --- a/src/CollectionTrait.php +++ b/src/CollectionTrait.php @@ -32,6 +32,11 @@ public function add($value): self|static return $this; } + public function has(mixed $value, bool $strict = true): bool + { + return in_array($value, $this->toArray(), $strict); + } + public function unique(): self|static { return static::fromIterable(array_unique($this->toArray(), SORT_REGULAR)); diff --git a/tests/CollectionTest.php b/tests/CollectionTest.php index 48e648b..d569fc2 100644 --- a/tests/CollectionTest.php +++ b/tests/CollectionTest.php @@ -6,6 +6,7 @@ use ArrayIterator; use Exception; use Generator; +use Kununu\Collection\Tests\Stub\AbstractItemStub; use Kununu\Collection\Tests\Stub\AutoSortedCollectionStub; use Kununu\Collection\Tests\Stub\CollectionStub; use Kununu\Collection\Tests\Stub\ToArrayStub; @@ -96,6 +97,108 @@ public static function fromIterableDataProvider(): array ]; } + #[DataProvider('hasDataProvider')] + public function testHas(iterable $collectionContents, mixed $hasValue, bool $hasStrict, bool $expectedHas): void + { + $collection = CollectionStub::fromIterable($collectionContents); + + $this->assertEquals($expectedHas, $collection->has($hasValue, $hasStrict)); + } + + public static function hasDataProvider(): array + { + return [ + 'has_with_integers_strict' => [ + [1, 2, 3], + 1, + true, + true, + ], + 'has_with_integers_loose' => [ + [1, 2, 3], + '1', + false, + true, + ], + 'missing_with_integers_strict' => [ + [1, 2, 3], + 4, + true, + false, + ], + 'missing_with_integers_strict_string_to_int' => [ + [1, 2, 3], + '1', + true, + false, + ], + 'has_with_strings_strict' => [ + ['one', 'two', 'three'], + 'one', + true, + true, + ], + 'missing_with_strings_strict' => [ + ['one', 'two', 'three'], + 'missing', + true, + false, + ], + 'has_with_strings_loose' => [ + ['one', 'two', 'three'], + 'one', + false, + true, + ], + 'missing_with_strings_loose' => [ + ['one', 'two', 'three'], + 'missing', + false, + false, + ], + 'has_with_object_strict' => [ + [ + $one = new AbstractItemStub(['name' => 'one']), + new AbstractItemStub(['name' => 'two']), + new AbstractItemStub(['name' => 'three']), + ], + $one, + true, + true, + ], + 'missing_with_object_strict' => [ + [ + new AbstractItemStub(['name' => 'one']), + new AbstractItemStub(['name' => 'two']), + new AbstractItemStub(['name' => 'three']), + ], + new AbstractItemStub(['name' => 'one']), + true, + false, + ], + 'has_with_object_loose' => [ + [ + new AbstractItemStub(['name' => 'one']), + new AbstractItemStub(['name' => 'two']), + new AbstractItemStub(['name' => 'three']), + ], + new AbstractItemStub(['name' => 'one']), + false, + true, + ], + 'missing_with_object_loose' => [ + [ + new AbstractItemStub(['name' => 'one']), + new AbstractItemStub(['name' => 'two']), + new AbstractItemStub(['name' => 'three']), + ], + new AbstractItemStub(['name' => 'vier']), + false, + false, + ], + ]; + } + public function testEmpty(): void { $collection = new CollectionStub(); From 1f543239bc2157ba4d72f6e702bd162ddcc7b6e6 Mon Sep 17 00:00:00 2001 From: Tyler Ashton Date: Thu, 11 Jul 2024 14:46:49 +0200 Subject: [PATCH 2/3] Add a method to find duplicate elements in a collection, unit test it and update the docs. --- docs/collection-trait.md | 8 +++ src/CollectionTrait.php | 16 +++++ tests/CollectionTest.php | 142 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+) diff --git a/docs/collection-trait.md b/docs/collection-trait.md index d328b0d..392bf0a 100644 --- a/docs/collection-trait.md +++ b/docs/collection-trait.md @@ -48,6 +48,14 @@ public function unique(): self|static; This method will produce a collection with distinct elements of your collection. +## duplicates + +```php +public function duplicates(): self|static; +``` + +This methods produces a collection which contains items which occur multiple times in the collection. Please note that this method encapsulates the php [in_array()](https://www.php.net/manual/en/function.in-array.php) method and as such can produce unexpected results when using loose checking. + ## reverse ```php diff --git a/src/CollectionTrait.php b/src/CollectionTrait.php index c7113f3..59f0fb0 100644 --- a/src/CollectionTrait.php +++ b/src/CollectionTrait.php @@ -42,6 +42,22 @@ public function unique(): self|static return static::fromIterable(array_unique($this->toArray(), SORT_REGULAR)); } + public function duplicates(bool $strict = true): self|static + { + $elements = new static(); + $duplicates = new static(); + + $this->each(function ($item) use (&$elements, &$duplicates, $strict): void { + if ($elements->has($item, $strict)) { + $duplicates->append($item); + + return; + } + $elements->append($item); + }); + + return $duplicates; + } public function reverse(): self|static { return static::fromIterable(array_reverse($this->toArray())); diff --git a/tests/CollectionTest.php b/tests/CollectionTest.php index d569fc2..766e424 100644 --- a/tests/CollectionTest.php +++ b/tests/CollectionTest.php @@ -222,6 +222,148 @@ public function testUnique(): void $this->assertEquals([1, 2, 3, 4], $uniqueCollection->toArray()); } + public static function duplicatesDataProvider(): array + { + return [ + 'integers_no_duplicates_strict' => [ + true, + [1, 2, 3], + [] + ], + 'integers_duplicates_strict' => [ + true, + [1, 2, 3, 4, 1, 2, 4], + [1, 2, 4], + ], + 'integers_duplicates_strict_mixed' => [ + true, + [1, 2, 3, 4, 1, '2', 4], + [1, 4], + ], + 'strings_no_duplicates_strict' => [ + true, + [ + '7178e84e-472d-4766-8e45-a571871ff988', + 'ca583de8-e6ba-4098-a0bf-10c981ff3d8d', + '909a8214-86b1-4d78-94f2-6a8e22a24020', + 'baaeace4-1aeb-4b47-ac2c-4b90ebd12342', + ], + [], + ], + 'strings_duplicates_strict' => [ + true, + [ + '7178e84e-472d-4766-8e45-a571871ff988', + '7178e84e-472d-4766-8e45-a571871ff988', // dup 1-1 + '7178e84e-472d-4766-8e45-a571871ff988', // dup 1-2 + 'ca583de8-e6ba-4098-a0bf-10c981ff3d8d', + 'ca583de8-e6ba-4098-a0bf-10c981ff3d8d', // dup 2-1 + '909a8214-86b1-4d78-94f2-6a8e22a24020', + 'baaeace4-1aeb-4b47-ac2c-4b90ebd12342', + ], + [ + '7178e84e-472d-4766-8e45-a571871ff988', // dup 1-1 + '7178e84e-472d-4766-8e45-a571871ff988', // dup 1-2 + 'ca583de8-e6ba-4098-a0bf-10c981ff3d8d', // dup 2-1 + ], + ], + 'item_stubs_no_duplicates_strict' => [ + true, + [ + new AbstractItemStub(['name' => 'one']), + new AbstractItemStub(['name' => 'two']), + new AbstractItemStub(['name' => 'three']), + ], + [], + ], + 'item_stubs_duplicates_strict' => [ + true, + [ + $one = new AbstractItemStub(['name' => 'one']), + $one, + $two = new AbstractItemStub(['name' => 'two']), + $two, + new AbstractItemStub(['name' => 'three']), + ], + [ + $one, + $two, + ], + ], + 'integers_no_duplicates_loose' => [ + false, + [1, 2, 3], + [] + ], + 'integers_duplicates_loose' => [ + false, + [1, 2, 3, 4, 1, '2', 4], + ['1', 2, '4'], + ], + 'strings_no_duplicates_loose' => [ + false, + [ + '7178e84e-472d-4766-8e45-a571871ff988', + 'ca583de8-e6ba-4098-a0bf-10c981ff3d8d', + '909a8214-86b1-4d78-94f2-6a8e22a24020', + 'baaeace4-1aeb-4b47-ac2c-4b90ebd12342', + ], + [], + ], + 'strings_duplicates_loose' => [ + false, + [ + '7178e84e-472d-4766-8e45-a571871ff988', + '7178e84e-472d-4766-8e45-a571871ff988', // dup 1-1 + '7178e84e-472d-4766-8e45-a571871ff988', // dup 1-2 + 'ca583de8-e6ba-4098-a0bf-10c981ff3d8d', + 'ca583de8-e6ba-4098-a0bf-10c981ff3d8d', // dup 2-1 + '909a8214-86b1-4d78-94f2-6a8e22a24020', + 'baaeace4-1aeb-4b47-ac2c-4b90ebd12342', + '2', + 2 + ], + [ + '7178e84e-472d-4766-8e45-a571871ff988', // dup 1-1 + '7178e84e-472d-4766-8e45-a571871ff988', // dup 1-2 + 'ca583de8-e6ba-4098-a0bf-10c981ff3d8d', // dup 2-1 + 2 + ], + ], + 'item_stubs_no_duplicates_loose' => [ + false, + [ + new AbstractItemStub(['name' => 'one']), + new AbstractItemStub(['name' => 'two']), + new AbstractItemStub(['name' => 'three']), + ], + [], + ], + 'item_stubs_duplicates_loose' => [ + false, + [ + new AbstractItemStub(['name' => 'one']), + new AbstractItemStub(['name' => 'one']), + new AbstractItemStub(['name' => 'two']), + new AbstractItemStub(['name' => 'two']), + new AbstractItemStub(['name' => 'three']), + ], + [ + new AbstractItemStub(['name' => 'one']), + new AbstractItemStub(['name' => 'two']), + ], + ], + ]; + } + + #[DataProvider('duplicatesDataProvider')] + public function testDuplicates(bool $strict, array $contents, array $expected): void + { + $collection = CollectionStub::fromIterable($contents); + $duplicateCollection = $collection->duplicates($strict); + $this->assertEquals($expected, $duplicateCollection->toArray()); + } + public function testReverse(): void { $this->assertEquals([5, 4, 3, 2, 1], CollectionStub::fromIterable([1, 2, 3, 4, 5])->reverse()->toArray()); From c868efea10ec39bad85becf3c3781861224f631c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20Gon=C3=A7alves?= Date: Mon, 15 Jul 2024 15:59:29 +0100 Subject: [PATCH 3/3] Define interfaces for collections Create FromIterable interface Change AbstractCollection and AbstractFilterableCollection to implement the interfaces Allow `duplicates` to remove only unique values Add `keys` and `values` method to collections Fix tests Add tests for new code Update documentation --- README.md | 9 +- docs/abstract-collections.md | 4 +- docs/collection-interfaces.md | 194 ++++++++++++++++++++ docs/collection-trait.md | 201 ++++++++++----------- docs/convertible.md | 13 +- docs/filterable-collection-trait.md | 38 +--- src/AbstractCollection.php | 18 +- src/AbstractFilterableCollection.php | 20 +- src/AutoSortableOffsetSetTrait.php | 3 +- src/Collection.php | 34 ++++ src/CollectionTrait.php | 97 ++++++---- src/Convertible/FromIterable.php | 9 + src/FilterableCollection.php | 13 ++ tests/AbstractItemTest.php | 122 +++++++------ tests/AbstractItemToArrayTest.php | 2 +- tests/CollectionTest.php | 241 +++++++++++++++++++------ tests/Filter/FilterOperatorAndTest.php | 12 +- tests/Filter/FilterOperatorOrTest.php | 12 +- tests/Filter/FilterOperatorXOrTest.php | 12 +- tests/FilterableCollectionTest.php | 35 ++-- tests/Mapper/DefaultMapperTest.php | 4 +- tests/Stub/DTOCollectionStub.php | 10 - tests/Stub/StringableStub.php | 21 +++ 23 files changed, 763 insertions(+), 361 deletions(-) create mode 100644 docs/collection-interfaces.md create mode 100644 src/Collection.php create mode 100644 src/Convertible/FromIterable.php create mode 100644 src/FilterableCollection.php create mode 100644 tests/Stub/StringableStub.php diff --git a/README.md b/README.md index b78aea4..7a586db 100644 --- a/README.md +++ b/README.md @@ -35,14 +35,17 @@ composer test-coverage ## Usage -The library provide three traits that you can add to your custom class extending `ArrayIterator`. +The library defines interfaces to deal with collections and also boilerplate code with default implementations. -It defines interfaces to convert collection items to `array`, `string` and `int` and to compare items. +You can either use the provided traits to your custom class extending `ArrayIterator` or simply expand the abstract collection classes using them. + +It has a default implementation for a "basic" collection and also one to filter and group data on your collections (a "filterable" collection). -It also provides some interfaces to filter and group data on your collections and base classes with default implementations. +It defines interfaces to convert collection items to `array`, `string` and `int` and to compare items. More details: +- [Collection Interfaces](docs/collection-interfaces.md) - [Collection Trait](docs/collection-trait.md) - [Filterable Collection Trait](docs/filterable-collection-trait.md) - [Auto Sortable OffsetSet Trait](docs/autosortable-offsetset-trait.md) diff --git a/docs/abstract-collections.md b/docs/abstract-collections.md index 4c7b32d..3152a5a 100644 --- a/docs/abstract-collections.md +++ b/docs/abstract-collections.md @@ -2,7 +2,7 @@ ## AbstractCollection -This is an abstract base class that you can use for your collections. It extends `ArrayIterator` (and already uses the `CollectionTrait`) so you just need to extend it to have a proper collection class. +This is an abstract base class that you can use for your collections. It extends `ArrayIterator` and implements the `Collection` interface (and already uses the `CollectionTrait`) so you just need to extend it to have a proper collection class. ```php add($item1)->add($item2); +``` + +### diff + +```php +public function diff(self $other): self|static; +``` + +This method will produce a collection with the difference between your collection and another instance. + +### duplicates + +```php +public function duplicates(bool $strict = true, bool $uniques = false): self|static; +``` + +This method produces a collection which contains items which occur multiple times in the collection. + +- `$strict` parameter allows you to use strict comparison (e.g. if your implementation uses PHP `in_array`). +- `$uniques` parameter allows you to specify if you want to return unique duplicates values instead of all duplicates entries. + +### each + +```php +public function each(callable $function, bool $rewind = true): self|static; +``` + +This method will iterate through each item of a collection, optionally rewind it at the end of the iteration, calling an anonymous function where you can do whatever you need with each item. + +Callable signature: + +```php +function(mixed $element, string|float|int|bool|null $elementKey): void; +``` + +### empty + +```php +public function empty(): bool; +``` + +Just a shortcut to see if your collection has a count of elements greater than zero. + +### has + +```php +public function has(mixed $value, bool $strict = true): bool; +``` + +This method will tell you if your collection contains the given value. + +- `$strict` parameter allows you to use strict comparison (e.g. if your implementation uses PHP `in_array`). + +### keys + +```php +public function keys(): array; +``` + +This method will return the keys of the collection. + +### map + +```php +public function map(callable $function, bool $rewind = true): array; +``` + +This method will map your collection to an array, optionally rewind it at the end of the iteration, calling an anonymous function where you can do whatever you need with each item. + +Callable signature: + +```php +function(mixed $element, string|float|int|bool|null $elementKey): mixed; +``` + +### reduce + +```php +public function reduce(callable $function, mixed $initial = null, bool $rewind = true): mixed; +``` + +This method will reduce your collection to a single value, optionally rewind it at the end of the iteration, calling an anonymous function where you can do whatever you need with each item. + +Callable signature: + +```php +function(mixed $carry, mixed $element, string|float|int|bool|null $elementKey): mixed; +``` + +### reverse + +```php +public function reverse(): self|static; +``` + +This method will produce a collection with elements of your collection in the reverse order. + +### unique + +```php +public function unique(): self|static; +``` + +This method will produce a collection with distinct elements of your collection. + +### values + +```php +public function values(): array; +``` + +This method will return the values of the collection. + +## FilterableCollection + +The `FilterableCollection` interface defines additional methods that your collection should provide in order to filter and/or group elements in it. + +### filter + +```php +public function filter(CollectionFilter $filter): self|static +``` + +This will accept a `CollectionFilter` instance (with the definitions of the filter being applied to the collection), and returns a new collection with only the elements that have met the criteria defined in `$filter`. + +### groupBy + +```php +public function groupBy(bool $removeEmptyGroups, CollectionFilter ...$filters): array +``` + +This method allows you to apply a series of filters to a collection and group the result by each filter. The `$removeEmptyGroups` flag means if we will remove or keep groups in the result without items. + +The result should be returned as an array with the following structure: + +```php +[ + 'filter_1_key' => [ + 'item_key_1' => item object 1 + ... + 'item_key_N' => item object X + ], + 'filter_2_key' => [ + 'item_key_1' => item object 1 + ... + 'item_key_N' => item object Y + ], + ... +] +``` diff --git a/docs/collection-trait.md b/docs/collection-trait.md index 392bf0a..5c4cb55 100644 --- a/docs/collection-trait.md +++ b/docs/collection-trait.md @@ -1,159 +1,158 @@ # CollectionTrait -This is the most basic trait, and it provides the following methods to your class: +This is the most basic trait, and it provides an implementation of the `Collection` interface. -## fromIterable -```php -public static function fromIterable(iterable $data): self|static; -``` +Some details about the implementation: -This method tries to create an instance of your collection class with data from a source that is an `iterable` (e.g. an array). +## fromIterable Internally it is iterating the items in the data source and calling the `append` method of your class (or the `ArrayIterator` one if you don't rewrite it in your class). With a concrete implementation on your class of the `append` method you can define on how to transform each iterable element into an instance of a valid object your collection will accept and handle. -## empty +## toArray + ```php -public function empty(): bool; +public function toArray(): array; ``` -Just a shortcut to see if your collection has a count of elements greater than zero. +This method will convert your collection to a representation of it as an array. -## add +To take full use of this method also take a look at the [Convertible](../src/Convertible) interfaces. -```php -public function add($value): self|static; -``` +If the members of your collection properly implement the `ToArray`, `ToInt` and `ToString` interfaces, this method will then recursively convert each field to a basic PHP representation. -A fluent version of `append`. To do stuff like: +Also, if any element is an implementation of [Stringable](https://www.php.net/manual/en/class.stringable.php), it will convert the element to string by casting it to string (in practice it will call the `__toString` method that `Stringable` enforces). -```php -$collection->add($item1)->add($item2); -``` +This applies also to collections that are defined inside a class that is an item in a top level collection. -## has +Example: ```php -public function has($value, $strict): bool; -``` - -This method will tell you if your collection contains the given value. Please note that this method encapsulates the php [in_array()](https://www.php.net/manual/en/function.in-array.php) method and as such can produce unexpected results when using loose checking. +age; + } +} -```php -public function duplicates(): self|static; -``` +final class MyTopItem implements ToArray +{ + public function __construct(public string $name, public MySubCollection $subCollection) + { + } -This methods produces a collection which contains items which occur multiple times in the collection. Please note that this method encapsulates the php [in_array()](https://www.php.net/manual/en/function.in-array.php) method and as such can produce unexpected results when using loose checking. + public function toArray(): array + { + return [ + 'name' => $this->name, + 'subCollection' => $this->subCollection->toArray(), + ]; + } +} -## reverse +$collection = new MyTopCollection(); + +$collection->add( + new MyTopItem( + 'The Name', + (new MySubCollection())->add(new MySubItem(100)) + ) +); + +$collection->toArray(); + +// Will result in: +[ + [ + 'name' => 'The Name', + 'subCollection' => [ + '100' + ] + ] +]; +``` -```php -public function reverse(): self|static; -``` +## add -This method will produce a collection with elements of your collection in the reverse order. +Internally it is call the `ArrayIterator::append` and returning the instance to allow fluent calls. ## diff -```php -public function diff(self $other): self|static; -``` +To check the difference between two collections first it checks that the other collection is of the same type as the current one. -This method will produce a collection with the difference between your collection and another instance. +Then it is calling the PHP [array_diff](https://www.php.net/manual/en/function.array-diff.php) between the [serialize](https://www.php.net/manual/en/https://www.php.net/manual/en/function.serialize.php) representation of each collection represented as an array (by calling the `toArray` method on each collection). -## each +Finally, it is creating a new instance of the collection by using [unserialize](https://www.php.net/manual/en/function.unserialize) on each member of the diff. -```php -public function each(callable $function, bool $rewind = true): self|static; -``` +## duplicates -This method will iterate through each item of a collection, optionally rewind it at the end of the iteration, calling an anonymous function where you can do whatever you need with each item. +Internally it creates two new collections. One for the non-duplicated elements. Another one for the duplicates. -Callable signature: +Internally it is iterating the collection with the `each` method and for each element checks if it is already in the non-duplicated elements collection (by using the `has` method). + - If it's already there the element will be added to the duplicated collection + - Otherwise it will be added to the non-duplicated collection -```php -function(mixed $element, string|float|int|bool|null $elementKey): void; -``` +Finally, it will return the duplicated collection, optionally calling the `unique` method if we don't want the same duplicated elements in the result. -## map +## each -```php -public function map(callable $function, bool $rewind = true): array; -``` +Internally, this method will iterate through each item of a collection, optionally rewind it at the end of the iteration, calling the anonymous function for each element. -This method will map your collection to an array, optionally rewind it at the end of the iteration, calling an anonymous function where you can do whatever you need with each item. +## empty -Callable signature: +Internally is checking if the `ArrayIterator::count` returns 0 -```php -function(mixed $element, string|float|int|bool|null $elementKey): mixed; -``` +## has -## reduce +Internally, this method is calling the PHP [in_array](https://www.php.net/manual/en/function.in-array.php) to check if the element is in the array representation of the collection (obtained via `toArray` method). -```php -public function reduce(callable $function, mixed $initial = null, bool $rewind = true): mixed; -``` +Please note that since it's using `in_array` it can produce unexpected results when using loose checking. -This method will reduce your collection to a single value, optionally rewind it at the end of the iteration, calling an anonymous function where you can do whatever you need with each item. +## keys -Callable signature: +Internally, this method is calling the PHP [array_keys](https://www.php.net/manual/en/function.array-keys) of the array held in the `ArrayIterator` (via the `ArrayIterator::getArrayCopy` method) -```php -function(mixed $carry, mixed $element, string|float|int|bool|null $elementKey): mixed; -``` +## map -## toArray +Internally, this method will iterate through your collection, optionally rewind it at the end of the iteration, calling the anonymous function and storing the result of that call in an array which is the result. -```php -public function toArray(): array; -``` +## reduce -This method will convert your collection to a representation of it as an array. +Internally, this method will reduce your collection to a single value, optionally rewind it at the end of the iteration, calling the anonymous function for each element and updating the `$initial` value with the result of the call. -To take full use of this method also take a look at the `Kununu\Collection\Convertible` interfaces. +Finally, it will return the updated `$initial` value as the result of the reduction. -If the members of your collection properly implement the `ToArray`, `ToInt` and `ToString` interfaces, this method will then recursively convert each field to a basic PHP representation. -This applies also to collections that are defined inside a class that is an item in a top level collection. +## reverse -Example: +Internally, this method will call the PHP [array_reverse](https://www.php.net/manual/en/function.array-reverse) on the array representation of the collection (obtained via `toArray` method). -```php - $this->name, - 'subCollection' => $this->subCollection->toArray() - ]; - } -} -``` +## values + +Internally, this method is calling the PHP [array_values](https://www.php.net/manual/en/function.array-values.php) of the array held in the `ArrayIterator` (via the `ArrayIterator::getArrayCopy` method) diff --git a/docs/convertible.md b/docs/convertible.md index 7fc67a8..bd83db9 100644 --- a/docs/convertible.md +++ b/docs/convertible.md @@ -1,6 +1,17 @@ # Convertible -The following interfaces are defined to easy the process of creating/converting collections/collection items to basic PHP types. +The following interfaces are defined to easy the process of creating/converting collections/collection items to basic PHP types. + +## fromIterable + +This interface defines how to create a collection item from an iterable: + +```php +interface FromIterable +{ + public static function fromIterable(iterable $data): self|static; +} +``` ## fromArray diff --git a/docs/filterable-collection-trait.md b/docs/filterable-collection-trait.md index 5a92821..4937cb2 100644 --- a/docs/filterable-collection-trait.md +++ b/docs/filterable-collection-trait.md @@ -1,42 +1,24 @@ # FilterableCollectionTrait -This trait (which internally also uses the `CollectionTrait`) adds filtering capabilities to your collection. +This trait (which internally also uses the `CollectionTrait`) adds filtering capabilities to your collection, by implementing the `FilterableCollection` interface. It provides the following methods to your collection: ## filter -```php -public function filter(CollectionFilter $filter): self|static -``` +Internally, this method will create a new empty collection and then iterate through your collection: -This will accept a `CollectionFilter` instance (with the definitions of the filter being applied to the collection), and returns a new collection with only the elements that have met the criteria defined in `$filter`. +If each element is an implementation of `FilterItem` and that item matches the criteria of the `CollectionFilter` (by calling the `isSatisfiedBy` method) the item will be added to the result collection. ## groupBy -```php -public function groupBy(bool $removeEmptyGroups, CollectionFilter ...$filters): array -``` - -This method allows you to apply a series of filters to a collection and group the result by each filter. The `$removeEmptyGroups` flag means if we will remove or keep groups in the result without items. - -The result is returned as an array with the following structure: +Internally, this method will create an array and initialize the groups, where each group will have as the key the value returned by the `key` of each filter passed. -```php -[ - 'filter_1_key' => [ - 'item_key_1' => item object 1 - ... - 'item_key_N' => item object X - ], - 'filter_2_key' => [ - 'item_key_1' => item object 1 - ... - 'item_key_N' => item object Y - ], - ... -] -``` +- Then it will iterate through your collection, and for each element it will iterate the filters. +- If each element is an implementation of `FilterItem` and that element matches the criteria of the current filter (by calling the `isSatisfiedBy` method) + - The element will be added to the result array on the group key + - On the group it will have a key that is calculated by calling the `groupByKey` of the element +- Finally, it will return the group array, optionally removing empty groups if the `$removeEmptyGroups` is set to `true` ## How to filter collections @@ -49,7 +31,7 @@ interface FilterItem } ``` -In order to the use the `FilterableCollectionTrait`, the items on your collection must implement the `FilteItem` interface. +In order to the use the `FilterableCollectionTrait`, the items on your collection must implement the `FilterItem` interface. Each item needs to implement the `groupByKey` method that will be used to group items. This method return the key of the group and will receive additional extra data that might be necessary in order to group the items. diff --git a/src/AbstractCollection.php b/src/AbstractCollection.php index 51dad66..763cee3 100644 --- a/src/AbstractCollection.php +++ b/src/AbstractCollection.php @@ -4,19 +4,17 @@ namespace Kununu\Collection; use ArrayIterator; -use Kununu\Collection\Convertible\ToArray; /** - * @method static self fromIterable(iterable $data) - * @method self add($value) - * @method self unique() - * @method self reverse() - * @method self diff(self $other) - * @method self each(callable $function, bool $rewind = true) - * @method array map(callable $function, bool $rewind = true) - * @method mixed reduce(callable $function, mixed $initial = null, bool $rewind = true) + * @method static self fromIterable(iterable $data) + * @method self add(mixed $value) + * @method self diff(Collection $other) + * @method self duplicates(bool $strict = true, bool $uniques = false) + * @method self each(callable $function, bool $rewind = true) + * @method self reverse() + * @method self unique() */ -abstract class AbstractCollection extends ArrayIterator implements ToArray +abstract class AbstractCollection extends ArrayIterator implements Collection { use CollectionTrait; } diff --git a/src/AbstractFilterableCollection.php b/src/AbstractFilterableCollection.php index 5787e41..6f31f34 100644 --- a/src/AbstractFilterableCollection.php +++ b/src/AbstractFilterableCollection.php @@ -4,21 +4,19 @@ namespace Kununu\Collection; use ArrayIterator; -use Kununu\Collection\Convertible\ToArray; use Kununu\Collection\Filter\CollectionFilter; /** - * @method static self fromIterable(iterable $data) - * @method self add($value) - * @method self unique() - * @method self reverse() - * @method self diff(self $other) - * @method self each(callable $function, bool $rewind = true) - * @method array map(callable $function, bool $rewind = true) - * @method mixed reduce(callable $function, mixed $initial = null, bool $rewind = true) - * @method self filter(CollectionFilter $filter) + * @method static self fromIterable(iterable $data) + * @method self add(mixed $value) + * @method self diff(Collection $other) + * @method self duplicates(bool $strict = true, bool $uniques = false) + * @method self each(callable $function, bool $rewind = true) + * @method self reverse() + * @method self unique() + * @method self filter(CollectionFilter $filter) */ -abstract class AbstractFilterableCollection extends ArrayIterator implements ToArray +abstract class AbstractFilterableCollection extends ArrayIterator implements FilterableCollection { use FilterableCollectionTrait; } diff --git a/src/AutoSortableOffsetSetTrait.php b/src/AutoSortableOffsetSetTrait.php index 2939dbe..3415af2 100644 --- a/src/AutoSortableOffsetSetTrait.php +++ b/src/AutoSortableOffsetSetTrait.php @@ -5,9 +5,10 @@ trait AutoSortableOffsetSetTrait { - public function offsetSet($key, $value): void + public function offsetSet(mixed $key, mixed $value): void { parent::offsetSet($key, $value); + $this->ksort(); } } diff --git a/src/Collection.php b/src/Collection.php new file mode 100644 index 0000000..6cf5988 --- /dev/null +++ b/src/Collection.php @@ -0,0 +1,34 @@ +count(); + return $this->mapToArray(); } public function add($value): self|static @@ -32,39 +34,12 @@ public function add($value): self|static return $this; } - public function has(mixed $value, bool $strict = true): bool - { - return in_array($value, $this->toArray(), $strict); - } - - public function unique(): self|static - { - return static::fromIterable(array_unique($this->toArray(), SORT_REGULAR)); - } - - public function duplicates(bool $strict = true): self|static - { - $elements = new static(); - $duplicates = new static(); - - $this->each(function ($item) use (&$elements, &$duplicates, $strict): void { - if ($elements->has($item, $strict)) { - $duplicates->append($item); - - return; - } - $elements->append($item); - }); - - return $duplicates; - } - public function reverse(): self|static + public function diff(Collection $other): self|static { - return static::fromIterable(array_reverse($this->toArray())); - } + if (!$other instanceof static) { + throw new InvalidArgumentException('Other collection must be of the same type'); + } - public function diff(self $other): self|static - { return static::fromIterable( array_values( array_map( @@ -78,6 +53,22 @@ public function diff(self $other): self|static ); } + public function duplicates(bool $strict = true, bool $uniques = false): self|static + { + $elements = new static(); + $duplicates = new static(); + + foreach ($this as $element) { + match ($elements->has($element, $strict)) { + true => $duplicates->add($element), + false => $elements->add($element) + }; + } + $this->rewind(); + + return $uniques ? $duplicates->unique() : $duplicates; + } + public function each(callable $function, bool $rewind = true): self|static { try { @@ -93,6 +84,21 @@ public function each(callable $function, bool $rewind = true): self|static return $this; } + public function empty(): bool + { + return 0 === $this->count(); + } + + public function has(mixed $value, bool $strict = true): bool + { + return in_array($value, $this->toArray(), $strict); + } + + public function keys(): array + { + return array_keys($this->getArrayCopy()); + } + public function map(callable $function, bool $rewind = true): array { $map = []; @@ -124,21 +130,32 @@ public function reduce(callable $function, mixed $initial = null, bool $rewind = return $initial; } - public function toArray(): array + public function reverse(): self|static { - return $this->mapToArray(); + return static::fromIterable(array_reverse($this->toArray())); + } + + public function unique(): self|static + { + return static::fromIterable(array_unique($this->toArray(), SORT_REGULAR)); + } + + public function values(): array + { + return array_values($this->getArrayCopy()); } protected function mapToArray(bool $withKeys = true): array { return array_map( static fn(mixed $element): mixed => match (true) { - $element instanceof ToArray => $element->toArray(), - $element instanceof ToString => $element->toString(), - $element instanceof ToInt => $element->toInt(), - default => $element, + $element instanceof ToArray => $element->toArray(), + $element instanceof ToString => $element->toString(), + $element instanceof ToInt => $element->toInt(), + $element instanceof Stringable => (string) $element, + default => $element, }, - $withKeys ? $this->getArrayCopy() : array_values($this->getArrayCopy()) + $withKeys ? $this->getArrayCopy() : $this->values() ); } } diff --git a/src/Convertible/FromIterable.php b/src/Convertible/FromIterable.php new file mode 100644 index 0000000..8b1e806 --- /dev/null +++ b/src/Convertible/FromIterable.php @@ -0,0 +1,9 @@ + 'My Name']); - $this->assertNull($item->getId()); - $this->assertEquals('My Name', $item->getName()); - $this->assertNull($item->getCreatedAt()); - $this->assertNull($item->getSimpleName()); - $this->assertNull($item->getVerified()); - $this->assertNull($item->getIndustryId()); - $this->assertNull($item->getSalary()); + self::assertNull($item->getId()); + self::assertEquals('My Name', $item->getName()); + self::assertNull($item->getCreatedAt()); + self::assertNull($item->getSimpleName()); + self::assertNull($item->getVerified()); + self::assertNull($item->getIndustryId()); + self::assertNull($item->getSalary()); $item->setId(100); - $this->assertSame(100, $item->getId()); + + self::assertSame(100, $item->getId()); $item->setCreatedAt($createdAt = new DateTime()); - $this->assertSame($createdAt, $item->getCreatedAt()); + + self::assertSame($createdAt, $item->getCreatedAt()); $item->setSimpleName('Simple name'); - $this->assertSame('Simple name', $item->getSimpleName()); + + self::assertSame('Simple name', $item->getSimpleName()); $item->setVerified(true); - $this->assertTrue($item->getVerified()); + + self::assertTrue($item->getVerified()); $item->setIndustryId(15); - $this->assertSame(15, $item->getIndustryId()); + + self::assertSame(15, $item->getIndustryId()); $item->setSalary(1500.29); - $this->assertSame(1500.29, $item->getSalary()); + + self::assertSame(1500.29, $item->getSalary()); } #[DataProvider('itemBuildDataProvider')] @@ -66,16 +72,16 @@ public function testItemBuild( ): void { $item = AbstractItemStub::build($data); - $this->assertSame($expectedId, $item->getId()); - $this->assertNotNull($item->getId()); - $this->assertSame($expectedName, $item->getName()); - $this->assertEquals($expectedCreatedAt, $item->getCreatedAt()); - $this->assertNull($item->getExtraFieldNotUsedInBuild()); - $this->assertSame($expectedSimpleName, $item->getSimpleName()); - $this->assertSame($expectedVerified, $item->getVerified()); - $this->assertNotNull($item->getVerified()); - $this->assertSame($expectedIndustryId, $item->getIndustryId()); - $this->assertSame($expectedSalary, $item->getSalary()); + self::assertSame($expectedId, $item->getId()); + self::assertNotNull($item->getId()); + self::assertSame($expectedName, $item->getName()); + self::assertEquals($expectedCreatedAt, $item->getCreatedAt()); + self::assertNull($item->getExtraFieldNotUsedInBuild()); + self::assertSame($expectedSimpleName, $item->getSimpleName()); + self::assertSame($expectedVerified, $item->getVerified()); + self::assertNotNull($item->getVerified()); + self::assertSame($expectedIndustryId, $item->getIndustryId()); + self::assertSame($expectedSalary, $item->getSalary()); } public static function itemBuildDataProvider(): array @@ -169,13 +175,13 @@ public function testItemBuildRequired(array $data, ?string $expectedExceptionMes $item = AbstractItemWithRequiredFieldsStub::build($data); if (null === $expectedExceptionMessage) { - $this->assertIsInt($item->giveMeTheId()); - $this->assertIsString($item->giveMeTheName()); - $this->assertInstanceOf(DateTime::class, $item->giveMeTheCreatedAt()); - $this->assertIsBool($item->giveMeTheVerified()); - $this->assertIsBool($item->giveMeTheVerified()); - $this->assertInstanceOf(DTOStub::class, $item->giveMeTheCustom()); - $this->assertIsFloat($item->giveMeTheScore()); + self::assertIsInt($item->giveMeTheId()); + self::assertIsString($item->giveMeTheName()); + self::assertInstanceOf(DateTime::class, $item->giveMeTheCreatedAt()); + self::assertIsBool($item->giveMeTheVerified()); + self::assertIsBool($item->giveMeTheVerified()); + self::assertInstanceOf(DTOStub::class, $item->giveMeTheCustom()); + self::assertIsFloat($item->giveMeTheScore()); } } @@ -252,13 +258,13 @@ public function testItemBuildFromArray(): void 'fromArray' => ['id' => 1, 'name' => 'The Name'], ]); - $this->assertInstanceOf(FromArrayStub::class, $item->fromArray()); - $this->assertEquals(1, $item->fromArray()->id); - $this->assertEquals('The Name', $item->fromArray()->name); - $this->assertNull($item->notFromArray()); - $this->assertInstanceOf(FromArrayStub::class, $item->defaultFromArray()); - $this->assertEquals(0, $item->defaultFromArray()->id); - $this->assertEquals('', $item->defaultFromArray()->name); + self::assertInstanceOf(FromArrayStub::class, $item->fromArray()); + self::assertEquals(1, $item->fromArray()->id); + self::assertEquals('The Name', $item->fromArray()->name); + self::assertNull($item->notFromArray()); + self::assertInstanceOf(FromArrayStub::class, $item->defaultFromArray()); + self::assertEquals(0, $item->defaultFromArray()->id); + self::assertEquals('', $item->defaultFromArray()->name); } public function testItemBuildCollection(): void @@ -271,9 +277,9 @@ public function testItemBuildCollection(): void ], ]); - $this->assertInstanceOf(DTOCollectionStub::class, $item->collection()); - $this->assertCount(3, $item->collection()); - $this->assertSame( + self::assertInstanceOf(DTOCollectionStub::class, $item->collection()); + self::assertCount(3, $item->collection()); + self::assertSame( [ 'field 1' => [ 'field' => 'field 1', @@ -290,9 +296,9 @@ public function testItemBuildCollection(): void ], $item->collection()->toArray() ); - $this->assertNull($item->notCollection()); - $this->assertInstanceOf(DTOCollectionStub::class, $item->defaultCollection()); - $this->assertEmpty($item->defaultCollection()); + self::assertNull($item->notCollection()); + self::assertInstanceOf(DTOCollectionStub::class, $item->defaultCollection()); + self::assertEmpty($item->defaultCollection()); } #[DataProvider('itemBuildConditionalDataProvider')] @@ -303,7 +309,7 @@ public function testItemBuildConditional(mixed $expected): void 'value' => 12.5, ]); - $this->assertSame($expected, $item->value()); + self::assertSame($expected, $item->value()); } public static function itemBuildConditionalDataProvider(): array @@ -324,6 +330,7 @@ public function testItemInvalidMethod(): void $this->expectExceptionMessage( 'Kununu\Collection\Tests\Stub\AbstractItemStub: Invalid method "thisMethodReallyDoesNotExists" called' ); + $item->thisMethodReallyDoesNotExists(); } @@ -335,6 +342,7 @@ public function testItemSetInvalidProperty(): void $this->expectExceptionMessage( 'Kununu\Collection\Tests\Stub\AbstractItemStub : Invalid attribute "invalidProperty"' ); + $item->setInvalidProperty(true); } @@ -346,6 +354,7 @@ public function testItemGetInvalidProperty(): void $this->expectExceptionMessage( 'Kununu\Collection\Tests\Stub\AbstractItemStub : Invalid attribute "invalidProperty"' ); + $item->getInvalidProperty(true); } @@ -357,6 +366,7 @@ public function testItemBuilderInvalidProperty(): void $this->expectExceptionMessage( 'Kununu\Collection\Tests\Stub\AbstractItemStub: Invalid method "withInvalidProperty" called' ); + $item->withInvalidProperty(false); } @@ -377,27 +387,27 @@ public function testItemBuilderWithSnakeCase(): void 'required_date_time_immutable_field' => '2023-10-11 12:34:04', ]); - $this->assertEquals('The string field', $item->stringField()); - $this->assertEquals('The required string field', $item->requiredStringField()); - $this->assertTrue($item->boolField()); - $this->assertFalse($item->requiredBoolField()); - $this->assertEquals(1, $item->intField()); - $this->assertEquals(2, $item->requiredIntField()); - $this->assertEquals(1.5, $item->floatField()); - $this->assertEquals(2.6, $item->requiredFloatField()); - $this->assertEquals( + self::assertEquals('The string field', $item->stringField()); + self::assertEquals('The required string field', $item->requiredStringField()); + self::assertTrue($item->boolField()); + self::assertFalse($item->requiredBoolField()); + self::assertEquals(1, $item->intField()); + self::assertEquals(2, $item->requiredIntField()); + self::assertEquals(1.5, $item->floatField()); + self::assertEquals(2.6, $item->requiredFloatField()); + self::assertEquals( DateTime::createFromFormat('Y-m-d H:i:s', '2023-10-11 12:34:01'), $item->dateTimeField() ); - $this->assertEquals( + self::assertEquals( DateTime::createFromFormat('Y-m-d H:i:s', '2023-10-11 12:34:02'), $item->requiredDateTimeField() ); - $this->assertEquals( + self::assertEquals( DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2023-10-11 12:34:03'), $item->dateTimeImmutableField() ); - $this->assertEquals( + self::assertEquals( DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2023-10-11 12:34:04'), $item->requiredDateTimeImmutableField() ); diff --git a/tests/AbstractItemToArrayTest.php b/tests/AbstractItemToArrayTest.php index f458590..33591f6 100644 --- a/tests/AbstractItemToArrayTest.php +++ b/tests/AbstractItemToArrayTest.php @@ -21,7 +21,7 @@ public function testAbstractItemToArray(): void ], ]); - $this->assertEquals( + self::assertEquals( [ 'id' => 50, 'name' => '1000: My Name is?', diff --git a/tests/CollectionTest.php b/tests/CollectionTest.php index 766e424..f438748 100644 --- a/tests/CollectionTest.php +++ b/tests/CollectionTest.php @@ -6,9 +6,15 @@ use ArrayIterator; use Exception; use Generator; +use InvalidArgumentException; +use Kununu\Collection\Collection; use Kununu\Collection\Tests\Stub\AbstractItemStub; use Kununu\Collection\Tests\Stub\AutoSortedCollectionStub; use Kununu\Collection\Tests\Stub\CollectionStub; +use Kununu\Collection\Tests\Stub\DTOCollectionStub; +use Kununu\Collection\Tests\Stub\DTOStub; +use Kununu\Collection\Tests\Stub\FilterableCollectionStub; +use Kununu\Collection\Tests\Stub\StringableStub; use Kununu\Collection\Tests\Stub\ToArrayStub; use Kununu\Collection\Tests\Stub\ToIntStub; use Kununu\Collection\Tests\Stub\ToStringStub; @@ -21,7 +27,7 @@ final class CollectionTest extends TestCase #[DataProvider('fromIterableDataProvider')] public function testFromIterable(iterable $data, array $expected): void { - $this->assertEquals($expected, CollectionStub::fromIterable($data)->toArray()); + self::assertEquals($expected, CollectionStub::fromIterable($data)->toArray()); } public static function fromIterableDataProvider(): array @@ -85,13 +91,15 @@ public static function fromIterableDataProvider(): array ToStringStub::create(ToIntStub::fromInt(1), 'ABC'), ToStringStub::create(ToIntStub::fromInt(2), 'DEF'), ToStringStub::create(ToIntStub::fromInt(3), 'GHI'), - ToStringStub::create(ToIntStub::fromInt(4), 'JKL') + ToStringStub::create(ToIntStub::fromInt(4), 'JKL'), + StringableStub::create(ToIntStub::fromInt(5), 'MNO') ), [ '1: ABC', '2: DEF', '3: GHI', '4: JKL', + '5: MNO', ], ], ]; @@ -102,25 +110,25 @@ public function testHas(iterable $collectionContents, mixed $hasValue, bool $has { $collection = CollectionStub::fromIterable($collectionContents); - $this->assertEquals($expectedHas, $collection->has($hasValue, $hasStrict)); + self::assertEquals($expectedHas, $collection->has($hasValue, $hasStrict)); } public static function hasDataProvider(): array { return [ - 'has_with_integers_strict' => [ + 'has_with_integers_strict' => [ [1, 2, 3], 1, true, true, ], - 'has_with_integers_loose' => [ + 'has_with_integers_loose' => [ [1, 2, 3], '1', false, true, ], - 'missing_with_integers_strict' => [ + 'missing_with_integers_strict' => [ [1, 2, 3], 4, true, @@ -132,31 +140,31 @@ public static function hasDataProvider(): array true, false, ], - 'has_with_strings_strict' => [ + 'has_with_strings_strict' => [ ['one', 'two', 'three'], 'one', true, true, ], - 'missing_with_strings_strict' => [ + 'missing_with_strings_strict' => [ ['one', 'two', 'three'], 'missing', true, false, ], - 'has_with_strings_loose' => [ + 'has_with_strings_loose' => [ ['one', 'two', 'three'], 'one', false, true, ], - 'missing_with_strings_loose' => [ + 'missing_with_strings_loose' => [ ['one', 'two', 'three'], 'missing', false, false, ], - 'has_with_object_strict' => [ + 'has_with_object_strict' => [ [ $one = new AbstractItemStub(['name' => 'one']), new AbstractItemStub(['name' => 'two']), @@ -166,7 +174,7 @@ public static function hasDataProvider(): array true, true, ], - 'missing_with_object_strict' => [ + 'missing_with_object_strict' => [ [ new AbstractItemStub(['name' => 'one']), new AbstractItemStub(['name' => 'two']), @@ -176,7 +184,7 @@ public static function hasDataProvider(): array true, false, ], - 'has_with_object_loose' => [ + 'has_with_object_loose' => [ [ new AbstractItemStub(['name' => 'one']), new AbstractItemStub(['name' => 'two']), @@ -186,7 +194,7 @@ public static function hasDataProvider(): array false, true, ], - 'missing_with_object_loose' => [ + 'missing_with_object_loose' => [ [ new AbstractItemStub(['name' => 'one']), new AbstractItemStub(['name' => 'two']), @@ -203,10 +211,11 @@ public function testEmpty(): void { $collection = new CollectionStub(); - $this->assertTrue($collection->empty()); + self::assertTrue($collection->empty()); $collection->add(1); - $this->assertFalse($collection->empty()); + + self::assertFalse($collection->empty()); } public function testUnique(): void @@ -217,31 +226,100 @@ public function testUnique(): void $uniqueCollection = $collection->unique(); - $this->assertEquals(16, $collection->count()); - $this->assertEquals(4, $uniqueCollection->count()); - $this->assertEquals([1, 2, 3, 4], $uniqueCollection->toArray()); + self::assertEquals(16, $collection->count()); + self::assertEquals(4, $uniqueCollection->count()); + self::assertEquals([1, 2, 3, 4], $uniqueCollection->toArray()); + } + + #[DataProvider('keysDataProvider')] + public function testKeys(Collection $collection, array $expected): void + { + self::assertEquals($expected, $collection->keys()); + } + + public static function keysDataProvider(): array + { + return [ + 'collection_with_no_offset_set' => [ + CollectionStub::fromIterable(self::getGenerator(1, 2, 3, 4)), + [0, 1, 2, 3], + ], + 'auto_sorted_collection_with_offset_set' => [ + AutoSortedCollectionStub::fromIterable(self::getArrayIterator('the', 'quick', 'brown', 'fox')), + ['brown', 'fox', 'quick', 'the'], + ], + 'dto_collection_with_offset_set' => [ + new DTOCollectionStub( + new DTOStub('key 1', 100), + new DTOStub('key 2', 101), + new DTOStub('key 3', 102) + ), + ['key 1', 'key 2', 'key 3'], + ], + ]; + } + + #[DataProvider('valuesDataProvider')] + public function testValues(Collection $collection, array $expected): void + { + self::assertEquals($expected, $collection->values()); + } + + public static function valuesDataProvider(): array + { + return [ + 'integer_collection' => [ + CollectionStub::fromIterable(self::getGenerator(1, 2, 3, 4)), + [1, 2, 3, 4], + ], + 'auto_sorted_collection' => [ + AutoSortedCollectionStub::fromIterable(self::getArrayIterator('the', 'quick', 'brown', 'fox')), + ['brown', 'fox', 'quick', 'the'], + ], + 'dto_collection' => [ + new DTOCollectionStub( + $item3 = new DTOStub('key 3', 102), + $item2 = new DTOStub('key 2', 101), + $item1 = new DTOStub('key 1', 100) + ), + [$item3, $item2, $item1], + ], + ]; + } + + #[DataProvider('duplicatesDataProvider')] + public function testDuplicates(bool $strict, bool $uniques, array $contents, array $expected): void + { + $collection = CollectionStub::fromIterable($contents); + $duplicateCollection = $collection->duplicates($strict, $uniques); + + self::assertEquals($expected, $duplicateCollection->toArray()); } public static function duplicatesDataProvider(): array { return [ - 'integers_no_duplicates_strict' => [ + 'integers_no_duplicates_strict' => [ true, + false, [1, 2, 3], - [] + [], ], - 'integers_duplicates_strict' => [ + 'integers_duplicates_strict' => [ true, + false, [1, 2, 3, 4, 1, 2, 4], [1, 2, 4], ], - 'integers_duplicates_strict_mixed' => [ + 'integers_duplicates_strict_mixed' => [ true, + false, [1, 2, 3, 4, 1, '2', 4], [1, 4], ], - 'strings_no_duplicates_strict' => [ + 'strings_no_duplicates_strict' => [ true, + false, [ '7178e84e-472d-4766-8e45-a571871ff988', 'ca583de8-e6ba-4098-a0bf-10c981ff3d8d', @@ -250,8 +328,9 @@ public static function duplicatesDataProvider(): array ], [], ], - 'strings_duplicates_strict' => [ + 'strings_duplicates_strict_no_uniques' => [ true, + false, [ '7178e84e-472d-4766-8e45-a571871ff988', '7178e84e-472d-4766-8e45-a571871ff988', // dup 1-1 @@ -267,8 +346,26 @@ public static function duplicatesDataProvider(): array 'ca583de8-e6ba-4098-a0bf-10c981ff3d8d', // dup 2-1 ], ], - 'item_stubs_no_duplicates_strict' => [ + 'strings_duplicates_strict_uniques' => [ + true, true, + [ + '7178e84e-472d-4766-8e45-a571871ff988', + '7178e84e-472d-4766-8e45-a571871ff988', // dup 1-1 + '7178e84e-472d-4766-8e45-a571871ff988', // dup 1-2 + 'ca583de8-e6ba-4098-a0bf-10c981ff3d8d', + 'ca583de8-e6ba-4098-a0bf-10c981ff3d8d', // dup 2-1 + '909a8214-86b1-4d78-94f2-6a8e22a24020', + 'baaeace4-1aeb-4b47-ac2c-4b90ebd12342', + ], + [ + '7178e84e-472d-4766-8e45-a571871ff988', // dup 1-1 and dup 1-2 + 'ca583de8-e6ba-4098-a0bf-10c981ff3d8d', // dup 2-1 + ], + ], + 'item_stubs_no_duplicates_strict' => [ + true, + false, [ new AbstractItemStub(['name' => 'one']), new AbstractItemStub(['name' => 'two']), @@ -276,8 +373,9 @@ public static function duplicatesDataProvider(): array ], [], ], - 'item_stubs_duplicates_strict' => [ + 'item_stubs_duplicates_strict' => [ true, + false, [ $one = new AbstractItemStub(['name' => 'one']), $one, @@ -290,17 +388,20 @@ public static function duplicatesDataProvider(): array $two, ], ], - 'integers_no_duplicates_loose' => [ + 'integers_no_duplicates_loose' => [ + false, false, [1, 2, 3], - [] + [], ], - 'integers_duplicates_loose' => [ + 'integers_duplicates_loose' => [ + false, false, [1, 2, 3, 4, 1, '2', 4], ['1', 2, '4'], ], - 'strings_no_duplicates_loose' => [ + 'strings_no_duplicates_loose' => [ + false, false, [ '7178e84e-472d-4766-8e45-a571871ff988', @@ -310,7 +411,8 @@ public static function duplicatesDataProvider(): array ], [], ], - 'strings_duplicates_loose' => [ + 'strings_duplicates_loose_no_uniques' => [ + false, false, [ '7178e84e-472d-4766-8e45-a571871ff988', @@ -321,16 +423,37 @@ public static function duplicatesDataProvider(): array '909a8214-86b1-4d78-94f2-6a8e22a24020', 'baaeace4-1aeb-4b47-ac2c-4b90ebd12342', '2', - 2 + 2, ], [ '7178e84e-472d-4766-8e45-a571871ff988', // dup 1-1 '7178e84e-472d-4766-8e45-a571871ff988', // dup 1-2 'ca583de8-e6ba-4098-a0bf-10c981ff3d8d', // dup 2-1 - 2 + 2, ], ], - 'item_stubs_no_duplicates_loose' => [ + 'strings_duplicates_loose_uniques' => [ + false, + true, + [ + '7178e84e-472d-4766-8e45-a571871ff988', + '7178e84e-472d-4766-8e45-a571871ff988', // dup 1-1 + '7178e84e-472d-4766-8e45-a571871ff988', // dup 1-2 + 'ca583de8-e6ba-4098-a0bf-10c981ff3d8d', + 'ca583de8-e6ba-4098-a0bf-10c981ff3d8d', // dup 2-1 + '909a8214-86b1-4d78-94f2-6a8e22a24020', + 'baaeace4-1aeb-4b47-ac2c-4b90ebd12342', + '2', + 2, + ], + [ + '7178e84e-472d-4766-8e45-a571871ff988', // dup 1-1 and dup 1-2 + 'ca583de8-e6ba-4098-a0bf-10c981ff3d8d', // dup 2-1 + 2, + ], + ], + 'item_stubs_no_duplicates_loose' => [ + false, false, [ new AbstractItemStub(['name' => 'one']), @@ -339,7 +462,8 @@ public static function duplicatesDataProvider(): array ], [], ], - 'item_stubs_duplicates_loose' => [ + 'item_stubs_duplicates_loose' => [ + false, false, [ new AbstractItemStub(['name' => 'one']), @@ -356,17 +480,9 @@ public static function duplicatesDataProvider(): array ]; } - #[DataProvider('duplicatesDataProvider')] - public function testDuplicates(bool $strict, array $contents, array $expected): void - { - $collection = CollectionStub::fromIterable($contents); - $duplicateCollection = $collection->duplicates($strict); - $this->assertEquals($expected, $duplicateCollection->toArray()); - } - public function testReverse(): void { - $this->assertEquals([5, 4, 3, 2, 1], CollectionStub::fromIterable([1, 2, 3, 4, 5])->reverse()->toArray()); + self::assertEquals([5, 4, 3, 2, 1], CollectionStub::fromIterable([1, 2, 3, 4, 5])->reverse()->toArray()); } public function testDiff(): void @@ -374,8 +490,13 @@ public function testDiff(): void $collection1 = CollectionStub::fromIterable([1, 2, 3, 4, 5]); $collection2 = CollectionStub::fromIterable([1, 2, 3, 6, 7]); - $this->assertEquals([4, 5], $collection1->diff($collection2)->toArray()); - $this->assertEquals([6, 7], $collection2->diff($collection1)->toArray()); + self::assertEquals([4, 5], $collection1->diff($collection2)->toArray()); + self::assertEquals([6, 7], $collection2->diff($collection1)->toArray()); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Other collection must be of the same type'); + + $collection1->diff(new FilterableCollectionStub()); } #[DataProvider('eachDataProvider')] @@ -398,12 +519,12 @@ function(int $value) use (&$collectedValues, $expectException, $exception): void ); } catch (Throwable $e) { $exceptionWasThrown = true; - $this->assertEquals($e, $exception); + self::assertEquals($e, $exception); } - $this->assertEquals($exceptionWasThrown, $expectException); - $this->assertEquals($expectException ? [2, 4] : [2, 4, 6, 8, 10], $collectedValues); - $this->assertEquals($expectedCurrent, $collection->current()); + self::assertEquals($exceptionWasThrown, $expectException); + self::assertEquals($expectException ? [2, 4] : [2, 4, 6, 8, 10], $collectedValues); + self::assertEquals($expectedCurrent, $collection->current()); } public static function eachDataProvider(): array @@ -454,12 +575,12 @@ function(array $value) use ($expectException, $exception): int { ); } catch (Throwable $e) { $exceptionWasThrown = true; - $this->assertEquals($e, $exception); + self::assertEquals($e, $exception); } - $this->assertEquals($exceptionWasThrown, $expectException); - $this->assertEquals($expectException ? null : [1, 2, 3], $mapResult); - $this->assertEquals($expectedCurrent, $collection->current()); + self::assertEquals($exceptionWasThrown, $expectException); + self::assertEquals($expectException ? null : [1, 2, 3], $mapResult); + self::assertEquals($expectedCurrent, $collection->current()); } public static function mapDataProvider(): array @@ -511,12 +632,12 @@ function(int $carry, int $element) use ($expectException, $exception): int { ); } catch (Throwable $e) { $exceptionWasThrown = true; - $this->assertEquals($e, $exception); + self::assertEquals($e, $exception); } - $this->assertEquals($exceptionWasThrown, $expectException); - $this->assertEquals($expectException ? null : 1015, $value); - $this->assertEquals($expectedCurrent, $collection->current()); + self::assertEquals($exceptionWasThrown, $expectException); + self::assertEquals($expectException ? null : 1015, $value); + self::assertEquals($expectedCurrent, $collection->current()); } public static function reduceDataProvider(): array @@ -548,7 +669,7 @@ public static function reduceDataProvider(): array #[DataProvider('autoSortedCollectionDataProvider')] public function testAutoSortedCollection(iterable $data, array $expected): void { - $this->assertEquals($expected, AutoSortedCollectionStub::fromIterable($data)->toArray()); + self::assertEquals($expected, AutoSortedCollectionStub::fromIterable($data)->toArray()); } public static function autoSortedCollectionDataProvider(): array diff --git a/tests/Filter/FilterOperatorAndTest.php b/tests/Filter/FilterOperatorAndTest.php index 6c379fe..89ba6f4 100644 --- a/tests/Filter/FilterOperatorAndTest.php +++ b/tests/Filter/FilterOperatorAndTest.php @@ -12,11 +12,11 @@ public function testOperator(): void { $operator = new FilterOperatorAnd(); - $this->assertTrue($operator->initialValue()); - $this->assertFalse($operator->exitConditionValue()); - $this->assertTrue($operator->calculate(true, true)); - $this->assertFalse($operator->calculate(false, false)); - $this->assertFalse($operator->calculate(true, false)); - $this->assertFalse($operator->calculate(false, true)); + self::assertTrue($operator->initialValue()); + self::assertFalse($operator->exitConditionValue()); + self::assertTrue($operator->calculate(true, true)); + self::assertFalse($operator->calculate(false, false)); + self::assertFalse($operator->calculate(true, false)); + self::assertFalse($operator->calculate(false, true)); } } diff --git a/tests/Filter/FilterOperatorOrTest.php b/tests/Filter/FilterOperatorOrTest.php index fade028..4b5f105 100644 --- a/tests/Filter/FilterOperatorOrTest.php +++ b/tests/Filter/FilterOperatorOrTest.php @@ -12,11 +12,11 @@ public function testOperator(): void { $operator = new FilterOperatorOr(); - $this->assertFalse($operator->initialValue()); - $this->assertTrue($operator->exitConditionValue()); - $this->assertFalse($operator->calculate(false, false)); - $this->assertTrue($operator->calculate(true, true)); - $this->assertTrue($operator->calculate(true, false)); - $this->assertTrue($operator->calculate(false, true)); + self::assertFalse($operator->initialValue()); + self::assertTrue($operator->exitConditionValue()); + self::assertFalse($operator->calculate(false, false)); + self::assertTrue($operator->calculate(true, true)); + self::assertTrue($operator->calculate(true, false)); + self::assertTrue($operator->calculate(false, true)); } } diff --git a/tests/Filter/FilterOperatorXOrTest.php b/tests/Filter/FilterOperatorXOrTest.php index b6356d7..85ffb4b 100644 --- a/tests/Filter/FilterOperatorXOrTest.php +++ b/tests/Filter/FilterOperatorXOrTest.php @@ -12,11 +12,11 @@ public function testOperator(): void { $operator = new FilterOperatorXor(); - $this->assertFalse($operator->initialValue()); - $this->assertTrue($operator->exitConditionValue()); - $this->assertFalse($operator->calculate(false, false)); - $this->assertFalse($operator->calculate(true, true)); - $this->assertTrue($operator->calculate(true, false)); - $this->assertTrue($operator->calculate(false, true)); + self::assertFalse($operator->initialValue()); + self::assertTrue($operator->exitConditionValue()); + self::assertFalse($operator->calculate(false, false)); + self::assertFalse($operator->calculate(true, true)); + self::assertTrue($operator->calculate(true, false)); + self::assertTrue($operator->calculate(false, true)); } } diff --git a/tests/FilterableCollectionTest.php b/tests/FilterableCollectionTest.php index c59f44c..4475217 100644 --- a/tests/FilterableCollectionTest.php +++ b/tests/FilterableCollectionTest.php @@ -38,12 +38,12 @@ public function isSatisfiedBy(FilterItem $item): bool $filteredCollection = $collection->filter($filter); - $this->assertCount(2, $filteredCollection); + self::assertCount(2, $filteredCollection); foreach ($filteredCollection as $item) { - $this->assertInstanceOf(FilterItemStub::class, $item); + self::assertInstanceOf(FilterItemStub::class, $item); } - $this->assertEquals('a', $filteredCollection[0]->groupByKey()); - $this->assertEquals('c', $filteredCollection[1]->groupByKey()); + self::assertEquals('a', $filteredCollection[0]->groupByKey()); + self::assertEquals('c', $filteredCollection[1]->groupByKey()); $collection = (new FilterableCollectionStub()) ->add(1) @@ -51,7 +51,7 @@ public function isSatisfiedBy(FilterItem $item): bool ->add(new FilterItemStub('d')) ->add('a string'); - $this->assertEmpty($collection->filter($filter)); + self::assertEmpty($collection->filter($filter)); } public function testGroupBy(): void @@ -119,19 +119,20 @@ public function isSatisfiedBy(FilterItem $item): bool $groups = $collection->groupBy(false, $filter1, $filter2, $filter3, $filter4, $filter5, $filter6); - $this->assertCount(6, $groups); - $this->assertCount(1, $groups['Filter 1']); - $this->assertCount(1, $groups['Filter 2']); - $this->assertCount(1, $groups['Filter 3']); - $this->assertEmpty($groups['Filter 4']); - $this->assertCount(2, $groups['Filter 5']); - $this->assertEmpty($groups['Filter 6']); + self::assertCount(6, $groups); + self::assertCount(1, $groups['Filter 1']); + self::assertCount(1, $groups['Filter 2']); + self::assertCount(1, $groups['Filter 3']); + self::assertEmpty($groups['Filter 4']); + self::assertCount(2, $groups['Filter 5']); + self::assertEmpty($groups['Filter 6']); $groups = $collection->groupBy(true, $filter1, $filter2, $filter3, $filter4, $filter5, $filter6); - $this->assertCount(4, $groups); - $this->assertCount(1, $groups['Filter 1']); - $this->assertCount(1, $groups['Filter 2']); - $this->assertCount(1, $groups['Filter 3']); - $this->assertCount(2, $groups['Filter 5']); + + self::assertCount(4, $groups); + self::assertCount(1, $groups['Filter 1']); + self::assertCount(1, $groups['Filter 2']); + self::assertCount(1, $groups['Filter 3']); + self::assertCount(2, $groups['Filter 5']); } } diff --git a/tests/Mapper/DefaultMapperTest.php b/tests/Mapper/DefaultMapperTest.php index fd05266..c4f26c1 100644 --- a/tests/Mapper/DefaultMapperTest.php +++ b/tests/Mapper/DefaultMapperTest.php @@ -16,7 +16,7 @@ public function testMapper(): void { $mapper = new MapperStub(DTOCollectionStub::class); - $this->assertEquals( + self::assertEquals( [ 'key 1' => 100, 'key 2' => 101, @@ -42,6 +42,6 @@ public function testMapperWithInvalidCallerRegistration(): void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid collection class: Kununu\Collection\Tests\Stub\CollectionStub'); - $this->assertNull(new MapperStub(CollectionStub::class)); + self::assertNull(new MapperStub(CollectionStub::class)); } } diff --git a/tests/Stub/DTOCollectionStub.php b/tests/Stub/DTOCollectionStub.php index c7630f9..682f8a8 100644 --- a/tests/Stub/DTOCollectionStub.php +++ b/tests/Stub/DTOCollectionStub.php @@ -6,16 +6,6 @@ use InvalidArgumentException; use Kununu\Collection\AbstractCollection; -/** - * @method static self fromIterable(iterable $data) - * @method self add($value) - * @method self unique() - * @method self reverse() - * @method self diff(self $other) - * @method self each(callable $function, bool $rewind = true) - * @method array map(callable $function, bool $rewind = true) - * @method mixed reduce(callable $function, mixed $initial = null, bool $rewind = true) - */ final class DTOCollectionStub extends AbstractCollection { private const INVALID = 'Can only append array or %s'; diff --git a/tests/Stub/StringableStub.php b/tests/Stub/StringableStub.php new file mode 100644 index 0000000..f469744 --- /dev/null +++ b/tests/Stub/StringableStub.php @@ -0,0 +1,21 @@ +id->toInt(), $this->value); + } +}