Passed
Push — master ( b734ba...da8304 )
by Adrien
05:54
created

Standard::makePlural()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 1
dl 0
loc 7
ccs 5
cts 5
cp 1
crap 1
rs 10
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\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
        return [
40
            [
41
                'name' => $plural,
42 1
                'type' => Type::nonNull(_types()->get($shortName . 'Pagination')),
43
                '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
                },
53
            ],
54
            [
55 1
                'name' => $name,
56 1
                'type' => Type::nonNull(_types()->getOutput($class)),
57
                'args' => $singleArgs,
58 1
                '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
                },
65
            ],
66
        ];
67
    }
68
69
    /**
70
     * Returns standard fields to mutate the object.
71
     */
72 6
    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
        return [
79
            [
80
                'name' => 'create' . $name,
81 1
                'type' => Type::nonNull(_types()->getOutput($class)),
82
                'description' => 'Create a new ' . $name,
83
                'args' => [
84 1
                    'input' => Type::nonNull(_types()->getInput($class)),
85
                ],
86 6
                'resolve' => function ($root, array $args) use ($class): AbstractModel {
87
                    // Do it
88 5
                    $object = new $class();
89 5
                    $input = $args['input'];
90 5
                    Helper::hydrate($object, $input);
91
92
                    // Check ACL
93 5
                    Helper::throwIfDenied($object, 'create');
94
95 4
                    _em()->persist($object);
96 4
                    _em()->flush();
97
98 4
                    return $object;
99
                },
100
            ],
101
            [
102 1
                'name' => 'update' . $name,
103 1
                'type' => Type::nonNull(_types()->getOutput($class)),
104
                'description' => 'Update an existing ' . $name,
105
                'args' => [
106 1
                    'id' => Type::nonNull(_types()->getId($class)),
107 1
                    'input' => Type::nonNull(_types()->getPartialInput($class)),
108
                ],
109 6
                'resolve' => function ($root, array $args) use ($class): AbstractModel {
110 5
                    $object = $args['id']->getEntity();
111
112
                    // Check ACL
113 5
                    Helper::throwIfDenied($object, 'update');
114
115
                    // Do it
116 3
                    $input = $args['input'];
117 3
                    $filteredInput = self::filterInput($class, $input);
118 3
                    Helper::hydrate($object, $filteredInput);
119
120 3
                    _em()->flush();
121
122 3
                    return $object;
123
                },
124
            ],
125
            [
126 1
                'name' => 'delete' . $plural,
127 1
                'type' => Type::nonNull(Type::boolean()),
128
                'description' => 'Delete one or several existing ' . $name,
129
                'args' => [
130 1
                    'ids' => Type::nonNull(Type::listOf(Type::nonNull(_types()->getId($class)))),
131
                ],
132 1
                'resolve' => function ($root, array $args): bool {
133 2
                    foreach ($args['ids'] as $id) {
134 2
                        $object = $id->getEntity();
135
136
                        // Check ACL
137 2
                        Helper::throwIfDenied($object, 'delete');
138
139
                        // Do it
140 1
                        _em()->remove($object);
141
                    }
142
143 1
                    _em()->flush();
144
145 1
                    return true;
146
                },
147
            ],
148
        ];
149
    }
150
151 3
    private static function filterInput(string $class, array $input): array
152
    {
153 3
        $user = User::getCurrent();
154 3
        if ($class === Bookable::class && $user && $user->getRole() === User::ROLE_TRAINER) {
155
            // whitelist
156 1
            $input = array_intersect_key($input, array_flip(['remarks']));
157
        }
158
159 3
        return $input;
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 1
    public static function buildRelationMutation(string $ownerClass, string $otherClass, ?string $otherName = null): array
170
    {
171 1
        $ownerReflect = new ReflectionClass($ownerClass);
172 1
        $ownerName = $ownerReflect->getShortName();
173 1
        $lowerOwnerName = lcfirst($ownerName);
174
175 1
        $otherReflect = new ReflectionClass($otherClass);
176 1
        $otherClassName = $otherReflect->getShortName();
177 1
        if ($otherName) {
178 1
            $semantic = ' as ' . $otherName;
179
        } else {
180 1
            $semantic = '';
181 1
            $otherName = $otherClassName;
182
        }
183 1
        $lowerOtherName = lcfirst($otherName);
184
185 1
        if ($lowerOwnerName === $lowerOtherName) {
186
            $lowerOwnerName .= 1;
187
            $lowerOtherName .= 2;
188
        }
189
190 1
        $args = [
191 1
            $lowerOwnerName => Type::nonNull(_types()->getId($ownerClass)),
192 1
            $lowerOtherName => Type::nonNull(_types()->getId($otherClass)),
193
        ];
194
195
        return [
196
            [
197
                'name' => 'link' . $ownerName . $otherName,
198 1
                'type' => Type::nonNull(_types()->getOutput($ownerClass)),
199
                'description' => 'Create a relation between ' . $ownerName . ' and ' . $otherClassName . $semantic . '.' . PHP_EOL . PHP_EOL
200
                    . 'If the relation already exists, it will have no effect.',
201
                'args' => $args,
202 1
                'resolve' => function ($root, array $args) use ($lowerOwnerName, $lowerOtherName, $otherName): AbstractModel {
203
                    $owner = $args[$lowerOwnerName]->getEntity();
204
                    $other = $args[$lowerOtherName]->getEntity();
205
206
                    // Check ACL
207
                    Helper::throwIfDenied($owner, 'update');
208
209
                    // Do it
210
                    $method = 'add' . $otherName;
211
                    $owner->$method($other);
212
                    _em()->flush();
213
214
                    return $owner;
215
                },
216
            ],
217
            [
218 1
                'name' => 'unlink' . $ownerName . $otherName,
219 1
                'type' => Type::nonNull(_types()->getOutput($ownerClass)),
220
                'description' => 'Delete a relation between ' . $ownerName . ' and ' . $otherClassName . $semantic . '.' . PHP_EOL . PHP_EOL
221
                    . 'If the relation does not exist, it will have no effect.',
222
                '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
                    if ($other) {
232
                        $method = 'remove' . $otherName;
233
                        $owner->$method($other);
234
                        _em()->flush();
235
                    }
236
237
                    return $owner;
238
                },
239
            ],
240
        ];
241
    }
242
243
    /**
244
     * Returns the plural form of the given name.
245
     */
246 1
    private static function makePlural(string $name): string
247
    {
248 1
        $plural = $name . 's';
249 1
        $plural = preg_replace('/ys$/', 'ies', $plural);
250 1
        $plural = preg_replace('/ss$/', 'ses', $plural);
251
252 1
        return $plural;
253
    }
254
255
    /**
256
     * Return arguments used for the list.
257
     */
258 1
    public static function getListArguments(string $class, bool $includePagination = true): array
259
    {
260 1
        $listArgs = [
261
            [
262 1
                'name' => 'filter',
263 1
                'type' => _types()->getFilter($class),
264
            ],
265
            [
266 1
                'name' => 'sorting',
267 1
                'type' => _types()->getSorting($class),
268 1
                'defaultValue' => self::getDefaultSorting($class),
269
            ],
270
        ];
271
272 1
        if ($includePagination) {
273 1
            $listArgs[] = PaginationInputType::build(_types());
274
        }
275
276 1
        return $listArgs;
277
    }
278
279
    /**
280
     * Return arguments used for single item.
281
     */
282 1
    private static function getSingleArguments(string $class): array
283
    {
284 1
        $args = [
285 1
            'id' => Type::nonNull(_types()->getId($class)),
286
        ];
287
288 1
        return $args;
289
    }
290
291
    /**
292
     * Get default sorting values with some fallback for some special cases.
293
     */
294 5
    private static function getDefaultSorting(string $class): array
295
    {
296 5
        $defaultSorting = [];
297
298 5
        if ($class === Account::class) {
299 1
            $defaultSorting[] = [
300
                'field' => 'code',
301
                'order' => 'ASC',
302
            ];
303 5
        } elseif ($class === TransactionLine::class) {
304 1
            $defaultSorting[] = [
305
                'field' => 'transactionDate',
306
                'order' => 'DESC',
307
            ];
308 5
        } elseif ($class === ExpenseClaim::class) {
309 1
            $defaultSorting[] = [
310
                'field' => 'creationDate',
311
                'order' => 'DESC',
312
            ];
313
        }
314
315 5
        return $defaultSorting;
316
    }
317
318
    /**
319
     * Recursively convert custom scalars that don't implement __toString() to their scalar
320
     * representation to injected back into DQL/SQL.
321
     */
322 5
    private static function customTypesToScalar(array $args): array
323
    {
324 5
        foreach ($args as &$p) {
325
            if (is_array($p)) {
326
                $p = self::customTypesToScalar($p);
327
            } elseif ($p instanceof Money) {
328
                $p = $p->getAmount();
329
            }
330
        }
331
332 5
        return $args;
333
    }
334
335 5
    public static function createFilteredQueryBuilder(string $class, array $args): QueryBuilder
336
    {
337 5
        $filters = self::customTypesToScalar($args['filter'] ?? []);
338
339
        // If null or empty list is provided by client, fallback on default sorting
340 5
        $sorting = $args['sorting'] ?? [];
341 5
        if (!$sorting) {
342 4
            $sorting = self::getDefaultSorting($class);
343
        }
344
345
        // And **always** sort by ID
346 5
        $sorting[] = [
347
            'field' => 'id',
348
            'order' => 'ASC',
349
        ];
350
351 5
        $qb = _types()->createFilteredQueryBuilder($class, $filters, $sorting);
352
353 5
        return $qb;
354
    }
355
}
356