Standard::buildMutation()   A
last analyzed

Complexity

Conditions 4
Paths 1

Size

Total Lines 82
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 53
CRAP Score 4.0001

Importance

Changes 0
Metric Value
eloc 47
c 0
b 0
f 0
dl 0
loc 82
rs 9.1563
ccs 53
cts 54
cp 0.9815
cc 4
nc 1
nop 1
crap 4.0001

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