Passed
Push — master ( d29638...473578 )
by Adrien
06:23
created

Standard::buildQuery()   A

Complexity

Conditions 4
Paths 1

Size

Total Lines 44
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 4.0519

Importance

Changes 0
Metric Value
cc 4
eloc 29
nc 1
nop 1
dl 0
loc 44
ccs 23
cts 27
cp 0.8519
crap 4.0519
rs 9.456
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Api\Field;
6
7
use Application\Api\Exception;
8
use Application\Api\Helper;
9
use Application\Api\Input\PaginationInputType;
10
use Application\Model\AbstractModel;
11
use Doctrine\ORM\Mapping\ClassMetadata;
12
use GraphQL\Type\Definition\Type;
13
use ReflectionClass;
14
15
/**
16
 * Provide easy way to build standard fields to query and mutate objects
17
 */
18
abstract class Standard
19
{
20
    /**
21
     * Returns standard fields to query the object
22
     *
23
     * @param string $class
24
     *
25
     * @return array
26
     */
27 4
    public static function buildQuery(string $class): array
28
    {
29 1
        $metadata = _em()->getClassMetadata($class);
30 1
        $reflect = $metadata->getReflectionClass();
31 1
        $name = lcfirst($reflect->getShortName());
32 1
        $shortName = $reflect->getShortName();
33 1
        $plural = self::makePlural($name);
34
35 1
        $listArgs = self::getListArguments($metadata, $class, $name);
36 1
        $singleArgs = self::getSingleArguments($class);
37
38
        return [
39
            [
40 1
                'name' => $plural,
41 1
                'type' => Type::nonNull(_types()->get($shortName . 'Pagination')),
42 1
                'args' => $listArgs,
43
                'resolve' => function ($root, array $args) use ($class): array {
44 2
                    if (($args['filters'] ?? false) && ($args['filter'] ?? false)) {
45
                        throw new Exception('Cannot use `filter` and `filters` at the same time');
46
                    }
47 2
                    if ($args['filters'] ?? false) {
48
                        $queryArgs = [$args['filters'] ?? []];
49
                        $queryArgs[] = $args['sorting'];
50
51
                        $qb = _em()->getRepository($class)->getFindAllQuery(...$queryArgs);
52
                    } else {
53 2
                        $qb = _types()->createFilteredQueryBuilder($class, $args['filter'] ?? [], $args['sorting'] ?? []);
54
                    }
55
56 2
                    $result = Helper::paginate($args['pagination'], $qb);
57
58 2
                    return $result;
59 1
                },
60
            ],
61
            [
62 1
                'name' => $name,
63 1
                'type' => Type::nonNull(_types()->getOutput($class)),
64 1
                'args' => $singleArgs,
65
                'resolve' => function ($root, array $args): ?AbstractModel {
66 4
                    $object = $args['id']->getEntity();
67
68 3
                    Helper::throwIfDenied($object, 'read');
69
70 3
                    return $object;
71 1
                },
72
            ],
73
        ];
74
    }
75
76
    /**
77
     * Returns standard fields to mutate the object
78
     *
79
     * @param string $class
80
     *
81
     * @return array
82
     */
83 4
    public static function buildMutation(string $class): array
84
    {
85 1
        $reflect = new ReflectionClass($class);
86 1
        $name = $reflect->getShortName();
87 1
        $plural = self::makePlural($name);
88 1
        $lowerName = lcfirst($name);
89
90
        return [
91
            [
92 1
                'name' => 'create' . $name,
93 1
                'type' => Type::nonNull(_types()->getOutput($class)),
94 1
                'description' => 'Create a new ' . $name,
95
                'args' => [
96 1
                    'input' => Type::nonNull(_types()->getInput($class)),
97
                ],
98
                'resolve' => function ($root, array $args) use ($class, $lowerName): AbstractModel {
0 ignored issues
show
Unused Code introduced by
The import $lowerName is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
99
                    // Do it
100 2
                    $object = new $class();
101 2
                    $input = $args['input'];
102 2
                    Helper::hydrate($object, $input);
103
104
                    // Check ACL
105 2
                    Helper::throwIfDenied($object, 'create');
106
107 2
                    _em()->persist($object);
108 2
                    _em()->flush();
109
110 2
                    return $object;
111 1
                },
112
            ],
113
            [
114 1
                'name' => 'update' . $name,
115 1
                'type' => Type::nonNull(_types()->getOutput($class)),
116 1
                'description' => 'Update an existing ' . $name,
117
                'args' => [
118 1
                    'id' => Type::nonNull(_types()->getId($class)),
119 1
                    'input' => Type::nonNull(_types()->getPartialInput($class)),
120
                ],
121
                'resolve' => function ($root, array $args) use ($lowerName): AbstractModel {
0 ignored issues
show
Unused Code introduced by
The import $lowerName is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
122 4
                    $object = $args['id']->getEntity();
123
124
                    // Check ACL
125 4
                    Helper::throwIfDenied($object, 'update');
126
127
                    // Do it
128 3
                    $input = $args['input'];
129 3
                    Helper::hydrate($object, $input);
130
131 3
                    _em()->flush();
132
133 3
                    return $object;
134 1
                },
135
            ],
136
            [
137 1
                'name' => 'delete' . $plural,
138 1
                'type' => Type::nonNull(Type::boolean()),
139 1
                'description' => 'Delete one or several existing ' . $name,
140
                'args' => [
141 1
                    'ids' => Type::nonNull(Type::listOf(Type::nonNull(_types()->getId($class)))),
142
                ],
143
                'resolve' => function ($root, array $args) use ($lowerName): bool {
0 ignored issues
show
Unused Code introduced by
The import $lowerName is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
144
                    foreach ($args['ids'] as $id) {
145
                        $object = $id->getEntity();
146
147
                        // Check ACL
148
                        Helper::throwIfDenied($object, 'update');
149
150
                        // Do it
151
                        _em()->remove($object);
152
                    }
153
154
                    _em()->flush();
155
156
                    return true;
157 1
                },
158
            ],
159
        ];
160
    }
161
162
    /**
163
     * Returns standard mutations to manage many-to-many relations between two given class
164
     *
165
     * @param string $ownerClass The class owning the relation
166
     * @param string $otherClass The other class, not-owning the relation
167
     * @param null|string $otherName a specific semantic, if needed, to be use as adder. If `$otherName = 'Parent'`, then we will call `addParent()`
168
     *
169
     * @throws \ReflectionException
170
     *
171
     * @return array
172
     */
173 1
    public static function buildRelationMutation(string $ownerClass, string $otherClass, ?string $otherName = null): array
174
    {
175 1
        $ownerReflect = new ReflectionClass($ownerClass);
176 1
        $ownerName = $ownerReflect->getShortName();
177 1
        $lowerOwnerName = lcfirst($ownerName);
178
179 1
        $otherReflect = new ReflectionClass($otherClass);
180 1
        $otherClassName = $otherReflect->getShortName();
181 1
        if ($otherName) {
182 1
            $semantic = ' as ' . $otherName;
183
        } else {
184 1
            $semantic = '';
185 1
            $otherName = $otherClassName;
186
        }
187 1
        $lowerOtherName = lcfirst($otherName);
188
189 1
        if ($lowerOwnerName === $lowerOtherName) {
190
            $lowerOwnerName .= 1;
191
            $lowerOtherName .= 2;
192
        }
193
194
        $args = [
195 1
            $lowerOwnerName => Type::nonNull(_types()->getId($ownerClass)),
196 1
            $lowerOtherName => Type::nonNull(_types()->getId($otherClass)),
197
        ];
198
199
        return [
200
            [
201 1
                'name' => 'link' . $ownerName . $otherName,
202 1
                'type' => Type::nonNull(_types()->getOutput($ownerClass)),
203 1
                'description' => 'Create a relation between ' . $ownerName . ' and ' . $otherClassName . $semantic . '.' . PHP_EOL . PHP_EOL .
204 1
                    'If the relation already exists, it will have no effect.',
205 1
                'args' => $args,
206
                'resolve' => function ($root, array $args) use ($lowerOwnerName, $lowerOtherName, $otherName, $otherClass): AbstractModel {
0 ignored issues
show
Unused Code introduced by
The import $otherClass is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
207
                    $owner = $args[$lowerOwnerName]->getEntity();
208
                    $other = $args[$lowerOtherName]->getEntity();
209
210
                    // Check ACL
211
                    Helper::throwIfDenied($owner, 'update');
212
213
                    // Do it
214
                    $method = 'add' . $otherName;
215
                    $owner->$method($other);
216
                    _em()->flush();
217
218
                    return $owner;
219 1
                },
220
            ],
221
            [
222 1
                'name' => 'unlink' . $ownerName . $otherName,
223 1
                'type' => Type::nonNull(_types()->getOutput($ownerClass)),
224 1
                'description' => 'Delete a relation between ' . $ownerName . ' and ' . $otherClassName . $semantic . '.' . PHP_EOL . PHP_EOL .
225 1
                    'If the relation does not exist, it will have no effect.',
226 1
                'args' => $args,
227
                'resolve' => function ($root, array $args) use ($lowerOwnerName, $lowerOtherName, $otherName, $otherClass): AbstractModel {
0 ignored issues
show
Unused Code introduced by
The import $otherClass is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
228
                    $owner = $args[$lowerOwnerName]->getEntity();
229
                    $other = $args[$lowerOtherName]->getEntity();
230
231
                    // Check ACL
232
                    Helper::throwIfDenied($owner, 'update');
233
234
                    // Do it
235
                    if ($other) {
236
                        $method = 'remove' . $otherName;
237
                        $owner->$method($other);
238
                        _em()->flush();
239
                    }
240
241
                    return $owner;
242 1
                },
243
            ],
244
        ];
245
    }
246
247
    /**
248
     * Returns the plural form of the given name
249
     *
250
     * @param string $name
251
     *
252
     * @return string
253
     */
254 1
    private static function makePlural(string $name): string
255
    {
256 1
        $plural = $name . 's';
257 1
        $plural = preg_replace('/ys$/', 'ies', $plural);
258 1
        $plural = preg_replace('/ss$/', 'ses', $plural);
259
260 1
        return $plural;
261
    }
262
263
    /**
264
     * Return arguments used for the list
265
     *
266
     * @param string $class
267
     *
268
     * @return array
269
     */
270 1
    private static function getListArguments(ClassMetadata $class, string $classs, string $name): array
271
    {
272
        $listArgs = [
273
            [
274 1
                'name' => 'filter',
275 1
                'type' => _types()->getFilter($class->getName()),
276
            ],
277
            [
278 1
                'name' => 'sorting',
279 1
                'type' => _types()->getSorting($class->getName()),
280 1
                'defaultValue' => self::getDefaultSorting($class),
281
            ],
282
        ];
283
284 1
        $filterTypeClass = 'Old' . $class->getReflectionClass()->getShortName() . 'Filter';
285 1
        if (_types()->has($filterTypeClass)) {
286
            $listArgs[] = [
287
                'name' => 'filters',
288
                'type' => _types()->get($filterTypeClass),
289
            ];
290
        }
291
292 1
        $listArgs[] = PaginationInputType::build();
293
294 1
        return $listArgs;
295
    }
296
297
    /**
298
     * Return arguments used for single item
299
     *
300
     * @param string $class
301
     *
302
     * @return array
303
     */
304 1
    private static function getSingleArguments(string $class): array
305
    {
306
        $args = [
307 1
            'id' => Type::nonNull(_types()->getId($class)),
308
        ];
309
310 1
        return $args;
311
    }
312
313
    /**
314
     * Get default sorting values with some fallback for some special cases
315
     *
316
     * @param ClassMetadata $class
317
     *
318
     * @return array
319
     */
320 1
    private static function getDefaultSorting(ClassMetadata $class): array
321
    {
322 1
        $defaultSorting = [];
323
324 1
        $defaultSorting[] = [
325
            'field' => 'id',
326
            'order' => 'ASC',
327
        ];
328
329 1
        return $defaultSorting;
330
    }
331
}
332