Completed
Push — master ( 3853ec...f78c46 )
by Adrien
07:49
created

Standard::buildRelationMutation()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 69
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 5.0613

Importance

Changes 0
Metric Value
cc 4
eloc 45
nc 4
nop 3
dl 0
loc 69
ccs 25
cts 42
cp 0.5951
crap 5.0613
rs 9.2
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
83
        return [
84
            [
85 1
                'name' => 'create' . $name,
86 1
                'type' => Type::nonNull(_types()->getOutput($class)),
87 1
                'description' => 'Create a new ' . $name,
88
                'args' => [
89 1
                    'input' => Type::nonNull(_types()->getInput($class)),
90
                ],
91
                'resolve' => function ($root, array $args) use ($class): AbstractModel {
92
                    // Do it
93 4
                    $object = new $class();
94 4
                    $input = $args['input'];
95 4
                    Helper::hydrate($object, $input);
96
97
                    // Check ACL
98 4
                    Helper::throwIfDenied($object, 'create');
99
100 3
                    _em()->persist($object);
101 3
                    _em()->flush();
102
103 3
                    return $object;
104 1
                },
105
            ],
106
            [
107 1
                'name' => 'update' . $name,
108 1
                'type' => Type::nonNull(_types()->getOutput($class)),
109 1
                'description' => 'Update an existing ' . $name,
110
                'args' => [
111 1
                    'id' => Type::nonNull(_types()->getId($class)),
112 1
                    'input' => Type::nonNull(_types()->getPartialInput($class)),
113
                ],
114
                'resolve' => function ($root, array $args): AbstractModel {
115 4
                    $object = $args['id']->getEntity();
116
117
                    // Check ACL
118 4
                    Helper::throwIfDenied($object, 'update');
119
120
                    // Do it
121 3
                    $input = $args['input'];
122 3
                    Helper::hydrate($object, $input);
123
124 3
                    _em()->flush();
125
126 3
                    return $object;
127 1
                },
128
            ],
129
            [
130 1
                'name' => 'delete' . $plural,
131 1
                'type' => Type::nonNull(Type::boolean()),
132 1
                'description' => 'Delete one or several existing ' . $name,
133
                'args' => [
134 1
                    'ids' => Type::nonNull(Type::listOf(Type::nonNull(_types()->getId($class)))),
135
                ],
136
                'resolve' => function ($root, array $args): bool {
137 1
                    foreach ($args['ids'] as $id) {
138 1
                        $object = $id->getEntity();
139
140
                        // Check ACL
141 1
                        Helper::throwIfDenied($object, 'delete');
142
143
                        // Do it
144
                        _em()->remove($object);
145
                    }
146
147
                    _em()->flush();
148
149
                    return true;
150 1
                },
151
            ],
152
        ];
153
    }
154
155
    /**
156
     * Returns standard mutations to manage many-to-many relations between two given class
157
     *
158
     * @param string $ownerClass The class owning the relation
159
     * @param string $otherClass The other class, not-owning the relation
160
     * @param null|string $otherName a specific semantic, if needed, to be use as adder. If `$otherName = 'Parent'`, then we will call `addParent()`
161
     *
162
     * @throws \ReflectionException
163
     *
164
     * @return array
165
     */
166 1
    public static function buildRelationMutation(string $ownerClass, string $otherClass, ?string $otherName = null): array
167
    {
168 1
        $ownerReflect = new ReflectionClass($ownerClass);
169 1
        $ownerName = $ownerReflect->getShortName();
170 1
        $lowerOwnerName = lcfirst($ownerName);
171
172 1
        $otherReflect = new ReflectionClass($otherClass);
173 1
        $otherClassName = $otherReflect->getShortName();
174 1
        if ($otherName) {
175 1
            $semantic = ' as ' . $otherName;
176
        } else {
177 1
            $semantic = '';
178 1
            $otherName = $otherClassName;
179
        }
180 1
        $lowerOtherName = lcfirst($otherName);
181
182 1
        if ($lowerOwnerName === $lowerOtherName) {
183
            $lowerOwnerName .= 1;
184
            $lowerOtherName .= 2;
185
        }
186
187
        $args = [
188 1
            $lowerOwnerName => Type::nonNull(_types()->getId($ownerClass)),
189 1
            $lowerOtherName => Type::nonNull(_types()->getId($otherClass)),
190
        ];
191
192
        return [
193
            [
194 1
                'name' => 'link' . $ownerName . $otherName,
195 1
                'type' => Type::nonNull(_types()->getOutput($ownerClass)),
196 1
                'description' => 'Create a relation between ' . $ownerName . ' and ' . $otherClassName . $semantic . '.' . PHP_EOL . PHP_EOL .
197 1
                    'If the relation already exists, it will have no effect.',
198 1
                'args' => $args,
199
                'resolve' => function ($root, array $args) use ($lowerOwnerName, $lowerOtherName, $otherName): AbstractModel {
200
                    $owner = $args[$lowerOwnerName]->getEntity();
201
                    $other = $args[$lowerOtherName]->getEntity();
202
203
                    // Check ACL
204
                    Helper::throwIfDenied($owner, 'update');
205
206
                    // Do it
207
                    $method = 'add' . $otherName;
208
                    $owner->$method($other);
209
                    _em()->flush();
210
211
                    return $owner;
212 1
                },
213
            ],
214
            [
215 1
                'name' => 'unlink' . $ownerName . $otherName,
216 1
                'type' => Type::nonNull(_types()->getOutput($ownerClass)),
217 1
                'description' => 'Delete a relation between ' . $ownerName . ' and ' . $otherClassName . $semantic . '.' . PHP_EOL . PHP_EOL .
218 1
                    'If the relation does not exist, it will have no effect.',
219 1
                'args' => $args,
220
                'resolve' => function ($root, array $args) use ($lowerOwnerName, $lowerOtherName, $otherName): AbstractModel {
221
                    $owner = $args[$lowerOwnerName]->getEntity();
222
                    $other = $args[$lowerOtherName]->getEntity();
223
224
                    // Check ACL
225
                    Helper::throwIfDenied($owner, 'update');
226
227
                    // Do it
228
                    if ($other) {
229
                        $method = 'remove' . $otherName;
230
                        $owner->$method($other);
231
                        _em()->flush();
232
                    }
233
234
                    return $owner;
235 1
                },
236
            ],
237
        ];
238
    }
239
240
    /**
241
     * Returns the plural form of the given name
242
     *
243
     * @param string $name
244
     *
245
     * @return string
246
     */
247 1
    private static function makePlural(string $name): string
248
    {
249 1
        $plural = $name . 's';
250 1
        $plural = preg_replace('/ys$/', 'ies', $plural);
251 1
        $plural = preg_replace('/ss$/', 'ses', $plural);
252
253 1
        return $plural;
254
    }
255
256
    /**
257
     * Return arguments used for the list
258
     *
259
     * @param ClassMetadata $class
260
     *
261
     * @return array
262
     */
263 1
    private static function getListArguments(ClassMetadata $class): array
264
    {
265
        $listArgs = [
266
            [
267 1
                'name' => 'filter',
268 1
                'type' => _types()->getFilter($class->getName()),
269
            ],
270
            [
271 1
                'name' => 'sorting',
272 1
                'type' => _types()->getSorting($class->getName()),
273 1
                'defaultValue' => self::getDefaultSorting($class),
274
            ],
275
        ];
276
277 1
        $listArgs[] = PaginationInputType::build();
278
279 1
        return $listArgs;
280
    }
281
282
    /**
283
     * Return arguments used for single item
284
     *
285
     * @param string $class
286
     *
287
     * @return array
288
     */
289 1
    private static function getSingleArguments(string $class): array
290
    {
291
        $args = [
292 1
            'id' => Type::nonNull(_types()->getId($class)),
293
        ];
294
295 1
        return $args;
296
    }
297
298
    /**
299
     * Get default sorting values with some fallback for some special cases
300
     *
301
     * @param ClassMetadata $class
302
     *
303
     * @return array
304
     */
305 1
    private static function getDefaultSorting(ClassMetadata $class): array
306
    {
307 1
        $defaultSorting = [];
308
309 1
        $defaultSorting[] = [
310
            'field' => 'id',
311
            'order' => 'ASC',
312
        ];
313
314 1
        return $defaultSorting;
315
    }
316
317
    /**
318
     * Recursively convert custom scalars that don't implement __toString() to their scalar
319
     * representation to injected back into DQL/SQL
320
     *
321
     * @param array $args
322
     *
323
     * @return array
324
     */
325 5
    private static function customTypesToScalar(array $args): array
326
    {
327 5
        foreach ($args as &$p) {
328
            if (is_array($p)) {
329
                $p = self::customTypesToScalar($p);
330
            } elseif ($p instanceof Money) {
331
                $p = $p->getAmount();
332
            }
333
        }
334
335 5
        return $args;
336
    }
337
}
338