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