Failed Conditions
Push — master ( d2dc84...8e498d )
by Adrien
07:27
created

Standard::buildQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 36
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 1

Importance

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