├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── .php-cs-fixer.php
├── CODEOWNERS
├── Dockerfile
├── LICENSE
├── README.md
├── bin
└── graphql-client
├── build
└── .gitignore
├── composer.json
├── docker-compose.yml
├── phpunit.xml
├── rector.php
├── src
├── Client.php
├── ClientBuilder.php
├── Config
│ ├── MutationConfigBuilder.php
│ ├── MutationTypeConfig.php
│ └── MutationsConfig.php
├── Console
│ └── Mutation
│ │ ├── GenerateConfig.php
│ │ └── GetIntrospection.php
├── DataObjectBuilder.php
├── DataObjects
│ ├── AbstractCollection.php
│ ├── AbstractItem.php
│ ├── AbstractObject.php
│ ├── CollectionIterator.php
│ ├── Interfaces
│ │ └── DataObject.php
│ ├── Mutation
│ │ ├── Collection.php
│ │ ├── FilteredCollection.php
│ │ ├── Item.php
│ │ ├── MutationObject.php
│ │ └── Traits
│ │ │ └── MutationObjectHandler.php
│ └── Query
│ │ ├── Collection.php
│ │ ├── Item.php
│ │ └── QueryObject.php
├── Exceptions
│ └── InaccessibleArgumentException.php
├── Mutation.php
├── Response.php
├── ResponseBuilder.php
└── Traits
│ ├── CollectionArrayAccess.php
│ ├── ItemIterator.php
│ └── JsonPathAccessor.php
└── tests
├── ClientBuilderTest.php
├── ClientTest.php
├── Config
└── MutationsConfigTest.php
├── ConfigGeneratorTest.php
├── DataObjectBuilderTest.php
├── MutationBuilderTest.php
├── MutationTest.php
├── Query
└── CollectionTest.php
├── ResponseBuilderTest.php
├── Traits
└── JsonPathAccessorTest.php
└── fixtures
├── config-from-mutation
├── config-from-query
└── introspection.json
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 |
17 | - name: Validate composer.json and composer.lock
18 | run: composer validate --strict
19 |
20 | - name: Cache Composer packages
21 | id: composer-cache
22 | uses: actions/cache@v3
23 | with:
24 | path: vendor
25 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
26 | restore-keys: ${{ runner.os }}-php-
27 |
28 | - name: Setup PHP
29 | uses: shivammathur/setup-php@v2
30 | with:
31 | php-version: '8.3'
32 |
33 | - name: Install dependencies
34 | run: composer install --prefer-dist --no-progress
35 |
36 | - name: Run test suite
37 | run: composer run-script tests
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.cache
2 | *.lock
3 | vendor
4 | build
5 |
--------------------------------------------------------------------------------
/.php-cs-fixer.php:
--------------------------------------------------------------------------------
1 | in(__DIR__ . '/src')
4 | ->in(__DIR__ . '/tests');
5 |
6 | return (new PhpCsFixer\Config())
7 | ->setRules([
8 | '@PSR2' => true,
9 | 'array_syntax' => ['syntax' => 'short'],
10 | 'concat_space' => ['spacing' => 'one'],
11 | 'new_with_parentheses' => true,
12 | 'no_blank_lines_after_phpdoc' => true,
13 | 'no_empty_phpdoc' => true,
14 | 'no_empty_comment' => true,
15 | 'no_leading_import_slash' => true,
16 | 'no_trailing_comma_in_singleline' => true,
17 | 'no_unused_imports' => true,
18 | 'ordered_imports' => ['imports_order' => null, 'sort_algorithm' => 'alpha'],
19 | 'phpdoc_add_missing_param_annotation' => ['only_untyped' => true],
20 | 'phpdoc_align' => true,
21 | 'phpdoc_no_empty_return' => true,
22 | 'phpdoc_order' => true,
23 | 'phpdoc_scalar' => true,
24 | 'phpdoc_to_comment' => true,
25 | 'psr_autoloading' => true,
26 | 'return_type_declaration' => ['space_before' => 'none'],
27 | 'blank_lines_before_namespace' => true,
28 | 'single_quote' => true,
29 | 'space_after_semicolon' => true,
30 | 'ternary_operator_spaces' => true,
31 | 'trailing_comma_in_multiline' => true,
32 | 'trim_array_spaces' => true,
33 | 'whitespace_after_comma_in_array' => true,
34 | ])
35 | ->setFinder($finder);
36 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @joskfg @xaviapa
2 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM composer:2.2
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2019 Softonic International S.A.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PHP GraphQL Client
2 |
3 | [](https://github.com/softonic/graphql-client/releases)
4 | [](LICENSE.md)
5 | [](https://github.com/softonic/graphql-client/actions/workflows/build.yml)
6 | [](https://packagist.org/packages/softonic/graphql-client)
7 | [](http://isitmaintained.com/project/softonic/graphql-client "Average time to resolve an issue")
8 | [](http://isitmaintained.com/project/softonic/graphql-client "Percentage of issues still open")
9 |
10 | PHP Client for [GraphQL](http://graphql.org/)
11 |
12 | ## Main features
13 |
14 | * Client with Oauth2 Support
15 | * Easy query/mutation execution
16 | * Simple array results for mutation and queries
17 | * Powerful object results for mutation and queries
18 | * Filter results
19 | * Manipulate results precisely and bulk
20 | * Transform query results in mutations
21 |
22 | ## Installation
23 |
24 | Via composer:
25 | ```
26 | composer require softonic/graphql-client
27 | ```
28 |
29 | ## Documentation
30 |
31 | ### Instantiate a client
32 |
33 | You can instantiate a simple client or with Oauth2 support.
34 |
35 | #### Simple Client
36 | ```php
37 | 'myclient',
53 | 'clientSecret' => 'mysecret',
54 | ];
55 |
56 | $provider = new Softonic\OAuth2\Client\Provider\Softonic($options);
57 |
58 | $config = ['grant_type' => 'client_credentials', 'scope' => 'myscope'];
59 |
60 | $cache = new \Symfony\Component\Cache\Adapter\FilesystemAdapter();
61 |
62 | $client = \Softonic\GraphQL\ClientBuilder::buildWithOAuth2Provider(
63 | 'https://your-domain/graphql',
64 | $provider,
65 | $config,
66 | $cache
67 | );
68 | ```
69 |
70 | ### Using the GraphQL Client
71 |
72 | You can use the client to execute queries and mutations and get the results.
73 |
74 | ```php
75 | 'foo',
93 | 'idBar' => 'bar',
94 | ];
95 |
96 | /** @var \Softonic\GraphQL\Client $client */
97 | $response = $client->query($query, $variables);
98 |
99 | if($response->hasErrors()) {
100 | // Returns an array with all the errors found.
101 | $response->getErrors();
102 | }
103 | else {
104 | // Returns an array with all the data returned by the GraphQL server.
105 | $response->getData();
106 | }
107 |
108 | /**
109 | * Mutation Example
110 | */
111 | $mutation = <<<'MUTATION'
112 | mutation ($foo: ObjectInput!){
113 | CreateObjectMutation (object: $foo) {
114 | status
115 | }
116 | }
117 | MUTATION;
118 | $variables = [
119 | 'foo' => [
120 | 'id_foo' => 'foo',
121 | 'bar' => [
122 | 'id_bar' => 'bar'
123 | ]
124 | ]
125 | ];
126 |
127 | /** @var \Softonic\GraphQL\Client $client */
128 | $response = $client->query($mutation, $variables);
129 |
130 | if($response->hasErrors()) {
131 | // Returns an array with all the errors found.
132 | $response->getErrors();
133 | }
134 | else {
135 | // Returns an array with all the data returned by the GraphQL server.
136 | $response->getData();
137 | }
138 |
139 | ```
140 |
141 | In the previous examples, the client is used to execute queries and mutations. The response object is used to
142 | get the results in array format.
143 |
144 | This can be convenient for simple use cases, but it is not recommended for complex
145 | results or when you need to use that output to generate mutations. For this reason, the client provides another output
146 | called data objects. Those objects allow you to get the results in a more convenient format, allowing you to generate
147 | mutations, apply filters, etc.
148 |
149 | ### How to use a data object and transform it to a mutation query
150 |
151 | The query result can be obtained as an object which will provide facilities to convert it to a mutation and modify the data easily.
152 | At the end, the mutation object will be able to be used as the variables of the mutation query in the GraphQL client.
153 |
154 | First we execute a "read" query and obtain the result as an object compound of Items and Collections.
155 |
156 | ``` php
157 | $response = $client->query($query, $variables);
158 |
159 | $data = $response->getDataObject();
160 |
161 | /**
162 | * $data = new QueryItem([
163 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
164 | * 'id_author' => 1234,
165 | * 'genre' => 'adventure',
166 | * 'chapters' => new QueryCollection([
167 | * new QueryItem([
168 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
169 | * 'id_chapter' => 1,
170 | * 'name' => 'Chapter One',
171 | * 'pov' => 'first person',
172 | * 'pages' => new QueryCollection([]),
173 | * ]),
174 | * new QueryItem([
175 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
176 | * 'id_chapter' => 2,
177 | * 'name' => 'Chapter two',
178 | * 'pov' => 'third person',
179 | * 'pages' => new QueryCollection([
180 | * new QueryItem([
181 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
182 | * 'id_chapter' => 2,
183 | * 'id_page' => 1,
184 | * 'has_illustrations' => false,
185 | * ]),
186 | * new QueryItem([
187 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
188 | * 'id_chapter' => 2,
189 | * 'id_page' => 2,
190 | * 'has_illustrations' => false,
191 | * ]),
192 | * ]),
193 | * ]),
194 | * ]),
195 | * ]);
196 | */
197 | ```
198 |
199 | We can also filter the results in order to work with fewer data later. The filter method returns a new object with
200 | the filtered results, so you need to reassign the object to the original one, if you want to modify it.
201 |
202 | ``` php
203 | $data->chapters = $data->chapters->filter(['pov' => 'third person']);
204 |
205 | /**
206 | * $data = new QueryItem([
207 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
208 | * 'id_author' => 1234,
209 | * 'genre' => 'adventure',
210 | * 'chapters' => new QueryCollection([
211 | * new QueryItem([
212 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
213 | * 'id_chapter' => 2,
214 | * 'name' => 'Chapter two',
215 | * 'pov' => 'third person',
216 | * 'pages' => new QueryCollection([
217 | * new QueryItem([
218 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
219 | * 'id_chapter' => 2,
220 | * 'id_page' => 1,
221 | * 'has_illustrations' => false,
222 | * ]),
223 | * new QueryItem([
224 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
225 | * 'id_chapter' => 2,
226 | * 'id_page' => 2,
227 | * 'has_illustrations' => false,
228 | * ]),
229 | * ]),
230 | * ]),
231 | * ]),
232 | * ]);
233 | */
234 | ```
235 |
236 | Then we can generate the mutation variables object from the previous query results. This is build using a mutation config.
237 | The config for each type has the following parameters:
238 | * linksTo: the location in the query result object where the data can be obtained for that type. If not present, it means it's a level that has no data from the source.
239 | * type: mutation object type (Item or Collection).
240 | * children: if the mutation has a key which value is another mutation type.
241 |
242 | ``` php
243 | $mutationConfig = [
244 | 'book' => [
245 | 'linksTo' => '.',
246 | 'type' => MutationItem::class,
247 | 'children' => [
248 | 'chapters' => [
249 | 'type' => MutationItem::class,
250 | 'children' => [
251 | 'upsert' => [
252 | 'linksTo' => '.chapters',
253 | 'type' => MutationCollection::class,
254 | 'children' => [
255 | 'pages' => [
256 | 'type' => MutationItem::class,
257 | 'children' => [
258 | 'upsert' => [
259 | 'linksTo' => '.chapters.pages',
260 | 'type' => MutationCollection::class,
261 | ],
262 | ],
263 | ],
264 | ],
265 | ],
266 | ],
267 | ],
268 | ],
269 | ],
270 | ];
271 |
272 | $mutation = Mutation::build($mutationConfig, $data);
273 |
274 | /**
275 | * $mutation = new MutationItem([
276 | * 'book' => new MutationItem([
277 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
278 | * 'id_author' => 1234,
279 | * 'genre' => 'adventure',
280 | * 'chapters' => new MutationItem([
281 | * 'upsert' => new MutationCollection([
282 | * new MutationItem([
283 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
284 | * 'id_chapter' => 1,
285 | * 'name' => 'Chapter One',
286 | * 'pov' => 'first person',
287 | * 'pages' => new MutationItem([
288 | * 'upsert' => new MutationCollection([]),
289 | * ]),
290 | * ]),
291 | * new MutationItem([
292 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
293 | * 'id_chapter' => 2,
294 | * 'name' => 'Chapter two',
295 | * 'pov' => 'third person',
296 | * 'pages' => new MutationItem([
297 | * 'upsert' => new MutationCollection([
298 | * new MutationItem([
299 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
300 | * 'id_chapter' => 2,
301 | * 'id_page' => 1,
302 | * 'has_illustrations' => false,
303 | * ]),
304 | * new MutationItem([
305 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
306 | * 'id_chapter' => 2,
307 | * 'id_page' => 2,
308 | * 'has_illustrations' => false,
309 | * ]),
310 | * ]),
311 | * ]),
312 | * ]),
313 | * ]),
314 | * ]),
315 | * ]),
316 | * ]);
317 | */
318 | ```
319 |
320 | #### Now we can modify the mutation data using the following methods:
321 | * add(): Adds an Item to a Collection.
322 | * set(): Updates some values of an Item. It also works on Collections, updating all its Items.
323 | * filter(): Filters the Items of a Collection.
324 | * count(): Counts the Items of a Collection.
325 | * isEmpty(): Check if a Collection is empty.
326 | * has(): Checks whether an Item has an argument or not. Works on Collections too. Dot notation is also allowed.
327 | * hasItem(): Checks whether a Collection has an Item with the provided data or not.
328 | * remove(): Removes an Item from a Collection.
329 | * __unset(): Removes a property from an Item or from all the Items of a Collection.
330 |
331 | ``` php
332 | $mutation->book->chapters->upsert->filter(['id_chapter' => 2])->pages->upsert->add([
333 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
334 | 'id_chapter' => 2,
335 | 'id_page' => 3,
336 | 'has_illustrations' => false,
337 | ]);
338 |
339 | $mutation->book->chapters->upsert->pages->upsert->filter([
340 | 'id_chapter' => 2,
341 | 'id_page' => 2,
342 | ])->set(['has_illustrations' => true]);
343 |
344 | $itemToRemove = new MutationItem([
345 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
346 | 'id_chapter' => 2,
347 | 'id_page' => 1,
348 | 'has_illustrations' => false,
349 | ]);
350 | $mutation->book->chapters->upsert->files->upsert->remove($itemToRemove);
351 |
352 | unset($mutation->book->chapters->upsert->pov);
353 |
354 | /**
355 | * $mutation = new MutationItem([
356 | * 'book' => new MutationItem([
357 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
358 | * 'id_author' => 1234,
359 | * 'genre' => 'adventure',
360 | * 'chapters' => new MutationItem([
361 | * 'upsert' => new MutationCollection([
362 | * new MutationItem([
363 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
364 | * 'id_chapter' => 1,
365 | * 'name' => 'Chapter One',
366 | * 'pages' => new MutationItem([
367 | * 'upsert' => new MutationCollection([]),
368 | * ]),
369 | * ]),
370 | * new MutationItem([
371 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
372 | * 'id_chapter' => 2,
373 | * 'name' => 'Chapter two',
374 | * 'pages' => new MutationItem([
375 | * 'upsert' => new MutationCollection([
376 | * new MutationItem([
377 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
378 | * 'id_chapter' => 2,
379 | * 'id_page' => 2,
380 | * 'has_illustrations' => true,
381 | * ]),
382 | * new MutationItem([
383 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
384 | * 'id_chapter' => 2,
385 | * 'id_page' => 3,
386 | * 'has_illustrations' => false,
387 | * ]),
388 | * ]),
389 | * ]),
390 | * ]),
391 | * ]),
392 | * ]),
393 | * ]),
394 | * ]);
395 | */
396 | ```
397 |
398 | Finally, the modified mutation data can be passed to the GraphQL client to execute the mutation.
399 | When the query is executed, the mutation variables are encoded using json_encode().
400 | This modifies the mutation data just returning the items changed and its parents.
401 |
402 | ``` php
403 | $mutationQuery = <<<'QUERY'
404 | mutation ($book: BookInput!){
405 | ReplaceBook (book: $book) {
406 | status
407 | }
408 | }
409 | QUERY;
410 |
411 | $client->mutate($mutationQuery, $mutation);
412 | ```
413 |
414 | So the final variables sent to the query would be:
415 |
416 | ``` php
417 | /**
418 | * $mutation = [
419 | * 'book' => [
420 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
421 | * 'id_author' => 1234,
422 | * 'genre' => 'adventure',
423 | * 'chapters' => [
424 | * 'upsert' => [
425 | * [
426 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
427 | * 'id_chapter' => 2,
428 | * 'name' => 'Chapter two',
429 | * 'pages' => [
430 | * 'upsert' => [
431 | * [
432 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
433 | * 'id_chapter' => 2,
434 | * 'id_page' => 2,
435 | * 'has_illustrations' => true,
436 | * ],
437 | * [
438 | * 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
439 | * 'id_chapter' => 2,
440 | * 'id_page' => 3,
441 | * 'has_illustrations' => false,
442 | * ],
443 | * ],
444 | * ],
445 | * ],
446 | * ],
447 | * ],
448 | * ],
449 | * ];
450 | */
451 | ```
452 |
453 | NOTE 2: The example has been done for a root Item "book", but it also works for a Collection as root object.
454 |
455 | ## Testing
456 |
457 | `softonic/graphql-client` has a [PHPUnit](https://phpunit.de) test suite, and a coding style compliance test suite using [PHP CS Fixer](http://cs.sensiolabs.org/).
458 |
459 | To run the tests, run the following command from the project folder.
460 |
461 | ``` bash
462 | $ make tests
463 | ```
464 |
465 | To open a terminal in the dev environment:
466 | ``` bash
467 | $ make debug
468 | ```
469 |
470 | ## License
471 |
472 | The Apache 2.0 license. Please see [LICENSE](LICENSE) for more information.
473 |
474 | [PSR-2]: http://www.php-fig.org/psr/psr-2/
475 | [PSR-4]: http://www.php-fig.org/psr/psr-4/
476 |
--------------------------------------------------------------------------------
/bin/graphql-client:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | add(new GenerateConfig());
19 | $application->add(new GetIntrospection());
20 |
21 | $application->run();
22 |
--------------------------------------------------------------------------------
/build/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "softonic/graphql-client",
3 | "type": "library",
4 | "description": "Softonic GraphQL client",
5 | "keywords": [
6 | "softonic",
7 | "oauth2",
8 | "graphql",
9 | "client"
10 | ],
11 | "license": "Apache-2.0",
12 | "homepage": "https://github.com/softonic/graphql-client",
13 | "support": {
14 | "issues": "https://github.com/softonic/graphql-client/issues"
15 | },
16 | "require": {
17 | "php": "^8.0",
18 | "guzzlehttp/guzzle": "^6.3 || ^7.0",
19 | "softonic/guzzle-oauth2-middleware": "^2.1",
20 | "ext-json": "*",
21 | "symfony/console": "^6.0 || ^7.0"
22 | },
23 | "require-dev": {
24 | "friendsofphp/php-cs-fixer": "^3.9",
25 | "phpunit/phpunit": "^11.0",
26 | "rector/rector": "^2.0",
27 | "squizlabs/php_codesniffer": "^3.7",
28 | "mockery/mockery": "^1.5"
29 | },
30 | "autoload": {
31 | "psr-4": {
32 | "Softonic\\GraphQL\\": "src/"
33 | }
34 | },
35 | "autoload-dev": {
36 | "psr-4": {
37 | "Softonic\\GraphQL\\": "tests/"
38 | }
39 | },
40 | "bin": [
41 | "bin/graphql-client"
42 | ],
43 | "scripts": {
44 | "tests": [
45 | "@checkstyle",
46 | "@phpunit"
47 | ],
48 | "phpunit": "phpunit",
49 | "checkstyle": [
50 | "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix -v --diff --dry-run --allow-risky=yes",
51 | "rector process"
52 | ],
53 | "fix-cs": [
54 | "@php-cs-fixer",
55 | "@rector"
56 | ],
57 | "php-cs-fixer": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix -v --diff --allow-risky=yes",
58 | "rector": "rector process"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | php:
5 | build:
6 | context: .
7 | dockerfile: Dockerfile
8 | volumes:
9 | - ./:/app
10 |
11 | install:
12 | build:
13 | context: .
14 | dockerfile: Dockerfile
15 | volumes:
16 | - ./:/app
17 | command: composer install
18 |
19 | update:
20 | build:
21 | context: .
22 | dockerfile: Dockerfile
23 | volumes:
24 | - ./:/app
25 | command: composer update
26 |
27 | phpunit:
28 | build:
29 | context: .
30 | dockerfile: Dockerfile
31 | volumes:
32 | - ./:/app
33 | command: composer phpunit
34 |
35 | tests:
36 | build:
37 | context: .
38 | dockerfile: Dockerfile
39 | volumes:
40 | - ./:/app
41 | command: composer run tests
42 |
43 | fix-cs:
44 | build:
45 | context: .
46 | dockerfile: Dockerfile
47 | volumes:
48 | - ./:/app
49 | command: composer run fix-cs
50 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
14 |
15 | tests
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/rector.php:
--------------------------------------------------------------------------------
1 | withSkip(
17 | [
18 | // CodeQuality
19 | CompleteDynamicPropertiesRector::class,
20 | DisallowedEmptyRuleFixerRector::class,
21 | // CodingStyle
22 | CatchExceptionNameMatchingTypeRector::class,
23 | EncapsedStringsToSprintfRector::class,
24 | // EarlyReturn
25 | ReturnBinaryOrToEarlyReturnRector::class,
26 | // TypeDeclaration
27 | AddArrowFunctionReturnTypeRector::class,
28 | ReturnTypeFromStrictTypedCallRector::class,
29 | ]
30 | )
31 | ->withAutoloadPaths([__DIR__ . '/vendor/autoload.php'])
32 | ->withPaths([
33 | __DIR__ . '/src',
34 | __DIR__ . '/tests',
35 | ])
36 | ->withImportNames()
37 | ->withPhpSets(php83: true)
38 | ->withSets(
39 | [
40 | PHPUnitSetList::PHPUNIT_100,
41 | PHPUnitSetList::PHPUNIT_110,
42 | ]
43 | )
44 | ->withPreparedSets(
45 | deadCode: true,
46 | codeQuality: true,
47 | codingStyle: true,
48 | typeDeclarations: true,
49 | earlyReturn: true
50 | );
51 |
--------------------------------------------------------------------------------
/src/Client.php:
--------------------------------------------------------------------------------
1 | executeQuery($query, $variables);
24 | }
25 |
26 | /**
27 | * @throws UnexpectedValueException When response body is not a valid json
28 | * @throws RuntimeException When there are transfer errors
29 | */
30 | public function mutate(string $query, MutationObject $mutation): Response
31 | {
32 | return $this->executeQuery($query, $mutation);
33 | }
34 |
35 | private function executeQuery(string $query, array|null|MutationObject $variables): Response
36 | {
37 | $body = ['query' => $query];
38 | if (!is_null($variables)) {
39 | $body['variables'] = $variables;
40 | }
41 |
42 | $options = [
43 | 'body' => json_encode($body, JSON_UNESCAPED_SLASHES),
44 | 'headers' => [
45 | 'Content-Type' => 'application/json',
46 | ],
47 | ];
48 |
49 | try {
50 | $response = $this->httpClient->request('POST', '', $options);
51 | } catch (TransferException $e) {
52 | throw new RuntimeException('Network Error.' . $e->getMessage(), 0, $e);
53 | }
54 |
55 | return $this->responseBuilder->build($response);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/ClientBuilder.php:
--------------------------------------------------------------------------------
1 | $endpoint], $guzzleOptions);
13 |
14 | return new Client(
15 | new \GuzzleHttp\Client($guzzleOptions),
16 | new ResponseBuilder(new DataObjectBuilder())
17 | );
18 | }
19 |
20 | public static function buildWithOAuth2Provider(
21 | string $endpoint,
22 | OAuth2Provider $oauthProvider,
23 | array $tokenOptions,
24 | Cache $cache,
25 | array $guzzleOptions = []
26 | ): Client {
27 | $guzzleOptions = array_merge(['base_uri' => $endpoint], $guzzleOptions);
28 |
29 |
30 | return new Client(
31 | \Softonic\OAuth2\Guzzle\Middleware\ClientBuilder::build(
32 | $oauthProvider,
33 | $tokenOptions,
34 | $cache,
35 | $guzzleOptions
36 | ),
37 | new ResponseBuilder(new DataObjectBuilder())
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Config/MutationConfigBuilder.php:
--------------------------------------------------------------------------------
1 | $typeConfig) {
13 | $mutationConfig[$variable] = $this->buildMutationTypeConfig($typeConfig);
14 | }
15 |
16 | return $mutationConfig;
17 | }
18 |
19 | private function buildMutationTypeConfig(array $typeConfig): MutationTypeConfig
20 | {
21 | $mutationTypeConfig = new MutationTypeConfig();
22 |
23 | foreach ($typeConfig as $propertyName => $propertyValue) {
24 | $this->setMutationTypeConfig($mutationTypeConfig, $propertyName, $propertyValue);
25 | }
26 |
27 | return $mutationTypeConfig;
28 | }
29 |
30 | private function setMutationTypeConfig(
31 | MutationTypeConfig $mutationTypeConfig,
32 | string $propertyName,
33 | $propertyValue
34 | ): void {
35 | if ($propertyName === self::CHILDREN_PROPERTY_NAME) {
36 | foreach ($propertyValue as $childName => $childConfig) {
37 | $mutationTypeConfig->children[$childName] = $this->buildMutationTypeConfig($childConfig);
38 | }
39 | } else {
40 | $mutationTypeConfig->{$propertyName} = $propertyValue;
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Config/MutationTypeConfig.php:
--------------------------------------------------------------------------------
1 | {$propertyName};
30 | }
31 |
32 | return $this->children[$propertyName] ?? null;
33 | }
34 |
35 | public function hasChild(string $key): bool
36 | {
37 | return array_key_exists($key, $this->children);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Config/MutationsConfig.php:
--------------------------------------------------------------------------------
1 | $mutationConfig) {
12 | $builder = new MutationConfigBuilder();
13 |
14 | $this->mutationsConfig[$mutationName] = $builder->build($mutationConfig);
15 | }
16 | }
17 |
18 | /**
19 | * @return array
20 | */
21 | public function get(string $mutationName): array
22 | {
23 | return $this->mutationsConfig[$mutationName];
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Console/Mutation/GenerateConfig.php:
--------------------------------------------------------------------------------
1 | setDescription('Creates a mutation config.')
34 | ->setHelp(
35 | 'This command allows you to create a mutation config based in the result of an introspection query'
36 | . ' for a specific mutation.'
37 | );
38 |
39 | $this->addArgument(
40 | 'instrospection-result',
41 | InputArgument::REQUIRED,
42 | 'Json file with the introspection query result'
43 | )
44 | ->addOption(
45 | 'from-same-mutation',
46 | 'z',
47 | InputOption::VALUE_NONE,
48 | 'Flag to determine if the input for this config is the same mutation output'
49 | )
50 | ->addArgument(
51 | 'mutation',
52 | InputArgument::REQUIRED,
53 | 'Mutation name to extract to the configuration'
54 | );
55 | }
56 |
57 | protected function execute(InputInterface $input, OutputInterface $output): int
58 | {
59 | if (!$this->checkArguments($input, $output)) {
60 | return 1;
61 | }
62 |
63 | $jsonSchema = file_get_contents($input->getArgument('instrospection-result'));
64 | $mutation = $input->getArgument('mutation');
65 | $this->fromSameMutation = $input->getOption('from-same-mutation');
66 |
67 | $mutationConfig = $this->generateConfig(json_decode($jsonSchema), $mutation);
68 |
69 | $output->writeln(var_export($mutationConfig, true));
70 |
71 | return 0;
72 | }
73 |
74 | private function checkArguments(InputInterface $input, OutputInterface $output): bool
75 | {
76 | $jsonPath = $input->getArgument('instrospection-result');
77 |
78 | if (!file_exists($jsonPath)) {
79 | $output->writeln("The file '{$jsonPath}' does not exist");
80 |
81 | return false;
82 | }
83 |
84 | return true;
85 | }
86 |
87 | private function generateConfig(StdClass $jsonSchema, string $mutation): array
88 | {
89 | foreach ($jsonSchema->data->__schema->types as $type) {
90 | if ($type->name === 'Mutation' && $type->fields[0]->name === $mutation) {
91 | $initialMutationField = $type->fields[0]->args[0]->name;
92 | $inputType = $type->fields[0]->args[0]->type->ofType->name;
93 | break;
94 | }
95 | }
96 |
97 | return [
98 | $mutation => [
99 | $initialMutationField => [
100 | 'linksTo' => '.',
101 | 'type' => Item::class,
102 | 'children' => $this->getMutationConfig($jsonSchema->data->__schema->types, $inputType),
103 | ],
104 | ],
105 | ];
106 | }
107 |
108 | private function getTypeFromField($field): array
109 | {
110 | $isCollection = false;
111 | $type = $field->type;
112 |
113 | while ($type->kind !== self::SCALAR && $type->kind !== self::INPUT_OBJECT) {
114 | if ($type->kind === self::LIST) {
115 | $isCollection = true;
116 | }
117 |
118 | $type = $type->ofType;
119 | }
120 |
121 | if ($type->kind === self::SCALAR) {
122 | return [
123 | 'type' => $type->kind,
124 | 'isCollection' => $isCollection,
125 | ];
126 | }
127 |
128 | return [
129 | 'type' => $type->name,
130 | 'isCollection' => $isCollection,
131 | ];
132 | }
133 |
134 | private function getMutationConfig(array $graphqlTypes, $inputType, string $parentLinksTo = ''): array
135 | {
136 | $children = [];
137 | foreach ($graphqlTypes as $graphqlType) {
138 | if ($graphqlType->name === $inputType) {
139 | foreach ($graphqlType->inputFields as $inputField) {
140 | [
141 | 'type' => $inputFieldType,
142 | 'isCollection' => $isCollection,
143 | ] = $this->getTypeFromField($inputField);
144 |
145 | if ($inputFieldType === self::SCALAR) {
146 | $children[$inputField->name] = [];
147 | continue;
148 | }
149 |
150 | // Avoid cyclic relations to define infinite configs.
151 | if ($this->isFieldPreviouslyAdded($inputFieldType, $parentLinksTo)) {
152 | continue;
153 | }
154 |
155 | $children[$inputField->name] = $this->getFieldInfo(
156 | $graphqlTypes,
157 | $inputFieldType,
158 | $inputField->name,
159 | $parentLinksTo,
160 | $isCollection
161 | );
162 | }
163 |
164 | break;
165 | }
166 | }
167 |
168 | return $children;
169 | }
170 |
171 | private function isFieldPreviouslyAdded($inputType, string $linksTo): bool
172 | {
173 | $linksParts = explode('.', $linksTo);
174 | for ($i=1, $iMax = count($linksParts); $i<= $iMax; $i++) {
175 | $parentLink = implode('.', array_slice($linksParts, 0, $i));
176 | if (
177 | in_array($inputType, $this->generatedFieldTypes[$parentLink] ?? [])
178 | ) {
179 | return true;
180 | }
181 | }
182 |
183 | $this->generatedFieldTypes[$linksTo][] = $inputType;
184 |
185 | return false;
186 | }
187 |
188 | private function getFieldInfo(
189 | array $graphqlTypes,
190 | $graphqlType,
191 | $inputFieldName,
192 | string $parentLinksTo,
193 | bool $isCollection
194 | ): array {
195 | if ($this->fromSameMutation) {
196 | return $this->defineConfigLinkedInputType(
197 | "{$parentLinksTo}.{$inputFieldName}",
198 | $isCollection,
199 | $graphqlTypes,
200 | $graphqlType
201 | );
202 | }
203 |
204 | $queryType = $this->getQueryTypeFromInputType($graphqlType);
205 | $queryTypeExists = $this->queryTypeExists($queryType, $graphqlTypes);
206 |
207 | if ($queryTypeExists) {
208 | return $this->defineConfigLinkedInputType(
209 | $parentLinksTo,
210 | $isCollection,
211 | $graphqlTypes,
212 | $graphqlType
213 | );
214 | }
215 |
216 | return $this->defineConfigNotLinkedInputType(
217 | "{$parentLinksTo}.{$inputFieldName}",
218 | $isCollection,
219 | $graphqlTypes,
220 | $graphqlType
221 | );
222 | }
223 |
224 | private function getQueryTypeFromInputType($inputType): string
225 | {
226 | return preg_replace('/Input$/', '', $inputType);
227 | }
228 |
229 | private function queryTypeExists(string $queryType, array $types): bool
230 | {
231 | foreach ($types as $type) {
232 | if ($type->name === $queryType) {
233 | return true;
234 | }
235 | }
236 |
237 | return false;
238 | }
239 |
240 | private function defineConfigLinkedInputType(
241 | string $linksTo,
242 | bool $isCollection,
243 | array $graphqlTypes,
244 | string $type
245 | ): array {
246 | return [
247 | 'linksTo' => $linksTo,
248 | 'type' => $isCollection ? Collection::class : Item::class,
249 | 'children' => $this->getMutationConfig($graphqlTypes, $type, $linksTo),
250 | ];
251 | }
252 |
253 | private function defineConfigNotLinkedInputType(
254 | string $linksTo,
255 | bool $isCollection,
256 | array $graphqlTypes,
257 | string $type
258 | ): array {
259 | return [
260 | 'type' => $isCollection ? Collection::class : Item::class,
261 | 'children' => $this->getMutationConfig($graphqlTypes, $type, $linksTo),
262 | ];
263 | }
264 | }
265 |
--------------------------------------------------------------------------------
/src/Console/Mutation/GetIntrospection.php:
--------------------------------------------------------------------------------
1 | setDescription('Returns a instrospection query.')
16 | ->setHelp('Returns the introspection query needed to execute in your GraphQL server in order ' .
17 | 'to generate the needed file to generate the config.');
18 | }
19 |
20 | protected function execute(InputInterface $input, OutputInterface $output): int
21 | {
22 | $output->writeln(
23 | <<<'GQL'
24 | query IntrospectionQuery {
25 | __schema {
26 | mutationType { name }
27 | types {
28 | ...FullType
29 | }
30 | }
31 | }
32 |
33 | fragment FullType on __Type {
34 | kind
35 | name
36 | description
37 | fields(includeDeprecated: true) {
38 | name
39 | description
40 | args {
41 | ...InputValue
42 | }
43 | }
44 | inputFields {
45 | ...InputValue
46 | }
47 | }
48 |
49 | fragment InputValue on __InputValue {
50 | name
51 | description
52 | type { ...TypeRef }
53 | defaultValue
54 | }
55 |
56 | fragment TypeRef on __Type {
57 | kind
58 | name
59 | ofType {
60 | kind
61 | name
62 | ofType {
63 | kind
64 | name
65 | ofType {
66 | kind
67 | name
68 | ofType {
69 | kind
70 | name
71 | ofType {
72 | kind
73 | name
74 | ofType {
75 | kind
76 | name
77 | ofType {
78 | kind
79 | name
80 | }
81 | }
82 | }
83 | }
84 | }
85 | }
86 | }
87 | }
88 | GQL
89 | );
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/DataObjectBuilder.php:
--------------------------------------------------------------------------------
1 | QueryItem::class,
18 | self::COLLECTION => QueryCollection::class,
19 | ];
20 |
21 | const MUTATION_OBJECTS = [
22 | self::ITEM => MutationItem::class,
23 | self::COLLECTION => MutationCollection::class,
24 | ];
25 |
26 | public function buildQuery(array $data): array
27 | {
28 | return $this->build($data, self::QUERY_OBJECTS);
29 | }
30 |
31 | public function buildMutation(array $data): array
32 | {
33 | return $this->build($data, self::MUTATION_OBJECTS);
34 | }
35 |
36 | private function build(array $data, array $objects): array
37 | {
38 | $dataObject = [];
39 | foreach ($data as $key => $value) {
40 | if (is_array($value)) {
41 | if ($this->isAList($value)) {
42 | if ($value === [] || is_array($value[0])) {
43 | $items = [];
44 | foreach ($value as $objectData) {
45 | $itemData = $this->build($objectData, $objects);
46 | $items[] = (new $objects[self::ITEM]($itemData));
47 | }
48 |
49 | $dataObject[$key] = (new $objects[self::COLLECTION]($items));
50 | } else {
51 | $dataObject[$key] = $value;
52 | }
53 | } else {
54 | $itemData = $this->build($value, $objects);
55 | $dataObject[$key] = (new $objects[self::ITEM]($itemData));
56 | }
57 | } else {
58 | $dataObject[$key] = $value;
59 | }
60 | }
61 |
62 | return $dataObject;
63 | }
64 |
65 | private function isAList(array $data): bool
66 | {
67 | return array_values($data) === $data;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/DataObjects/AbstractCollection.php:
--------------------------------------------------------------------------------
1 | arguments as $argument) {
20 | if ($argument->has($key)) {
21 | return true;
22 | }
23 | }
24 |
25 | return false;
26 | }
27 |
28 | public function getIterator(): RecursiveIteratorIterator
29 | {
30 | return new RecursiveIteratorIterator(new CollectionIterator($this->arguments));
31 | }
32 |
33 | public function count(): int
34 | {
35 | $count = 0;
36 | foreach ($this->arguments as $argument) {
37 | if ($argument instanceof AbstractCollection) {
38 | $count += $argument->count();
39 | } elseif ($argument instanceof AbstractItem) {
40 | ++$count;
41 | }
42 | }
43 |
44 | return $count;
45 | }
46 |
47 | public function isEmpty(): bool
48 | {
49 | return $this->count() === 0;
50 | }
51 |
52 | public function __get(string $key): AbstractCollection
53 | {
54 | if ($this->arguments === []) {
55 | throw InaccessibleArgumentException::fromEmptyArguments($key);
56 | }
57 |
58 | $items = [];
59 | foreach ($this->arguments as $argument) {
60 | $arguments = $argument->{$key};
61 | if ($arguments instanceof Collection) {
62 | foreach ($arguments as $item) {
63 | $items[] = $item;
64 | }
65 | } else {
66 | $items[] = $arguments;
67 | }
68 | }
69 |
70 | return $this->buildSubCollection($items, $key);
71 | }
72 |
73 | public function hasItem(array $itemData): bool
74 | {
75 | foreach ($this->arguments as $argument) {
76 | $method = $argument instanceof AbstractCollection ? 'hasItem' : 'equals';
77 |
78 | if ($argument->$method($itemData)) {
79 | return true;
80 | }
81 | }
82 |
83 | return false;
84 | }
85 |
86 | public function hasChildren(): bool
87 | {
88 | foreach ($this->arguments as $argument) {
89 | if ($argument instanceof MutationObject) {
90 | return true;
91 | }
92 | }
93 |
94 | return false;
95 | }
96 |
97 | public function filter(array $filters): AbstractCollection
98 | {
99 | $filteredData = [];
100 | if ($this->areAllArgumentsCollections()) {
101 | foreach ($this->arguments as $argument) {
102 | $data = $argument->filter($filters);
103 | if (!$data->isEmpty()) {
104 | $filteredData[] = $data;
105 | }
106 | }
107 | } else {
108 | $filteredData = $this->filterItems($this->arguments, $filters);
109 | }
110 |
111 | return $this->buildFilteredCollection($filteredData);
112 | }
113 |
114 | private function areAllArgumentsCollections(): bool
115 | {
116 | return (!empty($this->arguments[0]) && $this->arguments[0] instanceof AbstractCollection);
117 | }
118 |
119 | private function filterItems(array $arguments, array $filters): array
120 | {
121 | $filteredItems = array_filter(
122 | $arguments,
123 | function ($item) use ($filters): bool {
124 | foreach ($filters as $filterKey => $filterValue) {
125 | if ($item->{$filterKey} != $filterValue) {
126 | return false;
127 | }
128 | }
129 |
130 | return true;
131 | }
132 | );
133 |
134 | return array_values($filteredItems);
135 | }
136 |
137 | abstract protected function buildFilteredCollection($items);
138 |
139 | abstract protected function buildSubCollection(array $items, string $key);
140 | }
141 |
--------------------------------------------------------------------------------
/src/DataObjects/AbstractItem.php:
--------------------------------------------------------------------------------
1 | arguments)) {
21 | return false;
22 | }
23 |
24 | if ($keyPath === []) {
25 | return true;
26 | }
27 |
28 | if (!$this->arguments[$firstKey] instanceof DataObject) {
29 | return false;
30 | }
31 |
32 | $nextKey = implode('.', $keyPath);
33 |
34 | return $this->arguments[$firstKey]->has($nextKey);
35 | }
36 |
37 | public function __get(string $key)
38 | {
39 | return $this->arguments[$key] ?? null;
40 | }
41 |
42 | public function __set(string $key, $value): void
43 | {
44 | $this->arguments[$key] = $value;
45 | }
46 |
47 | public function equals(array $data): bool
48 | {
49 | return $data === $this->arguments;
50 | }
51 |
52 | public function isEmpty(): bool
53 | {
54 | return $this->arguments === [];
55 | }
56 |
57 | public function jsonSerialize(): array
58 | {
59 | $item = [];
60 | foreach ($this->arguments as $key => $value) {
61 | if ($value instanceof FilteredCollection && !$value->hasChildren()) {
62 | continue;
63 | }
64 |
65 | if ($value instanceof JsonSerializable) {
66 | if (!empty($valueSerialized = $value->jsonSerialize())) {
67 | $item[$key] = $valueSerialized;
68 | }
69 | } else {
70 | $item[$key] = $value;
71 | }
72 | }
73 |
74 | return $item;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/DataObjects/AbstractObject.php:
--------------------------------------------------------------------------------
1 | arguments as $key => $value) {
18 | $item[$key] = $value instanceof JsonSerializable ? $value->toArray() : $value;
19 | }
20 |
21 | return $item;
22 | }
23 |
24 | abstract public function isEmpty(): bool;
25 |
26 | abstract public function has(string $key): bool;
27 | }
28 |
--------------------------------------------------------------------------------
/src/DataObjects/CollectionIterator.php:
--------------------------------------------------------------------------------
1 | hasChildren() && $this->current() instanceof AbstractCollection) {
14 | $this->next();
15 |
16 | return $this->valid();
17 | }
18 |
19 | if ($isValid && $this->current()->isEmpty()) {
20 | $this->next();
21 |
22 | return $this->valid();
23 | }
24 |
25 | return $isValid;
26 | }
27 |
28 | public function hasChildren(): bool
29 | {
30 | $current = $this->current();
31 | if ($current instanceof AbstractItem) {
32 | return false;
33 | }
34 |
35 | if (is_array($current)) {
36 | return true;
37 | }
38 |
39 | if ($current instanceof AbstractCollection) {
40 | return $current->hasChildren();
41 | }
42 |
43 | throw new InvalidArgumentException("Collections only can contain Items or other Collection, instead '{$current}' value found");
44 | }
45 |
46 | public function getChildren(): ?RecursiveArrayIterator
47 | {
48 | return $this->current()
49 | ->getIterator()
50 | ->getInnerIterator();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/DataObjects/Interfaces/DataObject.php:
--------------------------------------------------------------------------------
1 | arguments[0]) && $this->arguments[0] instanceof Collection) {
10 | $elements = [];
11 | foreach ($this->arguments as $argument) {
12 | $elements[] = $argument->add($itemData);
13 | }
14 |
15 | return $this->buildFilteredCollection($elements);
16 | }
17 |
18 | $item = new Item($itemData, $this->config, true);
19 | $this->arguments[] = $item;
20 |
21 | return $item;
22 | }
23 |
24 | public function __unset($key): void
25 | {
26 | foreach ($this->arguments as $argument) {
27 | unset($argument->{$key});
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/DataObjects/Mutation/FilteredCollection.php:
--------------------------------------------------------------------------------
1 | hasChanged = $hasChanged;
21 | }
22 |
23 | public function set(array $data): void
24 | {
25 | foreach ($this->arguments as $argument) {
26 | $argument->set($data);
27 | }
28 | }
29 |
30 | public function jsonSerialize(): array
31 | {
32 | if (!$this->hasChildren()) {
33 | return [];
34 | }
35 |
36 | $items = [];
37 | foreach ($this->arguments as $item) {
38 | if ($item->hasChanged()) {
39 | $items[] = $item->jsonSerialize();
40 | }
41 | }
42 |
43 | return $items;
44 | }
45 |
46 | public function remove(Item $item): bool
47 | {
48 | foreach ($this->arguments as $key => $argument) {
49 | if ($argument instanceof Collection) {
50 | if ($argument->remove($item)) {
51 | return true;
52 | }
53 | } elseif ($argument === $item) {
54 | unset($this->arguments[$key]);
55 |
56 | return true;
57 | }
58 | }
59 |
60 | return false;
61 | }
62 |
63 | protected function buildFilteredCollection($items): FilteredCollection
64 | {
65 | return new FilteredCollection($items, $this->config);
66 | }
67 |
68 | protected function buildSubCollection(array $items, string $key): Collection
69 | {
70 | return new Collection($items, $this->config[$key]->children);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/DataObjects/Mutation/Item.php:
--------------------------------------------------------------------------------
1 | hasChanged = $hasChanged;
21 | }
22 |
23 | public function __get(string $key)
24 | {
25 | if ((!array_key_exists($key, $this->arguments) || ($this->arguments[$key] === null))
26 | && array_key_exists($key, $this->config)
27 | && (($this->config[$key]->type === Item::class) || ($this->config[$key]->type === Collection::class))
28 | ) {
29 | $mutationTypeClass = $this->config[$key]->type;
30 |
31 | $this->arguments[$key] = new $mutationTypeClass([], $this->config[$key]->children);
32 | }
33 |
34 | return parent::__get($key);
35 | }
36 |
37 | public function __set(string $key, $value): void
38 | {
39 | if (!array_key_exists($key, $this->arguments) || $this->arguments[$key] !== $value) {
40 | $this->hasChanged = true;
41 | }
42 |
43 | parent::__set($key, $value);
44 | }
45 |
46 | public function __unset(string $key): void
47 | {
48 | unset($this->arguments[$key]);
49 |
50 | $this->hasChanged = true;
51 | }
52 |
53 | public function set(array $data): void
54 | {
55 | foreach ($data as $key => $value) {
56 | $this->{$key} = $value;
57 | }
58 | }
59 |
60 | public function jsonSerialize(): array
61 | {
62 | if (!$this->hasChanged()) {
63 | return [];
64 | }
65 |
66 | return parent::jsonSerialize();
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/DataObjects/Mutation/MutationObject.php:
--------------------------------------------------------------------------------
1 | hasChanged) {
12 | return true;
13 | }
14 |
15 | foreach ($this->arguments as $argument) {
16 | if ($argument instanceof MutationObject && $argument->hasChanged()) {
17 | $this->hasChanged = true;
18 |
19 | return true;
20 | }
21 | }
22 |
23 | return false;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/DataObjects/Query/Collection.php:
--------------------------------------------------------------------------------
1 | hasChildren()) {
12 | return [];
13 | }
14 |
15 | $items = [];
16 | foreach ($this->arguments as $item) {
17 | $items[] = $item->jsonSerialize();
18 | }
19 |
20 | return $items;
21 | }
22 |
23 | protected function buildFilteredCollection($items): Collection
24 | {
25 | return new Collection($items);
26 | }
27 |
28 | protected function buildSubCollection(array $items, string $key): Collection
29 | {
30 | return new Collection($items);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/DataObjects/Query/Item.php:
--------------------------------------------------------------------------------
1 | $config
27 | */
28 | public static function build(array $config, QueryObject $source, bool $fromMutation = false): MutationObject
29 | {
30 | self::$config = $config;
31 | self::$hasChanged = $fromMutation;
32 |
33 | $mutationVariables = [];
34 | foreach (self::$config as $variableName => $mutationTypeConfig) {
35 | self::$mutationTypeConfig = $mutationTypeConfig;
36 | $path = self::SOURCE_ROOT_PATH;
37 | $config = $mutationTypeConfig->get($path);
38 | if ($config->type === MutationCollection::class) {
39 | $arguments = [];
40 | foreach ($source as $sourceItem) {
41 | $mutationItemArguments = self::generateMutationArguments($sourceItem, $path);
42 |
43 | $arguments[] = new MutationItem($mutationItemArguments, $config->children, self::$hasChanged);
44 | }
45 |
46 | $mutationVariables[$variableName] = new $config->type(
47 | $arguments,
48 | $config->children,
49 | self::$hasChanged
50 | );
51 | } else {
52 | $arguments = self::generateMutationArguments($source, $path);
53 |
54 | $mutationVariables[$variableName] = new $config->type(
55 | $arguments,
56 | $config->children,
57 | self::$hasChanged
58 | );
59 | }
60 | }
61 |
62 | return new MutationItem($mutationVariables, self::$config, self::$hasChanged);
63 | }
64 |
65 | private static function generateMutationArguments(QueryItem $source, string $path): array
66 | {
67 | $arguments = [];
68 | foreach ($source as $sourceKey => $sourceValue) {
69 | $childPath = self::createPathFromParent($path, $sourceKey);
70 | $childConfig = self::$mutationTypeConfig->get($childPath);
71 |
72 | if (is_null($childConfig)) {
73 | continue;
74 | }
75 |
76 | if (self::hasChildrenToMutate($childConfig)) {
77 | if (is_null($sourceValue)) {
78 | continue;
79 | }
80 |
81 | $mutatedChild = self::mutateChild($childConfig, $sourceValue, $childPath);
82 | if (!is_null($mutatedChild)) {
83 | $arguments[$sourceKey] = $mutatedChild;
84 | }
85 | } else {
86 | if ($sourceValue instanceof QueryObject) {
87 | $sourceValue = $sourceValue->toArray();
88 | }
89 |
90 | $arguments[$sourceKey] = $sourceValue;
91 | }
92 | }
93 |
94 | return $arguments;
95 | }
96 |
97 | private static function createPathFromParent(string $parent, string $child): string
98 | {
99 | return ('.' === $parent) ? ".{$child}" : "{$parent}.{$child}";
100 | }
101 |
102 | private static function hasChildrenToMutate(MutationTypeConfig $childConfig): bool
103 | {
104 | return !is_null($childConfig->type);
105 | }
106 |
107 | private static function mutateChild(
108 | MutationTypeConfig $config,
109 | QueryObject $sourceObject,
110 | string $path
111 | ): ?MutationObject {
112 | if (is_null($config->linksTo)) {
113 | $arguments = [];
114 | foreach ($config->children as $key => $childConfig) {
115 | if (!is_null($childConfig->type)) {
116 | $childPath = self::createPathFromParent($path, $key);
117 | $mutatedChild = self::mutateChild($childConfig, $sourceObject, $childPath);
118 | if (!is_null($mutatedChild)) {
119 | $arguments[$key] = $mutatedChild;
120 | }
121 | }
122 | }
123 |
124 | if ($arguments === []) {
125 | return null;
126 | }
127 |
128 | return new $config->type($arguments, $config->children, self::$hasChanged);
129 | }
130 |
131 | if ($sourceObject instanceof QueryItem) {
132 | return self::mutateItem($sourceObject, $path, $config);
133 | }
134 |
135 | $arguments = [];
136 | foreach ($sourceObject as $sourceItem) {
137 | $arguments[] = self::mutateItem($sourceItem, $path, $config);
138 | }
139 |
140 | return new $config->type($arguments, $config->children, self::$hasChanged);
141 | }
142 |
143 | private static function mutateItem(
144 | QueryItem $sourceItem,
145 | string $path,
146 | MutationTypeConfig $config
147 | ): MutationItem {
148 | $itemArguments = self::generateMutationArguments($sourceItem, $path);
149 |
150 | return new MutationItem($itemArguments, $config->children, self::$hasChanged);
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/Response.php:
--------------------------------------------------------------------------------
1 | data;
14 | }
15 |
16 | public function getErrors(): array
17 | {
18 | return $this->errors;
19 | }
20 |
21 | public function hasErrors(): bool
22 | {
23 | return $this->errors !== [];
24 | }
25 |
26 | public function getDataObject(): array
27 | {
28 | return $this->dataObject;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/ResponseBuilder.php:
--------------------------------------------------------------------------------
1 | getBody();
17 |
18 | $normalizedResponse = $this->getNormalizedResponse($body);
19 |
20 | $dataObject = array_key_exists('dataObject', $normalizedResponse)
21 | ? $normalizedResponse['dataObject']
22 | : [];
23 |
24 | return new Response(
25 | $normalizedResponse['data'],
26 | $normalizedResponse['errors'],
27 | $dataObject
28 | );
29 | }
30 |
31 | private function getNormalizedResponse(string $body): array
32 | {
33 | $decodedResponse = $this->getJsonDecodedResponse($body);
34 |
35 | if (false === array_key_exists('data', $decodedResponse) && empty($decodedResponse['errors'])) {
36 | throw new UnexpectedValueException(
37 | 'Invalid GraphQL JSON response. Response body: ' . json_encode($decodedResponse)
38 | );
39 | }
40 |
41 | $result = [
42 | 'data' => $decodedResponse['data'] ?? [],
43 | 'errors' => $decodedResponse['errors'] ?? [],
44 | ];
45 |
46 | if (!is_null($this->dataObjectBuilder)) {
47 | $result['dataObject'] = $this->dataObjectBuilder->buildQuery($decodedResponse['data'] ?? []);
48 | }
49 |
50 | return $result;
51 | }
52 |
53 | private function getJsonDecodedResponse(string $body)
54 | {
55 | $response = json_decode($body, true);
56 |
57 | $error = json_last_error();
58 | if (JSON_ERROR_NONE !== $error) {
59 | throw new UnexpectedValueException(
60 | 'Invalid JSON response. Response body: ' . $body
61 | );
62 | }
63 |
64 | return $response;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Traits/CollectionArrayAccess.php:
--------------------------------------------------------------------------------
1 | arguments);
17 | }
18 |
19 | public function offsetUnset($offset): void
20 | {
21 | throw new BadMethodCallException('Try using remove() instead');
22 | }
23 |
24 | public function offsetGet($offset): mixed
25 | {
26 | return $this->arguments[$offset] ?? null;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Traits/ItemIterator.php:
--------------------------------------------------------------------------------
1 | arguments);
10 | }
11 |
12 | public function current(): mixed
13 | {
14 | return current($this->arguments);
15 | }
16 |
17 | public function key(): int|string|null
18 | {
19 | return key($this->arguments);
20 | }
21 |
22 | public function next(): void
23 | {
24 | next($this->arguments);
25 | }
26 |
27 | public function valid(): bool
28 | {
29 | $key = key($this->arguments);
30 |
31 | return ($key !== null && $key !== false);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Traits/JsonPathAccessor.php:
--------------------------------------------------------------------------------
1 | hasChild($attribute) ? $this->children[$attribute] : $this->{$attribute};
23 |
24 | if ($attributes !== []) {
25 | return $value->get('.' . implode('.', $attributes));
26 | }
27 |
28 | return $value;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tests/ClientBuilderTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(Client::class, $client);
16 | }
17 |
18 | public function testBuildWithGuzzleOptions(): void
19 | {
20 | $guzzleOptions = [
21 | 'cookies' => new CookieJar(),
22 | ];
23 |
24 | $client = ClientBuilder::build('http://foo.bar/qux', $guzzleOptions);
25 | $this->assertInstanceOf(Client::class, $client);
26 | }
27 |
28 | public function testBuildWithOAuth2Provider(): void
29 | {
30 | $mockCache = $this->createMock(CacheItemPoolInterface::class);
31 | $mockProvider = $this->createMock(AbstractProvider::class);
32 | $mockTokenOptions = [
33 | 'grant_type' => 'client_credentials',
34 | 'scope' => 'myscope',
35 | ];
36 |
37 | $client = ClientBuilder::buildWithOAuth2Provider(
38 | 'http://foo.bar/qux',
39 | $mockProvider,
40 | $mockTokenOptions,
41 | $mockCache
42 | );
43 | $this->assertInstanceOf(Client::class, $client);
44 | }
45 |
46 | public function testBuildWithOAuth2ProviderAndGuzzleOptions(): void
47 | {
48 | $mockCache = $this->createMock(CacheItemPoolInterface::class);
49 | $mockProvider = $this->createMock(AbstractProvider::class);
50 | $mockTokenOptions = [
51 | 'grant_type' => 'client_credentials',
52 | 'scope' => 'myscope',
53 | ];
54 |
55 | $guzzleOptions = [
56 | 'cookies' => new CookieJar(),
57 | ];
58 |
59 | $client = ClientBuilder::buildWithOAuth2Provider(
60 | 'http://foo.bar/qux',
61 | $mockProvider,
62 | $mockTokenOptions,
63 | $mockCache,
64 | $guzzleOptions
65 | );
66 | $this->assertInstanceOf(Client::class, $client);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/tests/ClientTest.php:
--------------------------------------------------------------------------------
1 | httpClient = $this->createMock(ClientInterface::class);
28 | $this->mockGraphqlResponseBuilder = $this->createMock(ResponseBuilder::class);
29 | $this->client = new Client($this->httpClient, $this->mockGraphqlResponseBuilder);
30 | }
31 |
32 | public function testSimpleQueryWhenHasNetworkErrors(): void
33 | {
34 | $this->httpClient->expects($this->once())
35 | ->method('request')
36 | ->willThrowException(new TransferException('library error'));
37 |
38 | $this->expectException(RuntimeException::class);
39 | $this->expectExceptionMessage('Network Error.');
40 |
41 | $query = $this->getSimpleQuery();
42 | $this->client->query($query);
43 | }
44 |
45 | private function getSimpleQuery(): string
46 | {
47 | return <<<'QUERY'
48 | {
49 | foo(id:"bar") {
50 | id_foo
51 | }
52 | }
53 | QUERY;
54 | }
55 |
56 | public function testCanRetrievePreviousExceptionWhenSimpleQueryHasErrors(): void
57 | {
58 | $previousException = null;
59 | try {
60 | $originalException = new ServerException(
61 | 'Server side error',
62 | $this->createMock(RequestInterface::class),
63 | $this->createMock(ResponseInterface::class)
64 | );
65 |
66 | $this->httpClient->expects($this->once())
67 | ->method('request')
68 | ->willThrowException($originalException);
69 |
70 | $query = $this->getSimpleQuery();
71 | $this->client->query($query);
72 | } catch (Exception $e) {
73 | $previousException = $e->getPrevious();
74 | } finally {
75 | $this->assertSame($originalException, $previousException);
76 | }
77 | }
78 |
79 | public function testSimpleQueryWhenInvalidJsonIsReceived(): void
80 | {
81 | $query = $this->getSimpleQuery();
82 |
83 | $mockHttpResponse = $this->createMock(ResponseInterface::class);
84 | $this->mockGraphqlResponseBuilder->expects($this->once())
85 | ->method('build')
86 | ->with($mockHttpResponse)
87 | ->willThrowException(new UnexpectedValueException('Invalid JSON response.'));
88 | $this->httpClient->expects($this->once())
89 | ->method('request')
90 | ->with(
91 | 'POST',
92 | '',
93 | [
94 | 'body' => json_encode([
95 | 'query' => $query,
96 | ], JSON_UNESCAPED_SLASHES),
97 | 'headers' => [
98 | 'Content-Type' => 'application/json',
99 | ],
100 | ]
101 | )
102 | ->willReturn($mockHttpResponse);
103 |
104 | $this->expectException(UnexpectedValueException::class);
105 | $this->expectExceptionMessage('Invalid JSON response.');
106 |
107 | $this->client->query($query);
108 | }
109 |
110 | public function testSimpleQuery(): void
111 | {
112 | $mockResponse = $this->createMock(Response::class);
113 | $mockHttpResponse = $this->createMock(ResponseInterface::class);
114 |
115 | $query = $this->getSimpleQuery();
116 |
117 | $this->mockGraphqlResponseBuilder->expects($this->once())
118 | ->method('build')
119 | ->with($mockHttpResponse)
120 | ->willReturn($mockResponse);
121 | $this->httpClient->expects($this->once())
122 | ->method('request')
123 | ->with(
124 | 'POST',
125 | '',
126 | [
127 | 'body' => json_encode([
128 | 'query' => $query,
129 | ], JSON_UNESCAPED_SLASHES),
130 | 'headers' => [
131 | 'Content-Type' => 'application/json',
132 | ],
133 | ]
134 | )
135 | ->willReturn($mockHttpResponse);
136 |
137 | $response = $this->client->query($query);
138 | $this->assertInstanceOf(Response::class, $response);
139 | }
140 |
141 | public function testQueryWithVariables(): void
142 | {
143 | $mockResponse = $this->createMock(Response::class);
144 | $mockHttpResponse = $this->createMock(ResponseInterface::class);
145 |
146 | $query = $this->getQueryWithVariables();
147 | $variables = [
148 | 'idFoo' => '642e69c0-9b2e-11e6-9850-00163ed833e7',
149 | 'page' => 1,
150 | ];
151 |
152 | $this->mockGraphqlResponseBuilder->expects($this->once())
153 | ->method('build')
154 | ->with($mockHttpResponse)
155 | ->willReturn($mockResponse);
156 | $this->httpClient->expects($this->once())
157 | ->method('request')
158 | ->with(
159 | 'POST',
160 | '',
161 | [
162 | 'body' => json_encode([
163 | 'query' => $query,
164 | 'variables' => $variables,
165 | ], JSON_UNESCAPED_SLASHES),
166 | 'headers' => [
167 | 'Content-Type' => 'application/json',
168 | ],
169 | ]
170 | )
171 | ->willReturn($mockHttpResponse);
172 |
173 | $response = $this->client->query($query, $variables);
174 | $this->assertInstanceOf(Response::class, $response);
175 | }
176 |
177 | private function getQueryWithVariables(): string
178 | {
179 | return <<<'QUERY'
180 | query GetFooBar($idFoo: String, $idBar: String) {
181 | foo(id: $idFoo) {
182 | id_foo
183 | bar (id: $idBar) {
184 | id_bar
185 | }
186 | }
187 | }
188 | QUERY;
189 | }
190 |
191 | public function testMutate(): void
192 | {
193 | $mockResponse = $this->createMock(Response::class);
194 | $mockHttpResponse = $this->createMock(ResponseInterface::class);
195 |
196 | $query = $this->getMutationQuery();
197 | $variables = Mutation::build([], new Item(['idFoo' => '642e69c0-9b2e-11e6-9850-00163ed833e7']));
198 |
199 | $this->mockGraphqlResponseBuilder->expects($this->once())
200 | ->method('build')
201 | ->with($mockHttpResponse)
202 | ->willReturn($mockResponse);
203 | $this->httpClient->expects($this->once())
204 | ->method('request')
205 | ->with(
206 | 'POST',
207 | '',
208 | [
209 | 'body' => json_encode([
210 | 'query' => $query,
211 | 'variables' => $variables,
212 | ], JSON_UNESCAPED_SLASHES),
213 | 'headers' => [
214 | 'Content-Type' => 'application/json',
215 | ],
216 | ]
217 | )
218 | ->willReturn($mockHttpResponse);
219 |
220 | $response = $this->client->mutate($query, $variables);
221 | $this->assertInstanceOf(Response::class, $response);
222 | }
223 |
224 | private function getMutationQuery(): string
225 | {
226 | return <<<'QUERY'
227 | mutation replaceFoo($foo: FooInput!) {
228 | replaceFoo(foo: $foo) {
229 | status
230 | }
231 | }
232 | QUERY;
233 | }
234 | }
235 |
--------------------------------------------------------------------------------
/tests/Config/MutationsConfigTest.php:
--------------------------------------------------------------------------------
1 | [
16 | 'book' => [
17 | 'linksTo' => '.',
18 | 'type' => Item::class,
19 | 'children' => [
20 | 'chapters' => [
21 | 'type' => Item::class,
22 | 'children' => [
23 | 'upsert' => [
24 | 'linksTo' => '.chapters',
25 | 'type' => Collection::class,
26 | ],
27 | ],
28 | ],
29 | 'title' => [],
30 | ],
31 | ],
32 | ],
33 | ]
34 | );
35 |
36 | $upsertConfig = new MutationTypeConfig();
37 | $upsertConfig->type = Collection::class;
38 | $upsertConfig->linksTo = '.chapters';
39 |
40 | $chaptersConfig = new MutationTypeConfig();
41 | $chaptersConfig->type = Item::class;
42 | $chaptersConfig->children = ['upsert' => $upsertConfig];
43 |
44 | $titleConfig = new MutationTypeConfig();
45 |
46 | $bookConfig = new MutationTypeConfig();
47 | $bookConfig->type = Item::class;
48 | $bookConfig->linksTo = '.';
49 | $bookConfig->children = [
50 | 'chapters' => $chaptersConfig,
51 | 'title' => $titleConfig,
52 | ];
53 |
54 | $this->assertEquals(['book' => $bookConfig], $mutationsConfig->get('ReplaceBook'));
55 | }
56 |
57 | public function testWhenThereIsOneMutationsWithTwoVariables(): void
58 | {
59 | $mutationsConfig = new MutationsConfig(
60 | [
61 | 'ReplaceBook' => [
62 | 'book' => [
63 | 'linksTo' => '.',
64 | 'type' => Item::class,
65 | 'children' => [
66 | 'chapters' => [
67 | 'type' => Item::class,
68 | 'children' => [
69 | 'upsert' => [
70 | 'linksTo' => '.chapters',
71 | 'type' => Collection::class,
72 | ],
73 | ],
74 | ],
75 | ],
76 | ],
77 | 'author' => [
78 | 'linksTo' => '.',
79 | 'type' => Item::class,
80 | ],
81 | ],
82 | ]
83 | );
84 |
85 | $upsertConfig = new MutationTypeConfig();
86 | $upsertConfig->type = Collection::class;
87 | $upsertConfig->linksTo = '.chapters';
88 |
89 | $chaptersConfig = new MutationTypeConfig();
90 | $chaptersConfig->type = Item::class;
91 | $chaptersConfig->children = ['upsert' => $upsertConfig];
92 |
93 | $bookConfig = new MutationTypeConfig();
94 | $bookConfig->type = Item::class;
95 | $bookConfig->linksTo = '.';
96 | $bookConfig->children = ['chapters' => $chaptersConfig];
97 |
98 | $authorConfig = new MutationTypeConfig();
99 | $authorConfig->type = Item::class;
100 | $authorConfig->linksTo = '.';
101 |
102 | $expectedMutationConfig = [
103 | 'book' => $bookConfig,
104 | 'author' => $authorConfig,
105 | ];
106 | $this->assertEquals($expectedMutationConfig, $mutationsConfig->get('ReplaceBook'));
107 | }
108 |
109 | public function testWhenThereAreTwoMutations(): void
110 | {
111 | $mutationsConfig = new MutationsConfig(
112 | [
113 | 'ReplaceBook' => [
114 | 'book' => [
115 | 'linksTo' => '.',
116 | 'type' => Item::class,
117 | 'children' => [
118 | 'chapters' => [
119 | 'type' => Item::class,
120 | 'children' => [
121 | 'upsert' => [
122 | 'linksTo' => '.chapters',
123 | 'type' => Collection::class,
124 | ],
125 | ],
126 | ],
127 | ],
128 | ],
129 | ],
130 | 'ReplaceVideo' => [
131 | 'video' => [
132 | 'linksTo' => '.',
133 | 'type' => Item::class,
134 | ],
135 | ],
136 | ]
137 | );
138 |
139 | $upsertConfig = new MutationTypeConfig();
140 | $upsertConfig->type = Collection::class;
141 | $upsertConfig->linksTo = '.chapters';
142 |
143 | $chaptersConfig = new MutationTypeConfig();
144 | $chaptersConfig->type = Item::class;
145 | $chaptersConfig->children = ['upsert' => $upsertConfig];
146 |
147 | $bookConfig = new MutationTypeConfig();
148 | $bookConfig->type = Item::class;
149 | $bookConfig->linksTo = '.';
150 | $bookConfig->children = ['chapters' => $chaptersConfig];
151 |
152 | $videoConfig = new MutationTypeConfig();
153 | $videoConfig->type = Item::class;
154 | $videoConfig->linksTo = '.';
155 |
156 | $this->assertEquals(['book' => $bookConfig], $mutationsConfig->get('ReplaceBook'));
157 | $this->assertEquals(['video' => $videoConfig], $mutationsConfig->get('ReplaceVideo'));
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/tests/ConfigGeneratorTest.php:
--------------------------------------------------------------------------------
1 | execute(
15 | [
16 | 'instrospection-result' => __DIR__ . '/fixtures/introspection.json',
17 | 'mutation' => 'replaceProgram',
18 | ]
19 | );
20 | $output = $commandTester->getDisplay();
21 |
22 | $expectedOutput = file_get_contents(__DIR__ . '/fixtures/config-from-query');
23 | $this->assertSame($expectedOutput, $output);
24 | }
25 |
26 | public function testGenerateFromMutation(): void
27 | {
28 | $commandTester = new CommandTester(new GenerateConfig());
29 | $commandTester->execute(
30 | [
31 | 'instrospection-result' => __DIR__ . '/fixtures/introspection.json',
32 | 'mutation' => 'replaceProgram',
33 | '--from-same-mutation' => true,
34 | ]
35 | );
36 | $output = $commandTester->getDisplay();
37 |
38 | $expectedOutput = file_get_contents(__DIR__ . '/fixtures/config-from-mutation');
39 | $this->assertSame($expectedOutput, $output);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tests/DataObjectBuilderTest.php:
--------------------------------------------------------------------------------
1 | builder = new DataObjectBuilder();
18 | }
19 |
20 | public function testWhenDataIsNull(): void
21 | {
22 | $data = [
23 | 'book' => null,
24 | ];
25 |
26 | $dataObject = $this->builder->buildQuery($data);
27 |
28 | $expectedDataObject = [
29 | 'book' => null,
30 | ];
31 | $this->assertEquals($expectedDataObject, $dataObject);
32 | }
33 |
34 | public function testWhenDataIsAnEmptyArray(): void
35 | {
36 | $data = [
37 | 'search' => [],
38 | ];
39 |
40 | $dataObject = $this->builder->buildQuery($data);
41 |
42 | $expectedDataObject = [
43 | 'search' => new QueryCollection([]),
44 | ];
45 | $this->assertEquals($expectedDataObject, $dataObject);
46 | }
47 |
48 | public function testWhenDataHasAQueryItem(): void
49 | {
50 | $data = [
51 | 'book' => [
52 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
53 | 'id_author' => 1234,
54 | 'genre' => null,
55 | ],
56 | ];
57 |
58 | $dataObject = $this->builder->buildQuery($data);
59 |
60 | $expectedDataObject = [
61 | 'book' => new QueryItem(
62 | [
63 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
64 | 'id_author' => 1234,
65 | 'genre' => null,
66 | ]
67 | ),
68 | ];
69 | $this->assertEquals($expectedDataObject, $dataObject);
70 | }
71 |
72 | public function testWhenDataHasAQueryItemWithAnArrayOfStringsArgument(): void
73 | {
74 | $data = [
75 | 'book' => [
76 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
77 | 'id_author' => 1234,
78 | 'genre' => null,
79 | 'comments' => ['Good', 'Bad'],
80 | ],
81 | ];
82 |
83 | $dataObject = $this->builder->buildQuery($data);
84 |
85 | $expectedDataObject = [
86 | 'book' => new QueryItem(
87 | [
88 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
89 | 'id_author' => 1234,
90 | 'genre' => null,
91 | 'comments' => ['Good', 'Bad'],
92 | ]
93 | ),
94 | ];
95 | $this->assertEquals($expectedDataObject, $dataObject);
96 | }
97 |
98 | public function testWhenDataHasAnArrayOfQueryItems(): void
99 | {
100 | $data = [
101 | 'search' => [
102 | [
103 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
104 | 'id_author' => 1234,
105 | 'genre' => null,
106 | ],
107 | [
108 | 'id_book' => 'a53493b0-4a24-40c4-b786-317f8dfdf897',
109 | 'id_author' => 1122,
110 | 'genre' => 'drama',
111 | ],
112 | ],
113 | ];
114 |
115 | $dataObject = $this->builder->buildQuery($data);
116 |
117 | $expectedDataObject = [
118 | 'search' => new QueryCollection(
119 | [
120 | new QueryItem(
121 | [
122 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
123 | 'id_author' => 1234,
124 | 'genre' => null,
125 | ]
126 | ),
127 | new QueryItem(
128 | [
129 | 'id_book' => 'a53493b0-4a24-40c4-b786-317f8dfdf897',
130 | 'id_author' => 1122,
131 | 'genre' => 'drama',
132 | ]
133 | ),
134 | ]
135 | ),
136 | ];
137 | $this->assertEquals($expectedDataObject, $dataObject);
138 | }
139 |
140 | public function testWhenDataHasAQueryItemWithEmptySecondLevel(): void
141 | {
142 | $data = [
143 | 'book' => [
144 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
145 | 'id_author' => 1234,
146 | 'genre' => null,
147 | 'chapters' => [],
148 | ],
149 | ];
150 |
151 | $dataObject = $this->builder->buildQuery($data);
152 |
153 | $expectedDataObject = [
154 | 'book' => new QueryItem(
155 | [
156 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
157 | 'id_author' => 1234,
158 | 'genre' => null,
159 | 'chapters' => new QueryCollection([]),
160 | ]
161 | ),
162 | ];
163 | $this->assertEquals($expectedDataObject, $dataObject);
164 | }
165 |
166 | public function testWhenDataHasAQueryItemWithSecondLevel(): void
167 | {
168 | $data = [
169 | 'book' => [
170 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
171 | 'id_author' => 1234,
172 | 'genre' => null,
173 | 'chapters' => [
174 | [
175 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
176 | 'id_chapter' => 1,
177 | 'name' => 'Chapter name 1',
178 | ],
179 | [
180 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
181 | 'id_chapter' => 2,
182 | 'name' => 'Chapter name 2',
183 | ],
184 | ],
185 | ],
186 | ];
187 |
188 | $dataObject = $this->builder->buildQuery($data);
189 |
190 | $expectedDataObject = [
191 | 'book' => new QueryItem(
192 | [
193 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
194 | 'id_author' => 1234,
195 | 'genre' => null,
196 | 'chapters' => new QueryCollection(
197 | [
198 | new QueryItem(
199 | [
200 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
201 | 'id_chapter' => 1,
202 | 'name' => 'Chapter name 1',
203 | ]
204 | ),
205 | new QueryItem(
206 | [
207 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
208 | 'id_chapter' => 2,
209 | 'name' => 'Chapter name 2',
210 | ]
211 | ),
212 | ]
213 | ),
214 | ]
215 | ),
216 | ];
217 | $this->assertEquals($expectedDataObject, $dataObject);
218 | }
219 |
220 | public function testWhenDataHasAQueryItemWithThirdLevel(): void
221 | {
222 | $data = [
223 | 'book' => [
224 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
225 | 'id_author' => 1234,
226 | 'genre' => null,
227 | 'chapters' => [
228 | [
229 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
230 | 'id_chapter' => 1,
231 | 'name' => 'Chapter name 1',
232 | 'pages' => [
233 | [
234 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
235 | 'id_chapter' => 1,
236 | 'id_page' => 1,
237 | 'has_illustrations' => false,
238 | ],
239 | [
240 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
241 | 'id_chapter' => 1,
242 | 'id_page' => 2,
243 | 'has_illustrations' => false,
244 | ],
245 | ],
246 | ],
247 | [
248 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
249 | 'id_chapter' => 2,
250 | 'name' => 'Chapter name 2',
251 | 'pages' => [],
252 | ],
253 | ],
254 | ],
255 | ];
256 |
257 | $dataObject = $this->builder->buildQuery($data);
258 |
259 | $expectedDataObject = [
260 | 'book' => new QueryItem(
261 | [
262 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
263 | 'id_author' => 1234,
264 | 'genre' => null,
265 | 'chapters' => new QueryCollection(
266 | [
267 | new QueryItem(
268 | [
269 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
270 | 'id_chapter' => 1,
271 | 'name' => 'Chapter name 1',
272 | 'pages' => new QueryCollection(
273 | [
274 | new QueryItem(
275 | [
276 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
277 | 'id_chapter' => 1,
278 | 'id_page' => 1,
279 | 'has_illustrations' => false,
280 | ]
281 | ),
282 | new QueryItem(
283 | [
284 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
285 | 'id_chapter' => 1,
286 | 'id_page' => 2,
287 | 'has_illustrations' => false,
288 | ]
289 | ),
290 | ]
291 | ),
292 | ]
293 | ),
294 | new QueryItem(
295 | [
296 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
297 | 'id_chapter' => 2,
298 | 'name' => 'Chapter name 2',
299 | 'pages' => new QueryCollection([]),
300 | ]
301 | ),
302 | ]
303 | ),
304 | ]
305 | ),
306 | ];
307 | $this->assertEquals($expectedDataObject, $dataObject);
308 | }
309 |
310 | public function testWhenDataHasAQueryItemInsideAnotherQueryItem(): void
311 | {
312 | $data = [
313 | 'book' => [
314 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
315 | 'id_author' => 1234,
316 | 'genre' => null,
317 | 'chapters' => [
318 | 'upsert' => [
319 | [
320 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
321 | 'id_chapter' => 1,
322 | 'name' => 'Chapter name 1',
323 | 'pages' => [
324 | 'upsert' => [
325 | [
326 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
327 | 'id_chapter' => 1,
328 | 'id_page' => 1,
329 | 'has_illustrations' => false,
330 | ],
331 | [
332 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
333 | 'id_chapter' => 1,
334 | 'id_page' => 2,
335 | 'has_illustrations' => false,
336 | ],
337 | ],
338 | ],
339 | ],
340 | [
341 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
342 | 'id_chapter' => 2,
343 | 'name' => 'Chapter name 2',
344 | 'pages' => [
345 | 'upsert' => [],
346 | ],
347 | ],
348 | ],
349 | ],
350 | ],
351 | ];
352 |
353 | $dataObject = $this->builder->buildMutation($data);
354 |
355 | $expectedDataObject = [
356 | 'book' => new MutationItem(
357 | [
358 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
359 | 'id_author' => 1234,
360 | 'genre' => null,
361 | 'chapters' => new MutationItem(
362 | [
363 | 'upsert' => new MutationCollection(
364 | [
365 | new MutationItem(
366 | [
367 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
368 | 'id_chapter' => 1,
369 | 'name' => 'Chapter name 1',
370 | 'pages' => new MutationItem(
371 | [
372 | 'upsert' => new MutationCollection(
373 | [
374 | new MutationItem(
375 | [
376 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
377 | 'id_chapter' => 1,
378 | 'id_page' => 1,
379 | 'has_illustrations' => false,
380 | ]
381 | ),
382 | new MutationItem(
383 | [
384 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
385 | 'id_chapter' => 1,
386 | 'id_page' => 2,
387 | 'has_illustrations' => false,
388 | ]
389 | ),
390 | ]
391 | ),
392 | ]
393 | ),
394 | ]
395 | ),
396 | new MutationItem(
397 | [
398 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
399 | 'id_chapter' => 2,
400 | 'name' => 'Chapter name 2',
401 | 'pages' => new MutationItem(
402 | [
403 | 'upsert' => new MutationCollection([]),
404 | ]
405 | ),
406 | ]
407 | ),
408 | ]
409 | ),
410 | ]
411 | ),
412 | ]
413 | ),
414 | ];
415 | $this->assertEquals($expectedDataObject, $dataObject);
416 | }
417 | }
418 |
--------------------------------------------------------------------------------
/tests/MutationBuilderTest.php:
--------------------------------------------------------------------------------
1 | simpleConfigMock = $this->getConfigMock()
25 | ->get('ReplaceBookSimple');
26 | $this->complexConfigMock = $this->getConfigMock()
27 | ->get('ReplaceBookComplex');
28 | $this->sameQueryStructureConfigMock = $this->getConfigMock()
29 | ->get('ReplaceBookWithSameQueryStructure');
30 | $this->collectionConfigMock = $this->getConfigMock()
31 | ->get('ReplaceBooks');
32 | }
33 |
34 | private function getConfigMock(): MutationsConfig
35 | {
36 | return new MutationsConfig(
37 | [
38 | 'ReplaceBookSimple' => [
39 | 'book' => [
40 | 'linksTo' => '.',
41 | 'type' => MutationItem::class,
42 | 'children' => [
43 | 'id_book' => [],
44 | 'id_author' => [],
45 | 'genre' => [],
46 | 'chapters' => [
47 | 'linksTo' => '.chapters',
48 | 'type' => MutationCollection::class,
49 | 'children' => [
50 | 'id_book' => [],
51 | 'id_chapter' => [],
52 | 'name' => [],
53 | 'tags' => [],
54 | ],
55 | ],
56 | ],
57 | ],
58 | ],
59 | 'ReplaceBookComplex' => [
60 | 'book' => [
61 | 'linksTo' => '.',
62 | 'type' => MutationItem::class,
63 | 'children' => [
64 | 'id_book' => [],
65 | 'id_author' => [],
66 | 'genre' => [],
67 | 'chapters' => [
68 | 'type' => MutationItem::class,
69 | 'children' => [
70 | 'upsert' => [
71 | 'linksTo' => '.chapters',
72 | 'type' => MutationCollection::class,
73 | 'children' => [
74 | 'id_book' => [],
75 | 'id_chapter' => [],
76 | 'name' => [],
77 | 'pages' => [
78 | 'type' => MutationItem::class,
79 | 'children' => [
80 | 'upsert' => [
81 | 'linksTo' => '.chapters.pages',
82 | 'type' => MutationCollection::class,
83 | 'children' => [
84 | 'id_book' => [],
85 | 'id_chapter' => [],
86 | 'id_page' => [],
87 | 'has_illustrations' => [],
88 | 'lines' => [
89 | 'type' => MutationItem::class,
90 | 'children' => [
91 | 'upsert' => [
92 | 'linksTo' => '.chapters.pages.lines',
93 | 'type' => MutationCollection::class,
94 | 'children' => [
95 | 'id_book' => [],
96 | 'id_chapter' => [],
97 | 'id_page' => [],
98 | 'id_line' => [],
99 | 'words_count' => [],
100 | ],
101 | ],
102 | 'delete' => [
103 | 'type' => MutationCollection::class,
104 | 'children' => [
105 | 'id_book' => [],
106 | 'id_chapter' => [],
107 | 'id_page' => [],
108 | 'id_line' => [],
109 | ],
110 | ],
111 | ],
112 | ],
113 | ],
114 | ],
115 | 'delete' => [
116 | 'type' => MutationCollection::class,
117 | 'children' => [
118 | 'id_book' => [],
119 | 'id_chapter' => [],
120 | 'id_page' => [],
121 | ],
122 | ],
123 | ],
124 | ],
125 | ],
126 | ],
127 | 'delete' => [
128 | 'type' => MutationCollection::class,
129 | 'children' => [
130 | 'id_book' => [],
131 | 'id_chapter' => [],
132 | ],
133 | ],
134 | ],
135 | ],
136 | 'languages' => [
137 | 'type' => MutationItem::class,
138 | 'children' => [
139 | 'upsert' => [
140 | 'linksTo' => '.languages',
141 | 'type' => MutationCollection::class,
142 | 'children' => [
143 | 'id_book' => [],
144 | 'id_language' => [],
145 | ],
146 | ],
147 | 'delete' => [
148 | 'type' => MutationCollection::class,
149 | 'children' => [
150 | 'id_book' => [],
151 | 'id_language' => [],
152 | ],
153 | ],
154 | ],
155 | ],
156 | 'currentPage' => [
157 | 'type' => MutationItem::class,
158 | 'children' => [
159 | 'upsert' => [
160 | 'linksTo' => '.currentPage',
161 | 'type' => MutationItem::class,
162 | 'children' => [
163 | 'id_book' => [],
164 | 'id_chapter' => [],
165 | 'id_page' => [],
166 | ],
167 | ],
168 | ],
169 | ],
170 | ],
171 | ],
172 | ],
173 | 'ReplaceBookWithSameQueryStructure' => [
174 | 'book' => [
175 | 'linksTo' => '.',
176 | 'type' => MutationItem::class,
177 | 'children' => [
178 | 'id_book' => [],
179 | 'id_author' => [],
180 | 'genre' => [],
181 | 'chapters' => [
182 | 'linksTo' => '.chapters',
183 | 'type' => MutationItem::class,
184 | 'children' => [
185 | 'upsert' => [
186 | 'linksTo' => '.chapters.upsert',
187 | 'type' => MutationCollection::class,
188 | 'children' => [
189 | 'id_book' => [],
190 | 'id_chapter' => [],
191 | 'name' => [],
192 | ],
193 | ],
194 | ],
195 | ],
196 | ],
197 | ],
198 | ],
199 | 'ReplaceBooks' => [
200 | 'books' => [
201 | 'linksTo' => '.',
202 | 'type' => MutationCollection::class,
203 | 'children' => [
204 | 'id_book' => [],
205 | 'id_author' => [],
206 | 'genre' => [],
207 | 'chapters' => [
208 | 'linksTo' => '.chapters',
209 | 'type' => MutationCollection::class,
210 | 'children' => [
211 | 'id_book' => [],
212 | 'id_chapter' => [],
213 | 'name' => [],
214 | ],
215 | ],
216 | ],
217 | ],
218 | ],
219 | ]
220 | );
221 | }
222 |
223 | public function testWhenThereAreOnlyArguments(): void
224 | {
225 | $queryItem = new QueryItem(
226 | [
227 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
228 | 'id_author' => 1234,
229 | 'genre' => null,
230 | 'invalid' => 'nope',
231 | ]
232 | );
233 |
234 | $mutation = Mutation::build($this->simpleConfigMock, $queryItem, true);
235 |
236 | $expectedMutationArguments = [
237 | 'book' => [
238 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
239 | 'id_author' => 1234,
240 | 'genre' => null,
241 | ],
242 | ];
243 | $this->assertEquals($expectedMutationArguments, $mutation->jsonSerialize());
244 | }
245 |
246 | public function testWhenThereIsAnEmptyChild(): void
247 | {
248 | $queryItem = new QueryItem(
249 | [
250 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
251 | 'id_author' => 1234,
252 | 'genre' => null,
253 | 'chapters' => new QueryCollection([]),
254 | ]
255 | );
256 |
257 | $mutation = Mutation::build($this->simpleConfigMock, $queryItem, true);
258 |
259 | $expectedMutationArguments = [
260 | 'book' => [
261 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
262 | 'id_author' => 1234,
263 | 'genre' => null,
264 | ],
265 | ];
266 | $this->assertEquals($expectedMutationArguments, $mutation->jsonSerialize());
267 | }
268 |
269 | public function testWhenThereAreChildrenWithSimpleConfig(): void
270 | {
271 | $queryItem = new QueryItem(
272 | [
273 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
274 | 'id_author' => 1234,
275 | 'genre' => null,
276 | 'chapters' => new QueryCollection(
277 | [
278 | new QueryItem(
279 | [
280 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
281 | 'id_chapter' => 1,
282 | 'name' => 'Chapter name 1',
283 | 'tags' => new QueryCollection(['tag1', 'tag2']),
284 | 'invalid' => 'nope',
285 | ]
286 | ),
287 | new QueryItem(
288 | [
289 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
290 | 'id_chapter' => 2,
291 | 'name' => 'Chapter name 2',
292 | 'tags' => new QueryCollection([]),
293 | 'invalid' => 'nope',
294 | ]
295 | ),
296 | ]
297 | ),
298 | ]
299 | );
300 |
301 | $mutation = Mutation::build($this->simpleConfigMock, $queryItem, true);
302 |
303 | $expectedMutationArguments = [
304 | 'book' => [
305 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
306 | 'id_author' => 1234,
307 | 'genre' => null,
308 | 'chapters' => [
309 | [
310 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
311 | 'id_chapter' => 1,
312 | 'name' => 'Chapter name 1',
313 | 'tags' => ['tag1', 'tag2'],
314 | ],
315 | [
316 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
317 | 'id_chapter' => 2,
318 | 'name' => 'Chapter name 2',
319 | 'tags' => [],
320 | ],
321 | ],
322 | ],
323 | ];
324 | $this->assertEquals($expectedMutationArguments, $mutation->jsonSerialize());
325 | }
326 |
327 | public function testWhenThereAreChildrenWithComplexConfig(): void
328 | {
329 | $queryItem = new QueryItem(
330 | [
331 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
332 | 'id_author' => 1234,
333 | 'genre' => null,
334 | 'chapters' => new QueryCollection(
335 | [
336 | new QueryItem(
337 | [
338 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
339 | 'id_chapter' => 1,
340 | 'name' => 'Chapter name 1',
341 | ]
342 | ),
343 | new QueryItem(
344 | [
345 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
346 | 'id_chapter' => 2,
347 | 'name' => 'Chapter name 2',
348 | ]
349 | ),
350 | ]
351 | ),
352 | ]
353 | );
354 |
355 | $mutation = Mutation::build($this->complexConfigMock, $queryItem, true);
356 |
357 | $expectedMutationArguments = [
358 | 'book' => [
359 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
360 | 'id_author' => 1234,
361 | 'genre' => null,
362 | 'chapters' => [
363 | 'upsert' => [
364 | [
365 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
366 | 'id_chapter' => 1,
367 | 'name' => 'Chapter name 1',
368 | ],
369 | [
370 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
371 | 'id_chapter' => 2,
372 | 'name' => 'Chapter name 2',
373 | ],
374 | ],
375 | ],
376 | ],
377 | ];
378 | $this->assertEquals($expectedMutationArguments, $mutation->jsonSerialize());
379 | }
380 |
381 | public function testWhenThereAreTwoChildren(): void
382 | {
383 | $queryItem = new QueryItem(
384 | [
385 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
386 | 'id_author' => 1234,
387 | 'genre' => null,
388 | 'chapters' => new QueryCollection(
389 | [
390 | new QueryItem(
391 | [
392 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
393 | 'id_chapter' => 1,
394 | 'name' => 'Chapter name 1',
395 | ]
396 | ),
397 | new QueryItem(
398 | [
399 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
400 | 'id_chapter' => 2,
401 | 'name' => 'Chapter name 2',
402 | ]
403 | ),
404 | ]
405 | ),
406 | 'languages' => new QueryCollection(
407 | [
408 | new QueryItem(
409 | [
410 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
411 | 'id_language' => 'english',
412 | ]
413 | ),
414 | new QueryItem(
415 | [
416 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
417 | 'id_language' => 'italian',
418 | ]
419 | ),
420 | ]
421 | ),
422 | ]
423 | );
424 |
425 | $mutation = Mutation::build($this->complexConfigMock, $queryItem, true);
426 |
427 | $expectedMutationArguments = [
428 | 'book' => [
429 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
430 | 'id_author' => 1234,
431 | 'genre' => null,
432 | 'chapters' => [
433 | 'upsert' => [
434 | [
435 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
436 | 'id_chapter' => 1,
437 | 'name' => 'Chapter name 1',
438 | ],
439 | [
440 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
441 | 'id_chapter' => 2,
442 | 'name' => 'Chapter name 2',
443 | ],
444 | ],
445 | ],
446 | 'languages' => [
447 | 'upsert' => [
448 | [
449 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
450 | 'id_language' => 'english',
451 | ],
452 | [
453 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
454 | 'id_language' => 'italian',
455 | ],
456 | ],
457 | ],
458 | ],
459 | ];
460 | $this->assertEquals($expectedMutationArguments, $mutation->jsonSerialize());
461 | }
462 |
463 | public function testWhenThereIsAThirdLevel(): void
464 | {
465 | $queryItem = new QueryItem(
466 | [
467 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
468 | 'id_author' => 1234,
469 | 'genre' => null,
470 | 'chapters' => new QueryCollection(
471 | [
472 | new QueryItem(
473 | [
474 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
475 | 'id_chapter' => 1,
476 | 'name' => 'Chapter name 1',
477 | 'pages' => new QueryCollection([]),
478 | ]
479 | ),
480 | new QueryItem(
481 | [
482 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
483 | 'id_chapter' => 2,
484 | 'name' => 'Chapter name 2',
485 | 'pages' => new QueryCollection(
486 | [
487 | new QueryItem(
488 | [
489 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
490 | 'id_chapter' => 2,
491 | 'id_page' => 1,
492 | 'has_illustrations' => false,
493 | ]
494 | ),
495 | new QueryItem(
496 | [
497 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
498 | 'id_chapter' => 2,
499 | 'id_page' => 2,
500 | 'has_illustrations' => false,
501 | ]
502 | ),
503 | ]
504 | ),
505 | ]
506 | ),
507 | new QueryItem(
508 | [
509 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
510 | 'id_chapter' => 3,
511 | 'name' => 'Chapter name 3',
512 | 'pages' => new QueryCollection(
513 | [
514 | new QueryItem(
515 | [
516 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
517 | 'id_chapter' => 3,
518 | 'id_page' => 1,
519 | 'has_illustrations' => false,
520 | ]
521 | ),
522 | new QueryItem(
523 | [
524 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
525 | 'id_chapter' => 3,
526 | 'id_page' => 2,
527 | 'has_illustrations' => false,
528 | ]
529 | ),
530 | ]
531 | ),
532 | ]
533 | ),
534 | ]
535 | ),
536 | ]
537 | );
538 |
539 | $mutation = Mutation::build($this->complexConfigMock, $queryItem, true);
540 |
541 | $expectedMutationArguments = [
542 | 'book' => [
543 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
544 | 'id_author' => 1234,
545 | 'genre' => null,
546 | 'chapters' => [
547 | 'upsert' => [
548 | [
549 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
550 | 'id_chapter' => 1,
551 | 'name' => 'Chapter name 1',
552 | ],
553 | [
554 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
555 | 'id_chapter' => 2,
556 | 'name' => 'Chapter name 2',
557 | 'pages' => [
558 | 'upsert' => [
559 | [
560 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
561 | 'id_chapter' => 2,
562 | 'id_page' => 1,
563 | 'has_illustrations' => false,
564 | ],
565 | [
566 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
567 | 'id_chapter' => 2,
568 | 'id_page' => 2,
569 | 'has_illustrations' => false,
570 | ],
571 | ],
572 | ],
573 | ],
574 | [
575 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
576 | 'id_chapter' => 3,
577 | 'name' => 'Chapter name 3',
578 | 'pages' => [
579 | 'upsert' => [
580 | [
581 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
582 | 'id_chapter' => 3,
583 | 'id_page' => 1,
584 | 'has_illustrations' => false,
585 | ],
586 | [
587 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
588 | 'id_chapter' => 3,
589 | 'id_page' => 2,
590 | 'has_illustrations' => false,
591 | ],
592 | ],
593 | ],
594 | ],
595 | ],
596 | ],
597 | ],
598 | ];
599 | $this->assertEquals($expectedMutationArguments, $mutation->jsonSerialize());
600 | }
601 |
602 | public function testWhenThereIsAFourthLevel(): void
603 | {
604 | $queryItem = new QueryItem(
605 | [
606 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
607 | 'id_author' => 1234,
608 | 'genre' => null,
609 | 'chapters' => new QueryCollection(
610 | [
611 | new QueryItem(
612 | [
613 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
614 | 'id_chapter' => 1,
615 | 'name' => 'Chapter name 1',
616 | 'pages' => new QueryCollection(
617 | [
618 | new QueryItem(
619 | [
620 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
621 | 'id_chapter' => 1,
622 | 'id_page' => 1,
623 | 'has_illustrations' => false,
624 | 'lines' => new QueryCollection(
625 | [
626 | new QueryItem(
627 | [
628 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
629 | 'id_chapter' => 1,
630 | 'id_page' => 1,
631 | 'id_line' => 1,
632 | 'words_count' => 30,
633 | ]
634 | ),
635 | new QueryItem(
636 | [
637 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
638 | 'id_chapter' => 1,
639 | 'id_page' => 1,
640 | 'id_line' => 2,
641 | 'words_count' => 35,
642 | ]
643 | ),
644 | ]
645 | ),
646 | ]
647 | ),
648 | new QueryItem(
649 | [
650 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
651 | 'id_chapter' => 1,
652 | 'id_page' => 2,
653 | 'has_illustrations' => false,
654 | 'lines' => new QueryCollection(
655 | [
656 | new QueryItem(
657 | [
658 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
659 | 'id_chapter' => 1,
660 | 'id_page' => 2,
661 | 'id_line' => 1,
662 | 'words_count' => 40,
663 | ]
664 | ),
665 | ]
666 | ),
667 | ]
668 | ),
669 | ]
670 | ),
671 | ]
672 | ),
673 | new QueryItem(
674 | [
675 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
676 | 'id_chapter' => 2,
677 | 'name' => 'Chapter name 2',
678 | 'pages' => new QueryCollection(
679 | [
680 | new QueryItem(
681 | [
682 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
683 | 'id_chapter' => 2,
684 | 'id_page' => 1,
685 | 'has_illustrations' => false,
686 | 'lines' => new QueryCollection([]),
687 | ]
688 | ),
689 | new QueryItem(
690 | [
691 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
692 | 'id_chapter' => 2,
693 | 'id_page' => 2,
694 | 'has_illustrations' => false,
695 | 'lines' => new QueryCollection([]),
696 | ]
697 | ),
698 | ]
699 | ),
700 | ]
701 | ),
702 | ]
703 | ),
704 | ]
705 | );
706 |
707 | $mutation = Mutation::build($this->complexConfigMock, $queryItem, true);
708 |
709 | $expectedMutationArguments = [
710 | 'book' => [
711 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
712 | 'id_author' => 1234,
713 | 'genre' => null,
714 | 'chapters' => [
715 | 'upsert' => [
716 | [
717 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
718 | 'id_chapter' => 1,
719 | 'name' => 'Chapter name 1',
720 | 'pages' => [
721 | 'upsert' => [
722 | [
723 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
724 | 'id_chapter' => 1,
725 | 'id_page' => 1,
726 | 'has_illustrations' => false,
727 | 'lines' => [
728 | 'upsert' => [
729 | [
730 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
731 | 'id_chapter' => 1,
732 | 'id_page' => 1,
733 | 'id_line' => 1,
734 | 'words_count' => 30,
735 | ],
736 | [
737 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
738 | 'id_chapter' => 1,
739 | 'id_page' => 1,
740 | 'id_line' => 2,
741 | 'words_count' => 35,
742 | ],
743 | ],
744 | ],
745 | ],
746 | [
747 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
748 | 'id_chapter' => 1,
749 | 'id_page' => 2,
750 | 'has_illustrations' => false,
751 | 'lines' => [
752 | 'upsert' => [
753 | [
754 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
755 | 'id_chapter' => 1,
756 | 'id_page' => 2,
757 | 'id_line' => 1,
758 | 'words_count' => 40,
759 | ],
760 | ],
761 | ],
762 | ],
763 | ],
764 | ],
765 | ],
766 | [
767 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
768 | 'id_chapter' => 2,
769 | 'name' => 'Chapter name 2',
770 | 'pages' => [
771 | 'upsert' => [
772 | [
773 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
774 | 'id_chapter' => 2,
775 | 'id_page' => 1,
776 | 'has_illustrations' => false,
777 | ],
778 | [
779 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
780 | 'id_chapter' => 2,
781 | 'id_page' => 2,
782 | 'has_illustrations' => false,
783 | ],
784 | ],
785 | ],
786 | ],
787 | ],
788 | ],
789 | ],
790 | ];
791 | $this->assertEquals($expectedMutationArguments, $mutation->jsonSerialize());
792 | }
793 |
794 | public function testWhenTheSourceHasItemsWithItemArguments(): void
795 | {
796 | $queryItem = new QueryItem(
797 | [
798 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
799 | 'id_author' => 1234,
800 | 'genre' => null,
801 | 'currentPage' => new QueryItem(
802 | [
803 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
804 | 'id_chapter' => 2,
805 | 'id_page' => 2,
806 | ]
807 | ),
808 | ]
809 | );
810 |
811 | $mutation = Mutation::build($this->complexConfigMock, $queryItem, true);
812 |
813 | $expectedMutationArguments = [
814 | 'book' => [
815 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
816 | 'id_author' => 1234,
817 | 'genre' => null,
818 | 'currentPage' => [
819 | 'upsert' => [
820 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
821 | 'id_chapter' => 2,
822 | 'id_page' => 2,
823 | ],
824 | ],
825 | ],
826 | ];
827 | $this->assertEquals($expectedMutationArguments, $mutation->jsonSerialize());
828 | }
829 |
830 | public function testWhenTheSourceHasTheSameStructureThanTheConfig(): void
831 | {
832 | $queryItem = new QueryItem(
833 | [
834 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
835 | 'id_author' => 1234,
836 | 'genre' => null,
837 | 'chapters' => new QueryItem(
838 | [
839 | 'upsert' => new QueryCollection(
840 | [
841 | new QueryItem(
842 | [
843 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
844 | 'id_chapter' => 1,
845 | 'name' => 'Chapter name 1',
846 | ]
847 | ),
848 | new QueryItem(
849 | [
850 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
851 | 'id_chapter' => 2,
852 | 'name' => 'Chapter name 2',
853 | ]
854 | ),
855 | ]
856 | ),
857 | ]
858 | ),
859 | ]
860 | );
861 |
862 | $mutation = Mutation::build($this->sameQueryStructureConfigMock, $queryItem, true);
863 |
864 | $expectedMutationArguments = [
865 | 'book' => [
866 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
867 | 'id_author' => 1234,
868 | 'genre' => null,
869 | 'chapters' => [
870 | 'upsert' => [
871 | [
872 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
873 | 'id_chapter' => 1,
874 | 'name' => 'Chapter name 1',
875 | ],
876 | [
877 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
878 | 'id_chapter' => 2,
879 | 'name' => 'Chapter name 2',
880 | ],
881 | ],
882 | ],
883 | ],
884 | ];
885 | $this->assertEquals($expectedMutationArguments, $mutation->jsonSerialize());
886 | }
887 |
888 | public function testWhenRootIsACollectionWithoutChildren(): void
889 | {
890 | $queryCollection = new QueryCollection(
891 | [
892 | new QueryItem(
893 | [
894 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
895 | 'id_author' => 1234,
896 | 'genre' => null,
897 | ]
898 | ),
899 | new QueryItem(
900 | [
901 | 'id_book' => 'a53493b0-4a24-40c4-b786-317f8dfdf897',
902 | 'id_author' => 1122,
903 | 'genre' => 'drama',
904 | ]
905 | ),
906 | ]
907 | );
908 |
909 | $mutation = Mutation::build($this->collectionConfigMock, $queryCollection, true);
910 |
911 | $expectedMutationArguments = [
912 | 'books' => [
913 | [
914 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
915 | 'id_author' => 1234,
916 | 'genre' => null,
917 | ],
918 | [
919 | 'id_book' => 'a53493b0-4a24-40c4-b786-317f8dfdf897',
920 | 'id_author' => 1122,
921 | 'genre' => 'drama',
922 | ],
923 | ],
924 | ];
925 | $this->assertEquals($expectedMutationArguments, $mutation->jsonSerialize());
926 | }
927 |
928 | public function testWhenRootIsACollectionWithChildren(): void
929 | {
930 | $queryCollection = new QueryCollection(
931 | [
932 | new QueryItem(
933 | [
934 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
935 | 'id_author' => 1234,
936 | 'genre' => null,
937 | 'chapters' => new QueryCollection(
938 | [
939 | new QueryItem(
940 | [
941 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
942 | 'id_chapter' => 1,
943 | 'name' => 'Chapter name 1',
944 | ]
945 | ),
946 | ]
947 | ),
948 | ]
949 | ),
950 | new QueryItem(
951 | [
952 | 'id_book' => 'a53493b0-4a24-40c4-b786-317f8dfdf897',
953 | 'id_author' => 1122,
954 | 'genre' => 'drama',
955 | 'chapters' => new QueryCollection(
956 | [
957 | new QueryItem(
958 | [
959 | 'id_book' => 'a53493b0-4a24-40c4-b786-317f8dfdf897',
960 | 'id_chapter' => 1,
961 | 'name' => 'Chapter name 1',
962 | ]
963 | ),
964 | ]
965 | ),
966 | ]
967 | ),
968 | ]
969 | );
970 |
971 | $mutation = Mutation::build($this->collectionConfigMock, $queryCollection, true);
972 |
973 | $expectedMutationArguments = [
974 | 'books' => [
975 | [
976 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
977 | 'id_author' => 1234,
978 | 'genre' => null,
979 | 'chapters' => [
980 | [
981 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
982 | 'id_chapter' => 1,
983 | 'name' => 'Chapter name 1',
984 | ],
985 | ],
986 | ],
987 | [
988 | 'id_book' => 'a53493b0-4a24-40c4-b786-317f8dfdf897',
989 | 'id_author' => 1122,
990 | 'genre' => 'drama',
991 | 'chapters' => [
992 | [
993 | 'id_book' => 'a53493b0-4a24-40c4-b786-317f8dfdf897',
994 | 'id_chapter' => 1,
995 | 'name' => 'Chapter name 1',
996 | ],
997 | ],
998 | ],
999 | ],
1000 | ];
1001 | $this->assertEquals($expectedMutationArguments, $mutation->jsonSerialize());
1002 | }
1003 | }
1004 |
--------------------------------------------------------------------------------
/tests/Query/CollectionTest.php:
--------------------------------------------------------------------------------
1 | [
18 | 'collection' => new Collection(),
19 | 'isEmpty' => true,
20 | ],
21 | 'Filled collection' => [
22 | 'collection' => new Collection([new Item(['key' => 'value'])]),
23 | 'isEmpty' => false,
24 | ],
25 | ];
26 | }
27 |
28 | #[DataProvider('emptyCollectionProvider')]
29 | #[Test]
30 | public function checkEmptyCollection(Collection $collection, bool $isEmpty): void
31 | {
32 | $this->assertSame($isEmpty, $collection->isEmpty());
33 | }
34 |
35 | public static function filterProvider(): array
36 | {
37 | return [
38 | 'Filter matches no item' => [
39 | 'filters' => [
40 | 'genre' => 'adventure',
41 | ],
42 | 'expectedResult' => new Collection([]),
43 | ],
44 | 'Filter matches one item' => [
45 | 'filters' => [
46 | 'id_book' => 'a53493b0-4a24-40c4-b786-317f8dfdf897',
47 | ],
48 | 'expectedResult' => new Collection(
49 | [
50 | new Item(
51 | [
52 | 'id_book' => 'a53493b0-4a24-40c4-b786-317f8dfdf897',
53 | 'id_author' => 1122,
54 | 'genre' => 'drama',
55 | ]
56 | ),
57 | ]
58 | ),
59 | ],
60 | 'Filter matches two items' => [
61 | 'filters' => [
62 | 'id_author' => 1234,
63 | ],
64 | 'expectedResult' => new Collection(
65 | [
66 | new Item(
67 | [
68 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
69 | 'id_author' => 1234,
70 | 'genre' => null,
71 | ]
72 | ),
73 | new Item(
74 | [
75 | 'id_book' => '8477244b-d939-4e34-8b45-446f85399a85',
76 | 'id_author' => 1234,
77 | 'genre' => 'drama',
78 | ]
79 | ),
80 | ]
81 | ),
82 | ],
83 | 'Filter composed of two values' => [
84 | 'filters' => [
85 | 'id_author' => 1234,
86 | 'genre' => 'drama',
87 | ],
88 | 'expectedResult' => new Collection(
89 | [
90 | new Item(
91 | [
92 | 'id_book' => '8477244b-d939-4e34-8b45-446f85399a85',
93 | 'id_author' => 1234,
94 | 'genre' => 'drama',
95 | ]
96 | ),
97 | ]
98 | ),
99 | ],
100 | 'Filter value is null' => [
101 | 'filters' => [
102 | 'genre' => null,
103 | ],
104 | 'expectedResult' => new Collection(
105 | [
106 | new Item(
107 | [
108 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
109 | 'id_author' => 1234,
110 | 'genre' => null,
111 | ]
112 | ),
113 | ]
114 | ),
115 | ],
116 | ];
117 | }
118 |
119 | /**
120 | * @dataProvider filterProvider
121 | */
122 | public function testFilter(array $filters, Collection $expectedResult): void
123 | {
124 | $books = new Collection(
125 | [
126 | new Item(
127 | [
128 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
129 | 'id_author' => 1234,
130 | 'genre' => null,
131 | ]
132 | ),
133 | new Item(
134 | [
135 | 'id_book' => 'a53493b0-4a24-40c4-b786-317f8dfdf897',
136 | 'id_author' => 1122,
137 | 'genre' => 'drama',
138 | ]
139 | ),
140 | new Item(
141 | [
142 | 'id_book' => '8477244b-d939-4e34-8b45-446f85399a85',
143 | 'id_author' => 1234,
144 | 'genre' => 'drama',
145 | ]
146 | ),
147 | ]
148 | );
149 |
150 | $filteredBooks = $books->filter($filters);
151 |
152 | $this->assertEquals($expectedResult, $filteredBooks);
153 | }
154 |
155 | public function testFilterWhenRootIsAnItemAndTheFilterIsInSecondLevel(): void
156 | {
157 | $book = new Item(
158 | [
159 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
160 | 'id_author' => 1234,
161 | 'genre' => null,
162 | 'chapters' => new Collection(
163 | [
164 | new Item(
165 | [
166 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
167 | 'id_chapter' => 1,
168 | 'name' => 'Chapter one',
169 | ]
170 | ),
171 | new Item(
172 | [
173 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
174 | 'id_chapter' => 2,
175 | 'name' => 'Chapter two',
176 | ]
177 | ),
178 | ]
179 | ),
180 | ]
181 | );
182 |
183 | $book->chapters = $book->chapters->filter(['id_chapter' => 2]);
184 |
185 | $expectedResult = new Item(
186 | [
187 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
188 | 'id_author' => 1234,
189 | 'genre' => null,
190 | 'chapters' => new Collection(
191 | [
192 | new Item(
193 | [
194 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
195 | 'id_chapter' => 2,
196 | 'name' => 'Chapter two',
197 | ]
198 | ),
199 | ]
200 | ),
201 | ]
202 | );
203 | $this->assertEquals($expectedResult, $book);
204 | }
205 |
206 | public function testUniqueLevelToArray(): void
207 | {
208 | $book = new Item(
209 | [
210 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
211 | 'id_author' => 1234,
212 | 'genre' => null,
213 | 'chapters' => new Collection(
214 | [
215 | new Item(
216 | [
217 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
218 | 'id_chapter' => 1,
219 | 'name' => 'Chapter one',
220 | ]
221 | ),
222 | new Item(
223 | [
224 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
225 | 'id_chapter' => 2,
226 | 'name' => 'Chapter two',
227 | ]
228 | ),
229 | ]
230 | ),
231 | ]
232 | );
233 |
234 | $chapters = $book->chapters->toArray();
235 |
236 | $expectedResult = [
237 | [
238 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
239 | 'id_chapter' => 1,
240 | 'name' => 'Chapter one',
241 | ],
242 | [
243 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
244 | 'id_chapter' => 2,
245 | 'name' => 'Chapter two',
246 | ],
247 | ];
248 | $this->assertEquals($expectedResult, $chapters);
249 | }
250 |
251 | public function testSiblingsToArray(): void
252 | {
253 | $book = new Collection(
254 | [
255 | new Item(
256 | [
257 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
258 | 'id_author' => 1234,
259 | 'genre' => null,
260 | 'chapters' => new Collection(
261 | [
262 | new Item(
263 | [
264 | 'id_book' => 'ba828dd3-951f-4cb4-b731-b4601f19414f',
265 | 'id_chapter' => 1,
266 | 'name' => 'Chapter one - Book one',
267 | ]
268 | ),
269 | ]
270 | ),
271 | ]
272 | ),
273 | new Item(
274 | [
275 | 'id_book' => '0c72d70e-3e24-4975-b8c2-704ac1723f5f',
276 | 'id_author' => 4321,
277 | 'genre' => null,
278 | 'chapters' => new Collection(
279 | [
280 | new Item(
281 | [
282 | 'id_book' => '2001fe69-e28a-4c2f-accf-7210d575051c',
283 | 'id_chapter' => 1,
284 | 'name' => 'Chapter one - Book two',
285 | ]
286 | ),
287 | ]
288 | ),
289 | ]
290 | ),
291 | ]
292 | );
293 |
294 | $chapters = $book->chapters->toArray();
295 |
296 | $expectedResult = [
297 | [
298 | 'id_book' => 'ba828dd3-951f-4cb4-b731-b4601f19414f',
299 | 'id_chapter' => 1,
300 | 'name' => 'Chapter one - Book one',
301 | ],
302 | [
303 | 'id_book' => '2001fe69-e28a-4c2f-accf-7210d575051c',
304 | 'id_chapter' => 1,
305 | 'name' => 'Chapter one - Book two',
306 | ],
307 | ];
308 | $this->assertEquals($expectedResult, $chapters);
309 |
310 | $this->assertEquals(
311 | [
312 | 'Chapter one - Book one',
313 | 'Chapter one - Book two',
314 | ],
315 | $book->chapters->name->toArray()
316 | );
317 | }
318 |
319 | public function testItemHas(): void
320 | {
321 | $book = new Item(
322 | [
323 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
324 | 'id_author' => 1234,
325 | 'genre' => null,
326 | ]
327 | );
328 |
329 | $this->assertTrue($book->has('id_author'));
330 | $this->assertTrue($book->has('genre'));
331 | $this->assertFalse($book->has('invalid'));
332 | }
333 |
334 | public function testHasMethodForThirdLevelItems(): void
335 | {
336 | $book = new Item(
337 | [
338 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
339 | 'id_author' => 1234,
340 | 'genre' => null,
341 | 'chapters' => new Collection(
342 | [
343 | new Item(
344 | [
345 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
346 | 'id_chapter' => 1,
347 | 'name' => 'Chapter name',
348 | 'pov' => null,
349 | ]
350 | ),
351 | new Item(
352 | [
353 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
354 | 'id_chapter' => 1,
355 | 'name' => 'Chapter name',
356 | 'pov' => null,
357 | 'pages' => new Collection(
358 | [
359 | new Item(
360 | [
361 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
362 | 'id_chapter' => 1,
363 | 'id_page' => 1,
364 | 'has_illustrations' => false,
365 | ]
366 | ),
367 | ]
368 | ),
369 | ]
370 | ),
371 | ]
372 | ),
373 | ]
374 | );
375 |
376 | $this->assertTrue($book->has('chapters.pages'));
377 | $this->assertFalse($book->has('chapters.invalid'));
378 | $this->assertTrue($book->has('chapters.pages.has_illustrations'));
379 | $this->assertFalse($book->has('chapters.pages.invalid'));
380 | $this->assertTrue($book->chapters->has('pages.has_illustrations'));
381 | $this->assertFalse($book->chapters->has('pages.invalid'));
382 | $this->assertTrue($book->chapters->has('pages.has_illustrations'));
383 | $this->assertFalse($book->chapters->has('pages.invalid'));
384 | $this->assertFalse($book->has('not_existing.invalid'));
385 | }
386 |
387 | public function testWhenFourthLevelItemsExistenceIsChecked(): void
388 | {
389 | $book = new Item(
390 | [
391 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
392 | 'id_author' => 1234,
393 | 'genre' => null,
394 | 'chapters' => new Collection(
395 | [
396 | new Item(
397 | [
398 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
399 | 'id_chapter' => 1,
400 | 'name' => 'Chapter name',
401 | 'pov' => 'first person',
402 | 'pages' => new Collection(
403 | [
404 | new Item(
405 | [
406 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
407 | 'id_chapter' => 1,
408 | 'id_page' => 1,
409 | 'has_illustrations' => false,
410 | 'lines' => new Collection(
411 | [
412 | new Item(
413 | [
414 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
415 | 'id_chapter' => 1,
416 | 'id_page' => 1,
417 | 'id_line' => 1,
418 | 'words_count' => 30,
419 | ]
420 | ),
421 | ]
422 | ),
423 | ]
424 | ),
425 | new Item(
426 | [
427 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
428 | 'id_chapter' => 1,
429 | 'id_page' => 2,
430 | 'has_illustrations' => false,
431 | 'lines' => new Collection(
432 | [
433 | new Item(
434 | [
435 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
436 | 'id_chapter' => 1,
437 | 'id_page' => 2,
438 | 'id_line' => 1,
439 | 'words_count' => 35,
440 | ]
441 | ),
442 | new Item(
443 | [
444 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
445 | 'id_chapter' => 1,
446 | 'id_page' => 2,
447 | 'id_line' => 2,
448 | 'words_count' => 40,
449 | ]
450 | ),
451 | ]
452 | ),
453 | ]
454 | ),
455 | ]
456 | ),
457 | ]
458 | ),
459 | new Item(
460 | [
461 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
462 | 'id_chapter' => 2,
463 | 'name' => 'Chapter name',
464 | 'pov' => 'first person',
465 | 'pages' => new Collection(
466 | [
467 | new Item(
468 | [
469 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
470 | 'id_chapter' => 2,
471 | 'id_page' => 1,
472 | 'has_illustrations' => false,
473 | 'lines' => new Collection(
474 | [
475 | new Item(
476 | [
477 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
478 | 'id_chapter' => 2,
479 | 'id_page' => 1,
480 | 'id_line' => 1,
481 | 'words_count' => 45,
482 | ]
483 | ),
484 | ]
485 | ),
486 | ]
487 | ),
488 | ]
489 | ),
490 | ]
491 | ),
492 | ]
493 | ),
494 | ]
495 | );
496 |
497 | $lines = $book->chapters->pages->lines;
498 |
499 | $itemDataThatExists = [
500 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
501 | 'id_chapter' => 1,
502 | 'id_page' => 2,
503 | 'id_line' => 1,
504 | 'words_count' => 35,
505 | ];
506 | $this->assertTrue($lines->hasItem($itemDataThatExists));
507 | $itemDataThatDoesNotExist = [
508 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
509 | 'id_chapter' => 2,
510 | 'id_page' => 1,
511 | 'id_line' => 2,
512 | 'words_count' => 50,
513 | ];
514 | $this->assertFalse($lines->hasItem($itemDataThatDoesNotExist));
515 | }
516 |
517 | public function testArrayAccessOffsetSetShouldThrowABadMethodCallException(): void
518 | {
519 | $book = new Item(
520 | [
521 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
522 | 'id_author' => 1234,
523 | 'genre' => null,
524 | 'chapters' => new Collection(
525 | [
526 | new Item(
527 | [
528 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
529 | 'id_chapter' => 1,
530 | 'name' => 'Chapter one',
531 | 'pov' => 'first person',
532 | ]
533 | ),
534 | new Item(
535 | [
536 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
537 | 'id_chapter' => 2,
538 | 'name' => 'Chapter two',
539 | 'pov' => 'third person',
540 | ]
541 | ),
542 | ]
543 | ),
544 | ]
545 | );
546 |
547 | $this->expectException(BadMethodCallException::class);
548 | $this->expectExceptionMessage('Try using add() instead');
549 |
550 | $book->chapters->name[0] = 'Chapter three';
551 | }
552 |
553 | public function testArrayAccessOffsetExists(): void
554 | {
555 | $book = new Item(
556 | [
557 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
558 | 'id_author' => 1234,
559 | 'genre' => null,
560 | 'chapters' => new Collection(
561 | [
562 | new Item(
563 | [
564 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
565 | 'id_chapter' => 1,
566 | 'name' => 'Chapter one',
567 | 'pov' => 'first person',
568 | ]
569 | ),
570 | new Item(
571 | [
572 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
573 | 'id_chapter' => 2,
574 | 'name' => 'Chapter two',
575 | 'pov' => 'third person',
576 | ]
577 | ),
578 | ]
579 | ),
580 | ]
581 | );
582 |
583 | $this->assertTrue(isset($book->chapters->name[1]));
584 | $this->assertFalse(isset($book->chapters->name[2]));
585 | $this->assertFalse(empty($book->chapters->name[1]));
586 | $this->assertTrue(empty($book->chapters->name[2]));
587 | }
588 |
589 | public function testArrayAccessOffsetUnsetShouldThrowABadMethodCallException(): void
590 | {
591 | $book = new Item(
592 | [
593 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
594 | 'id_author' => 1234,
595 | 'genre' => null,
596 | 'chapters' => new Collection(
597 | [
598 | new Item(
599 | [
600 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
601 | 'id_chapter' => 1,
602 | 'name' => 'Chapter one',
603 | 'pov' => 'first person',
604 | ]
605 | ),
606 | new Item(
607 | [
608 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
609 | 'id_chapter' => 2,
610 | 'name' => 'Chapter two',
611 | 'pov' => 'third person',
612 | ]
613 | ),
614 | ]
615 | ),
616 | ]
617 | );
618 |
619 | $this->expectException(BadMethodCallException::class);
620 | $this->expectExceptionMessage('Try using remove() instead');
621 |
622 | unset($book->chapters->name[0]);
623 | }
624 |
625 | public function testArrayAccessOffsetGet(): void
626 | {
627 | $book = new Item(
628 | [
629 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
630 | 'id_author' => 1234,
631 | 'genre' => null,
632 | 'chapters' => new Collection(
633 | [
634 | new Item(
635 | [
636 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
637 | 'id_chapter' => 1,
638 | 'name' => 'Chapter one',
639 | 'pov' => 'first person',
640 | ]
641 | ),
642 | new Item(
643 | [
644 | 'id_book' => 'f7cfd732-e3d8-3642-a919-ace8c38c2c6d',
645 | 'id_chapter' => 2,
646 | 'name' => 'Chapter two',
647 | 'pov' => 'third person',
648 | ]
649 | ),
650 | ]
651 | ),
652 | ]
653 | );
654 |
655 | $this->assertEquals('Chapter one', $book->chapters->name[0]);
656 | $this->assertEquals('Chapter two', $book->chapters->name[1]);
657 | }
658 | }
659 |
--------------------------------------------------------------------------------
/tests/ResponseBuilderTest.php:
--------------------------------------------------------------------------------
1 | dataObjectBuilder = $this->createMock(DataObjectBuilder::class);
22 |
23 | $this->responseBuilder = new ResponseBuilder($this->dataObjectBuilder);
24 | }
25 |
26 | public function testBuildMalformedResponse(): void
27 | {
28 | $mockHttpResponse = $this->createMock(ResponseInterface::class);
29 | $mockHttpResponse->expects($this->once())
30 | ->method('getBody')
31 | ->willReturn($this->stringToStream('malformed response'));
32 |
33 | $this->expectException(UnexpectedValueException::class);
34 | $this->expectExceptionMessage('Invalid JSON response. Response body: ');
35 |
36 | $this->responseBuilder->build($mockHttpResponse);
37 | }
38 |
39 | public static function buildInvalidGraphqlJsonResponseProvider(): array
40 | {
41 | return [
42 | 'Invalid structure' => [
43 | 'body' => '["hola mundo"]',
44 | ],
45 | 'No data in structure' => [
46 | 'body' => '{"foo": "bar"}',
47 | ],
48 | ];
49 | }
50 |
51 | #[DataProvider('buildInvalidGraphqlJsonResponseProvider')]
52 | public function testBuildInvalidGraphqlJsonResponse(string $body): void
53 | {
54 | $mockHttpResponse = $this->createMock(ResponseInterface::class);
55 |
56 | $mockHttpResponse->expects($this->once())
57 | ->method('getBody')
58 | ->willReturn($this->stringToStream($body));
59 |
60 | $this->expectException(UnexpectedValueException::class);
61 | $this->expectExceptionMessage('Invalid GraphQL JSON response. Response body: ');
62 |
63 | $this->responseBuilder->build($mockHttpResponse);
64 | }
65 |
66 | public function testBuildValidGraphqlJsonWithoutErrors(): void
67 | {
68 | $mockHttpResponse = $this->createMock(ResponseInterface::class);
69 |
70 | $mockHttpResponse->expects($this->once())
71 | ->method('getBody')
72 | ->willReturn($this->stringToStream('{"data": {"foo": "bar"}}'));
73 |
74 | $expectedData = ['foo' => 'bar'];
75 | $dataObjectMock = [
76 | 'query' => [
77 | 'key1' => 'value1',
78 | 'key2' => 'value2',
79 | ],
80 | ];
81 | $this->dataObjectBuilder->expects($this->once())
82 | ->method('buildQuery')
83 | ->with($expectedData)
84 | ->willReturn($dataObjectMock);
85 | $response = $this->responseBuilder->build($mockHttpResponse);
86 |
87 | $this->assertEquals($expectedData, $response->getData());
88 | $this->assertEquals($dataObjectMock, $response->getDataObject());
89 | }
90 |
91 | public static function buildValidGraphqlJsonWithErrorsProvider(): array
92 | {
93 | return [
94 | 'Response with null data' => [
95 | 'body' => '{"data": null, "errors": [{"foo": "bar"}]}',
96 | ],
97 | 'Response without data' => [
98 | 'body' => '{"errors": [{"foo": "bar"}]}',
99 | ],
100 | ];
101 | }
102 |
103 | #[DataProvider('buildValidGraphqlJsonWithErrorsProvider')]
104 | public function testBuildValidGraphqlJsonWithErrors(string $body): void
105 | {
106 | $mockHttpResponse = $this->createMock(ResponseInterface::class);
107 |
108 | $mockHttpResponse->expects($this->once())
109 | ->method('getBody')
110 | ->willReturn($this->stringToStream($body));
111 |
112 | $this->dataObjectBuilder->expects($this->once())
113 | ->method('buildQuery')
114 | ->with([])
115 | ->willReturn([]);
116 |
117 | $response = $this->responseBuilder->build($mockHttpResponse);
118 |
119 | $this->assertEquals([], $response->getData());
120 | $this->assertEquals([], $response->getDataObject());
121 | $this->assertTrue($response->hasErrors());
122 | $this->assertEquals([['foo' => 'bar']], $response->getErrors());
123 | }
124 |
125 | public function stringToStream(string $string): StreamInterface
126 | {
127 | $buffer = new BufferStream();
128 |
129 | $buffer->write($string);
130 |
131 | return $buffer;
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/tests/Traits/JsonPathAccessorTest.php:
--------------------------------------------------------------------------------
1 | assertSame($obj, $obj->get('.'));
17 | }
18 |
19 | public function testWhenChildrenAreRetrieved(): void
20 | {
21 | $upsertConfig = new MutationTypeConfig();
22 | $upsertConfig->type = Collection::class;
23 | $upsertConfig->linksTo = '.chapters';
24 |
25 | $chaptersConfig = new MutationTypeConfig();
26 | $chaptersConfig->type = Item::class;
27 | $chaptersConfig->children = ['upsert' => $upsertConfig];
28 |
29 | $typeConfig = new MutationTypeConfig();
30 |
31 | $bookConfig = new MutationTypeConfig();
32 | $bookConfig->type = Item::class;
33 | $bookConfig->linksTo = '.';
34 | $bookConfig->children = [
35 | 'chapters' => $chaptersConfig,
36 | 'type' => $typeConfig,
37 | ];
38 |
39 | $this->assertEquals($chaptersConfig, $bookConfig->get('.chapters'));
40 | $this->assertEquals($upsertConfig, $bookConfig->get('.chapters.upsert'));
41 | }
42 |
43 | public function testWhenObjectAttributesAreRetrieved(): void
44 | {
45 | $bookConfig = new MutationTypeConfig();
46 | $bookConfig->type = Item::class;
47 | $bookConfig->linksTo = '.';
48 | $bookConfig->children = [];
49 |
50 | $this->assertEquals(Item::class, $bookConfig->get('.type'));
51 | $this->assertEquals('.', $bookConfig->get('.linksTo'));
52 | }
53 | }
54 |
--------------------------------------------------------------------------------