├── phpunit.phar
├── .editorconfig
├── .gitignore
├── ObjectQueryOrder.php
├── QueryContextInterface.php
├── Operation
├── Count.php
├── Max.php
├── Min.php
├── Select.php
├── Each.php
├── SelectOne.php
├── Sum.php
├── Average.php
├── Concat.php
├── SelectMany.php
└── AbstractQueryOperation.php
├── Tests
├── Fixtures
│ ├── Child.php
│ ├── Person.php
│ └── City.php
├── Operation
│ ├── ConcatTest.php
│ ├── EachTest.php
│ ├── CountTest.php
│ ├── MaxTest.php
│ ├── MinTest.php
│ ├── SelectTest.php
│ ├── SumTest.php
│ ├── AverageTest.php
│ ├── SelectManyTest.php
│ └── SelectOneTest.php
├── AbstractQueryTest.php
├── Modifier
│ ├── OffsetTest.php
│ ├── WhereTest.php
│ ├── LimitTest.php
│ └── OrderByTest.php
├── QueryContextTest.php
└── QueryTest.php
├── Exception
├── NonUniqueResultException.php
├── IncompatibleFieldException.php
├── InvalidModifierConfigurationException.php
├── AliasAlreadyTakenInQueryContextException.php
├── IncompatibleCollectionException.php
├── InvalidAliasException.php
└── AlreadyRegisteredWhereFunctionException.php
├── Modifier
├── AbstractQueryModifier.php
├── Limit.php
├── Offset.php
├── Where.php
└── OrderBy.php
├── ObjectQueryContextEnvironment.php
├── QueryOperationInterface.php
├── .github
└── workflows
│ └── php.yml
├── phpunit.xml.dist
├── QueryModifierInterface.php
├── composer.json
├── LICENSE
├── ObjectQueryContext.php
├── QueryInterface.php
├── ObjectQuery.php
└── README.md
/phpunit.phar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexandre-daubois/poq/HEAD/phpunit.phar
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | indent_style = space
6 | indent_size = 4
7 | charset = utf-8
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | ###> symfony/framework-bundle ###
3 | /.env.local
4 | /.env.local.php
5 | /.env.*.local
6 | /config/secrets/prod/prod.decrypt.private.php
7 | /public/bundles/
8 | /var/
9 | /vendor/
10 | ###< symfony/framework-bundle ###
11 |
12 | ###> symfony/phpunit-bridge ###
13 | .phpunit.result.cache
14 | /phpunit.xml
15 | ###< symfony/phpunit-bridge ###
16 |
17 | composer.lock
18 |
--------------------------------------------------------------------------------
/ObjectQueryOrder.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery;
11 |
12 | enum ObjectQueryOrder {
13 | case Ascending;
14 | case Descending;
15 | case None;
16 | case Shuffle;
17 | }
18 |
--------------------------------------------------------------------------------
/QueryContextInterface.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery;
11 |
12 | /**
13 | * Defines the context of a Query. This could be used to pass additional data to the query, such as
14 | * already used alias in the current Query context.
15 | */
16 | interface QueryContextInterface
17 | {
18 | }
19 |
--------------------------------------------------------------------------------
/Operation/Count.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Operation;
11 |
12 | use ObjectQuery\QueryInterface;
13 |
14 | final class Count extends AbstractQueryOperation
15 | {
16 | public function apply(QueryInterface $query): int
17 | {
18 | return \count((array) $this->applySelect($query));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Tests/Fixtures/Child.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Tests\Fixtures;
11 |
12 | class Child
13 | {
14 | public string $name;
15 |
16 | public int $age;
17 |
18 | public function __construct(string $name, int $age)
19 | {
20 | $this->name = $name;
21 | $this->age = $age;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Tests/Fixtures/Person.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Tests\Fixtures;
11 |
12 | class Person
13 | {
14 | public array $children;
15 |
16 | public int $height;
17 |
18 | public function __construct(array $children, int $height)
19 | {
20 | $this->children = $children;
21 | $this->height = $height;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Exception/NonUniqueResultException.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Exception;
11 |
12 | class NonUniqueResultException extends \Exception
13 | {
14 | protected $message = 'The query returned %d result(s). You may use "select" instead of "selectOne"';
15 |
16 | public function __construct(int $count)
17 | {
18 | parent::__construct(\sprintf($this->message, $count));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Exception/IncompatibleFieldException.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Exception;
11 |
12 | class IncompatibleFieldException extends \Exception
13 | {
14 | protected $message = 'The given field is incompatible with "%s" because of the following reason: %s.';
15 |
16 | public function __construct(string $place, string $message)
17 | {
18 | parent::__construct(\sprintf($this->message, $place, $message));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Exception/InvalidModifierConfigurationException.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Exception;
11 |
12 | final class InvalidModifierConfigurationException extends \Exception
13 | {
14 | protected $message = 'The modifier "%s" is wrongly configured: %s.';
15 |
16 | public function __construct(string $modifier, string $message)
17 | {
18 | parent::__construct(\sprintf($this->message, $modifier, $message));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Tests/Fixtures/City.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Tests\Fixtures;
11 |
12 | class City
13 | {
14 | public string $name;
15 |
16 | public array $persons;
17 |
18 | public int $minimalAge;
19 |
20 | public function __construct(string $name, array $persons, int $minimalAge)
21 | {
22 | $this->name = $name;
23 | $this->persons = $persons;
24 | $this->minimalAge = $minimalAge;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Modifier/AbstractQueryModifier.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Modifier;
11 |
12 | use ObjectQuery\ObjectQuery;
13 | use ObjectQuery\QueryModifierInterface;
14 |
15 | abstract class AbstractQueryModifier implements QueryModifierInterface
16 | {
17 | protected readonly ObjectQuery $parentQuery;
18 |
19 | public function __construct(ObjectQuery $parentQuery)
20 | {
21 | $this->parentQuery = $parentQuery;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Exception/AliasAlreadyTakenInQueryContextException.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Exception;
11 |
12 | class AliasAlreadyTakenInQueryContextException extends \Exception
13 | {
14 | protected $message = 'Alias "%s" is already taken in the query. You should choose another name for your alias.';
15 |
16 | public function __construct(string $alias)
17 | {
18 | parent::__construct(\sprintf($this->message, $alias));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Exception/IncompatibleCollectionException.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Exception;
11 |
12 | class IncompatibleCollectionException extends \Exception
13 | {
14 | protected $message = 'The given collection is incompatible with "%s" because of the following reason: %s.';
15 |
16 | public function __construct(string $place, string $message)
17 | {
18 | parent::__construct(\sprintf($this->message, $place, $message));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Exception/InvalidAliasException.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Exception;
11 |
12 | class InvalidAliasException extends \Exception
13 | {
14 | protected $message = 'Alias "%s" is not defined in the context. Available alias are: %s.';
15 |
16 | public function __construct(string $alias, array $availableAliases)
17 | {
18 | parent::__construct(\sprintf($this->message, $alias, \implode(', ', $availableAliases)));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Exception/AlreadyRegisteredWhereFunctionException.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Exception;
11 |
12 | class AlreadyRegisteredWhereFunctionException extends \Exception
13 | {
14 | protected $message = 'Function "%s" has already been globally registered to be used in the "where" clause of ObjectQuery.';
15 |
16 | public function __construct(string $functionName)
17 | {
18 | parent::__construct(\sprintf($this->message, $functionName));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Tests/Operation/ConcatTest.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Tests\Operation;
11 |
12 | use ObjectQuery\ObjectQuery;
13 | use ObjectQuery\Tests\AbstractQueryTest;
14 |
15 | class ConcatTest extends AbstractQueryTest
16 | {
17 | public function testConcat(): void
18 | {
19 | $query = ObjectQuery::from($this->cities);
20 |
21 | $query->selectMany('persons', 'p')
22 | ->selectMany('children', 'c');
23 |
24 | $this->assertSame('Hubert, Alex, Will, Fabien, Nicolas, Salah, Bob', $query->concat(', ', 'name'));
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Operation/Max.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Operation;
11 |
12 | use ObjectQuery\ObjectQuery;
13 | use ObjectQuery\QueryInterface;
14 |
15 | final class Max extends AbstractQueryOperation
16 | {
17 | public function __construct(ObjectQuery $parentQuery, string $field)
18 | {
19 | parent::__construct($parentQuery, $field);
20 | }
21 |
22 | public function apply(QueryInterface $query): mixed
23 | {
24 | $source = (array) $this->applySelect($query);
25 |
26 | return empty($source) ? null : \max($source);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Operation/Min.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Operation;
11 |
12 | use ObjectQuery\ObjectQuery;
13 | use ObjectQuery\QueryInterface;
14 |
15 | final class Min extends AbstractQueryOperation
16 | {
17 | public function __construct(ObjectQuery $parentQuery, string $field)
18 | {
19 | parent::__construct($parentQuery, $field);
20 | }
21 |
22 | public function apply(QueryInterface $query): mixed
23 | {
24 | $source = (array) $this->applySelect($query);
25 |
26 | return empty($source) ? null : \min($source);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Operation/Select.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Operation;
11 |
12 | use ObjectQuery\ObjectQuery;
13 | use ObjectQuery\QueryInterface;
14 |
15 | final class Select extends AbstractQueryOperation
16 | {
17 | public function __construct(ObjectQuery $parentQuery, array|string|null $fields = null)
18 | {
19 | parent::__construct($parentQuery, $fields);
20 |
21 | $this->parentQuery = $parentQuery;
22 | }
23 |
24 | public function apply(QueryInterface $query): iterable
25 | {
26 | return $this->applySelect($query);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Tests/Operation/EachTest.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Tests\Operation;
11 |
12 | use ObjectQuery\ObjectQuery;
13 | use ObjectQuery\Tests\AbstractQueryTest;
14 |
15 | class EachTest extends AbstractQueryTest
16 | {
17 | public function testEach(): void
18 | {
19 | $query = ObjectQuery::from($this->cities);
20 |
21 | $query->selectMany('persons', 'p');
22 |
23 | $result = $query
24 | ->each(fn($element) => $element->height * 2);
25 |
26 | $this->assertSame(362, $result[0]);
27 | $this->assertSame(352, $result[1]);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Tests/Operation/CountTest.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Tests\Operation;
11 |
12 | use ObjectQuery\ObjectQuery;
13 | use ObjectQuery\Tests\AbstractQueryTest;
14 |
15 | class CountTest extends AbstractQueryTest
16 | {
17 | public function testCount(): void
18 | {
19 | $query = ObjectQuery::from($this->cities);
20 | $query
21 | ->selectMany('persons', 'person')
22 | ->selectMany('children', 'child')
23 | ;
24 |
25 | $query->where(fn($child) => $child->age < 9 || $child->age > 28);
26 |
27 | $this->assertSame(3, $query->count());
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Operation/Each.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Operation;
11 |
12 | use ObjectQuery\ObjectQuery;
13 | use ObjectQuery\QueryInterface;
14 |
15 | final class Each extends AbstractQueryOperation
16 | {
17 | private \Closure $callback;
18 |
19 | public function __construct(ObjectQuery $parentQuery, callable $callback)
20 | {
21 | parent::__construct($parentQuery);
22 |
23 | $this->callback = $callback(...);
24 | }
25 |
26 | public function apply(QueryInterface $query): array
27 | {
28 | return \array_map($this->callback, (array) $this->applySelect($query));
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/ObjectQueryContextEnvironment.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery;
11 |
12 | use ObjectQuery\Exception\InvalidAliasException;
13 |
14 | class ObjectQueryContextEnvironment
15 | {
16 | private readonly array $environment;
17 |
18 | public function __construct(array $environment)
19 | {
20 | $this->environment = $environment;
21 | }
22 |
23 | public function get(string $alias): object
24 | {
25 | if (!\array_key_exists($alias, $this->environment)) {
26 | throw new InvalidAliasException($alias, \array_keys($this->environment));
27 | }
28 |
29 | return $this->environment[$alias];
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/QueryOperationInterface.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery;
11 |
12 | /**
13 | * Defines a final operation done to the source, after modifiers has been applied. An operation can be
14 | * a simple concatenation, selecting data, get an average/min/max value, etc.
15 | */
16 | interface QueryOperationInterface
17 | {
18 | /**
19 | * @param QueryInterface $query
20 | * The source to apply the operation on.
21 | *
22 | * @return mixed
23 | * The result of the operation. This can be any type of data, depending on what the operation actually does.
24 | */
25 | public function apply(QueryInterface $query): mixed;
26 | }
27 |
--------------------------------------------------------------------------------
/.github/workflows/php.yml:
--------------------------------------------------------------------------------
1 | name: PHP Composer
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 |
20 | - name: Validate composer.json and composer.lock
21 | run: composer validate --strict
22 |
23 | - name: Cache Composer packages
24 | id: composer-cache
25 | uses: actions/cache@v3
26 | with:
27 | path: vendor
28 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
29 | restore-keys: |
30 | ${{ runner.os }}-php-
31 |
32 | - name: Install dependencies
33 | run: composer install --prefer-dist --no-progress
34 |
35 | - name: Run test suite
36 | run: composer run-script test
37 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ./Tests
19 |
20 |
21 |
22 |
23 |
24 | src
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/QueryModifierInterface.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery;
11 |
12 | /**
13 | * Defines a Query modifier. A modifier is applied before any operation. This could be a `where` clause, as well
14 | * as an ordering, a shuffling, limiting the max number of results, etc.
15 | */
16 | interface QueryModifierInterface
17 | {
18 | /**
19 | * Applies the modifier to the given source, and returns the result of this modifier.
20 | *
21 | * @param QueryInterface $query
22 | * The source to apply the modifier on.
23 | *
24 | * Optional context if needed by the modifier.
25 | *
26 | * @return iterable
27 | * The modified source.
28 | */
29 | public function apply(QueryInterface $query): iterable;
30 | }
31 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "alexandre-daubois/poq",
3 | "type": "library",
4 | "description": "Provides an object-oriented API to query in-memory collections in a SQL-style.",
5 | "keywords": ["query", "linq", "collection", "object"],
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "Alexandre Daubois",
10 | "email": "alex.daubois@gmail.com"
11 | }
12 | ],
13 | "minimum-stability": "stable",
14 | "prefer-stable": true,
15 | "require": {
16 | "php": ">=8.1",
17 | "symfony/property-access": "^6.1"
18 | },
19 | "config": {
20 | "optimize-autoloader": true,
21 | "preferred-install": {
22 | "*": "dist"
23 | },
24 | "sort-packages": true
25 | },
26 | "autoload": {
27 | "psr-4": { "ObjectQuery\\": "" },
28 | "exclude-from-classmap": [
29 | "/Tests/"
30 | ]
31 | },
32 | "scripts": {
33 | "test": "./phpunit.phar"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Operation/SelectOne.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Operation;
11 |
12 | use ObjectQuery\Exception\NonUniqueResultException;
13 | use ObjectQuery\ObjectQuery;
14 | use ObjectQuery\QueryInterface;
15 |
16 | final class SelectOne extends AbstractQueryOperation
17 | {
18 | public function __construct(ObjectQuery $parentQuery, ?string $fields = null)
19 | {
20 | parent::__construct($parentQuery, $fields);
21 |
22 | $this->parentQuery = $parentQuery;
23 | }
24 |
25 | public function apply(QueryInterface $query): mixed
26 | {
27 | $result = (array) $this->applySelect($query);
28 |
29 | $resultCount = \count($result);
30 | if ($resultCount > 1) {
31 | throw new NonUniqueResultException($resultCount);
32 | }
33 |
34 | return $result[0] ?? null;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Operation/Sum.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Operation;
11 |
12 | use ObjectQuery\Exception\IncompatibleCollectionException;
13 | use ObjectQuery\ObjectQuery;
14 | use ObjectQuery\QueryInterface;
15 |
16 | final class Sum extends AbstractQueryOperation
17 | {
18 | public function __construct(ObjectQuery $parentQuery, string $field)
19 | {
20 | parent::__construct($parentQuery, $field);
21 | }
22 |
23 | public function apply(QueryInterface $query): int|float
24 | {
25 | $source = (array) $this->applySelect($query);
26 |
27 | if (\count($source) !== \count(\array_filter($source, 'is_numeric'))) {
28 | throw new IncompatibleCollectionException('sum', 'Operation can only be applied to a collection of numerics');
29 | }
30 |
31 | return \array_sum($source);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Tests/Operation/MaxTest.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Tests\Operation;
11 |
12 | use ObjectQuery\ObjectQuery;
13 | use ObjectQuery\Tests\AbstractQueryTest;
14 |
15 | class MaxTest extends AbstractQueryTest
16 | {
17 | public function testMax(): void
18 | {
19 | $query = (ObjectQuery::from($this->cities))
20 | ->selectMany('persons', 'person')
21 | ->selectMany('children', 'child');
22 |
23 | $this->assertSame(45, $query->max('age'));
24 | }
25 |
26 | public function testMaxWithoutResult(): void
27 | {
28 | $query = (ObjectQuery::from($this->cities))
29 | ->selectMany('persons', 'person')
30 | ->where(fn($person) => $person->height > 190)
31 | ->selectMany('children', 'child');
32 |
33 | $this->assertNull($query->max('age'));
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Operation/Average.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Operation;
11 |
12 | use ObjectQuery\Exception\IncompatibleCollectionException;
13 | use ObjectQuery\ObjectQuery;
14 | use ObjectQuery\QueryInterface;
15 |
16 | final class Average extends AbstractQueryOperation
17 | {
18 | public function __construct(ObjectQuery $parentQuery, string $field)
19 | {
20 | parent::__construct($parentQuery, $field);
21 | }
22 |
23 | public function apply(QueryInterface $query): float
24 | {
25 | $source = (array) $this->applySelect($query);
26 |
27 | $count = \count($source);
28 | if ($count !== \count(\array_filter($source, 'is_numeric'))) {
29 | throw new IncompatibleCollectionException('average', 'Operation can only be applied to a collection of numerics');
30 | }
31 |
32 | return (float) (\array_sum($source) / $count);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Tests/Operation/MinTest.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Tests\Operation;
11 |
12 | use ObjectQuery\ObjectQuery;
13 | use ObjectQuery\Tests\AbstractQueryTest;
14 |
15 | class MinTest extends AbstractQueryTest
16 | {
17 | public function testMin(): void
18 | {
19 | $query = (ObjectQuery::from($this->cities))
20 | ->selectMany('persons', 'person')
21 | ->selectMany('children', 'child');
22 |
23 | $this->assertSame(8, $query->min('age'));
24 | }
25 |
26 | public function testMinWithoutResult(): void
27 | {
28 | $query = (ObjectQuery::from($this->cities))
29 | ->from($this->cities, alias: 'city')
30 | ->selectMany('persons', 'person')
31 | ->selectMany('children', 'child')
32 | ->where(fn($child) => $child->age < 0);
33 |
34 | $this->assertNull($query->min('age'));
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Alexandre Daubois
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Operation/Concat.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Operation;
11 |
12 | use ObjectQuery\ObjectQuery;
13 | use ObjectQuery\QueryInterface;
14 |
15 | final class Concat extends AbstractQueryOperation
16 | {
17 | private readonly string $separator;
18 |
19 | public function __construct(ObjectQuery $parentQuery, string $field, string $separator = ' ')
20 | {
21 | parent::__construct($parentQuery, $field);
22 |
23 | $this->separator = $separator;
24 | }
25 |
26 | public function apply(QueryInterface $query): string
27 | {
28 | $source = (array) $this->applySelect($query);
29 |
30 | $string = '';
31 | foreach ($source as $key => $value) {
32 | $string .= $value;
33 |
34 | if ($key !== \array_key_last($source)) {
35 | $string .= $this->separator;
36 | }
37 | }
38 |
39 | return $string;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Tests/Operation/SelectTest.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Tests\Operation;
11 |
12 | use ObjectQuery\ObjectQuery;
13 | use ObjectQuery\Tests\AbstractQueryTest;
14 |
15 | class SelectTest extends AbstractQueryTest
16 | {
17 | public function testObjectsSelect(): void
18 | {
19 | $query = ObjectQuery::from($this->cities);
20 | $result = $query->select('name');
21 |
22 | $this->assertSame('Lyon', $result[0]);
23 | $this->assertSame('Paris', $result[1]);
24 | }
25 |
26 | public function testObjectsMultipleSelect(): void
27 | {
28 | $query = ObjectQuery::from($this->cities);
29 | $result = $query->select(['name', 'minimalAge']);
30 |
31 | $this->assertSame('Lyon', $result[0]['name']);
32 | $this->assertSame(21, $result[0]['minimalAge']);
33 | $this->assertSame('Paris', $result[1]['name']);
34 | $this->assertSame(10, $result[1]['minimalAge']);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Modifier/Limit.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Modifier;
11 |
12 | use ObjectQuery\Exception\InvalidModifierConfigurationException;
13 | use ObjectQuery\ObjectQuery;
14 | use ObjectQuery\QueryInterface;
15 |
16 | final class Limit extends AbstractQueryModifier
17 | {
18 | private readonly ?int $limit;
19 |
20 | public function __construct(ObjectQuery $parentQuery, ?int $limit)
21 | {
22 | parent::__construct($parentQuery);
23 |
24 | $this->limit = $limit;
25 | }
26 |
27 | public function apply(QueryInterface $query): iterable
28 | {
29 | if (null === $this->limit) {
30 | return $query->getSource();
31 | }
32 |
33 | if ($this->limit <= 0) {
34 | throw new InvalidModifierConfigurationException('limit', 'The limit must be a positive integer or null to set no limit');
35 | }
36 |
37 | return \array_slice((array) $query->getSource(), 0, $this->limit);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Modifier/Offset.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Modifier;
11 |
12 | use ObjectQuery\Exception\InvalidModifierConfigurationException;
13 | use ObjectQuery\ObjectQuery;
14 | use ObjectQuery\QueryInterface;
15 |
16 | final class Offset extends AbstractQueryModifier
17 | {
18 | private readonly ?int $offset;
19 |
20 | public function __construct(ObjectQuery $parentQuery, ?int $offset)
21 | {
22 | parent::__construct($parentQuery);
23 |
24 | $this->offset = $offset;
25 | }
26 |
27 | public function apply(QueryInterface $query): iterable
28 | {
29 | if (null === $this->offset) {
30 | return $query->getSource();
31 | }
32 |
33 | if ($this->offset <= 0) {
34 | throw new InvalidModifierConfigurationException('offset', 'The offset must be a positive integer or null to set no offset.');
35 | }
36 |
37 | return \array_slice((array) $query->getSource(), $this->offset);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Tests/AbstractQueryTest.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Tests;
11 |
12 | use ObjectQuery\Tests\Fixtures\Child;
13 | use ObjectQuery\Tests\Fixtures\City;
14 | use ObjectQuery\Tests\Fixtures\Person;
15 | use PHPUnit\Framework\TestCase;
16 |
17 | abstract class AbstractQueryTest extends TestCase
18 | {
19 | protected const NUMBERS = [5, 4, 1, 3, 9, 8, 6, 7, 2, 0];
20 |
21 | protected array $cities = [];
22 |
23 | protected function setUp(): void
24 | {
25 | $this->cities[] = new City('Lyon', [
26 | new Person([
27 | new Child('Hubert', 30),
28 | new Child('Alex', 26),
29 | new Child('Will', 22),
30 | ], 181),
31 | new Person([
32 | new Child('Fabien', 10),
33 | new Child('Nicolas', 8),
34 | new Child('Salah', 11),
35 | new Child('Bob', 45),
36 | ], 176)
37 | ], 21);
38 |
39 | $this->cities[] = new City('Paris', [], 10);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Modifier/Where.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Modifier;
11 |
12 | use ObjectQuery\ObjectQuery;
13 | use ObjectQuery\ObjectQueryContextEnvironment;
14 | use ObjectQuery\QueryInterface;
15 |
16 | final class Where extends AbstractQueryModifier
17 | {
18 | private readonly \Closure $callback;
19 |
20 | public function __construct(ObjectQuery $parentQuery, callable $callback)
21 | {
22 | parent::__construct($parentQuery);
23 |
24 | $this->callback = $callback(...)->bindTo(null);
25 | }
26 |
27 | public function apply(QueryInterface $query): array
28 | {
29 | $final = [];
30 | foreach ($query->getSource() as $item) {
31 | $localContext = $query->getContext()
32 | ->withEnvironment($item, [$this->parentQuery->getSourceAlias() => $item])
33 | ->getEnvironment($item);
34 |
35 | if (true === \call_user_func($this->callback, $item, new ObjectQueryContextEnvironment($localContext))) {
36 | $final[] = $item;
37 | }
38 | }
39 |
40 | return $final;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Tests/Operation/SumTest.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Tests\Operation;
11 |
12 | use ObjectQuery\Exception\IncompatibleCollectionException;
13 | use ObjectQuery\ObjectQuery;
14 | use ObjectQuery\Tests\AbstractQueryTest;
15 |
16 | class SumTest extends AbstractQueryTest
17 | {
18 | public function testSum(): void
19 | {
20 | $query = (ObjectQuery::from($this->cities))
21 | ->selectMany('persons', 'person');
22 |
23 | $query->selectMany('children', 'child')
24 | ->where(fn($child) => $child->age > 20);
25 |
26 | $this->assertSame(123, $query->sum('age'));
27 | }
28 |
29 | public function testSumOnNonNumericCollection(): void
30 | {
31 | $foo = new class {
32 | public array $collection = [1, 2, 3, 'average'];
33 | };
34 |
35 | $query = ObjectQuery::from([$foo]);
36 |
37 | $this->expectException(IncompatibleCollectionException::class);
38 | $this->expectExceptionMessage('The given collection is incompatible with "sum" because of the following reason: Operation can only be applied to a collection of numerics.');
39 | $query->sum('collection');
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Tests/Operation/AverageTest.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Tests\Operation;
11 |
12 | use ObjectQuery\Exception\IncompatibleCollectionException;
13 | use ObjectQuery\ObjectQuery;
14 | use ObjectQuery\Tests\AbstractQueryTest;
15 |
16 | class AverageTest extends AbstractQueryTest
17 | {
18 | public function testAverage(): void
19 | {
20 | $query = ObjectQuery::from($this->cities);
21 |
22 | $query->selectMany('persons', 'person');
23 |
24 | $query->selectMany('children', 'child')
25 | ->where(fn($child) => $child->age > 20);
26 |
27 | $this->assertSame(30.75, $query->average('age'));
28 | }
29 |
30 | public function testAverageOnNonNumericCollection(): void
31 | {
32 | $foo = new class {
33 | public array $collection = [1, 2, 3, 'average'];
34 | };
35 |
36 | $query = ObjectQuery::from([$foo]);
37 |
38 | $this->expectException(IncompatibleCollectionException::class);
39 | $this->expectExceptionMessage('The given collection is incompatible with "average" because of the following reason: Operation can only be applied to a collection of numerics.');
40 | $query->average('collection');
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Tests/Modifier/OffsetTest.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Tests\Modifier;
11 |
12 | use ObjectQuery\Exception\InvalidModifierConfigurationException;
13 | use ObjectQuery\ObjectQuery;
14 | use ObjectQuery\Tests\AbstractQueryTest;
15 |
16 | class OffsetTest extends AbstractQueryTest
17 | {
18 | public function testOffset(): void
19 | {
20 | $query = ObjectQuery::from($this->cities);
21 |
22 | $result = $query->offset(1)
23 | ->select();
24 |
25 | $this->assertCount(1, $result);
26 | $this->assertSame('Paris', $result[0]->name);
27 | }
28 |
29 | public function testNullOffset(): void
30 | {
31 | $query = ObjectQuery::from($this->cities);
32 |
33 | $result = $query->offset(null)
34 | ->select();
35 |
36 | $this->assertCount(\count($this->cities), $result);
37 | }
38 |
39 | public function testNegativeOffset(): void
40 | {
41 | $query = ObjectQuery::from($this->cities);
42 |
43 | $this->expectException(InvalidModifierConfigurationException::class);
44 | $this->expectExceptionMessage('The offset must be a positive integer or null to set no offset.');
45 | $query->offset(-1)
46 | ->select();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Tests/Operation/SelectManyTest.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Tests\Operation;
11 |
12 | use ObjectQuery\Exception\IncompatibleFieldException;
13 | use ObjectQuery\ObjectQuery;
14 | use ObjectQuery\Tests\AbstractQueryTest;
15 |
16 | class SelectManyTest extends AbstractQueryTest
17 | {
18 | public function testSelectMany(): void
19 | {
20 | $cityQuery = ObjectQuery::from($this->cities);
21 | $result = $cityQuery
22 | ->where(fn($city) => $city->name === 'Lyon')
23 | ->selectMany('persons', '__')
24 | ->where(fn($person) => $person->height > 180)
25 | ->select();
26 |
27 | $this->assertCount(1, $result);
28 | $this->assertSame(181, $result[0]->height);
29 | }
30 |
31 | public function testSelectManyOnScalarField(): void
32 | {
33 | $cityQuery = ObjectQuery::from($this->cities);
34 |
35 | $this->expectException(IncompatibleFieldException::class);
36 | $this->expectExceptionMessage('The given field is incompatible with "selectMany" because of the following reason: You can only selectMany on fields that are collections of objects.');
37 | $cityQuery
38 | ->from($this->cities)
39 | ->selectMany('minimalAge');
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/ObjectQueryContext.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery;
11 |
12 | /**
13 | * @author Alexandre Daubois
14 | * @author Hubert Lenoir
15 | */
16 | class ObjectQueryContext implements QueryContextInterface
17 | {
18 | public function __construct(
19 | /** @var \SplObjectStorage> */
20 | readonly private \SplObjectStorage $environments = new \SplObjectStorage,
21 | /** @var array */
22 | readonly private array $usedAliases = [],
23 | )
24 | {}
25 |
26 | public function getEnvironment(object $environment): array
27 | {
28 | return $this->environments[$environment] ?? [];
29 | }
30 |
31 | public function withEnvironment(object $environment, array $info): QueryContextInterface
32 | {
33 | $environments = clone($this->environments);
34 | $environments[$environment] = $info + $this->getEnvironment($environment);
35 |
36 | return new self($environments, $this->usedAliases);
37 | }
38 |
39 | public function isUsedAlias(string $alias): bool
40 | {
41 | return isset($this->usedAliases[$alias]);
42 | }
43 |
44 | public function withUsedAlias(string $alias): QueryContextInterface
45 | {
46 | return new self(clone($this->environments), $this->usedAliases + [$alias => true]);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Tests/QueryContextTest.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Tests;
11 |
12 | use ObjectQuery\ObjectQueryContext;
13 | use PHPUnit\Framework\TestCase;
14 |
15 | class QueryContextTest extends TestCase
16 | {
17 | public function testWithEnvironment(): void
18 | {
19 | $context = new ObjectQueryContext();
20 |
21 | $environmentA = new \stdClass;
22 | $environmentB = new \stdClass;
23 | $newContext = $context
24 | ->withEnvironment($environmentA, ['a' => 1])
25 | ->withEnvironment($environmentA, ['b' => 2])
26 | ->withEnvironment($environmentB, ['c' => 3])
27 | ;
28 |
29 | $this->assertEmpty((array) $context->getEnvironment(new \stdClass));
30 | $this->assertEmpty($context->getEnvironment($environmentA));
31 | $this->assertEmpty($context->getEnvironment($environmentB));
32 |
33 | $this->assertEmpty($newContext->getEnvironment(new \stdClass));
34 | $this->assertEquals(['a' => 1, 'b' => 2], $newContext->getEnvironment($environmentA));
35 | $this->assertEquals(['c' => 3], $newContext->getEnvironment($environmentB));
36 | }
37 |
38 | public function testWithUsedAlias(): void
39 | {
40 | $context = new ObjectQueryContext();
41 |
42 | $newContext = $context->withUsedAlias('a');
43 |
44 | $this->assertFalse($context->isUsedAlias('a'));
45 | $this->assertFalse($context->isUsedAlias('b'));
46 | $this->assertTrue($newContext->isUsedAlias('a'));
47 | $this->assertFalse($newContext->isUsedAlias('b'));
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Tests/Modifier/WhereTest.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Tests\Modifier;
11 |
12 | use ObjectQuery\ObjectQuery;
13 | use ObjectQuery\ObjectQueryContextEnvironment;
14 | use ObjectQuery\Tests\AbstractQueryTest;
15 |
16 | class WhereTest extends AbstractQueryTest
17 | {
18 | public function testObjectsSelectWhere(): void
19 | {
20 | $query = ObjectQuery::from($this->cities);
21 | $query->where(fn($city): bool => $city->minimalAge > 20);
22 |
23 | $this->assertSame('Lyon', \current($query->select())->name);
24 | }
25 |
26 | public function testWhereWithoutResult(): void
27 | {
28 | $query = ObjectQuery::from($this->cities);
29 | $query->where(fn($city): bool => $city->minimalAge < 1);
30 |
31 | $this->assertEmpty($query->select());
32 | }
33 |
34 | public function testWhereWithAncestorNode(): void
35 | {
36 | $cityQuery = ObjectQuery::from($this->cities, alias: 'city');
37 |
38 | $result = $cityQuery
39 | ->where(fn($city) => \str_contains($city->name, 'Lyon'))
40 | ->selectMany('persons', 'person')
41 | ->where(fn($person) => $person->height > 180)
42 | ->selectMany('children', 'child')
43 | ->where(fn($child, ObjectQueryContextEnvironment $context): bool => $child->age > $context->get('city')->minimalAge)
44 | ->select('name');
45 |
46 | $this->assertCount(3, $result);
47 | $this->assertSame('Hubert', $result[0]);
48 | $this->assertSame('Alex', $result[1]);
49 | $this->assertSame('Will', $result[2]);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Tests/Modifier/LimitTest.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Tests\Modifier;
11 |
12 | use ObjectQuery\Exception\InvalidModifierConfigurationException;
13 | use ObjectQuery\ObjectQuery;
14 | use ObjectQuery\ObjectQueryOrder;
15 | use ObjectQuery\Tests\AbstractQueryTest;
16 |
17 | class LimitTest extends AbstractQueryTest
18 | {
19 | public function testLimit(): void
20 | {
21 | $query = ObjectQuery::from($this->cities);
22 | $result = $query->limit(1)
23 | ->select();
24 |
25 | $this->assertCount(1, $result);
26 | }
27 |
28 | public function testNullLimit(): void
29 | {
30 | $query = ObjectQuery::from($this->cities);
31 | $result = $query->limit(null)
32 | ->select();
33 |
34 | $this->assertCount(\count($this->cities), $result);
35 | }
36 |
37 | public function testNegativeLimit(): void
38 | {
39 | $query = ObjectQuery::from($this->cities);
40 |
41 | $this->expectException(InvalidModifierConfigurationException::class);
42 | $this->expectExceptionMessage('The limit must be a positive integer or null to set no limit.');
43 | $query->from($this->cities)
44 | ->limit(-1)
45 | ->select();
46 | }
47 |
48 | public function testLimitWithObjects(): void
49 | {
50 | $query = ObjectQuery::from($this->cities);
51 |
52 | $query->orderBy(ObjectQueryOrder::Descending, 'minimalAge')
53 | ->limit(1);
54 |
55 | $result = $query->select();
56 | $this->assertCount(1, $result);
57 | $this->assertSame('Lyon', $result[0]->name);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Tests/Operation/SelectOneTest.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Tests\Operation;
11 |
12 | use ObjectQuery\ObjectQuery;
13 | use ObjectQuery\Tests\AbstractQueryTest;
14 | use ObjectQuery\Tests\Fixtures\City;
15 |
16 | class SelectOneTest extends AbstractQueryTest
17 | {
18 | public function testObjectsSelectOne(): void
19 | {
20 | $query = ObjectQuery::from($this->cities);
21 | $result = $query
22 | ->where(fn($city) => $city->name === 'Lyon')
23 | ->selectOne();
24 |
25 | $this->assertInstanceOf(City::class, $result);
26 | $this->assertSame('Lyon', $result->name);
27 | }
28 |
29 | public function testObjectsSelectOneWithoutResult(): void
30 | {
31 | $query = ObjectQuery::from($this->cities);
32 | $result = $query
33 | ->where(fn($city) => $city->name === 'Invalid City')
34 | ->selectOne();
35 |
36 | $this->assertNull($result);
37 | }
38 |
39 | public function testSelectOneWithField(): void
40 | {
41 | $query = ObjectQuery::from($this->cities);
42 | $result = $query
43 | ->where(fn($city) => $city->name === 'Lyon')
44 | ->selectOne('name');
45 |
46 | $this->assertIsString($result);
47 | $this->assertSame('Lyon', $result);
48 | }
49 |
50 | public function testSelectOneWithFieldWithoutResult(): void
51 | {
52 | $query = ObjectQuery::from($this->cities);
53 | $result = $query
54 | ->where(fn($city) => $city->name === 'Rouen')
55 | ->selectOne('name');
56 |
57 | $this->assertNull($result);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/QueryInterface.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery;
11 |
12 | /**
13 | * Defines an object which will execute modifiers and operations to query data.
14 | * A QueryInterface could receive any iterable depending on the implementation. This could go from the
15 | * basic array of data to generators.
16 | */
17 | interface QueryInterface
18 | {
19 | /**
20 | * Creates a new object to query data.
21 | *
22 | * @param iterable $source
23 | * The source to apply manipulations on.
24 | *
25 | * @param QueryContextInterface|null $context
26 | * The optional context that forward needed information to the Query for its execution.
27 | *
28 | * @return QueryInterface
29 | */
30 | public static function from(iterable $source, QueryContextInterface $context = null): QueryInterface;
31 |
32 | /**
33 | * Get the context of the current Query, possibly modified by the latter.
34 | *
35 | * @return QueryContextInterface
36 | */
37 | public function getContext(): QueryContextInterface;
38 |
39 | /**
40 | * Gets the source of the Query.
41 | *
42 | * @return iterable
43 | */
44 | public function getSource(): iterable;
45 |
46 | /**
47 | * Applies a modifier to the Query.
48 | *
49 | * @param QueryModifierInterface $modifier
50 | * The actual modifier to apply.
51 | *
52 | * @return QueryInterface
53 | */
54 | public function applyModifier(QueryModifierInterface $modifier): QueryInterface;
55 |
56 | /**
57 | * Applies an operation to the Query.
58 | *
59 | * @param QueryOperationInterface $operation
60 | * The actual operation to apply.
61 | *
62 | * @return mixed
63 | */
64 | public function applyOperation(QueryOperationInterface $operation): mixed;
65 | }
66 |
--------------------------------------------------------------------------------
/Operation/SelectMany.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Operation;
11 |
12 | use ObjectQuery\Exception\IncompatibleFieldException;
13 | use ObjectQuery\ObjectQuery;
14 | use ObjectQuery\QueryInterface;
15 | use Symfony\Component\PropertyAccess\PropertyAccess;
16 |
17 | final class SelectMany extends AbstractQueryOperation
18 | {
19 | private readonly string $field;
20 | private readonly string $alias;
21 |
22 | public function __construct(ObjectQuery $parentQuery, string $field, string $alias)
23 | {
24 | parent::__construct($parentQuery);
25 |
26 | $this->field = $field;
27 | $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
28 | $this->alias = $alias;
29 | }
30 |
31 | public function apply(QueryInterface $query): ObjectQuery
32 | {
33 | $source = $this->applySelect($query);
34 |
35 | $final = [];
36 | $context = $this->parentQuery->getContext();
37 | foreach ($source as $item) {
38 | $subfields = $this->propertyAccessor->getValue($item, $this->field);
39 |
40 | if (!\is_array($subfields) || \count(\array_filter($subfields, 'is_object')) !== \count($subfields)) {
41 | throw new IncompatibleFieldException('selectMany', 'You can only selectMany on fields that are collections of objects');
42 | }
43 |
44 | foreach ($subfields as $subfield) {
45 | $final[] = $subfield;
46 |
47 | $context = $context->withEnvironment($subfield, [$this->parentQuery->getSourceAlias() => $item]);
48 |
49 | // Transmit current context to descendants
50 | $context = $context->withEnvironment($subfield, $context->getEnvironment($item));
51 | }
52 | }
53 |
54 | return ObjectQuery::from($final, $context, $this->alias);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Tests/Modifier/OrderByTest.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Tests\Modifier;
11 |
12 | use ObjectQuery\Exception\InvalidModifierConfigurationException;
13 | use ObjectQuery\ObjectQuery;
14 | use ObjectQuery\ObjectQueryOrder;
15 | use ObjectQuery\Tests\AbstractQueryTest;
16 |
17 | class OrderByTest extends AbstractQueryTest
18 | {
19 | public function testObjectsAscendingOrderBy(): void
20 | {
21 | $query = ObjectQuery::from($this->cities);
22 |
23 | $query->orderBy(ObjectQueryOrder::Ascending, 'minimalAge');
24 |
25 | $result = $query->select();
26 | $this->assertSame('Paris', $result[0]->name);
27 | $this->assertSame('Lyon', $result[1]->name);
28 | }
29 |
30 | public function testObjectsDescendingOrderBy(): void
31 | {
32 | $query = ObjectQuery::from($this->cities);
33 | $query->orderBy(ObjectQueryOrder::Descending, 'minimalAge');
34 |
35 | $result = $query->select();
36 | $this->assertSame('Lyon', $result[0]->name);
37 | $this->assertSame('Paris', $result[1]->name);
38 | }
39 |
40 | public function testObjectsShuffleWithOrderFieldFailure(): void
41 | {
42 | $query = ObjectQuery::from($this->cities);
43 | $query->orderBy(ObjectQueryOrder::Shuffle, 'minimalAge');
44 |
45 | $this->expectException(InvalidModifierConfigurationException::class);
46 | $this->expectExceptionMessage('The modifier "orderBy" is wrongly configured: An order field must not be provided when shuffling a collection.');
47 | $query->select();
48 | }
49 |
50 | public function testObjectsShuffle(): void
51 | {
52 | $query = ObjectQuery::from($this->cities);
53 |
54 | $query->selectMany('persons', 'person')
55 | ->selectMany('children', 'child')
56 | ->orderBy(ObjectQueryOrder::Shuffle);
57 |
58 | $firstShuffle = $query->concat(', ', 'name');
59 | $secondShuffle = $query->concat(', ', 'name');
60 |
61 | $this->assertNotSame($firstShuffle, $secondShuffle);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Operation/AbstractQueryOperation.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Operation;
11 |
12 | use ObjectQuery\ObjectQuery;
13 | use ObjectQuery\QueryInterface;
14 | use ObjectQuery\QueryOperationInterface;
15 | use Symfony\Component\PropertyAccess\PropertyAccess;
16 | use Symfony\Component\PropertyAccess\PropertyAccessor;
17 |
18 | abstract class AbstractQueryOperation implements QueryOperationInterface
19 | {
20 | protected readonly array|string|null $fields;
21 | protected ObjectQuery $parentQuery;
22 | protected PropertyAccessor $propertyAccessor;
23 |
24 | public function __construct(ObjectQuery $parentQuery, array|string|null $fields = null)
25 | {
26 | $this->parentQuery = $parentQuery;
27 | $this->fields = $fields;
28 | $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
29 | }
30 |
31 | protected function applySelect(QueryInterface $query): iterable
32 | {
33 | if ($where = $query->getWhere()) {
34 | $query->applyModifier($where);
35 | }
36 |
37 | if ($offset = $query->getOffset()) {
38 | $query->applyModifier($offset);
39 | }
40 |
41 | if ($limit = $query->getLimit()) {
42 | $query->applyModifier($limit);
43 | }
44 |
45 | if ($orderBy = $query->getOrderBy()) {
46 | $query->applyModifier($orderBy);
47 | }
48 |
49 | $source = $query->getSource();
50 | if (null !== $this->fields) {
51 | $filteredResult = [];
52 |
53 | foreach ($source as $item) {
54 | if (\is_string($this->fields)) {
55 | $fieldsValues = $this->propertyAccessor->getValue($item, $this->fields);
56 | } else {
57 | $fieldsValues = [];
58 | foreach ($this->fields as $field) {
59 | $fieldsValues[$field] = $this->propertyAccessor->getValue($item, $field);
60 | }
61 | }
62 |
63 | $filteredResult[] = $fieldsValues;
64 | }
65 |
66 | $source = $filteredResult;
67 | }
68 |
69 | return $source;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Modifier/OrderBy.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Modifier;
11 |
12 | use ObjectQuery\Exception\InvalidModifierConfigurationException;
13 | use ObjectQuery\ObjectQuery;
14 | use ObjectQuery\ObjectQueryOrder;
15 | use ObjectQuery\QueryInterface;
16 | use Symfony\Component\PropertyAccess\PropertyAccess;
17 | use Symfony\Component\PropertyAccess\PropertyAccessor;
18 |
19 | final class OrderBy extends AbstractQueryModifier
20 | {
21 | private readonly ObjectQueryOrder $orderBy;
22 | private readonly ?string $orderField;
23 |
24 | protected PropertyAccessor $propertyAccessor;
25 |
26 | public function __construct(ObjectQuery $parentQuery, ObjectQueryOrder $orderBy = ObjectQueryOrder::None, ?string $orderField = null)
27 | {
28 | parent::__construct($parentQuery);
29 |
30 | $this->orderBy = $orderBy;
31 | $this->orderField = $orderField;
32 | $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
33 | }
34 |
35 | public function apply(QueryInterface $query): iterable
36 | {
37 | if (null !== $this->orderField && ObjectQueryOrder::Shuffle === $this->orderBy) {
38 | throw new InvalidModifierConfigurationException('orderBy', 'An order field must not be provided when shuffling a collection');
39 | }
40 |
41 | $source = (array) $query->getSource();
42 |
43 | if (ObjectQueryOrder::Shuffle === $this->orderBy) {
44 | \shuffle($source);
45 |
46 | return $source;
47 | }
48 |
49 | if (ObjectQueryOrder::None !== $this->orderBy) {
50 | if (null === $this->orderField) {
51 | throw new InvalidModifierConfigurationException('orderBy', 'An order field must be provided');
52 | }
53 |
54 | \usort($source, function ($elementA, $elementB) {
55 | return $this->propertyAccessor->getValue($elementA, $this->orderField) <=> $this->propertyAccessor->getValue($elementB, $this->orderField);
56 | });
57 |
58 | if (ObjectQueryOrder::Descending === $this->orderBy) {
59 | $source = \array_reverse($source);
60 | }
61 |
62 | return $source;
63 | }
64 |
65 | return $source;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Tests/QueryTest.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery\Tests;
11 |
12 | use ObjectQuery\Exception\AliasAlreadyTakenInQueryContextException;
13 | use ObjectQuery\Exception\IncompatibleCollectionException;
14 | use ObjectQuery\Exception\InvalidAliasException;
15 | use ObjectQuery\ObjectQuery;
16 | use ObjectQuery\ObjectQueryContextEnvironment;
17 | use ObjectQuery\ObjectQueryOrder;
18 | use ObjectQuery\QueryContextInterface;
19 |
20 | class QueryTest extends AbstractQueryTest
21 | {
22 | public function testSimpleAlias(): void
23 | {
24 | $query = ObjectQuery::from($this->cities, alias: 'city')
25 | ->selectMany('persons', 'person')
26 | ->where(fn($person, ObjectQueryContextEnvironment $context) => $context->get('city')->name === 'Lyon');
27 |
28 | $this->assertCount(2, $query->select());
29 | }
30 |
31 | public function testWrongAlias(): void
32 | {
33 | $query = ObjectQuery::from($this->cities, alias: 'element')
34 | ->selectMany('persons', 'person')
35 | ->where(fn($city, ObjectQueryContextEnvironment $context) => $context->get('city')->name === 'Lyon');
36 |
37 | $this->expectException(InvalidAliasException::class);
38 | $this->expectExceptionMessage('Alias "city" is not defined in the context. Available alias are: person, element.');
39 | $query->select();
40 | }
41 |
42 | public function testAliasAlreadyInUse(): void
43 | {
44 | $this->expectException(AliasAlreadyTakenInQueryContextException::class);
45 | $this->expectExceptionMessage('Alias "__" is already taken in the query. You should choose another name for your alias.');
46 |
47 | $query = ObjectQuery::from($this->cities, alias: '__');
48 | $query->selectMany('persons', '__');
49 | }
50 |
51 | public function testBadContextClass(): void
52 | {
53 | $this->expectException(\InvalidArgumentException::class);
54 | ObjectQuery::from($this->cities, new class implements QueryContextInterface {});
55 | }
56 |
57 | public function testFromScalarCollection(): void
58 | {
59 | $this->expectException(IncompatibleCollectionException::class);
60 | $this->expectExceptionMessage('The given collection is incompatible with "from" because of the following reason: Mixed and scalar collections are not supported. Collection must only contain objects to be used by ObjectQuery.');
61 | ObjectQuery::from(self::NUMBERS);
62 | }
63 |
64 | public function testFromMixedCollection(): void
65 | {
66 | $this->expectException(IncompatibleCollectionException::class);
67 | $this->expectExceptionMessage('The given collection is incompatible with "from" because of the following reason: Mixed and scalar collections are not supported. Collection must only contain objects to be used by ObjectQuery.');
68 | ObjectQuery::from($this->cities + self::NUMBERS);
69 | }
70 |
71 | public function testSelectOnInitialQueryWithSubQueries(): void
72 | {
73 | $query = ObjectQuery::from($this->cities);
74 | $query
75 | ->orderBy(ObjectQueryOrder::Ascending, 'name')
76 | ->limit(1)
77 | ;
78 |
79 | $this->assertSame('Lyon', $query->selectOne('name'));
80 |
81 | $query
82 | ->selectMany('persons', '__')
83 | ;
84 |
85 | $query
86 | ->selectMany('children', '___')
87 | ->where(fn($child) => $child->age >= 30)
88 | ;
89 |
90 | $this->assertSame('Hubert, Bob', $query->concat(', ', 'name'));
91 | }
92 |
93 | public function testSelectOnInitialQueryWithSubQueriesAndIntermediateWhere(): void
94 | {
95 | $query = ObjectQuery::from($this->cities);
96 | $query
97 | ->orderBy(ObjectQueryOrder::Ascending, 'name')
98 | ->limit(1)
99 | ;
100 |
101 | $this->assertSame('Lyon', $query->selectOne('name'));
102 |
103 | $query
104 | ->selectMany('persons', '__')
105 | ->where(fn($person) => $person->height > 180)
106 | ;
107 |
108 | $query
109 | ->selectMany('children', '___')
110 | ->where(fn($child) => $child->age >= 30)
111 | ;
112 |
113 | $this->assertSame('Hubert', $query->selectOne('name'));
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/ObjectQuery.php:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | namespace ObjectQuery;
11 |
12 | use ObjectQuery\Exception\AliasAlreadyTakenInQueryContextException;
13 | use ObjectQuery\Exception\IncompatibleCollectionException;
14 | use ObjectQuery\Modifier\Limit;
15 | use ObjectQuery\Modifier\Offset;
16 | use ObjectQuery\Modifier\OrderBy;
17 | use ObjectQuery\Modifier\Where;
18 | use ObjectQuery\Operation\Average;
19 | use ObjectQuery\Operation\Concat;
20 | use ObjectQuery\Operation\Count;
21 | use ObjectQuery\Operation\Each;
22 | use ObjectQuery\Operation\Max;
23 | use ObjectQuery\Operation\Min;
24 | use ObjectQuery\Operation\Select;
25 | use ObjectQuery\Operation\SelectMany;
26 | use ObjectQuery\Operation\SelectOne;
27 | use ObjectQuery\Operation\Sum;
28 |
29 | class ObjectQuery implements QueryInterface
30 | {
31 | private iterable $source;
32 | private string $sourceAlias;
33 |
34 | private ?Where $where = null;
35 | private ?OrderBy $orderBy = null;
36 | private ?Limit $limit = null;
37 | private ?Offset $offset = null;
38 |
39 | private ObjectQueryContext $context;
40 |
41 | private ?ObjectQuery $subQuery = null;
42 |
43 | private function __construct(iterable $source, string $alias = '_', ObjectQueryContext $context = null)
44 | {
45 | $this->source = $source;
46 | $this->sourceAlias = $alias;
47 | $this->context = $context;
48 | }
49 |
50 | public static function from(iterable $source, QueryContextInterface $context = null, string $alias = '_'): QueryInterface
51 | {
52 | if (null !== $context && !$context instanceof ObjectQueryContext) {
53 | throw new \InvalidArgumentException(\sprintf('Context of class %s is not compatible with ObjectQuery.', $context::class));
54 | }
55 |
56 | $context ??= new ObjectQueryContext();
57 |
58 | if ($context->isUsedAlias($alias)) {
59 | throw new AliasAlreadyTakenInQueryContextException($alias);
60 | }
61 |
62 | foreach ($source as $item) {
63 | if (!\is_object($item)) {
64 | throw new IncompatibleCollectionException('from', 'Mixed and scalar collections are not supported. Collection must only contain objects to be used by ObjectQuery');
65 | }
66 | }
67 |
68 | return new ObjectQuery($source, $alias, $context->withUsedAlias($alias));
69 | }
70 |
71 | public function where(callable $callback): ObjectQuery
72 | {
73 | if ($this->subQuery) {
74 | return $this->subQuery->where($callback);
75 | }
76 |
77 | $this->where = new Where($this, $callback);
78 |
79 | return $this;
80 | }
81 |
82 | public function orderBy(ObjectQueryOrder $order, ?string $field = null): ObjectQuery
83 | {
84 | if ($this->subQuery) {
85 | return $this->subQuery->orderBy($order, $field);
86 | }
87 |
88 | $this->orderBy = new OrderBy($this, $order, $field);
89 |
90 | return $this;
91 | }
92 |
93 | public function limit(?int $limit): ObjectQuery
94 | {
95 | if ($this->subQuery) {
96 | return $this->subQuery->limit($limit);
97 | }
98 |
99 | $this->limit = new Limit($this, $limit);
100 |
101 | return $this;
102 | }
103 |
104 | public function offset(?int $offset): ObjectQuery
105 | {
106 | if ($this->subQuery) {
107 | return $this->subQuery->offset($offset);
108 | }
109 |
110 | $this->offset = new Offset($this, $offset);
111 |
112 | return $this;
113 | }
114 |
115 | public function selectMany(string $field, ?string $alias = '_'): ObjectQuery
116 | {
117 | if ($this->subQuery) {
118 | $this->subQuery->selectMany($field, $alias);
119 |
120 | return $this;
121 | }
122 |
123 | $this->subQuery = (new SelectMany($this, $field, $alias))
124 | ->apply($this);
125 |
126 | return $this;
127 | }
128 |
129 | public function select(array|string|null $fields = null): array
130 | {
131 | return $this->applyOperation(new Select($this, $fields));
132 | }
133 |
134 | public function selectOne(string|null $fields = null): mixed
135 | {
136 | return $this->applyOperation(new SelectOne($this, $fields));
137 | }
138 |
139 | public function count(): int
140 | {
141 | return $this->applyOperation(new Count($this));
142 | }
143 |
144 | public function concat(string $separator = ' ', ?string $field = null): string
145 | {
146 | return $this->applyOperation(new Concat($this, $field, $separator));
147 | }
148 |
149 | public function each(callable $callback): array
150 | {
151 | return $this->applyOperation(new Each($this, $callback));
152 | }
153 |
154 | public function max(?string $field = null): mixed
155 | {
156 | return $this->applyOperation(new Max($this, $field));
157 | }
158 |
159 | public function min(?string $field = null): mixed
160 | {
161 | return $this->applyOperation(new Min($this, $field));
162 | }
163 |
164 | public function average(?string $field = null): float
165 | {
166 | return $this->applyOperation(new Average($this, $field));
167 | }
168 |
169 | public function sum(?string $field = null): int|float
170 | {
171 | return $this->applyOperation(new Sum($this, $field));
172 | }
173 |
174 | public function getSourceAlias(): string
175 | {
176 | return $this->sourceAlias;
177 | }
178 |
179 | public function getWhere(): ?Where
180 | {
181 | return $this->where;
182 | }
183 |
184 | public function getOrderBy(): ?OrderBy
185 | {
186 | return $this->orderBy;
187 | }
188 |
189 | public function getLimit(): ?Limit
190 | {
191 | return $this->limit;
192 | }
193 |
194 | public function getOffset(): ?Offset
195 | {
196 | return $this->offset;
197 | }
198 |
199 | public function getContext(): ObjectQueryContext
200 | {
201 | return $this->context;
202 | }
203 |
204 | public function getSource(): iterable
205 | {
206 | return $this->source;
207 | }
208 |
209 | public function applyOperation(QueryOperationInterface $operation): mixed
210 | {
211 | if ($this->subQuery) {
212 | return $this->subQuery->applyOperation($operation);
213 | }
214 |
215 | return $operation->apply($this);
216 | }
217 |
218 | public function applyModifier(QueryModifierInterface $modifier): QueryInterface
219 | {
220 | if ($this->subQuery) {
221 | return $this->subQuery->applyModifier($modifier);
222 | }
223 |
224 | $this->source = $modifier->apply($this);
225 |
226 | return $this;
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # POQ - PHP Object Query
2 |
3 | [](https://php.net/)
4 | 
5 | [](https://packagist.org/packages/alexandre-daubois/poq)
6 | [](https://packagist.org/packages/alexandre-daubois/poq)
7 |
8 | ## Install
9 |
10 | ```bash
11 | composer require alexandre-daubois/poq 1.0.0-beta2
12 | ```
13 |
14 | That's it, ready to go! 🎉
15 |
16 | ## Usage
17 |
18 | Here is the set of data we're going to use in the follow examples:
19 |
20 | ```php
21 | $cities = [
22 | new City('Lyon', [
23 | new Person([
24 | new Child('Hubert', age: 30),
25 | new Child('Aleksandr', age: 18),
26 | new Child('Alexandre', age: 26),
27 | new Child('Alex', age: 25),
28 | ], height: 181),
29 | new Person([
30 | new Child('Fabien', age: 23),
31 | new Child('Nicolas', age: 8),
32 | ], height: 176),
33 | new Person([
34 | new Child('Alexis', age: 33),
35 | new Child('Pierre', age: 5),
36 | ], height: 185)
37 | ], minimalAge: 21),
38 | new City('Paris', [
39 | new Person([
40 | new Child('Will', age: 33),
41 | new Child('Alix', age: 32),
42 | new Child('Alan', age: 45),
43 | ], height: 185)
44 | ], minimalAge: 45)
45 | ];
46 | ```
47 |
48 | ## The ObjectQuery object
49 |
50 | The `ObjectQuery` object allows you to easily fetch deep information in your collections with ease. Just like Doctrine's `QueryBuilder`, plenty of utils methods are present for easy manipulation.
51 |
52 | Moreover, you're able to create your query step-by-step and conditionally if needed. To create a simple query, all you need to do is call `ObjectQuery`'s `from` factory, and pass your collection as its source:
53 |
54 | ```php
55 | use ObjectQuery\ObjectQuery;
56 |
57 | $query = ObjectQuery::from($this->cities, 'city');
58 | ```
59 |
60 | From here, you're able to manipulate your collections and fetch data. First, let's see how to filter your collections. Note that the `city` argument is optional and defines an alias for the current collection. By default, the alias is `_`. Each alias must be unique in the query, meaning it is mandatory you pass an alias if you are dealing with deep collections. Defining an alias allows you to reference to the object later in the query. See the `selectMany` operation explanation for more details.
61 |
62 | ## Modifiers (filtering, ordering, limiting, etc.)
63 |
64 | Modifiers allow to filter results, order them, limit them and so on.
65 |
66 | > 🔀 Modifiers can be given to the query in any order, as they are only applied when an operation is called.
67 |
68 | ### Where
69 |
70 | #### Usage
71 |
72 | `where`, which takes a callback as an argument. This callback must return a boolean value.
73 |
74 | ```php
75 | use ObjectQuery\ObjectQuery;
76 |
77 | $query = (ObjectQuery::from($this->cities, 'city'))
78 | ->where(
79 | function(City $city) {
80 | return \str_contains($city->name, 'Lyon') || \in_array($city->name, ['Paris', 'Rouen']);
81 | }
82 | );
83 | ```
84 |
85 | ### Order by
86 |
87 | `orderBy`, which will order the collection. If the collection only contains scalar values, then you only have to pass an order. If your collection contains objects, you have to pass the order as well as the field to order on. Available orders are: `QueryOrder::Ascending`, `QueryOrder::Descending`, `QueryOrder::None` and `QueryOrder::Shuffle`.
88 |
89 | ```php
90 | use ObjectQuery\ObjectQuery;
91 | use ObjectQuery\ObjectQueryOrder;
92 |
93 | $query = (ObjectQuery::from($this->cities))
94 | ->orderBy(ObjectQueryOrder::Ascending, 'name');
95 | ```
96 |
97 | ### Offset
98 |
99 | `offset` modifier changes the position of the first element that will be retrieved from the collection. This is particularly useful when doing pagination, in conjunction with the `limit` modifier. The offset must be a positive integer, or `null` to remove any offset.
100 |
101 | ```php
102 | use ObjectQuery\ObjectQuery;
103 |
104 | $query = ObjectQuery::from($this->cities);
105 |
106 | // Skip the 2 first cities of the collection and fetch the rest
107 | $query->offset(2)
108 | ->select();
109 |
110 | // Unset any offset, no data will be skipped
111 | $query->offset(null)
112 | ->select();
113 | ```
114 |
115 | ### Limit
116 |
117 | The `limit` modifier limit the number of results that will be used by different operations, such as `select`. The limit must be a positive integer, or `null` to remove any limit.
118 |
119 | ```php
120 | use ObjectQuery\ObjectQuery;
121 |
122 | $query = ObjectQuery::from($this->cities);
123 |
124 | // Only the first 2 results will be fetched by the `select` operation
125 | $query->limit(2)
126 | ->select();
127 |
128 | // Unset any limitation, all matching results will be used in the `select` operations
129 | $query->limit(null)
130 | ->select();
131 | ```
132 |
133 | ## Operations
134 |
135 | Operations allow you to fetch filtered data in a certain format. Here is a list of the available operations and how to use them.
136 |
137 | ### Select
138 |
139 | This is the most basic operation. It returns filtered data of the query. It is possible to pass the exact field we want to retrieve, as well as multiple fields. If no argument is passed to `select`, it will retrieve the whole object. You must not pass any argument when dealing with scalar collections.
140 |
141 | ```php
142 | use ObjectQuery\ObjectQuery;
143 |
144 | $query = ObjectQuery::from($this->cities);
145 |
146 | // Retrieve the whole object
147 | $query->select();
148 |
149 | // Retrieve one field
150 | $query->select('name');
151 |
152 | // Retrieve multiple fields
153 | $query->select(['name', 'minimalAge']);
154 | ```
155 |
156 | ### Select One
157 |
158 | When querying a collection, and we know in advance that only one result is going to match, this could be redundant to use `select` and retrieve result array's first element everytime. `selectOne` is designed exactly for this case. The behavior of this operation is the following:
159 |
160 | * If a single result is found, it will be returned directly without enclosing it in an array of 1 element.
161 | * If no result is found, the `selectOne` operation returns `null`.
162 | * If more than on result is found, then a `NonUniqueResultException` is thrown.
163 |
164 | ```php
165 | use ObjectQuery\Exception\NonUniqueResultException;
166 |
167 | $query = (ObjectQuery::from($this->cities, 'city'))
168 | ->where(fn($city) => $city->name === 'Lyon');
169 |
170 | try {
171 | $city = $query->selectOne(); // $city is an instance of City
172 |
173 | // You can also query a precise field
174 | $cityName = $query->selectOne('name'); // $cityName is a string
175 | } catch (NonUniqueResultException) {
176 | // ...
177 | }
178 | ```
179 |
180 | ### Select Many
181 |
182 | This operation allows you to go deeper in a collection. Let's say your collection contains many objects with collections inside them, this is what you're going to use to fetch and filter collections.
183 |
184 | Note that we defined an alias for city, **which allows to reference the parent city in the last `where` call**.
185 |
186 | ```php
187 | use ObjectQuery\ObjectQuery;
188 | use ObjectQuery\ObjectQueryContextEnvironment;
189 |
190 | $query = (ObjectQuery::from($this->cities, 'city'))
191 | ->where(fn($city) => \in_array($city->name, ['Paris', 'Rouen']))
192 | ->selectMany('persons', 'person')
193 | ->where(fn($person) => $person->height >= 180)
194 | ->selectMany('children', 'child')
195 | ->where(fn($child, ObjectQueryContextEnvironment $context) => \str_starts_with($child->name, 'Al') && $child->age >= $context->get('city')->minimalAge);
196 | ```
197 |
198 | Like `from`, `selectMany` also takes an alias as an argument. This way, you will be able to reference ancestors in your `where` calls, as shown in the above example.
199 |
200 | ### Count
201 |
202 | This operation returns the size of the current filtered collection:
203 |
204 | ```php
205 | $query = ObjectQuery::from($this->cities);
206 |
207 | $query->count();
208 | ```
209 |
210 | ### Concat
211 |
212 | This operation will concatenate the collection with a given separator. If you're dealing with a scalar collection, there is no mandatory argument. If dealing with collections of objects, the `field` argument must be passed.
213 |
214 | ```php
215 | $query = ObjectQuery::from($this->cities);
216 |
217 | $query->concat(', ', 'name');
218 | ```
219 |
220 | ### Each
221 |
222 | This operation allows you to pass a callback, which will be applied to each element of the filtered collection. You can see this as a `foreach`.
223 |
224 | ```php
225 | $query = ObjectQuery::from($this->cities);
226 |
227 | // Append an exclamation point to every city name
228 | $query->each(fn($element) => $element->name.' !');
229 | ```
230 |
231 | ### Min and Max
232 |
233 | These operations will return the maximum and the minimum of the collection. You can use this on scalar collections. Internally, these operations use `min()` and `max()` functions of the Standard PHP Library, so the same rules apply.
234 |
235 | ```php
236 | use ObjectQuery\ObjectQuery;
237 |
238 | $query = (ObjectQuery::from($this->cities))
239 | ->selectMany('persons', 'person')
240 | ->selectMany('children', 'child');
241 |
242 | $query->min('age'); // 5
243 | $query->max('age'); // 45
244 | $query->min('name'); // "Alan"
245 | $query->max('name'); // "Will"
246 | ```
247 |
248 | ### Sum
249 |
250 | `sum` returns the sum of a collection. If the collection contains objects, a field must be provided in order to calculate the sum of it. This only works with collections of numerics, and an exception is thrown if any item of the collection returns `false` to the `\is_numeric()` function.
251 |
252 | ```php
253 | use ObjectQuery\ObjectQuery;
254 |
255 | $query = (ObjectQuery::from($this->cities))
256 | ->selectMany('persons', 'person')
257 | ->selectMany('children', 'child');
258 |
259 | $query->sum('age');
260 | ```
261 |
262 | ### Average
263 |
264 | `average` returns the average of a collection. If the collection contains objects, a field must be provided in order to calculate the average of it. This only works with collections of numerics, and an exception is thrown if any item of the collection returns `false` to the `\is_numeric()` function.
265 |
266 | ```php
267 | use ObjectQuery\ObjectQuery;
268 |
269 | $query = (ObjectQuery::from($this->cities))
270 | ->selectMany('persons', 'person')
271 | ->selectMany('children', 'child');
272 |
273 | $query->average('age');
274 | ```
275 |
--------------------------------------------------------------------------------