├── .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 | [![Latest Version](https://img.shields.io/github/release/softonic/graphql-client.svg?style=flat-square)](https://github.com/softonic/graphql-client/releases) 4 | [![Software License](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square)](LICENSE.md) 5 | [![Build Status](https://github.com/softonic/graphql-client/actions/workflows/build.yml/badge.svg)](https://github.com/softonic/graphql-client/actions/workflows/build.yml) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/softonic/graphql-client.svg?style=flat-square)](https://packagist.org/packages/softonic/graphql-client) 7 | [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/softonic/graphql-client.svg?style=flat-square)](http://isitmaintained.com/project/softonic/graphql-client "Average time to resolve an issue") 8 | [![Percentage of issues still open](http://isitmaintained.com/badge/open/softonic/graphql-client.svg?style=flat-square)](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 | --------------------------------------------------------------------------------