Failed Conditions
Push — master ( 82004d...d07a9f )
by Adrien
06:13
created

Standard::buildMutation()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 79
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 56
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 45
c 0
b 0
f 0
dl 0
loc 79
ccs 56
cts 56
cp 1
rs 9.2
cc 3
nc 1
nop 1
crap 3

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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