Passed
Push — master ( 1d5fb4...e5c3c4 )
by Sam
06:33 queued 01:55
created

Standard   A

Complexity

Total Complexity 17

Size/Duplication

Total Lines 301
Duplicated Lines 0 %

Test Coverage

Coverage 74.42%

Importance

Changes 0
Metric Value
wmc 17
eloc 147
dl 0
loc 301
ccs 96
cts 129
cp 0.7442
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A makePlural() 0 7 1
A customTypesToScalar() 0 11 4
A getDefaultSorting() 0 13 2
A getSingleArguments() 0 7 1
A getListArguments() 0 17 1
A buildMutation() 0 73 2
A buildQuery() 0 50 2
A buildRelationMutation() 0 69 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Api\Field;
6
7
use Application\Api\Helper;
8
use Application\Model\AbstractModel;
9
use Application\Model\Order;
10
use Doctrine\ORM\Mapping\ClassMetadata;
11
use Ecodev\Felix\Api\Input\PaginationInputType;
12
use GraphQL\Type\Definition\Type;
13
use Money\Money;
14
use ReflectionClass;
15
16
/**
17
 * Provide easy way to build standard fields to query and mutate objects.
18
 */
19
abstract class Standard
20
{
21
    /**
22
     * Returns standard fields to query the object.
23
     */
24 5
    public static function buildQuery(string $class): array
25
    {
26 1
        $metadata = _em()->getClassMetadata($class);
27 1
        $reflect = $metadata->getReflectionClass();
28 1
        $name = lcfirst($reflect->getShortName());
29 1
        $shortName = $reflect->getShortName();
30 1
        $plural = self::makePlural($name);
31
32 1
        $listArgs = self::getListArguments($metadata);
33 1
        $singleArgs = self::getSingleArguments($class);
34
35
        return [
36
            [
37
                'name' => $plural,
38 1
                'type' => Type::nonNull(_types()->get($shortName . 'Pagination')),
39
                'args' => $listArgs,
40 5
                'resolve' => function ($root, array $args) use ($class, $metadata): array {
41 4
                    $filters = self::customTypesToScalar($args['filter'] ?? []);
42
43
                    // If null or empty list is provided by client, fallback on default sorting
44 4
                    $sorting = $args['sorting'] ?? [];
45 4
                    if (!$sorting) {
46 4
                        $sorting = self::getDefaultSorting($metadata);
47
                    }
48
49
                    // And **always** sort by ID
50 4
                    $sorting[] = [
51
                        'field' => 'id',
52
                        'order' => 'ASC',
53
                    ];
54
55 4
                    $qb = _types()->createFilteredQueryBuilder($class, $filters, $sorting);
56
57 4
                    $items = Helper::paginate($args['pagination'], $qb);
58 4
                    $aggregatedFields = Helper::aggregatedFields($class, $qb);
59 4
                    $result = array_merge($aggregatedFields, $items);
60
61 4
                    return $result;
62
                },
63
            ],
64
            [
65 1
                'name' => $name,
66 1
                'type' => Type::nonNull(_types()->getOutput($class)),
67
                'args' => $singleArgs,
68 1
                'resolve' => function ($root, array $args): ?AbstractModel {
69 3
                    $object = $args['id']->getEntity();
70
71 2
                    Helper::throwIfDenied($object, 'read');
72
73 2
                    return $object;
74
                },
75
            ],
76
        ];
77
    }
78
79
    /**
80
     * Returns standard fields to mutate the object.
81
     */
82 3
    public static function buildMutation(string $class): array
83
    {
84 1
        $reflect = new ReflectionClass($class);
85 1
        $name = $reflect->getShortName();
86 1
        $plural = self::makePlural($name);
87
88
        return [
89
            [
90
                'name' => 'create' . $name,
91 1
                'type' => Type::nonNull(_types()->getOutput($class)),
92
                'description' => 'Create a new ' . $name,
93
                'args' => [
94 1
                    'input' => Type::nonNull(_types()->getInput($class)),
95
                ],
96 3
                'resolve' => function ($root, array $args) use ($class): AbstractModel {
97
                    // Do it
98 2
                    $object = new $class();
99 2
                    $input = $args['input'];
100 2
                    Helper::hydrate($object, $input);
101
102
                    // Check ACL
103 2
                    Helper::throwIfDenied($object, 'create');
104
105 2
                    _em()->persist($object);
106 2
                    _em()->flush();
107
108 2
                    return $object;
109
                },
110
            ],
111
            [
112 1
                'name' => 'update' . $name,
113 1
                'type' => Type::nonNull(_types()->getOutput($class)),
114
                'description' => 'Update an existing ' . $name,
115
                'args' => [
116 1
                    'id' => Type::nonNull(_types()->getId($class)),
117 1
                    'input' => Type::nonNull(_types()->getPartialInput($class)),
118
                ],
119 1
                'resolve' => function ($root, array $args): AbstractModel {
120
                    $object = $args['id']->getEntity();
121
122
                    // Check ACL
123
                    Helper::throwIfDenied($object, 'update');
124
125
                    // Do it
126
                    $input = $args['input'];
127
                    Helper::hydrate($object, $input);
128
129
                    _em()->flush();
130
131
                    return $object;
132
                },
133
            ],
134
            [
135 1
                'name' => 'delete' . $plural,
136 1
                'type' => Type::nonNull(Type::boolean()),
137
                'description' => 'Delete one or several existing ' . $name,
138
                'args' => [
139 1
                    'ids' => Type::nonNull(Type::listOf(Type::nonNull(_types()->getId($class)))),
140
                ],
141 1
                'resolve' => function ($root, array $args): bool {
142
                    foreach ($args['ids'] as $id) {
143
                        $object = $id->getEntity();
144
145
                        // Check ACL
146
                        Helper::throwIfDenied($object, 'delete');
147
148
                        // Do it
149
                        _em()->remove($object);
150
                    }
151
152
                    _em()->flush();
153
154
                    return true;
155
                },
156
            ],
157
        ];
158
    }
159
160
    /**
161
     * Returns standard mutations to manage many-to-many relations between two given class.
162
     *
163
     * @param string $ownerClass The class owning the relation
164
     * @param string $otherClass The other class, not-owning the relation
165
     * @param null|string $otherName a specific semantic, if needed, to be use as adder. If `$otherName = 'Parent'`, then we will call `addParent()`
166
     */
167 1
    public static function buildRelationMutation(string $ownerClass, string $otherClass, ?string $otherName = null): array
168
    {
169 1
        $ownerReflect = new ReflectionClass($ownerClass);
170 1
        $ownerName = $ownerReflect->getShortName();
171 1
        $lowerOwnerName = lcfirst($ownerName);
172
173 1
        $otherReflect = new ReflectionClass($otherClass);
174 1
        $otherClassName = $otherReflect->getShortName();
175 1
        if ($otherName) {
176 1
            $semantic = ' as ' . $otherName;
177
        } else {
178 1
            $semantic = '';
179 1
            $otherName = $otherClassName;
180
        }
181 1
        $lowerOtherName = lcfirst($otherName);
182
183 1
        if ($lowerOwnerName === $lowerOtherName) {
184
            $lowerOwnerName .= 1;
185
            $lowerOtherName .= 2;
186
        }
187
188 1
        $args = [
189 1
            $lowerOwnerName => Type::nonNull(_types()->getId($ownerClass)),
190 1
            $lowerOtherName => Type::nonNull(_types()->getId($otherClass)),
191
        ];
192
193
        return [
194
            [
195
                'name' => 'link' . $ownerName . $otherName,
196 1
                'type' => Type::nonNull(_types()->getOutput($ownerClass)),
197
                'description' => 'Create a relation between ' . $ownerName . ' and ' . $otherClassName . $semantic . '.' . PHP_EOL . PHP_EOL
198
                    . 'If the relation already exists, it will have no effect.',
199
                'args' => $args,
200 1
                'resolve' => function ($root, array $args) use ($lowerOwnerName, $lowerOtherName, $otherName): AbstractModel {
201
                    $owner = $args[$lowerOwnerName]->getEntity();
202
                    $other = $args[$lowerOtherName]->getEntity();
203
204
                    // Check ACL
205
                    Helper::throwIfDenied($owner, 'update');
206
207
                    // Do it
208
                    $method = 'add' . $otherName;
209
                    $owner->$method($other);
210
                    _em()->flush();
211
212
                    return $owner;
213
                },
214
            ],
215
            [
216 1
                'name' => 'unlink' . $ownerName . $otherName,
217 1
                'type' => Type::nonNull(_types()->getOutput($ownerClass)),
218
                'description' => 'Delete a relation between ' . $ownerName . ' and ' . $otherClassName . $semantic . '.' . PHP_EOL . PHP_EOL
219
                    . 'If the relation does not exist, it will have no effect.',
220
                'args' => $args,
221 1
                'resolve' => function ($root, array $args) use ($lowerOwnerName, $lowerOtherName, $otherName): AbstractModel {
222
                    $owner = $args[$lowerOwnerName]->getEntity();
223
                    $other = $args[$lowerOtherName]->getEntity();
224
225
                    // Check ACL
226
                    Helper::throwIfDenied($owner, 'update');
227
228
                    // Do it
229
                    if ($other) {
230
                        $method = 'remove' . $otherName;
231
                        $owner->$method($other);
232
                        _em()->flush();
233
                    }
234
235
                    return $owner;
236
                },
237
            ],
238
        ];
239
    }
240
241
    /**
242
     * Returns the plural form of the given name.
243
     */
244 1
    private static function makePlural(string $name): string
245
    {
246 1
        $plural = $name . 's';
247 1
        $plural = preg_replace('/ys$/', 'ies', $plural);
248 1
        $plural = preg_replace('/ss$/', 'ses', $plural);
249
250 1
        return $plural;
251
    }
252
253
    /**
254
     * Return arguments used for the list.
255
     */
256 1
    private static function getListArguments(ClassMetadata $class): array
257
    {
258 1
        $listArgs = [
259
            [
260 1
                'name' => 'filter',
261 1
                'type' => _types()->getFilter($class->getName()),
262
            ],
263
            [
264 1
                'name' => 'sorting',
265 1
                'type' => _types()->getSorting($class->getName()),
266 1
                'defaultValue' => self::getDefaultSorting($class),
267
            ],
268
        ];
269
270 1
        $listArgs[] = PaginationInputType::build(_types());
271
272 1
        return $listArgs;
273
    }
274
275
    /**
276
     * Return arguments used for single item.
277
     */
278 1
    private static function getSingleArguments(string $class): array
279
    {
280 1
        $args = [
281 1
            'id' => Type::nonNull(_types()->getId($class)),
282
        ];
283
284 1
        return $args;
285
    }
286
287
    /**
288
     * Get default sorting values with some fallback for some special cases.
289
     */
290 5
    private static function getDefaultSorting(ClassMetadata $metadata): array
291
    {
292 5
        $defaultSorting = [];
293
294 5
        $class = $metadata->getName();
295 5
        if ($class === Order::class) {
296 1
            $defaultSorting[] = [
297
                'field' => 'creationDate',
298
                'order' => 'DESC',
299
            ];
300
        }
301
302 5
        return $defaultSorting;
303
    }
304
305
    /**
306
     * Recursively convert custom scalars that don't implement __toString() to their scalar
307
     * representation to injected back into DQL/SQL.
308
     */
309 4
    private static function customTypesToScalar(array $args): array
310
    {
311 4
        foreach ($args as &$p) {
312
            if (is_array($p)) {
313
                $p = self::customTypesToScalar($p);
314
            } elseif ($p instanceof Money) {
315
                $p = $p->getAmount();
316
            }
317
        }
318
319 4
        return $args;
320
    }
321
}
322