├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── UPGRADE.md ├── composer.json ├── docs └── en │ ├── derived-collections.rst │ ├── expression-builder.rst │ ├── expressions.rst │ ├── index.rst │ ├── lazy-collections.rst │ ├── serialization.rst │ └── sidebar.rst └── src ├── AbstractLazyCollection.php ├── ArrayCollection.php ├── Collection.php ├── Criteria.php ├── Expr ├── ClosureExpressionVisitor.php ├── Comparison.php ├── CompositeExpression.php ├── Expression.php ├── ExpressionVisitor.php └── Value.php ├── ExpressionBuilder.php ├── Order.php ├── ReadableCollection.php └── Selectable.php /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute to Doctrine 2 | 3 | Thank you for contributing to Doctrine! 4 | 5 | Before we can merge your Pull-Request here are some guidelines that you need to follow. 6 | These guidelines exist not to annoy you, but to keep the code base clean, 7 | unified and future proof. 8 | 9 | ## Coding Standard 10 | 11 | We use the [Doctrine Coding Standard](https://github.com/doctrine/coding-standard). 12 | 13 | ## Unit-Tests 14 | 15 | Please try to add a test for your pull-request. 16 | 17 | * If you want to contribute new functionality add unit- or functional tests 18 | depending on the scope of the feature. 19 | 20 | You can run the unit-tests by calling ``vendor/bin/phpunit`` from the root of the project. 21 | It will run all the project tests. 22 | 23 | In order to do that, you will need a fresh copy of doctrine/collections, and you 24 | will have to run a composer installation in the project: 25 | 26 | ```sh 27 | git clone git@github.com:doctrine/collections.git 28 | cd collections 29 | curl -sS https://getcomposer.org/installer | php -- 30 | ./composer.phar install 31 | ``` 32 | 33 | ## Github Actions 34 | 35 | We automatically run your pull request through Github Actions against supported 36 | PHP versions. If you break the tests, we cannot merge your code, so please make 37 | sure that your code is working before opening up a Pull-Request. 38 | 39 | ## Getting merged 40 | 41 | Please allow us time to review your pull requests. We will give our best to review 42 | everything as fast as possible, but cannot always live up to our own expectations. 43 | 44 | Thank you very much again for your contribution! 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006-2013 Doctrine Project 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doctrine Collections 2 | 3 | [![Build Status](https://github.com/doctrine/collections/workflows/Continuous%20Integration/badge.svg)](https://github.com/doctrine/collections/actions) 4 | [![Code Coverage](https://codecov.io/gh/doctrine/collections/branch/2.0.x/graph/badge.svg)](https://codecov.io/gh/doctrine/collections/branch/2.0.x) 5 | 6 | Collections Abstraction library 7 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | Note about upgrading: Doctrine uses static and runtime mechanisms to raise 2 | awareness about deprecated code. 3 | 4 | - Use of `@deprecated` docblock that is detected by IDEs (like PHPStorm) or 5 | Static Analysis tools (like Psalm, phpstan) 6 | - Use of our low-overhead runtime deprecation API, details: 7 | https://github.com/doctrine/deprecations/ 8 | 9 | # Upgrade to 2.2 10 | 11 | ## Deprecated string representation of sort order 12 | 13 | Criteria orderings direction is now represented by the 14 | `Doctrine\Common\Collection\Order` enum. 15 | 16 | As a consequence: 17 | 18 | - `Criteria::ASC` and `Criteria::DESC` are deprecated in favor of 19 | `Order::Ascending` and `Order::Descending`, respectively. 20 | - `Criteria::getOrderings()` is deprecated in favor of `Criteria::orderings()`, 21 | which returns `array`. 22 | - `Criteria::orderBy()` accepts `array`, but passing 23 | anything other than `array` is deprecated. 24 | 25 | # Upgrade to 2.0 26 | 27 | ## BC breaking changes 28 | 29 | Native parameter types were added. Native return types will be added in 3.0.x 30 | As a consequence, some signatures were changed and will have to be adjusted in sub-classes. 31 | 32 | Note that in order to keep compatibility with both 1.x and 2.x versions, 33 | extending code would have to omit the added parameter types. 34 | This would only work in PHP 7.2+ which is the first version featuring 35 | [parameter widening](https://wiki.php.net/rfc/parameter-no-type-variance). 36 | It is also recommended to add return types according to the tables below 37 | 38 | You can find a list of major changes to public API below. 39 | 40 | ### Doctrine\Common\Collections\Collection 41 | 42 | | 1.0.x | 3.0.x | 43 | |---------------------------------:|:-------------------------------------------------| 44 | | `add($element)` | `add(mixed $element): void` | 45 | | `clear()` | `clear(): void` | 46 | | `contains($element)` | `contains(mixed $element): bool` | 47 | | `isEmpty()` | `isEmpty(): bool` | 48 | | `removeElement($element)` | `removeElement(mixed $element): bool` | 49 | | `containsKey($key)` | `containsKey(string\|int $key): bool` | 50 | | `get()` | `get(string\|int $key): mixed` | 51 | | `getKeys()` | `getKeys(): array` | 52 | | `getValues()` | `getValues(): array` | 53 | | `set($key, $value)` | `set(string\|int $key, $value): void` | 54 | | `toArray()` | `toArray(): array` | 55 | | `first()` | `first(): mixed` | 56 | | `last()` | `last(): mixed` | 57 | | `key()` | `key(): int\|string\|null` | 58 | | `current()` | `current(): mixed` | 59 | | `next()` | `next(): mixed` | 60 | | `exists(Closure $p)` | `exists(Closure $p): bool` | 61 | | `filter(Closure $p)` | `filter(Closure $p): self` | 62 | | `forAll(Closure $p)` | `forAll(Closure $p): bool` | 63 | | `map(Closure $func)` | `map(Closure $func): self` | 64 | | `partition(Closure $p)` | `partition(Closure $p): array` | 65 | | `indexOf($element)` | `indexOf(mixed $element): int\|string\|false` | 66 | | `slice($offset, $length = null)` | `slice(int $offset, ?int $length = null): array` | 67 | | `count()` | `count(): int` | 68 | | `getIterator()` | `getIterator(): \Traversable` | 69 | | `offsetSet($offset, $value)` | `offsetSet(mixed $offset, mixed $value): void` | 70 | | `offsetUnset($offset)` | `offsetUnset(mixed $offset): void` | 71 | | `offsetExists($offset)` | `offsetExists(mixed $offset): bool` | 72 | 73 | ### Doctrine\Common\Collections\AbstractLazyCollection 74 | 75 | | 1.0.x | 3.0.x | 76 | |------------------:|:------------------------| 77 | | `isInitialized()` | `isInitialized(): bool` | 78 | | `initialize()` | `initialize(): void` | 79 | | `doInitialize()` | `doInitialize(): void` | 80 | 81 | ### Doctrine\Common\Collections\ArrayCollection 82 | 83 | | 1.0.x | 3.0.x | 84 | |------------------------------:|:--------------------------------------| 85 | | `createFrom(array $elements)` | `createFrom(array $elements): static` | 86 | | `__toString()` | `__toString(): string` | 87 | 88 | ### Doctrine\Common\Collections\Criteria 89 | 90 | | 1.0.x | 3.0.x | 91 | |------------------------------------------:|:--------------------------------------------| 92 | | `where(Expression $expression): self` | `where(Expression $expression): static` | 93 | | `andWhere(Expression $expression): self` | `andWhere(Expression $expression): static` | 94 | | `orWhere(Expression $expression): self` | `orWhere(Expression $expression): static` | 95 | | `orderBy(array $orderings): self` | `orderBy(array $orderings): static` | 96 | | `setFirstResult(?int $firstResult): self` | `setFirstResult(?int $firstResult): static` | 97 | | `setMaxResult(?int $maxResults): self` | `setMaxResults(?int $maxResults): static` | 98 | 99 | ### Doctrine\Common\Collections\Selectable 100 | 101 | | 1.0.x | 3.0.x | 102 | |-------------------------------:|:-------------------------------------------| 103 | | `matching(Criteria $criteria)` | `matching(Criteria $criteria): Collection` | 104 | 105 | # Upgrade to 1.7 106 | 107 | ## Deprecated null first result 108 | 109 | Passing null as `$firstResult` to 110 | `Doctrine\Common\Collections\Criteria::__construct()` and to 111 | `Doctrine\Common\Collections\Criteria::setFirstResult()` is deprecated. 112 | Use `0` instead. 113 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doctrine/collections", 3 | "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "php", 8 | "collections", 9 | "array", 10 | "iterators" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Guilherme Blanco", 15 | "email": "guilhermeblanco@gmail.com" 16 | }, 17 | { 18 | "name": "Roman Borschel", 19 | "email": "roman@code-factory.org" 20 | }, 21 | { 22 | "name": "Benjamin Eberlei", 23 | "email": "kontakt@beberlei.de" 24 | }, 25 | { 26 | "name": "Jonathan Wage", 27 | "email": "jonwage@gmail.com" 28 | }, 29 | { 30 | "name": "Johannes Schmitt", 31 | "email": "schmittjoh@gmail.com" 32 | } 33 | ], 34 | "homepage": "https://www.doctrine-project.org/projects/collections.html", 35 | "require": { 36 | "php": "^8.1", 37 | "doctrine/deprecations": "^1", 38 | "symfony/polyfill-php84": "^1.30" 39 | }, 40 | "require-dev": { 41 | "ext-json": "*", 42 | "doctrine/coding-standard": "^12", 43 | "phpstan/phpstan": "^1.8", 44 | "phpstan/phpstan-phpunit": "^1.0", 45 | "phpunit/phpunit": "^10.5" 46 | }, 47 | "autoload": { 48 | "psr-4": { 49 | "Doctrine\\Common\\Collections\\": "src" 50 | } 51 | }, 52 | "autoload-dev": { 53 | "psr-4": { 54 | "Doctrine\\Tests\\Common\\Collections\\": "tests" 55 | } 56 | }, 57 | "config": { 58 | "allow-plugins": { 59 | "composer/package-versions-deprecated": true, 60 | "dealerdirect/phpcodesniffer-composer-installer": true 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /docs/en/derived-collections.rst: -------------------------------------------------------------------------------- 1 | Derived Collections 2 | =================== 3 | 4 | You can create custom collection classes by extending the 5 | ``Doctrine\Common\Collections\ArrayCollection`` class. If the 6 | ``__construct`` semantics are different from the default ``ArrayCollection`` 7 | you can override the ``createFrom`` method: 8 | 9 | .. code-block:: php 10 | final class DerivedArrayCollection extends ArrayCollection 11 | { 12 | /** @var \stdClass */ 13 | private $foo; 14 | 15 | public function __construct(\stdClass $foo, array $elements = []) 16 | { 17 | $this->foo = $foo; 18 | 19 | parent::__construct($elements); 20 | } 21 | 22 | protected function createFrom(array $elements) : self 23 | { 24 | return new static($this->foo, $elements); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs/en/expression-builder.rst: -------------------------------------------------------------------------------- 1 | Expression Builder 2 | ================== 3 | 4 | The Expression Builder is a convenient fluent interface for 5 | building expressions to be used with the ``Doctrine\Common\Collections\Criteria`` 6 | class: 7 | 8 | .. code-block:: php 9 | $expressionBuilder = Criteria::expr(); 10 | 11 | $criteria = new Criteria(); 12 | $criteria->where($expressionBuilder->eq('name', 'jwage')); 13 | $criteria->orWhere($expressionBuilder->eq('name', 'romanb')); 14 | 15 | $collection->matching($criteria); 16 | 17 | The ``ExpressionBuilder`` has the following API: 18 | 19 | andX 20 | ---- 21 | 22 | .. code-block:: php 23 | $expressionBuilder = Criteria::expr(); 24 | 25 | $expression = $expressionBuilder->andX( 26 | $expressionBuilder->eq('foo', 1), 27 | $expressionBuilder->eq('bar', 1) 28 | ); 29 | 30 | $collection->matching(new Criteria($expression)); 31 | 32 | orX 33 | --- 34 | 35 | .. code-block:: php 36 | $expressionBuilder = Criteria::expr(); 37 | 38 | $expression = $expressionBuilder->orX( 39 | $expressionBuilder->eq('foo', 1), 40 | $expressionBuilder->eq('bar', 1) 41 | ); 42 | 43 | $collection->matching(new Criteria($expression)); 44 | 45 | not 46 | --- 47 | 48 | .. code-block:: php 49 | $expressionBuilder = Criteria::expr(); 50 | 51 | $expression = $expressionBuilder->not( 52 | $expressionBuilder->eq('foo', 1) 53 | ); 54 | 55 | $collection->matching(new Criteria($expression)); 56 | 57 | eq 58 | --- 59 | 60 | .. code-block:: php 61 | $expressionBuilder = Criteria::expr(); 62 | 63 | $expression = $expressionBuilder->eq('foo', 1); 64 | 65 | $collection->matching(new Criteria($expression)); 66 | 67 | gt 68 | --- 69 | 70 | .. code-block:: php 71 | $expressionBuilder = Criteria::expr(); 72 | 73 | $expression = $expressionBuilder->gt('foo', 1); 74 | 75 | $collection->matching(new Criteria($expression)); 76 | 77 | lt 78 | --- 79 | 80 | .. code-block:: php 81 | $expressionBuilder = Criteria::expr(); 82 | 83 | $expression = $expressionBuilder->lt('foo', 1); 84 | 85 | $collection->matching(new Criteria($expression)); 86 | 87 | gte 88 | --- 89 | 90 | .. code-block:: php 91 | $expressionBuilder = Criteria::expr(); 92 | 93 | $expression = $expressionBuilder->gte('foo', 1); 94 | 95 | $collection->matching(new Criteria($expression)); 96 | 97 | lte 98 | --- 99 | 100 | .. code-block:: php 101 | $expressionBuilder = Criteria::expr(); 102 | 103 | $expression = $expressionBuilder->lte('foo', 1); 104 | 105 | $collection->matching(new Criteria($expression)); 106 | 107 | neq 108 | --- 109 | 110 | .. code-block:: php 111 | $expressionBuilder = Criteria::expr(); 112 | 113 | $expression = $expressionBuilder->neq('foo', 1); 114 | 115 | $collection->matching(new Criteria($expression)); 116 | 117 | isNull 118 | ------ 119 | 120 | .. code-block:: php 121 | $expressionBuilder = Criteria::expr(); 122 | 123 | $expression = $expressionBuilder->isNull('foo'); 124 | 125 | $collection->matching(new Criteria($expression)); 126 | 127 | isNotNull 128 | --------- 129 | 130 | .. code-block:: php 131 | $expressionBuilder = Criteria::expr(); 132 | 133 | $expression = $expressionBuilder->isNotNull('foo'); 134 | 135 | $collection->matching(new Criteria($expression)); 136 | 137 | in 138 | --- 139 | 140 | .. code-block:: php 141 | $expressionBuilder = Criteria::expr(); 142 | 143 | $expression = $expressionBuilder->in('foo', ['value1', 'value2']); 144 | 145 | $collection->matching(new Criteria($expression)); 146 | 147 | notIn 148 | ----- 149 | 150 | .. code-block:: php 151 | $expressionBuilder = Criteria::expr(); 152 | 153 | $expression = $expressionBuilder->notIn('foo', ['value1', 'value2']); 154 | 155 | $collection->matching(new Criteria($expression)); 156 | 157 | contains 158 | -------- 159 | 160 | .. code-block:: php 161 | $expressionBuilder = Criteria::expr(); 162 | 163 | $expression = $expressionBuilder->contains('foo', 'value1'); 164 | 165 | $collection->matching(new Criteria($expression)); 166 | 167 | memberOf 168 | -------- 169 | 170 | .. code-block:: php 171 | $expressionBuilder = Criteria::expr(); 172 | 173 | $expression = $expressionBuilder->memberOf('foo', ['value1', 'value2']); 174 | 175 | $collection->matching(new Criteria($expression)); 176 | 177 | startsWith 178 | ---------- 179 | 180 | .. code-block:: php 181 | $expressionBuilder = Criteria::expr(); 182 | 183 | $expression = $expressionBuilder->startsWith('foo', 'hello'); 184 | 185 | $collection->matching(new Criteria($expression)); 186 | 187 | endsWith 188 | -------- 189 | 190 | .. code-block:: php 191 | $expressionBuilder = Criteria::expr(); 192 | 193 | $expression = $expressionBuilder->endsWith('foo', 'world'); 194 | 195 | $collection->matching(new Criteria($expression)); 196 | -------------------------------------------------------------------------------- /docs/en/expressions.rst: -------------------------------------------------------------------------------- 1 | Expressions 2 | =========== 3 | 4 | The ``Doctrine\Common\Collections\Expr\Comparison`` class 5 | can be used to create comparison expressions to be used with the 6 | ``Doctrine\Common\Collections\Criteria`` class. It has the 7 | following operator constants: 8 | 9 | - ``Comparison::EQ`` 10 | - ``Comparison::NEQ`` 11 | - ``Comparison::LT`` 12 | - ``Comparison::LTE`` 13 | - ``Comparison::GT`` 14 | - ``Comparison::GTE`` 15 | - ``Comparison::IS`` 16 | - ``Comparison::IN`` 17 | - ``Comparison::NIN`` 18 | - ``Comparison::CONTAINS`` 19 | - ``Comparison::MEMBER_OF`` 20 | - ``Comparison::STARTS_WITH`` 21 | - ``Comparison::ENDS_WITH`` 22 | 23 | The ``Doctrine\Common\Collections\Expr\CompositeExpression`` class 24 | can be used to create composite expressions to be used with the 25 | ``Doctrine\Common\Collections\Criteria`` class. It has the 26 | following operator constants: 27 | 28 | - ``CompositeExpression::TYPE_AND`` 29 | - ``CompositeExpression::TYPE_OR`` 30 | - ``CompositeExpression::TYPE_NOT`` 31 | 32 | When using the ``TYPE_OR`` and ``TYPE_AND`` operators the 33 | ``CompositeExpression`` accepts multiple expressions as parameter 34 | but only one expression can be provided when using the ``NOT`` operator. 35 | 36 | The ``Doctrine\Common\Collections\Criteria`` class has the following 37 | API to be used with expressions: 38 | 39 | where 40 | ----- 41 | 42 | Sets the where expression to evaluate when this Criteria is searched for. 43 | 44 | .. code-block:: php 45 | $expr = new Comparison('key', Comparison::EQ, 'value'); 46 | 47 | $criteria->where($expr); 48 | 49 | andWhere 50 | -------- 51 | 52 | Appends the where expression to evaluate when this Criteria is searched for 53 | using an AND with previous expression. 54 | 55 | .. code-block:: php 56 | $expr = new Comparison('key', Comparison::EQ, 'value'); 57 | 58 | $criteria->andWhere($expr); 59 | 60 | orWhere 61 | ------- 62 | 63 | Appends the where expression to evaluate when this Criteria is searched for 64 | using an OR with previous expression. 65 | 66 | .. code-block:: php 67 | $expr1 = new Comparison('key', Comparison::EQ, 'value1'); 68 | $expr2 = new Comparison('key', Comparison::EQ, 'value2'); 69 | 70 | $criteria->where($expr1); 71 | $criteria->orWhere($expr2); 72 | 73 | orderBy 74 | ------- 75 | 76 | Sets the ordering of the result of this Criteria. 77 | 78 | .. code-block:: php 79 | use Doctrine\Common\Collections\Order; 80 | 81 | $criteria->orderBy(['name' => Order::Ascending]); 82 | 83 | setFirstResult 84 | -------------- 85 | 86 | Set the number of first result that this Criteria should return. 87 | 88 | .. code-block:: php 89 | $criteria->setFirstResult(0); 90 | 91 | getFirstResult 92 | -------------- 93 | 94 | Gets the current first result option of this Criteria. 95 | 96 | .. code-block:: php 97 | $criteria->setFirstResult(10); 98 | 99 | echo $criteria->getFirstResult(); // 10 100 | 101 | setMaxResults 102 | ------------- 103 | 104 | Sets the max results that this Criteria should return. 105 | 106 | .. code-block:: php 107 | $criteria->setMaxResults(20); 108 | 109 | getMaxResults 110 | ------------- 111 | 112 | Gets the current max results option of this Criteria. 113 | 114 | .. code-block:: php 115 | $criteria->setMaxResults(20); 116 | 117 | echo $criteria->getMaxResults(); // 20 118 | -------------------------------------------------------------------------------- /docs/en/index.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | Introduction 5 | ------------ 6 | 7 | Doctrine Collections is a library that contains classes for working with 8 | arrays of data. Here is an example using the simple 9 | ``Doctrine\Common\Collections\ArrayCollection`` class: 10 | 11 | .. code-block:: php 12 | filter(function($element) { 19 | return $element > 1; 20 | }); // [2, 3] 21 | 22 | Collection Methods 23 | ------------------ 24 | 25 | Doctrine Collections provides an interface named ``Doctrine\Common\Collections\Collection`` 26 | that resembles the nature of a regular PHP array. That is, 27 | it is essentially an **ordered map** that can also be used 28 | like a list. 29 | 30 | A Collection has an internal iterator just like a PHP array. In addition, 31 | a Collection can be iterated with external iterators, which is preferable. 32 | To use an external iterator simply use the foreach language construct to 33 | iterate over the collection, which calls ``getIterator()`` internally, or 34 | explicitly retrieve an iterator though ``getIterator()`` which can then be 35 | used to iterate over the collection. You can not rely on the internal iterator 36 | of the collection being at a certain position unless you explicitly positioned it before. 37 | 38 | Methods that do not alter the collection or have template types 39 | appearing in invariant or contravariant positions are not directly 40 | defined in ``Doctrine\Common\Collections\Collection``, but are inherited 41 | from the ``Doctrine\Common\Collections\ReadableCollection`` interface. 42 | 43 | The methods available on the interface are: 44 | 45 | add 46 | ^^^ 47 | 48 | Adds an element at the end of the collection. 49 | 50 | .. code-block:: php 51 | $collection->add('test'); 52 | 53 | clear 54 | ^^^^^ 55 | 56 | Clears the collection, removing all elements. 57 | 58 | .. code-block:: php 59 | $collection->clear(); 60 | 61 | contains 62 | ^^^^^^^^ 63 | 64 | Checks whether an element is contained in the collection. This is an O(n) operation, where n is the size of the collection. 65 | 66 | .. code-block:: php 67 | $collection = new Collection(['test']); 68 | 69 | $contains = $collection->contains('test'); // true 70 | 71 | containsKey 72 | ^^^^^^^^^^^ 73 | 74 | Checks whether the collection contains an element with the specified key/index. 75 | 76 | .. code-block:: php 77 | $collection = new Collection(['test' => true]); 78 | 79 | $contains = $collection->containsKey('test'); // true 80 | 81 | current 82 | ^^^^^^^ 83 | 84 | Gets the element of the collection at the current iterator position. 85 | 86 | .. code-block:: php 87 | $collection = new Collection(['first', 'second', 'third']); 88 | 89 | $current = $collection->current(); // first 90 | 91 | get 92 | ^^^ 93 | 94 | Gets the element at the specified key/index. 95 | 96 | .. code-block:: php 97 | $collection = new Collection([ 98 | 'key' => 'value', 99 | ]); 100 | 101 | $value = $collection->get('key'); // value 102 | 103 | getKeys 104 | ^^^^^^^ 105 | 106 | Gets all keys/indices of the collection. 107 | 108 | .. code-block:: php 109 | $collection = new Collection(['a', 'b', 'c']); 110 | 111 | $keys = $collection->getKeys(); // [0, 1, 2] 112 | 113 | getValues 114 | ^^^^^^^^^ 115 | 116 | Gets all values of the collection. 117 | 118 | .. code-block:: php 119 | $collection = new Collection([ 120 | 'key1' => 'value1', 121 | 'key2' => 'value2', 122 | 'key3' => 'value3', 123 | ]); 124 | 125 | $values = $collection->getValues(); // ['value1', 'value2', 'value3'] 126 | 127 | isEmpty 128 | ^^^^^^^ 129 | 130 | Checks whether the collection is empty (contains no elements). 131 | 132 | .. code-block:: php 133 | $collection = new Collection(['a', 'b', 'c']); 134 | 135 | $isEmpty = $collection->isEmpty(); // false 136 | 137 | first 138 | ^^^^^ 139 | 140 | Sets the internal iterator to the first element in the collection and returns this element. 141 | 142 | .. code-block:: php 143 | $collection = new Collection(['first', 'second', 'third']); 144 | 145 | $first = $collection->first(); // first 146 | 147 | exists 148 | ^^^^^^ 149 | 150 | Tests for the existence of an element that satisfies the given predicate. 151 | 152 | .. code-block:: php 153 | $collection = new Collection(['first', 'second', 'third']); 154 | 155 | $exists = $collection->exists(function($key, $value) { 156 | return $value === 'first'; 157 | }); // true 158 | 159 | findFirst 160 | ^^^^^^^^^ 161 | 162 | Returns the first element of this collection that satisfies the given predicate. 163 | 164 | .. code-block:: php 165 | $collection = new Collection([1, 2, 3, 2, 1]); 166 | 167 | $one = $collection->findFirst(function(int $key, int $value): bool { 168 | return $value > 2 && $key > 1; 169 | }); // 3 170 | 171 | filter 172 | ^^^^^^ 173 | 174 | Returns all the elements of this collection for which your callback function returns `true`. 175 | The order and keys of the elements are preserved. 176 | 177 | .. code-block:: php 178 | $collection = new ArrayCollection([1, 2, 3]); 179 | 180 | $filteredCollection = $collection->filter(function($element) { 181 | return $element > 1; 182 | }); // [2, 3] 183 | 184 | forAll 185 | ^^^^^^ 186 | 187 | Tests whether the given predicate holds for all elements of this collection. 188 | 189 | .. code-block:: php 190 | $collection = new ArrayCollection([1, 2, 3]); 191 | 192 | $forAll = $collection->forAll(function($key, $value) { 193 | return $value > 1; 194 | }); // false 195 | 196 | indexOf 197 | ^^^^^^^ 198 | 199 | Gets the index/key of a given element. The comparison of two elements is strict, that means not only the value but also the type must match. For objects this means reference equality. 200 | 201 | .. code-block:: php 202 | $collection = new ArrayCollection([1, 2, 3]); 203 | 204 | $indexOf = $collection->indexOf(3); // 2 205 | 206 | key 207 | ^^^ 208 | 209 | Gets the key/index of the element at the current iterator position. 210 | 211 | .. code-block:: php 212 | $collection = new ArrayCollection([1, 2, 3]); 213 | 214 | $collection->next(); 215 | 216 | $key = $collection->key(); // 1 217 | 218 | last 219 | ^^^^ 220 | 221 | Sets the internal iterator to the last element in the collection and returns this element. 222 | 223 | .. code-block:: php 224 | $collection = new ArrayCollection([1, 2, 3]); 225 | 226 | $last = $collection->last(); // 3 227 | 228 | map 229 | ^^^ 230 | 231 | Applies the given function to each element in the collection and returns a new collection with the elements returned by the function. 232 | 233 | .. code-block:: php 234 | $collection = new ArrayCollection([1, 2, 3]); 235 | 236 | $mappedCollection = $collection->map(function($value) { 237 | return $value + 1; 238 | }); // [2, 3, 4] 239 | 240 | reduce 241 | ^^^^^^ 242 | 243 | Applies iteratively the given function to each element in the collection, so as to reduce the collection to a single value. 244 | 245 | .. code-block:: php 246 | $collection = new ArrayCollection([1, 2, 3]); 247 | 248 | $reduce = $collection->reduce(function(int $accumulator, int $value): int { 249 | return $accumulator + $value; 250 | }, 0); // 6 251 | 252 | next 253 | ^^^^ 254 | 255 | Moves the internal iterator position to the next element and returns this element. 256 | 257 | .. code-block:: php 258 | $collection = new ArrayCollection([1, 2, 3]); 259 | 260 | $next = $collection->next(); // 2 261 | 262 | partition 263 | ^^^^^^^^^ 264 | 265 | Partitions this collection in two collections according to a predicate. Keys are preserved in the resulting collections. 266 | 267 | .. code-block:: php 268 | $collection = new ArrayCollection([1, 2, 3]); 269 | 270 | $mappedCollection = $collection->partition(function($key, $value) { 271 | return $value > 1 272 | }); // [[2, 3], [1]] 273 | 274 | remove 275 | ^^^^^^ 276 | 277 | Removes the element at the specified index from the collection. 278 | 279 | .. code-block:: php 280 | $collection = new ArrayCollection([1, 2, 3]); 281 | 282 | $collection->remove(0); // [2, 3] 283 | 284 | removeElement 285 | ^^^^^^^^^^^^^ 286 | 287 | Removes the specified element from the collection, if it is found. 288 | 289 | .. code-block:: php 290 | $collection = new ArrayCollection([1, 2, 3]); 291 | 292 | $collection->removeElement(3); // [1, 2] 293 | 294 | set 295 | ^^^ 296 | 297 | Sets an element in the collection at the specified key/index. 298 | 299 | .. code-block:: php 300 | $collection = new ArrayCollection(); 301 | 302 | $collection->set('name', 'jwage'); 303 | 304 | slice 305 | ^^^^^ 306 | 307 | Extracts a slice of $length elements starting at position $offset from the Collection. If $length is null it returns all elements from $offset to the end of the Collection. Keys have to be preserved by this method. Calling this method will only return the selected slice and NOT change the elements contained in the collection slice is called on. 308 | 309 | .. code-block:: php 310 | $collection = new ArrayCollection([0, 1, 2, 3, 4, 5]); 311 | 312 | $slice = $collection->slice(1, 2); // [1, 2] 313 | 314 | toArray 315 | ^^^^^^^ 316 | 317 | Gets a native PHP array representation of the collection. 318 | 319 | .. code-block:: php 320 | $collection = new ArrayCollection([0, 1, 2, 3, 4, 5]); 321 | 322 | $array = $collection->toArray(); // [0, 1, 2, 3, 4, 5] 323 | 324 | Selectable Methods 325 | ------------------ 326 | 327 | Some Doctrine Collections, like ``Doctrine\Common\Collections\ArrayCollection``, 328 | implement an interface named ``Doctrine\Common\Collections\Selectable`` 329 | that offers the usage of a powerful expressions API, where conditions 330 | can be applied to a collection to get a result with matching elements 331 | only. 332 | 333 | matching 334 | ^^^^^^^^ 335 | 336 | Selects all elements from a selectable that match the expression and 337 | returns a new collection containing these elements and preserved keys. 338 | 339 | .. code-block:: php 340 | use Doctrine\Common\Collections\Criteria; 341 | use Doctrine\Common\Collections\Expr\Comparison; 342 | 343 | $collection = new ArrayCollection([ 344 | 'wage' => [ 345 | 'name' => 'jwage', 346 | ], 347 | 'roman' => [ 348 | 'name' => 'romanb', 349 | ], 350 | ]); 351 | 352 | $expr = new Comparison('name', '=', 'jwage'); 353 | 354 | $criteria = new Criteria(); 355 | 356 | $criteria->where($expr); 357 | 358 | $matchingCollection = $collection->matching($criteria); // [ 'wage' => [ 'name' => 'jwage' ]] 359 | 360 | You can read more about expressions :ref:`here `. 361 | 362 | .. note:: 363 | 364 | Currently, expressions use strict comparison for the ``EQ`` (equal) and ``NEQ`` (not equal) 365 | checks. That makes them behave more naturally as long as only scalar values are involved. 366 | For example, ``'04'`` and ``4`` are *not* equal. 367 | 368 | However, this can lead to surprising results when working with objects, especially objects 369 | representing values. ``DateTime`` and ``DateTimeImmutable`` are two widespread examples for 370 | objects that would typically rather be compared by their value than by identity. 371 | 372 | Comparative operators like ``GT`` or ``LTE`` as well as ``IN`` and ``NIN`` do 373 | not exhibit this behavior. 374 | 375 | Also, multi-dimensional sorting based on non-scalar values will only consider the 376 | next sort criteria for *identical* matches, which may not give the expected results 377 | when objects come into play. Keep this in mind, for example, when sorting by fields that 378 | contain ``DateTime`` or ``DateTimeImmutable`` objects. 379 | -------------------------------------------------------------------------------- /docs/en/lazy-collections.rst: -------------------------------------------------------------------------------- 1 | Lazy Collections 2 | ================ 3 | 4 | To create a lazy collection you can extend the 5 | ``Doctrine\Common\Collections\AbstractLazyCollection`` class 6 | and define the ``doInitialize`` method. Here is an example where 7 | we lazily query the database for a collection of user records: 8 | 9 | .. code-block:: php 10 | use Doctrine\DBAL\Connection; 11 | 12 | class UsersLazyCollection extends AbstractLazyCollection 13 | { 14 | /** @var Connection */ 15 | private $connection; 16 | 17 | public function __construct(Connection $connection) 18 | { 19 | $this->connection = $connection; 20 | } 21 | 22 | protected function doInitialize() : void 23 | { 24 | $this->collection = $this->connection->fetchAll('SELECT * FROM users'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs/en/serialization.rst: -------------------------------------------------------------------------------- 1 | Serialization 2 | ============= 3 | 4 | Using (un-)serialize() on a collection is not a supported use-case 5 | and may break when changes on the collection's internals happen in the future. 6 | If a collection needs to be serialized, use ``toArray()`` and reconstruct 7 | the collection manually. 8 | 9 | .. code-block:: php 10 | 11 | $collection = new ArrayCollection([1, 2, 3]); 12 | $serialized = serialize($collection->toArray()); 13 | 14 | A reconstruction is also necessary when the collection contains objects with 15 | infinite recursion of dependencies like in this ``json_serialize()`` example: 16 | 17 | .. code-block:: php 18 | 19 | $foo = new Foo(); 20 | $bar = new Bar(); 21 | 22 | $foo->setBar($bar); 23 | $bar->setFoo($foo); 24 | 25 | $collection = new ArrayCollection([$foo]); 26 | $json = json_serialize($collection->toArray()); // recursion detected 27 | 28 | Serializer libraries can be used to create the serialization-output to prevent 29 | errors. 30 | -------------------------------------------------------------------------------- /docs/en/sidebar.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | .. toctree:: 4 | :depth: 3 5 | 6 | index 7 | expressions 8 | expression-builder 9 | derived-collections 10 | lazy-collections 11 | serialization 12 | -------------------------------------------------------------------------------- /src/AbstractLazyCollection.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | abstract class AbstractLazyCollection implements Collection 20 | { 21 | /** 22 | * The backed collection to use 23 | * 24 | * @phpstan-var Collection|null 25 | * @var Collection|null 26 | */ 27 | protected Collection|null $collection; 28 | 29 | protected bool $initialized = false; 30 | 31 | /** 32 | * {@inheritDoc} 33 | * 34 | * @return int 35 | */ 36 | #[ReturnTypeWillChange] 37 | public function count() 38 | { 39 | $this->initialize(); 40 | 41 | return $this->collection->count(); 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | public function add(mixed $element) 48 | { 49 | $this->initialize(); 50 | 51 | $this->collection->add($element); 52 | } 53 | 54 | /** 55 | * {@inheritDoc} 56 | */ 57 | public function clear() 58 | { 59 | $this->initialize(); 60 | $this->collection->clear(); 61 | } 62 | 63 | /** 64 | * {@inheritDoc} 65 | */ 66 | public function contains(mixed $element) 67 | { 68 | $this->initialize(); 69 | 70 | return $this->collection->contains($element); 71 | } 72 | 73 | /** 74 | * {@inheritDoc} 75 | */ 76 | public function isEmpty() 77 | { 78 | $this->initialize(); 79 | 80 | return $this->collection->isEmpty(); 81 | } 82 | 83 | /** 84 | * {@inheritDoc} 85 | */ 86 | public function remove(string|int $key) 87 | { 88 | $this->initialize(); 89 | 90 | return $this->collection->remove($key); 91 | } 92 | 93 | /** 94 | * {@inheritDoc} 95 | */ 96 | public function removeElement(mixed $element) 97 | { 98 | $this->initialize(); 99 | 100 | return $this->collection->removeElement($element); 101 | } 102 | 103 | /** 104 | * {@inheritDoc} 105 | */ 106 | public function containsKey(string|int $key) 107 | { 108 | $this->initialize(); 109 | 110 | return $this->collection->containsKey($key); 111 | } 112 | 113 | /** 114 | * {@inheritDoc} 115 | */ 116 | public function get(string|int $key) 117 | { 118 | $this->initialize(); 119 | 120 | return $this->collection->get($key); 121 | } 122 | 123 | /** 124 | * {@inheritDoc} 125 | */ 126 | public function getKeys() 127 | { 128 | $this->initialize(); 129 | 130 | return $this->collection->getKeys(); 131 | } 132 | 133 | /** 134 | * {@inheritDoc} 135 | */ 136 | public function getValues() 137 | { 138 | $this->initialize(); 139 | 140 | return $this->collection->getValues(); 141 | } 142 | 143 | /** 144 | * {@inheritDoc} 145 | */ 146 | public function set(string|int $key, mixed $value) 147 | { 148 | $this->initialize(); 149 | $this->collection->set($key, $value); 150 | } 151 | 152 | /** 153 | * {@inheritDoc} 154 | */ 155 | public function toArray() 156 | { 157 | $this->initialize(); 158 | 159 | return $this->collection->toArray(); 160 | } 161 | 162 | /** 163 | * {@inheritDoc} 164 | */ 165 | public function first() 166 | { 167 | $this->initialize(); 168 | 169 | return $this->collection->first(); 170 | } 171 | 172 | /** 173 | * {@inheritDoc} 174 | */ 175 | public function last() 176 | { 177 | $this->initialize(); 178 | 179 | return $this->collection->last(); 180 | } 181 | 182 | /** 183 | * {@inheritDoc} 184 | */ 185 | public function key() 186 | { 187 | $this->initialize(); 188 | 189 | return $this->collection->key(); 190 | } 191 | 192 | /** 193 | * {@inheritDoc} 194 | */ 195 | public function current() 196 | { 197 | $this->initialize(); 198 | 199 | return $this->collection->current(); 200 | } 201 | 202 | /** 203 | * {@inheritDoc} 204 | */ 205 | public function next() 206 | { 207 | $this->initialize(); 208 | 209 | return $this->collection->next(); 210 | } 211 | 212 | /** 213 | * {@inheritDoc} 214 | */ 215 | public function exists(Closure $p) 216 | { 217 | $this->initialize(); 218 | 219 | return $this->collection->exists($p); 220 | } 221 | 222 | /** 223 | * {@inheritDoc} 224 | */ 225 | public function findFirst(Closure $p) 226 | { 227 | $this->initialize(); 228 | 229 | return $this->collection->findFirst($p); 230 | } 231 | 232 | /** 233 | * {@inheritDoc} 234 | */ 235 | public function filter(Closure $p) 236 | { 237 | $this->initialize(); 238 | 239 | return $this->collection->filter($p); 240 | } 241 | 242 | /** 243 | * {@inheritDoc} 244 | */ 245 | public function forAll(Closure $p) 246 | { 247 | $this->initialize(); 248 | 249 | return $this->collection->forAll($p); 250 | } 251 | 252 | /** 253 | * {@inheritDoc} 254 | */ 255 | public function map(Closure $func) 256 | { 257 | $this->initialize(); 258 | 259 | return $this->collection->map($func); 260 | } 261 | 262 | /** 263 | * {@inheritDoc} 264 | */ 265 | public function reduce(Closure $func, mixed $initial = null) 266 | { 267 | $this->initialize(); 268 | 269 | return $this->collection->reduce($func, $initial); 270 | } 271 | 272 | /** 273 | * {@inheritDoc} 274 | */ 275 | public function partition(Closure $p) 276 | { 277 | $this->initialize(); 278 | 279 | return $this->collection->partition($p); 280 | } 281 | 282 | /** 283 | * {@inheritDoc} 284 | * 285 | * @template TMaybeContained 286 | */ 287 | public function indexOf(mixed $element) 288 | { 289 | $this->initialize(); 290 | 291 | return $this->collection->indexOf($element); 292 | } 293 | 294 | /** 295 | * {@inheritDoc} 296 | */ 297 | public function slice(int $offset, int|null $length = null) 298 | { 299 | $this->initialize(); 300 | 301 | return $this->collection->slice($offset, $length); 302 | } 303 | 304 | /** 305 | * {@inheritDoc} 306 | * 307 | * @return Traversable 308 | * @phpstan-return Traversable 309 | */ 310 | #[ReturnTypeWillChange] 311 | public function getIterator() 312 | { 313 | $this->initialize(); 314 | 315 | return $this->collection->getIterator(); 316 | } 317 | 318 | /** 319 | * {@inheritDoc} 320 | * 321 | * @param TKey $offset 322 | * 323 | * @return bool 324 | */ 325 | #[ReturnTypeWillChange] 326 | public function offsetExists(mixed $offset) 327 | { 328 | $this->initialize(); 329 | 330 | return $this->collection->offsetExists($offset); 331 | } 332 | 333 | /** 334 | * {@inheritDoc} 335 | * 336 | * @param TKey $offset 337 | * 338 | * @return T|null 339 | */ 340 | #[ReturnTypeWillChange] 341 | public function offsetGet(mixed $offset) 342 | { 343 | $this->initialize(); 344 | 345 | return $this->collection->offsetGet($offset); 346 | } 347 | 348 | /** 349 | * {@inheritDoc} 350 | * 351 | * @param TKey|null $offset 352 | * @param T $value 353 | * 354 | * @return void 355 | */ 356 | #[ReturnTypeWillChange] 357 | public function offsetSet(mixed $offset, mixed $value) 358 | { 359 | $this->initialize(); 360 | $this->collection->offsetSet($offset, $value); 361 | } 362 | 363 | /** 364 | * @param TKey $offset 365 | * 366 | * @return void 367 | */ 368 | #[ReturnTypeWillChange] 369 | public function offsetUnset(mixed $offset) 370 | { 371 | $this->initialize(); 372 | $this->collection->offsetUnset($offset); 373 | } 374 | 375 | /** 376 | * Is the lazy collection already initialized? 377 | * 378 | * @return bool 379 | * 380 | * @phpstan-assert-if-true Collection $this->collection 381 | */ 382 | public function isInitialized() 383 | { 384 | return $this->initialized; 385 | } 386 | 387 | /** 388 | * Initialize the collection 389 | * 390 | * @return void 391 | * 392 | * @phpstan-assert Collection $this->collection 393 | */ 394 | protected function initialize() 395 | { 396 | if ($this->initialized) { 397 | return; 398 | } 399 | 400 | $this->doInitialize(); 401 | $this->initialized = true; 402 | 403 | if ($this->collection === null) { 404 | throw new LogicException('You must initialize the collection property in the doInitialize() method.'); 405 | } 406 | } 407 | 408 | /** 409 | * Do the initialization logic 410 | * 411 | * @return void 412 | */ 413 | abstract protected function doInitialize(); 414 | } 415 | -------------------------------------------------------------------------------- /src/ArrayCollection.php: -------------------------------------------------------------------------------- 1 | 49 | * @template-implements Selectable 50 | * @phpstan-consistent-constructor 51 | */ 52 | class ArrayCollection implements Collection, Selectable, Stringable 53 | { 54 | /** 55 | * An array containing the entries of this collection. 56 | * 57 | * @phpstan-var array 58 | * @var mixed[] 59 | */ 60 | private array $elements = []; 61 | 62 | /** 63 | * Initializes a new ArrayCollection. 64 | * 65 | * @phpstan-param array $elements 66 | */ 67 | public function __construct(array $elements = []) 68 | { 69 | $this->elements = $elements; 70 | } 71 | 72 | /** 73 | * {@inheritDoc} 74 | */ 75 | public function toArray() 76 | { 77 | return $this->elements; 78 | } 79 | 80 | /** 81 | * {@inheritDoc} 82 | */ 83 | public function first() 84 | { 85 | return reset($this->elements); 86 | } 87 | 88 | /** 89 | * Creates a new instance from the specified elements. 90 | * 91 | * This method is provided for derived classes to specify how a new 92 | * instance should be created when constructor semantics have changed. 93 | * 94 | * @param array $elements Elements. 95 | * @phpstan-param array $elements 96 | * 97 | * @return static 98 | * @phpstan-return static 99 | * 100 | * @phpstan-template K of array-key 101 | * @phpstan-template V 102 | */ 103 | protected function createFrom(array $elements) 104 | { 105 | return new static($elements); 106 | } 107 | 108 | /** 109 | * {@inheritDoc} 110 | */ 111 | public function last() 112 | { 113 | return end($this->elements); 114 | } 115 | 116 | /** 117 | * {@inheritDoc} 118 | */ 119 | public function key() 120 | { 121 | return key($this->elements); 122 | } 123 | 124 | /** 125 | * {@inheritDoc} 126 | */ 127 | public function next() 128 | { 129 | return next($this->elements); 130 | } 131 | 132 | /** 133 | * {@inheritDoc} 134 | */ 135 | public function current() 136 | { 137 | return current($this->elements); 138 | } 139 | 140 | /** 141 | * {@inheritDoc} 142 | */ 143 | public function remove(string|int $key) 144 | { 145 | if (! isset($this->elements[$key]) && ! array_key_exists($key, $this->elements)) { 146 | return null; 147 | } 148 | 149 | $removed = $this->elements[$key]; 150 | unset($this->elements[$key]); 151 | 152 | return $removed; 153 | } 154 | 155 | /** 156 | * {@inheritDoc} 157 | */ 158 | public function removeElement(mixed $element) 159 | { 160 | $key = array_search($element, $this->elements, true); 161 | 162 | if ($key === false) { 163 | return false; 164 | } 165 | 166 | unset($this->elements[$key]); 167 | 168 | return true; 169 | } 170 | 171 | /** 172 | * Required by interface ArrayAccess. 173 | * 174 | * @param TKey $offset 175 | * 176 | * @return bool 177 | */ 178 | #[ReturnTypeWillChange] 179 | public function offsetExists(mixed $offset) 180 | { 181 | return $this->containsKey($offset); 182 | } 183 | 184 | /** 185 | * Required by interface ArrayAccess. 186 | * 187 | * @param TKey $offset 188 | * 189 | * @return T|null 190 | */ 191 | #[ReturnTypeWillChange] 192 | public function offsetGet(mixed $offset) 193 | { 194 | return $this->get($offset); 195 | } 196 | 197 | /** 198 | * Required by interface ArrayAccess. 199 | * 200 | * @param TKey|null $offset 201 | * @param T $value 202 | * 203 | * @return void 204 | */ 205 | #[ReturnTypeWillChange] 206 | public function offsetSet(mixed $offset, mixed $value) 207 | { 208 | if ($offset === null) { 209 | $this->add($value); 210 | 211 | return; 212 | } 213 | 214 | $this->set($offset, $value); 215 | } 216 | 217 | /** 218 | * Required by interface ArrayAccess. 219 | * 220 | * @param TKey $offset 221 | * 222 | * @return void 223 | */ 224 | #[ReturnTypeWillChange] 225 | public function offsetUnset(mixed $offset) 226 | { 227 | $this->remove($offset); 228 | } 229 | 230 | /** 231 | * {@inheritDoc} 232 | */ 233 | public function containsKey(string|int $key) 234 | { 235 | return isset($this->elements[$key]) || array_key_exists($key, $this->elements); 236 | } 237 | 238 | /** 239 | * {@inheritDoc} 240 | */ 241 | public function contains(mixed $element) 242 | { 243 | return in_array($element, $this->elements, true); 244 | } 245 | 246 | /** 247 | * {@inheritDoc} 248 | */ 249 | public function exists(Closure $p) 250 | { 251 | return array_any( 252 | $this->elements, 253 | static fn (mixed $element, mixed $key): bool => (bool) $p($key, $element), 254 | ); 255 | } 256 | 257 | /** 258 | * {@inheritDoc} 259 | * 260 | * @phpstan-param TMaybeContained $element 261 | * 262 | * @return int|string|false 263 | * @phpstan-return (TMaybeContained is T ? TKey|false : false) 264 | * 265 | * @template TMaybeContained 266 | */ 267 | public function indexOf($element) 268 | { 269 | return array_search($element, $this->elements, true); 270 | } 271 | 272 | /** 273 | * {@inheritDoc} 274 | */ 275 | public function get(string|int $key) 276 | { 277 | return $this->elements[$key] ?? null; 278 | } 279 | 280 | /** 281 | * {@inheritDoc} 282 | */ 283 | public function getKeys() 284 | { 285 | return array_keys($this->elements); 286 | } 287 | 288 | /** 289 | * {@inheritDoc} 290 | */ 291 | public function getValues() 292 | { 293 | return array_values($this->elements); 294 | } 295 | 296 | /** 297 | * {@inheritDoc} 298 | * 299 | * @return int<0, max> 300 | */ 301 | #[ReturnTypeWillChange] 302 | public function count() 303 | { 304 | return count($this->elements); 305 | } 306 | 307 | /** 308 | * {@inheritDoc} 309 | */ 310 | public function set(string|int $key, mixed $value) 311 | { 312 | $this->elements[$key] = $value; 313 | } 314 | 315 | /** 316 | * {@inheritDoc} 317 | * 318 | * This breaks assumptions about the template type, but it would 319 | * be a backwards-incompatible change to remove this method 320 | */ 321 | public function add(mixed $element) 322 | { 323 | $this->elements[] = $element; 324 | } 325 | 326 | /** 327 | * {@inheritDoc} 328 | */ 329 | public function isEmpty() 330 | { 331 | return empty($this->elements); 332 | } 333 | 334 | /** 335 | * {@inheritDoc} 336 | * 337 | * @return Traversable 338 | * @phpstan-return Traversable 339 | */ 340 | #[ReturnTypeWillChange] 341 | public function getIterator() 342 | { 343 | return new ArrayIterator($this->elements); 344 | } 345 | 346 | /** 347 | * {@inheritDoc} 348 | * 349 | * @phpstan-param Closure(T):U $func 350 | * 351 | * @return static 352 | * @phpstan-return static 353 | * 354 | * @phpstan-template U 355 | */ 356 | public function map(Closure $func) 357 | { 358 | return $this->createFrom(array_map($func, $this->elements)); 359 | } 360 | 361 | /** 362 | * {@inheritDoc} 363 | */ 364 | public function reduce(Closure $func, $initial = null) 365 | { 366 | return array_reduce($this->elements, $func, $initial); 367 | } 368 | 369 | /** 370 | * {@inheritDoc} 371 | * 372 | * @phpstan-param Closure(T, TKey):bool $p 373 | * 374 | * @return static 375 | * @phpstan-return static 376 | */ 377 | public function filter(Closure $p) 378 | { 379 | return $this->createFrom(array_filter($this->elements, $p, ARRAY_FILTER_USE_BOTH)); 380 | } 381 | 382 | /** 383 | * {@inheritDoc} 384 | */ 385 | public function findFirst(Closure $p) 386 | { 387 | return array_find( 388 | $this->elements, 389 | static fn (mixed $element, mixed $key): bool => (bool) $p($key, $element), 390 | ); 391 | } 392 | 393 | /** 394 | * {@inheritDoc} 395 | */ 396 | public function forAll(Closure $p) 397 | { 398 | return array_all( 399 | $this->elements, 400 | static fn (mixed $element, mixed $key): bool => (bool) $p($key, $element), 401 | ); 402 | } 403 | 404 | /** 405 | * {@inheritDoc} 406 | */ 407 | public function partition(Closure $p) 408 | { 409 | $matches = $noMatches = []; 410 | 411 | foreach ($this->elements as $key => $element) { 412 | if ($p($key, $element)) { 413 | $matches[$key] = $element; 414 | } else { 415 | $noMatches[$key] = $element; 416 | } 417 | } 418 | 419 | return [$this->createFrom($matches), $this->createFrom($noMatches)]; 420 | } 421 | 422 | /** 423 | * Returns a string representation of this object. 424 | * {@inheritDoc} 425 | * 426 | * @return string 427 | */ 428 | #[ReturnTypeWillChange] 429 | public function __toString() 430 | { 431 | return self::class . '@' . spl_object_hash($this); 432 | } 433 | 434 | /** 435 | * {@inheritDoc} 436 | */ 437 | public function clear() 438 | { 439 | $this->elements = []; 440 | } 441 | 442 | /** 443 | * {@inheritDoc} 444 | */ 445 | public function slice(int $offset, int|null $length = null) 446 | { 447 | return array_slice($this->elements, $offset, $length, true); 448 | } 449 | 450 | /** @phpstan-return Collection&Selectable */ 451 | public function matching(Criteria $criteria) 452 | { 453 | $expr = $criteria->getWhereExpression(); 454 | $filtered = $this->elements; 455 | 456 | if ($expr) { 457 | $visitor = new ClosureExpressionVisitor(); 458 | $filter = $visitor->dispatch($expr); 459 | $filtered = array_filter($filtered, $filter); 460 | } 461 | 462 | $orderings = $criteria->orderings(); 463 | 464 | if ($orderings) { 465 | $next = null; 466 | foreach (array_reverse($orderings) as $field => $ordering) { 467 | $next = ClosureExpressionVisitor::sortByField($field, $ordering === Order::Descending ? -1 : 1, $next); 468 | } 469 | 470 | uasort($filtered, $next); 471 | } 472 | 473 | $offset = $criteria->getFirstResult(); 474 | $length = $criteria->getMaxResults(); 475 | 476 | if ($offset !== null && $offset > 0 || $length !== null && $length > 0) { 477 | $filtered = array_slice($filtered, (int) $offset, $length, true); 478 | } 479 | 480 | return $this->createFrom($filtered); 481 | } 482 | } 483 | -------------------------------------------------------------------------------- /src/Collection.php: -------------------------------------------------------------------------------- 1 | ordered map that can also be used 15 | * like a list. 16 | * 17 | * A Collection has an internal iterator just like a PHP array. In addition, 18 | * a Collection can be iterated with external iterators, which is preferable. 19 | * To use an external iterator simply use the foreach language construct to 20 | * iterate over the collection (which calls {@link getIterator()} internally) or 21 | * explicitly retrieve an iterator though {@link getIterator()} which can then be 22 | * used to iterate over the collection. 23 | * You can not rely on the internal iterator of the collection being at a certain 24 | * position unless you explicitly positioned it before. Prefer iteration with 25 | * external iterators. 26 | * 27 | * @phpstan-template TKey of array-key 28 | * @phpstan-template T 29 | * @template-extends ReadableCollection 30 | * @template-extends ArrayAccess 31 | */ 32 | interface Collection extends ReadableCollection, ArrayAccess 33 | { 34 | /** 35 | * Adds an element at the end of the collection. 36 | * 37 | * @param mixed $element The element to add. 38 | * @phpstan-param T $element 39 | * 40 | * @return void we will require a native return type declaration in 3.0 41 | */ 42 | public function add(mixed $element); 43 | 44 | /** 45 | * Clears the collection, removing all elements. 46 | * 47 | * @return void 48 | */ 49 | public function clear(); 50 | 51 | /** 52 | * Removes the element at the specified index from the collection. 53 | * 54 | * @param string|int $key The key/index of the element to remove. 55 | * @phpstan-param TKey $key 56 | * 57 | * @return mixed The removed element or NULL, if the collection did not contain the element. 58 | * @phpstan-return T|null 59 | */ 60 | public function remove(string|int $key); 61 | 62 | /** 63 | * Removes the specified element from the collection, if it is found. 64 | * 65 | * @param mixed $element The element to remove. 66 | * @phpstan-param T $element 67 | * 68 | * @return bool TRUE if this collection contained the specified element, FALSE otherwise. 69 | */ 70 | public function removeElement(mixed $element); 71 | 72 | /** 73 | * Sets an element in the collection at the specified key/index. 74 | * 75 | * @param string|int $key The key/index of the element to set. 76 | * @param mixed $value The element to set. 77 | * @phpstan-param TKey $key 78 | * @phpstan-param T $value 79 | * 80 | * @return void 81 | */ 82 | public function set(string|int $key, mixed $value); 83 | 84 | /** 85 | * {@inheritDoc} 86 | * 87 | * @phpstan-param Closure(T):U $func 88 | * 89 | * @return Collection 90 | * @phpstan-return Collection 91 | * 92 | * @phpstan-template U 93 | */ 94 | public function map(Closure $func); 95 | 96 | /** 97 | * {@inheritDoc} 98 | * 99 | * @phpstan-param Closure(T, TKey):bool $p 100 | * 101 | * @return Collection A collection with the results of the filter operation. 102 | * @phpstan-return Collection 103 | */ 104 | public function filter(Closure $p); 105 | 106 | /** 107 | * {@inheritDoc} 108 | * 109 | * @phpstan-param Closure(TKey, T):bool $p 110 | * 111 | * @return Collection[] An array with two elements. The first element contains the collection 112 | * of elements where the predicate returned TRUE, the second element 113 | * contains the collection of elements where the predicate returned FALSE. 114 | * @phpstan-return array{0: Collection, 1: Collection} 115 | */ 116 | public function partition(Closure $p); 117 | } 118 | -------------------------------------------------------------------------------- /src/Criteria.php: -------------------------------------------------------------------------------- 1 | */ 31 | private array $orderings = []; 32 | 33 | private int|null $firstResult = null; 34 | private int|null $maxResults = null; 35 | 36 | /** 37 | * Creates an instance of the class. 38 | * 39 | * @return static 40 | */ 41 | public static function create() 42 | { 43 | return new static(); 44 | } 45 | 46 | /** 47 | * Returns the expression builder. 48 | * 49 | * @return ExpressionBuilder 50 | */ 51 | public static function expr() 52 | { 53 | if (self::$expressionBuilder === null) { 54 | self::$expressionBuilder = new ExpressionBuilder(); 55 | } 56 | 57 | return self::$expressionBuilder; 58 | } 59 | 60 | /** 61 | * Construct a new Criteria. 62 | * 63 | * @param array|null $orderings 64 | */ 65 | public function __construct( 66 | private Expression|null $expression = null, 67 | array|null $orderings = null, 68 | int|null $firstResult = null, 69 | int|null $maxResults = null, 70 | ) { 71 | $this->expression = $expression; 72 | 73 | if ($firstResult === null && func_num_args() > 2) { 74 | Deprecation::trigger( 75 | 'doctrine/collections', 76 | 'https://github.com/doctrine/collections/pull/311', 77 | 'Passing null as $firstResult to the constructor of %s is deprecated. Pass 0 instead or omit the argument.', 78 | self::class, 79 | ); 80 | } 81 | 82 | $this->setFirstResult($firstResult); 83 | $this->setMaxResults($maxResults); 84 | 85 | if ($orderings === null) { 86 | return; 87 | } 88 | 89 | $this->orderBy($orderings); 90 | } 91 | 92 | /** 93 | * Sets the where expression to evaluate when this Criteria is searched for. 94 | * 95 | * @return $this 96 | */ 97 | public function where(Expression $expression) 98 | { 99 | $this->expression = $expression; 100 | 101 | return $this; 102 | } 103 | 104 | /** 105 | * Appends the where expression to evaluate when this Criteria is searched for 106 | * using an AND with previous expression. 107 | * 108 | * @return $this 109 | */ 110 | public function andWhere(Expression $expression) 111 | { 112 | if ($this->expression === null) { 113 | return $this->where($expression); 114 | } 115 | 116 | $this->expression = new CompositeExpression( 117 | CompositeExpression::TYPE_AND, 118 | [$this->expression, $expression], 119 | ); 120 | 121 | return $this; 122 | } 123 | 124 | /** 125 | * Appends the where expression to evaluate when this Criteria is searched for 126 | * using an OR with previous expression. 127 | * 128 | * @return $this 129 | */ 130 | public function orWhere(Expression $expression) 131 | { 132 | if ($this->expression === null) { 133 | return $this->where($expression); 134 | } 135 | 136 | $this->expression = new CompositeExpression( 137 | CompositeExpression::TYPE_OR, 138 | [$this->expression, $expression], 139 | ); 140 | 141 | return $this; 142 | } 143 | 144 | /** 145 | * Gets the expression attached to this Criteria. 146 | * 147 | * @return Expression|null 148 | */ 149 | public function getWhereExpression() 150 | { 151 | return $this->expression; 152 | } 153 | 154 | /** 155 | * Gets the current orderings of this Criteria. 156 | * 157 | * @deprecated use orderings() instead 158 | * 159 | * @return array 160 | */ 161 | public function getOrderings() 162 | { 163 | Deprecation::trigger( 164 | 'doctrine/collections', 165 | 'https://github.com/doctrine/collections/pull/389', 166 | 'Calling %s() is deprecated. Use %s::orderings() instead.', 167 | __METHOD__, 168 | self::class, 169 | ); 170 | 171 | return array_map( 172 | static fn (Order $ordering): string => $ordering->value, 173 | $this->orderings, 174 | ); 175 | } 176 | 177 | /** 178 | * Gets the current orderings of this Criteria. 179 | * 180 | * @return array 181 | */ 182 | public function orderings(): array 183 | { 184 | return $this->orderings; 185 | } 186 | 187 | /** 188 | * Sets the ordering of the result of this Criteria. 189 | * 190 | * Keys are field and values are the order, being a valid Order enum case. 191 | * 192 | * @see Order::Ascending 193 | * @see Order::Descending 194 | * 195 | * @param array $orderings 196 | * 197 | * @return $this 198 | */ 199 | public function orderBy(array $orderings) 200 | { 201 | $method = __METHOD__; 202 | $this->orderings = array_map( 203 | static function (string|Order $ordering) use ($method): Order { 204 | if ($ordering instanceof Order) { 205 | return $ordering; 206 | } 207 | 208 | static $triggered = false; 209 | 210 | if (! $triggered) { 211 | Deprecation::trigger( 212 | 'doctrine/collections', 213 | 'https://github.com/doctrine/collections/pull/389', 214 | 'Passing non-Order enum values to %s() is deprecated. Pass Order enum values instead.', 215 | $method, 216 | ); 217 | } 218 | 219 | $triggered = true; 220 | 221 | return strtoupper($ordering) === Order::Ascending->value ? Order::Ascending : Order::Descending; 222 | }, 223 | $orderings, 224 | ); 225 | 226 | return $this; 227 | } 228 | 229 | /** 230 | * Gets the current first result option of this Criteria. 231 | * 232 | * @return int|null 233 | */ 234 | public function getFirstResult() 235 | { 236 | return $this->firstResult; 237 | } 238 | 239 | /** 240 | * Set the number of first result that this Criteria should return. 241 | * 242 | * @param int|null $firstResult The value to set. 243 | * 244 | * @return $this 245 | */ 246 | public function setFirstResult(int|null $firstResult) 247 | { 248 | if ($firstResult === null) { 249 | Deprecation::triggerIfCalledFromOutside( 250 | 'doctrine/collections', 251 | 'https://github.com/doctrine/collections/pull/311', 252 | 'Passing null to %s() is deprecated, pass 0 instead.', 253 | __METHOD__, 254 | ); 255 | } 256 | 257 | $this->firstResult = $firstResult; 258 | 259 | return $this; 260 | } 261 | 262 | /** 263 | * Gets maxResults. 264 | * 265 | * @return int|null 266 | */ 267 | public function getMaxResults() 268 | { 269 | return $this->maxResults; 270 | } 271 | 272 | /** 273 | * Sets maxResults. 274 | * 275 | * @param int|null $maxResults The value to set. 276 | * 277 | * @return $this 278 | */ 279 | public function setMaxResults(int|null $maxResults) 280 | { 281 | $this->maxResults = $maxResults; 282 | 283 | return $this; 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/Expr/ClosureExpressionVisitor.php: -------------------------------------------------------------------------------- 1 | $accessor(); 63 | } 64 | } 65 | 66 | if (preg_match('/^is[A-Z]+/', $field) === 1 && method_exists($object, $field)) { 67 | return $object->$field(); 68 | } 69 | 70 | // __call should be triggered for get. 71 | $accessor = $accessors[0] . $field; 72 | 73 | if (method_exists($object, '__call')) { 74 | return $object->$accessor(); 75 | } 76 | 77 | if ($object instanceof ArrayAccess) { 78 | return $object[$field]; 79 | } 80 | 81 | if (isset($object->$field)) { 82 | return $object->$field; 83 | } 84 | 85 | // camelcase field name to support different variable naming conventions 86 | $ccField = preg_replace_callback('/_(.?)/', static fn ($matches) => strtoupper((string) $matches[1]), $field); 87 | 88 | foreach ($accessors as $accessor) { 89 | $accessor .= $ccField; 90 | 91 | if (method_exists($object, $accessor)) { 92 | return $object->$accessor(); 93 | } 94 | } 95 | 96 | return $object->$field; 97 | } 98 | 99 | /** 100 | * Helper for sorting arrays of objects based on multiple fields + orientations. 101 | * 102 | * @return Closure 103 | */ 104 | public static function sortByField(string $name, int $orientation = 1, Closure|null $next = null) 105 | { 106 | if (! $next) { 107 | $next = static fn (): int => 0; 108 | } 109 | 110 | return static function ($a, $b) use ($name, $next, $orientation): int { 111 | $aValue = ClosureExpressionVisitor::getObjectFieldValue($a, $name); 112 | 113 | $bValue = ClosureExpressionVisitor::getObjectFieldValue($b, $name); 114 | 115 | if ($aValue === $bValue) { 116 | return $next($a, $b); 117 | } 118 | 119 | return ($aValue > $bValue ? 1 : -1) * $orientation; 120 | }; 121 | } 122 | 123 | /** 124 | * {@inheritDoc} 125 | */ 126 | public function walkComparison(Comparison $comparison) 127 | { 128 | $field = $comparison->getField(); 129 | $value = $comparison->getValue()->getValue(); 130 | 131 | return match ($comparison->getOperator()) { 132 | Comparison::EQ => static fn ($object): bool => self::getObjectFieldValue($object, $field) === $value, 133 | Comparison::NEQ => static fn ($object): bool => self::getObjectFieldValue($object, $field) !== $value, 134 | Comparison::LT => static fn ($object): bool => self::getObjectFieldValue($object, $field) < $value, 135 | Comparison::LTE => static fn ($object): bool => self::getObjectFieldValue($object, $field) <= $value, 136 | Comparison::GT => static fn ($object): bool => self::getObjectFieldValue($object, $field) > $value, 137 | Comparison::GTE => static fn ($object): bool => self::getObjectFieldValue($object, $field) >= $value, 138 | Comparison::IN => static function ($object) use ($field, $value): bool { 139 | $fieldValue = ClosureExpressionVisitor::getObjectFieldValue($object, $field); 140 | 141 | return in_array($fieldValue, $value, is_scalar($fieldValue)); 142 | }, 143 | Comparison::NIN => static function ($object) use ($field, $value): bool { 144 | $fieldValue = ClosureExpressionVisitor::getObjectFieldValue($object, $field); 145 | 146 | return ! in_array($fieldValue, $value, is_scalar($fieldValue)); 147 | }, 148 | Comparison::CONTAINS => static fn ($object): bool => str_contains((string) self::getObjectFieldValue($object, $field), (string) $value), 149 | Comparison::MEMBER_OF => static function ($object) use ($field, $value): bool { 150 | $fieldValues = ClosureExpressionVisitor::getObjectFieldValue($object, $field); 151 | 152 | if (! is_array($fieldValues)) { 153 | $fieldValues = iterator_to_array($fieldValues); 154 | } 155 | 156 | return in_array($value, $fieldValues, true); 157 | }, 158 | Comparison::STARTS_WITH => static fn ($object): bool => str_starts_with((string) self::getObjectFieldValue($object, $field), (string) $value), 159 | Comparison::ENDS_WITH => static fn ($object): bool => str_ends_with((string) self::getObjectFieldValue($object, $field), (string) $value), 160 | default => throw new RuntimeException('Unknown comparison operator: ' . $comparison->getOperator()), 161 | }; 162 | } 163 | 164 | /** 165 | * {@inheritDoc} 166 | */ 167 | public function walkValue(Value $value) 168 | { 169 | return $value->getValue(); 170 | } 171 | 172 | /** 173 | * {@inheritDoc} 174 | */ 175 | public function walkCompositeExpression(CompositeExpression $expr) 176 | { 177 | $expressionList = []; 178 | 179 | foreach ($expr->getExpressionList() as $child) { 180 | $expressionList[] = $this->dispatch($child); 181 | } 182 | 183 | return match ($expr->getType()) { 184 | CompositeExpression::TYPE_AND => $this->andExpressions($expressionList), 185 | CompositeExpression::TYPE_OR => $this->orExpressions($expressionList), 186 | CompositeExpression::TYPE_NOT => $this->notExpression($expressionList), 187 | default => throw new RuntimeException('Unknown composite ' . $expr->getType()), 188 | }; 189 | } 190 | 191 | /** @param callable[] $expressions */ 192 | private function andExpressions(array $expressions): Closure 193 | { 194 | return static fn ($object): bool => array_all( 195 | $expressions, 196 | static fn (callable $expression): bool => (bool) $expression($object), 197 | ); 198 | } 199 | 200 | /** @param callable[] $expressions */ 201 | private function orExpressions(array $expressions): Closure 202 | { 203 | return static fn ($object): bool => array_any( 204 | $expressions, 205 | static fn (callable $expression): bool => (bool) $expression($object), 206 | ); 207 | } 208 | 209 | /** @param callable[] $expressions */ 210 | private function notExpression(array $expressions): Closure 211 | { 212 | return static fn ($object) => ! $expressions[0]($object); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/Expr/Comparison.php: -------------------------------------------------------------------------------- 1 | '; 14 | final public const LT = '<'; 15 | final public const LTE = '<='; 16 | final public const GT = '>'; 17 | final public const GTE = '>='; 18 | final public const IS = '='; // no difference with EQ 19 | final public const IN = 'IN'; 20 | final public const NIN = 'NIN'; 21 | final public const CONTAINS = 'CONTAINS'; 22 | final public const MEMBER_OF = 'MEMBER_OF'; 23 | final public const STARTS_WITH = 'STARTS_WITH'; 24 | final public const ENDS_WITH = 'ENDS_WITH'; 25 | 26 | private readonly Value $value; 27 | 28 | public function __construct(private readonly string $field, private readonly string $op, mixed $value) 29 | { 30 | if (! ($value instanceof Value)) { 31 | $value = new Value($value); 32 | } 33 | 34 | $this->value = $value; 35 | } 36 | 37 | /** @return string */ 38 | public function getField() 39 | { 40 | return $this->field; 41 | } 42 | 43 | /** @return Value */ 44 | public function getValue() 45 | { 46 | return $this->value; 47 | } 48 | 49 | /** @return string */ 50 | public function getOperator() 51 | { 52 | return $this->op; 53 | } 54 | 55 | /** 56 | * {@inheritDoc} 57 | */ 58 | public function visit(ExpressionVisitor $visitor) 59 | { 60 | return $visitor->walkComparison($this); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Expr/CompositeExpression.php: -------------------------------------------------------------------------------- 1 | */ 21 | private array $expressions = []; 22 | 23 | /** 24 | * @param Expression[] $expressions 25 | * 26 | * @throws RuntimeException 27 | */ 28 | public function __construct(private readonly string $type, array $expressions) 29 | { 30 | foreach ($expressions as $expr) { 31 | if ($expr instanceof Value) { 32 | throw new RuntimeException('Values are not supported expressions as children of and/or expressions.'); 33 | } 34 | 35 | if (! ($expr instanceof Expression)) { 36 | throw new RuntimeException('No expression given to CompositeExpression.'); 37 | } 38 | 39 | $this->expressions[] = $expr; 40 | } 41 | 42 | if ($type === self::TYPE_NOT && count($this->expressions) !== 1) { 43 | throw new RuntimeException('Not expression only allows one expression as child.'); 44 | } 45 | } 46 | 47 | /** 48 | * Returns the list of expressions nested in this composite. 49 | * 50 | * @return list 51 | */ 52 | public function getExpressionList() 53 | { 54 | return $this->expressions; 55 | } 56 | 57 | /** @return string */ 58 | public function getType() 59 | { 60 | return $this->type; 61 | } 62 | 63 | /** 64 | * {@inheritDoc} 65 | */ 66 | public function visit(ExpressionVisitor $visitor) 67 | { 68 | return $visitor->walkCompositeExpression($this); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Expr/Expression.php: -------------------------------------------------------------------------------- 1 | $this->walkComparison($expr), 47 | $expr instanceof Value => $this->walkValue($expr), 48 | $expr instanceof CompositeExpression => $this->walkCompositeExpression($expr), 49 | default => throw new RuntimeException('Unknown Expression ' . $expr::class), 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Expr/Value.php: -------------------------------------------------------------------------------- 1 | value; 17 | } 18 | 19 | /** 20 | * {@inheritDoc} 21 | */ 22 | public function visit(ExpressionVisitor $visitor) 23 | { 24 | return $visitor->walkValue($this); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ExpressionBuilder.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | interface ReadableCollection extends Countable, IteratorAggregate 17 | { 18 | /** 19 | * Checks whether an element is contained in the collection. 20 | * This is an O(n) operation, where n is the size of the collection. 21 | * 22 | * @param mixed $element The element to search for. 23 | * @phpstan-param TMaybeContained $element 24 | * 25 | * @return bool TRUE if the collection contains the element, FALSE otherwise. 26 | * @phpstan-return (TMaybeContained is T ? bool : false) 27 | * 28 | * @template TMaybeContained 29 | */ 30 | public function contains(mixed $element); 31 | 32 | /** 33 | * Checks whether the collection is empty (contains no elements). 34 | * 35 | * @return bool TRUE if the collection is empty, FALSE otherwise. 36 | */ 37 | public function isEmpty(); 38 | 39 | /** 40 | * Checks whether the collection contains an element with the specified key/index. 41 | * 42 | * @param string|int $key The key/index to check for. 43 | * @phpstan-param TKey $key 44 | * 45 | * @return bool TRUE if the collection contains an element with the specified key/index, 46 | * FALSE otherwise. 47 | */ 48 | public function containsKey(string|int $key); 49 | 50 | /** 51 | * Gets the element at the specified key/index. 52 | * 53 | * @param string|int $key The key/index of the element to retrieve. 54 | * @phpstan-param TKey $key 55 | * 56 | * @return mixed 57 | * @phpstan-return T|null 58 | */ 59 | public function get(string|int $key); 60 | 61 | /** 62 | * Gets all keys/indices of the collection. 63 | * 64 | * @return int[]|string[] The keys/indices of the collection, in the order of the corresponding 65 | * elements in the collection. 66 | * @phpstan-return list 67 | */ 68 | public function getKeys(); 69 | 70 | /** 71 | * Gets all values of the collection. 72 | * 73 | * @return mixed[] The values of all elements in the collection, in the 74 | * order they appear in the collection. 75 | * @phpstan-return list 76 | */ 77 | public function getValues(); 78 | 79 | /** 80 | * Gets a native PHP array representation of the collection. 81 | * 82 | * @return mixed[] 83 | * @phpstan-return array 84 | */ 85 | public function toArray(); 86 | 87 | /** 88 | * Sets the internal iterator to the first element in the collection and returns this element. 89 | * 90 | * @return mixed 91 | * @phpstan-return T|false 92 | */ 93 | public function first(); 94 | 95 | /** 96 | * Sets the internal iterator to the last element in the collection and returns this element. 97 | * 98 | * @return mixed 99 | * @phpstan-return T|false 100 | */ 101 | public function last(); 102 | 103 | /** 104 | * Gets the key/index of the element at the current iterator position. 105 | * 106 | * @return int|string|null 107 | * @phpstan-return TKey|null 108 | */ 109 | public function key(); 110 | 111 | /** 112 | * Gets the element of the collection at the current iterator position. 113 | * 114 | * @return mixed 115 | * @phpstan-return T|false 116 | */ 117 | public function current(); 118 | 119 | /** 120 | * Moves the internal iterator position to the next element and returns this element. 121 | * 122 | * @return mixed 123 | * @phpstan-return T|false 124 | */ 125 | public function next(); 126 | 127 | /** 128 | * Extracts a slice of $length elements starting at position $offset from the Collection. 129 | * 130 | * If $length is null it returns all elements from $offset to the end of the Collection. 131 | * Keys have to be preserved by this method. Calling this method will only return the 132 | * selected slice and NOT change the elements contained in the collection slice is called on. 133 | * 134 | * @param int $offset The offset to start from. 135 | * @param int|null $length The maximum number of elements to return, or null for no limit. 136 | * 137 | * @return mixed[] 138 | * @phpstan-return array 139 | */ 140 | public function slice(int $offset, int|null $length = null); 141 | 142 | /** 143 | * Tests for the existence of an element that satisfies the given predicate. 144 | * 145 | * @param Closure $p The predicate. 146 | * @phpstan-param Closure(TKey, T):bool $p 147 | * 148 | * @return bool TRUE if the predicate is TRUE for at least one element, FALSE otherwise. 149 | */ 150 | public function exists(Closure $p); 151 | 152 | /** 153 | * Returns all the elements of this collection that satisfy the predicate p. 154 | * The order of the elements is preserved. 155 | * 156 | * @param Closure $p The predicate used for filtering. 157 | * @phpstan-param Closure(T, TKey):bool $p 158 | * 159 | * @return ReadableCollection A collection with the results of the filter operation. 160 | * @phpstan-return ReadableCollection 161 | */ 162 | public function filter(Closure $p); 163 | 164 | /** 165 | * Applies the given function to each element in the collection and returns 166 | * a new collection with the elements returned by the function. 167 | * 168 | * @phpstan-param Closure(T):U $func 169 | * 170 | * @return ReadableCollection 171 | * @phpstan-return ReadableCollection 172 | * 173 | * @phpstan-template U 174 | */ 175 | public function map(Closure $func); 176 | 177 | /** 178 | * Partitions this collection in two collections according to a predicate. 179 | * Keys are preserved in the resulting collections. 180 | * 181 | * @param Closure $p The predicate on which to partition. 182 | * @phpstan-param Closure(TKey, T):bool $p 183 | * 184 | * @return ReadableCollection[] An array with two elements. The first element contains the collection 185 | * of elements where the predicate returned TRUE, the second element 186 | * contains the collection of elements where the predicate returned FALSE. 187 | * @phpstan-return array{0: ReadableCollection, 1: ReadableCollection} 188 | */ 189 | public function partition(Closure $p); 190 | 191 | /** 192 | * Tests whether the given predicate p holds for all elements of this collection. 193 | * 194 | * @param Closure $p The predicate. 195 | * @phpstan-param Closure(TKey, T):bool $p 196 | * 197 | * @return bool TRUE, if the predicate yields TRUE for all elements, FALSE otherwise. 198 | */ 199 | public function forAll(Closure $p); 200 | 201 | /** 202 | * Gets the index/key of a given element. The comparison of two elements is strict, 203 | * that means not only the value but also the type must match. 204 | * For objects this means reference equality. 205 | * 206 | * @param mixed $element The element to search for. 207 | * @phpstan-param TMaybeContained $element 208 | * 209 | * @return int|string|bool The key/index of the element or FALSE if the element was not found. 210 | * @phpstan-return (TMaybeContained is T ? TKey|false : false) 211 | * 212 | * @template TMaybeContained 213 | */ 214 | public function indexOf(mixed $element); 215 | 216 | /** 217 | * Returns the first element of this collection that satisfies the predicate p. 218 | * 219 | * @param Closure $p The predicate. 220 | * @phpstan-param Closure(TKey, T):bool $p 221 | * 222 | * @return mixed The first element respecting the predicate, 223 | * null if no element respects the predicate. 224 | * @phpstan-return T|null 225 | */ 226 | public function findFirst(Closure $p); 227 | 228 | /** 229 | * Applies iteratively the given function to each element in the collection, 230 | * so as to reduce the collection to a single value. 231 | * 232 | * @phpstan-param Closure(TReturn|TInitial, T):TReturn $func 233 | * @phpstan-param TInitial $initial 234 | * 235 | * @return mixed 236 | * @phpstan-return TReturn|TInitial 237 | * 238 | * @phpstan-template TReturn 239 | * @phpstan-template TInitial 240 | */ 241 | public function reduce(Closure $func, mixed $initial = null); 242 | } 243 | -------------------------------------------------------------------------------- /src/Selectable.php: -------------------------------------------------------------------------------- 1 | &Selectable 29 | * @phpstan-return ReadableCollection&Selectable 30 | */ 31 | public function matching(Criteria $criteria); 32 | } 33 | --------------------------------------------------------------------------------