Completed
Pull Request — master (#204)
by Ryan
11:34
created

DeferredTest   A

Complexity

Total Complexity 5

Size/Duplication

Total Lines 250
Duplicated Lines 30.4 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
wmc 5
lcom 1
cbo 5
dl 76
loc 250
rs 10
c 0
b 0
f 0

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
/**
3
 * Copyright (c) 2015–2018 Alexandr Viniychuk <http://youshido.com>.
4
 * Copyright (c) 2015–2018 Portey Vasil <https://github.com/portey>.
5
 * Copyright (c) 2018 Ryan Parman <https://github.com/skyzyx>.
6
 * Copyright (c) 2018 Ashley Hutson <https://github.com/asheliahut>.
7
 * Copyright (c) 2015–2018 Contributors.
8
 *
9
 * http://opensource.org/licenses/MIT
10
 */
11
12
declare(strict_types=1);
13
/**
14
 * This file is a part of GraphQL project.
15
 */
16
17
namespace Youshido\Tests\Schema;
18
19
use Youshido\GraphQL\Config\Schema\SchemaConfig;
20
use Youshido\GraphQL\Execution\DeferredResolver;
21
use Youshido\GraphQL\Execution\Processor;
22
use Youshido\GraphQL\Field\Field;
23
use Youshido\GraphQL\Schema\AbstractSchema;
24
use Youshido\GraphQL\Type\ListType\ListType;
25
use Youshido\GraphQL\Type\Object\AbstractObjectType;
26
use Youshido\GraphQL\Type\Object\ObjectType;
27
use Youshido\GraphQL\Type\Scalar\StringType;
28
29
/**
30
 * Interface definition for a database service that will be mocked.
31
 */
32
interface DeferredDatabase
33
{
34
    /**
35
     * Retrieve a list of users by their id.
36
     *
37
     * @param string[] $ids
38
     *                      The user ids.
39
     *
40
     * @return array
41
     *               A 2-dimensional array of user objects, keyed by their id.
42
     */
43
    public function query(array $ids);
44
}
45
46
/**
47
 * Buffers queries until they are actually required.
48
 */
49
class DeferredQueryBuffer
50
{
51
    protected $database;
52
53
    protected $buffer = [];
54
55
    protected $results = [];
56
57
    public function __construct(DeferredDatabase $database)
58
    {
59
        $this->database = $database;
60
    }
61
62
    public function add(array $ids)
63
    {
64
        $key                = \md5(\serialize($ids));
65
        $this->buffer[$key] = $ids;
66
67
        return function () use ($key) {
68
            return $this->fetch($key);
69
        };
70
    }
71
72
    protected function fetch($resultId)
73
    {
74
        if (!\array_key_exists($resultId, $this->results)) {
75
            $query = \array_unique(
76
              \array_reduce($this->buffer, 'array_merge', [])
77
            );
78
            \sort($query);
79
            $result = $this->database->query($query);
80
81
            foreach ($this->buffer as $index => $query) {
82
                foreach ($query as $id) {
83
                    $this->results[$index][$id] = $result[$id];
84
                }
85
                unset($this->buffer[$index]);
86
            }
87
        }
88
89
        return $this->results[$resultId];
90
    }
91
}
92
93
class DeferredUserType extends AbstractObjectType
94
{
95
    /**
96
     * @var \Youshido\Tests\Schema\DeferredQueryBuffer
97
     */
98
    protected $database;
99
100
    public function __construct(DeferredQueryBuffer $database)
101
    {
102
        $this->database = $database;
103
        parent::__construct();
104
    }
105
106
    public function build($config): void
107
    {
108
        $config->addField(
109
          new Field(
110
            [
111
              'name'    => 'name',
112
              'type'    => new StringType(),
113
              'resolve' => static function ($value) {
114
                  return $value['name'];
115
              },
116
            ]
117
          )
118
        );
119
120
        $config->addField(
121
          new Field(
122
            [
123
              'name'    => 'friends',
124
              'type'    => new ListType(new self($this->database)),
125
              'resolve' => function ($value) {
126
                  return new DeferredResolver(
127
                    $this->database->add($value['friends'])
128
                  );
129
              },
130
            ]
131
          )
132
        );
133
134
        $config->addField(
135
          new Field(
136
            [
137
              'name'    => 'foes',
138
              'type'    => new ListType(new self($this->database)),
139
              'resolve' => function ($value) {
140
                  return new DeferredResolver(
141
                    $this->database->add($value['foes'])
142
                  );
143
              },
144
            ]
145
          )
146
        );
147
    }
148
}
149
150
class DeferredSchema extends AbstractSchema
151
{
152
    public function __construct(DeferredQueryBuffer $buffer)
153
    {
154
        $usersField = new Field(
155
          [
156
            'name'    => 'users',
157
            'type'    => new ListType(new DeferredUserType($buffer)),
158
            'resolve' => static function ($value, $args) use ($buffer) {
159
                return new DeferredResolver($buffer->add($args['ids']));
160
            },
161
          ]
162
        );
163
164
        $usersField->addArgument(
165
          'ids',
166
          [
167
            'type' => new ListType(new StringType()),
168
          ]
169
        );
170
        parent::__construct(
171
          [
172
            'query' => new ObjectType(
173
              [
174
                'name'   => 'RootQuery',
175
                'fields' => [$usersField],
176
              ]
177
            ),
178
          ]
179
        );
180
    }
181
182
    public function build(SchemaConfig $config): void
183
    {
184
    }
185
}
186
187
188
/**
189
 * Test the deferred resolving under different circumstances.
190
 */
191
class DeferredTest extends \PHPUnit_Framework_TestCase
192
{
193
    /**
194
     * @var Processor
195
     */
196
    protected $processor;
197
198
    /**
199
     * Test a simple single deferred field.
200
     */
201
    public function testSingleResolve(): void
202
    {
203
        $query = 'query {
204
          users(ids: ["a", "b"]) {
205
            name
206
          }
207
        }';
208
        $database = $this->prophesize(DeferredDatabase::class);
209
210
        $database->query(['a', 'b'])->willReturn(
211
          [
212
            'a' => ['id' => 'a', 'name' => 'User A'],
213
            'b' => ['id' => 'b', 'name' => 'User B'],
214
          ]
215
        )->shouldBeCalledTimes(1);
216
217
        $result = $this->query(
218
          $query,
219
          new DeferredQueryBuffer($database->reveal())
220
        );
221
222
        $database->checkProphecyMethodsPredictions();
223
224
        $this->assertEquals(
225
          [
226
            'users' => [
227
              ['name' => 'User A'],
228
              ['name' => 'User B'],
229
            ],
230
          ],
231
          $result['data'],
232
          'Retrieved correct data.'
233
        );
234
    }
235
236
    /**
237
     * Test if multiple calls to the same field result in a single query.
238
     */
239
    public function testMultiResolve(): void
240
    {
241
        $query = 'query {
242
          a:users(ids: ["a"]) {
243
            name
244
          }
245
          b:users(ids: ["b"]) {
246
            name
247
          }
248
        }';
249
250
        $database = $this->prophesize(DeferredDatabase::class);
251
252
        $database->query(['a', 'b'])->willReturn(
253
          [
254
            'a' => ['id' => 'a', 'name' => 'User A'],
255
            'b' => ['id' => 'b', 'name' => 'User B'],
256
          ]
257
        )->shouldBeCalledTimes(1);
258
259
        $result = $this->query(
260
          $query,
261
          new DeferredQueryBuffer($database->reveal())
262
        );
263
264
        $database->checkProphecyMethodsPredictions();
265
266
        $this->assertEquals(
267
          [
268
            'a' => [
269
              ['name' => 'User A'],
270
            ],
271
            'b' => [
272
              ['name' => 'User B'],
273
            ],
274
          ],
275
          $result['data'],
276
          'Retrieved correct data.'
277
        );
278
    }
279
280
    /**
281
     * Test if recursive deferred resolvers work properly.
282
     */
283
    public function testRecursiveResolve(): void
284
    {
285
        $query = 'query {
286
          a:users(ids: ["a"]) {
287
            name
288
            friends {
289
              name
290
            }
291
          }
292
        }';
293
294
        $database = $this->prophesize(DeferredDatabase::class);
295
296
        $database->query(['a'])->willReturn(
297
          [
298
            'a' => ['id' => 'a', 'name' => 'User A', 'friends' => ['b', 'c']],
299
          ]
300
        )->shouldBeCalledTimes(1);
301
302
        $database->query(['b', 'c'])->willReturn(
303
          [
304
            'b' => ['id' => 'b', 'name' => 'User B'],
305
            'c' => ['id' => 'c', 'name' => 'User C'],
306
          ]
307
        );
308
309
        $result = $this->query(
310
          $query,
311
          new DeferredQueryBuffer($database->reveal())
312
        );
313
314
        $database->checkProphecyMethodsPredictions();
315
316
        $this->assertEquals(
317
          [
318
            'a' => [
319
              [
320
                'name'    => 'User A',
321
                'friends' => [
322
                  ['name' => 'User B'],
323
                  ['name' => 'User C'],
324
                ],
325
              ],
326
            ],
327
          ],
328
          $result['data'],
329
          'Retrieved correct data.'
330
        );
331
    }
332
333
    /**
334
     * Test if multiple deferred resolvers are optimized into two queries.
335
     */
336
    public function testMultiRecursiveResolve(): void
337
    {
338
        $query = 'query {
339
          a:users(ids: ["a"]) {
340
            name
341
            friends {
342
              name
343
            }
344
            foes {
345
              name
346
            }
347
          }
348
          b:users(ids: ["b"]) {
349
            name
350
            friends {
351
              name
352
            }
353
            foes {
354
              name
355
            }
356
          }
357
        }';
358
359
        $database = $this->prophesize(DeferredDatabase::class);
360
361
        $database->query(['a', 'b'])->willReturn(
362
          [
363
            'a' => [
364
              'id'      => 'a',
365
              'name'    => 'User A',
366
              'friends' => ['b', 'c'],
367
              'foes'    => ['d', 'e'],
368
            ],
369
            'b' => [
370
              'id'      => 'b',
371
              'name'    => 'User B',
372
              'friends' => ['a'],
373
              'foes'    => ['c'],
374
            ],
375
          ]
376
        )->shouldBeCalledTimes(1);
377
378
        $database->query(['a', 'b', 'c', 'd', 'e'])->willReturn(
379
          [
380
            'a' => ['id' => 'a', 'name' => 'User A'],
381
            'b' => ['id' => 'b', 'name' => 'User B'],
382
            'c' => ['id' => 'c', 'name' => 'User C'],
383
            'd' => ['id' => 'd', 'name' => 'User D'],
384
            'e' => ['id' => 'e', 'name' => 'User E'],
385
          ]
386
        )->shouldBeCalledTimes(1);
387
388
        $result = $this->query(
389
          $query,
390
          new DeferredQueryBuffer($database->reveal())
391
        );
392
393
        $database->checkProphecyMethodsPredictions();
394
395
        $this->assertEquals(
396
          [
397
            'a' => [
398
              [
399
                'name'    => 'User A',
400
                'friends' => [
401
                  ['name' => 'User B'],
402
                  ['name' => 'User C'],
403
                ],
404
                'foes' => [
405
                  ['name' => 'User D'],
406
                  ['name' => 'User E'],
407
                ],
408
              ],
409
            ],
410
            'b' => [
411
              [
412
                'name'    => 'User B',
413
                'friends' => [
414
                  ['name' => 'User A'],
415
                ],
416
                'foes' => [
417
                  ['name' => 'User C'],
418
                ],
419
              ],
420
            ],
421
          ],
422
          $result['data'],
423
          'Retrieved data is correct.'
424
        );
425
    }
426
427
    protected function query($query, DeferredQueryBuffer $buffer)
428
    {
429
        $processor = new Processor(new DeferredSchema($buffer));
430
        $processor->processPayload($query, []);
431
432
        return $processor->getResponseData();
433
    }
434
}
435