Failed Conditions
Push — master ( 27554e...857869 )
by Luca
09:08
created

Standard::buildQuery()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 46
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 2.0001

Importance

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