Failed Conditions
Push — master ( dfb37d...2871bb )
by Adrien
06:38
created

Standard::filterInput()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4

Importance

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