Passed
Push — master ( 776edf...a0775a )
by Adrien
09:01
created

Standard   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 339
Duplicated Lines 0 %

Test Coverage

Coverage 83.33%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 152
c 2
b 0
f 0
dl 0
loc 339
ccs 120
cts 144
cp 0.8333
rs 10
wmc 18

7 Methods

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