Failed Conditions
Push — master ( c4208e...4b96fe )
by Sylvain
09:02
created

Standard::getDefaultSorting()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 1
dl 0
loc 10
ccs 4
cts 4
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\Api\Input\PaginationInputType;
9
use Application\Model\AbstractModel;
10
use Doctrine\ORM\Mapping\ClassMetadata;
11
use GraphQL\Type\Definition\Type;
12
use Money\Money;
13
use ReflectionClass;
14
15
/**
16
 * Provide easy way to build standard fields to query and mutate objects
17
 */
18
abstract class Standard
19
{
20
    /**
21
     * Returns standard fields to query the object
22
     *
23
     * @param string $class
24
     *
25
     * @return array
26
     */
27 5
    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($metadata);
36 1
        $singleArgs = self::getSingleArguments($class);
37
38
        return [
39
            [
40 1
                'name' => $plural,
41 1
                'type' => Type::nonNull(_types()->get($shortName . 'Pagination')),
1 ignored issue
show
Bug introduced by
_types()->get($shortName . 'Pagination') of type GraphQL\Type\Definition\Type is incompatible with the type GraphQL\Type\Definition\NullableType expected by parameter $wrappedType of GraphQL\Type\Definition\Type::nonNull(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

41
                'type' => Type::nonNull(/** @scrutinizer ignore-type */ _types()->get($shortName . 'Pagination')),
Loading history...
42 1
                'args' => $listArgs,
43
                'resolve' => function ($root, array $args) use ($class): array {
44 5
                    $filters = self::customTypesToScalar($args['filter'] ?? []);
45 5
                    $qb = _types()->createFilteredQueryBuilder($class, $filters, $args['sorting'] ?? []);
46
47 5
                    $items = Helper::paginate($args['pagination'], $qb);
48 5
                    $exportExcelField = Helper::excelExportField($class, $qb);
49 5
                    $aggregatedFields = Helper::aggregatedFields($class, $qb);
50 5
                    $result = array_merge($aggregatedFields, $exportExcelField, $items);
51
52 5
                    return $result;
53 1
                },
54
            ],
55
            [
56 1
                'name' => $name,
57 1
                'type' => Type::nonNull(_types()->getOutput($class)),
58 1
                'args' => $singleArgs,
59
                'resolve' => function ($root, array $args): ?AbstractModel {
60 3
                    $object = $args['id']->getEntity();
61
62 2
                    Helper::throwIfDenied($object, 'read');
63
64 2
                    return $object;
65 1
                },
66
            ],
67
        ];
68
    }
69
70
    /**
71
     * Returns standard fields to mutate the object
72
     *
73
     * @param string $class
74
     *
75
     * @return array
76
     */
77 4
    public static function buildMutation(string $class): array
78
    {
79 1
        $reflect = new ReflectionClass($class);
80 1
        $name = $reflect->getShortName();
81 1
        $plural = self::makePlural($name);
82 1
        $lowerName = lcfirst($name);
83
84
        return [
85
            [
86 1
                'name' => 'create' . $name,
87 1
                'type' => Type::nonNull(_types()->getOutput($class)),
88 1
                'description' => 'Create a new ' . $name,
89
                'args' => [
90 1
                    'input' => Type::nonNull(_types()->getInput($class)),
91
                ],
92
                'resolve' => function ($root, array $args) use ($class, $lowerName): AbstractModel {
0 ignored issues
show
Unused Code introduced by
The import $lowerName is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
93
                    // Do it
94 4
                    $object = new $class();
95 4
                    $input = $args['input'];
96 4
                    Helper::hydrate($object, $input);
97
98
                    // Check ACL
99 4
                    Helper::throwIfDenied($object, 'create');
100
101 3
                    _em()->persist($object);
102 3
                    _em()->flush();
103
104 3
                    return $object;
105 1
                },
106
            ],
107
            [
108 1
                'name' => 'update' . $name,
109 1
                'type' => Type::nonNull(_types()->getOutput($class)),
110 1
                'description' => 'Update an existing ' . $name,
111
                'args' => [
112 1
                    'id' => Type::nonNull(_types()->getId($class)),
113 1
                    'input' => Type::nonNull(_types()->getPartialInput($class)),
114
                ],
115
                'resolve' => function ($root, array $args) use ($lowerName): AbstractModel {
0 ignored issues
show
Unused Code introduced by
The import $lowerName is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
116 4
                    $object = $args['id']->getEntity();
117
118
                    // Check ACL
119 4
                    Helper::throwIfDenied($object, 'update');
120
121
                    // Do it
122 3
                    $input = $args['input'];
123 3
                    Helper::hydrate($object, $input);
124
125 3
                    _em()->flush();
126
127 3
                    return $object;
128 1
                },
129
            ],
130
            [
131 1
                'name' => 'delete' . $plural,
132 1
                'type' => Type::nonNull(Type::boolean()),
133 1
                'description' => 'Delete one or several existing ' . $name,
134
                'args' => [
135 1
                    'ids' => Type::nonNull(Type::listOf(Type::nonNull(_types()->getId($class)))),
136
                ],
137
                'resolve' => function ($root, array $args) use ($lowerName): bool {
0 ignored issues
show
Unused Code introduced by
The import $lowerName is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
138 1
                    foreach ($args['ids'] as $id) {
139 1
                        $object = $id->getEntity();
140
141
                        // Check ACL
142 1
                        Helper::throwIfDenied($object, 'delete');
143
144
                        // Do it
145
                        _em()->remove($object);
146
                    }
147
148
                    _em()->flush();
149
150
                    return true;
151 1
                },
152
            ],
153
        ];
154
    }
155
156
    /**
157
     * Returns standard mutations to manage many-to-many relations between two given class
158
     *
159
     * @param string $ownerClass The class owning the relation
160
     * @param string $otherClass The other class, not-owning the relation
161
     * @param null|string $otherName a specific semantic, if needed, to be use as adder. If `$otherName = 'Parent'`, then we will call `addParent()`
162
     *
163
     * @throws \ReflectionException
164
     *
165
     * @return array
166
     */
167 1
    public static function buildRelationMutation(string $ownerClass, string $otherClass, ?string $otherName = null): array
168
    {
169 1
        $ownerReflect = new ReflectionClass($ownerClass);
170 1
        $ownerName = $ownerReflect->getShortName();
171 1
        $lowerOwnerName = lcfirst($ownerName);
172
173 1
        $otherReflect = new ReflectionClass($otherClass);
174 1
        $otherClassName = $otherReflect->getShortName();
175 1
        if ($otherName) {
176 1
            $semantic = ' as ' . $otherName;
177
        } else {
178 1
            $semantic = '';
179 1
            $otherName = $otherClassName;
180
        }
181 1
        $lowerOtherName = lcfirst($otherName);
182
183 1
        if ($lowerOwnerName === $lowerOtherName) {
184
            $lowerOwnerName .= 1;
185
            $lowerOtherName .= 2;
186
        }
187
188
        $args = [
189 1
            $lowerOwnerName => Type::nonNull(_types()->getId($ownerClass)),
190 1
            $lowerOtherName => Type::nonNull(_types()->getId($otherClass)),
191
        ];
192
193
        return [
194
            [
195 1
                'name' => 'link' . $ownerName . $otherName,
196 1
                'type' => Type::nonNull(_types()->getOutput($ownerClass)),
197 1
                'description' => 'Create a relation between ' . $ownerName . ' and ' . $otherClassName . $semantic . '.' . PHP_EOL . PHP_EOL .
198 1
                    'If the relation already exists, it will have no effect.',
199 1
                'args' => $args,
200
                'resolve' => function ($root, array $args) use ($lowerOwnerName, $lowerOtherName, $otherName, $otherClass): AbstractModel {
0 ignored issues
show
Unused Code introduced by
The import $otherClass is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
201
                    $owner = $args[$lowerOwnerName]->getEntity();
202
                    $other = $args[$lowerOtherName]->getEntity();
203
204
                    // Check ACL
205
                    Helper::throwIfDenied($owner, 'update');
206
207
                    // Do it
208
                    $method = 'add' . $otherName;
209
                    $owner->$method($other);
210
                    _em()->flush();
211
212
                    return $owner;
213 1
                },
214
            ],
215
            [
216 1
                'name' => 'unlink' . $ownerName . $otherName,
217 1
                'type' => Type::nonNull(_types()->getOutput($ownerClass)),
218 1
                'description' => 'Delete a relation between ' . $ownerName . ' and ' . $otherClassName . $semantic . '.' . PHP_EOL . PHP_EOL .
219 1
                    'If the relation does not exist, it will have no effect.',
220 1
                'args' => $args,
221
                'resolve' => function ($root, array $args) use ($lowerOwnerName, $lowerOtherName, $otherName, $otherClass): AbstractModel {
0 ignored issues
show
Unused Code introduced by
The import $otherClass is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
222
                    $owner = $args[$lowerOwnerName]->getEntity();
223
                    $other = $args[$lowerOtherName]->getEntity();
224
225
                    // Check ACL
226
                    Helper::throwIfDenied($owner, 'update');
227
228
                    // Do it
229
                    if ($other) {
230
                        $method = 'remove' . $otherName;
231
                        $owner->$method($other);
232
                        _em()->flush();
233
                    }
234
235
                    return $owner;
236 1
                },
237
            ],
238
        ];
239
    }
240
241
    /**
242
     * Returns the plural form of the given name
243
     *
244
     * @param string $name
245
     *
246
     * @return string
247
     */
248 1
    private static function makePlural(string $name): string
249
    {
250 1
        $plural = $name . 's';
251 1
        $plural = preg_replace('/ys$/', 'ies', $plural);
252 1
        $plural = preg_replace('/ss$/', 'ses', $plural);
253
254 1
        return $plural;
255
    }
256
257
    /**
258
     * Return arguments used for the list
259
     *
260
     * @param ClassMetadata $class
261
     *
262
     * @return array
263
     */
264 1
    private static function getListArguments(ClassMetadata $class): array
265
    {
266
        $listArgs = [
267
            [
268 1
                'name' => 'filter',
269 1
                'type' => _types()->getFilter($class->getName()),
270
            ],
271
            [
272 1
                'name' => 'sorting',
273 1
                'type' => _types()->getSorting($class->getName()),
274 1
                'defaultValue' => self::getDefaultSorting($class),
275
            ],
276
        ];
277
278 1
        $listArgs[] = PaginationInputType::build();
279
280 1
        return $listArgs;
281
    }
282
283
    /**
284
     * Return arguments used for single item
285
     *
286
     * @param string $class
287
     *
288
     * @return array
289
     */
290 1
    private static function getSingleArguments(string $class): array
291
    {
292
        $args = [
293 1
            'id' => Type::nonNull(_types()->getId($class)),
294
        ];
295
296 1
        return $args;
297
    }
298
299
    /**
300
     * Get default sorting values with some fallback for some special cases
301
     *
302
     * @param ClassMetadata $class
303
     *
304
     * @return array
305
     */
306 1
    private static function getDefaultSorting(ClassMetadata $class): array
307
    {
308 1
        $defaultSorting = [];
309
310 1
        $defaultSorting[] = [
311
            'field' => 'id',
312
            'order' => 'ASC',
313
        ];
314
315 1
        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
     * @param array $args
323
     *
324
     * @return array
325
     */
326 5
    private static function customTypesToScalar(array $args): array
327
    {
328 5
        foreach ($args as &$p) {
329
            if (is_array($p)) {
330
                $p = self::customTypesToScalar($p);
331
            } elseif ($p instanceof Money) {
332
                $p = $p->getAmount();
333
            }
334
        }
335
336 5
        return $args;
337
    }
338
}
339