Failed Conditions
Push — master ( ce2a97...d73709 )
by Adrien
13:38 queued 11:33
created

Standard::buildRelationMutation()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 67
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 4.7571

Importance

Changes 0
Metric Value
eloc 44
dl 0
loc 67
ccs 30
cts 47
cp 0.6383
rs 9.216
c 0
b 0
f 0
cc 4
nc 4
nop 3
crap 4.7571

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\Model\AbstractModel;
9
use Application\Model\Order;
10
use Doctrine\ORM\Mapping\ClassMetadata;
11
use Ecodev\Felix\Api\Field\FieldInterface;
12
use Ecodev\Felix\Api\Input\PaginationInputType;
13
use Ecodev\Felix\Api\Plural;
14
use GraphQL\Type\Definition\Type;
15
use Money\Money;
16
use ReflectionClass;
17
18
/**
19
 * Provide easy way to build standard fields to query and mutate objects.
20
 *
21
 * @phpstan-import-type PermissiveFieldsConfig from FieldInterface
22
 */
23
abstract class Standard
24
{
25
    /**
26
     * Returns standard fields to query the object.
27
     *
28
     * @return PermissiveFieldsConfig
29
     */
30 4
    public static function buildQuery(string $class): iterable
31
    {
32 1
        $metadata = _em()->getClassMetadata($class);
33 1
        $reflect = $metadata->getReflectionClass();
34 1
        $name = lcfirst($reflect->getShortName());
35 1
        $shortName = $reflect->getShortName();
36 1
        $plural = Plural::make($name);
37
38 1
        $listArgs = self::getListArguments($metadata);
39 1
        $singleArgs = self::getSingleArguments($class);
40
41 1
        yield $plural => fn () => [
0 ignored issues
show
Bug Best Practice introduced by
The expression yield $plural => function(...) { /* ... */ } returns the type Generator which is incompatible with the documented return type Application\Api\Field\PermissiveFieldsConfig.
Loading history...
42 4
            'type' => Type::nonNull(_types()->get($shortName . 'Pagination')),
43 4
            'args' => $listArgs,
44 4
            'resolve' => function ($root, array $args) use ($class, $metadata): array {
45 3
                $filters = self::customTypesToScalar($args['filter'] ?? []);
46
47
                // If null or empty list is provided by client, fallback on default sorting
48 3
                $sorting = $args['sorting'] ?? [];
49 3
                if (!$sorting) {
50 3
                    $sorting = self::getDefaultSorting($metadata);
51
                }
52
53
                // And **always** sort by ID
54 3
                $sorting[] = [
55 3
                    'field' => 'id',
56 3
                    'order' => 'ASC',
57 3
                ];
58
59 3
                $qb = _types()->createFilteredQueryBuilder($class, $filters, $sorting);
60
61 3
                $items = Helper::paginate($args['pagination'], $qb);
62 3
                $aggregatedFields = Helper::aggregatedFields($class, $qb);
63 3
                $result = array_merge($aggregatedFields, $items);
64
65 3
                return $result;
66 4
            },
67 4
        ];
68
69 1
        yield $name => fn () => [
70 1
            'type' => Type::nonNull(_types()->getOutput($class)),
71 1
            'args' => $singleArgs,
72 1
            'resolve' => function ($root, array $args): ?AbstractModel {
73 3
                $object = $args['id']->getEntity();
74
75 2
                Helper::throwIfDenied($object, 'read');
76
77 2
                return $object;
78 1
            },
79 1
        ];
80
    }
81
82
    /**
83
     * Returns standard fields to mutate the object.
84
     *
85
     * @return PermissiveFieldsConfig
86
     */
87 3
    public static function buildMutation(string $class): iterable
88
    {
89 1
        $reflect = new ReflectionClass($class);
90 1
        $name = $reflect->getShortName();
91 1
        $plural = Plural::make($name);
92
93 1
        yield 'create' . $name => fn () => [
0 ignored issues
show
Bug Best Practice introduced by
The expression yield 'create' . $name =...tion(...) { /* ... */ } returns the type Generator which is incompatible with the documented return type Application\Api\Field\PermissiveFieldsConfig.
Loading history...
94 3
            'type' => Type::nonNull(_types()->getOutput($class)),
95 3
            'description' => 'Create a new ' . $name,
96 3
            'args' => [
97 3
                'input' => Type::nonNull(_types()->getInput($class)),
98 3
            ],
99 3
            'resolve' => function ($root, array $args) use ($class): AbstractModel {
100
                // Do it
101 2
                $object = new $class();
102 2
                $input = $args['input'];
103 2
                Helper::hydrate($object, $input);
104
105
                // Check ACL
106 2
                Helper::throwIfDenied($object, 'create');
107
108 2
                _em()->persist($object);
109 2
                _em()->flush();
110
111 2
                return $object;
112 3
            },
113 3
        ];
114
115 1
        yield 'update' . $name => fn () => [
116 1
            'type' => Type::nonNull(_types()->getOutput($class)),
117 1
            'description' => 'Update an existing ' . $name,
118 1
            'args' => [
119 1
                'id' => Type::nonNull(_types()->getId($class)),
120 1
                'input' => Type::nonNull(_types()->getPartialInput($class)),
121 1
            ],
122 1
            'resolve' => function ($root, array $args): AbstractModel {
123 2
                $object = $args['id']->getEntity();
124
125
                // Check ACL
126 2
                Helper::throwIfDenied($object, 'update');
127
128
                // Do it
129 1
                $input = $args['input'];
130 1
                Helper::hydrate($object, $input);
131
132 1
                _em()->flush();
133
134 1
                return $object;
135 1
            },
136 1
        ];
137
138 1
        yield 'delete' . $plural => fn () => [
139 1
            'type' => Type::nonNull(Type::boolean()),
140 1
            'description' => 'Delete one or several existing ' . $name,
141 1
            'args' => [
142 1
                'ids' => Type::nonNull(Type::listOf(Type::nonNull(_types()->getId($class)))),
143 1
            ],
144 1
            'resolve' => function ($root, array $args): bool {
145
                foreach ($args['ids'] as $id) {
146
                    $object = $id->getEntity();
147
148
                    // Check ACL
149
                    Helper::throwIfDenied($object, 'delete');
150
151
                    // Do it
152
                    _em()->remove($object);
153
                }
154
155
                _em()->flush();
156
157
                return true;
158 1
            },
159 1
        ];
160
    }
161
162
    /**
163
     * Returns standard mutations to manage many-to-many relations between two given class.
164
     *
165
     * @param string $ownerClass The class owning the relation
166
     * @param string $otherClass The other class, not-owning the relation
167
     * @param null|string $otherName a specific semantic, if needed, to be use as adder. If `$otherName = 'Parent'`, then we will call `addParent()`
168
     *
169
     * @return PermissiveFieldsConfig
170
     */
171 1
    public static function buildRelationMutation(string $ownerClass, string $otherClass, ?string $otherName = null): iterable
172
    {
173 1
        $ownerReflect = new ReflectionClass($ownerClass);
174 1
        $ownerName = $ownerReflect->getShortName();
175 1
        $lowerOwnerName = lcfirst($ownerName);
176
177 1
        $otherReflect = new ReflectionClass($otherClass);
178 1
        $otherClassName = $otherReflect->getShortName();
179 1
        if ($otherName) {
180 1
            $semantic = ' as ' . $otherName;
181
        } else {
182 1
            $semantic = '';
183 1
            $otherName = $otherClassName;
184
        }
185 1
        $lowerOtherName = lcfirst($otherName);
186
187 1
        if ($lowerOwnerName === $lowerOtherName) {
188
            $lowerOwnerName .= 1;
189
            $lowerOtherName .= 2;
190
        }
191
192 1
        $args = [
193 1
            $lowerOwnerName => Type::nonNull(_types()->getId($ownerClass)),
194 1
            $lowerOtherName => Type::nonNull(_types()->getId($otherClass)),
195 1
        ];
196
197 1
        yield 'link' . $ownerName . $otherName => fn () => [
0 ignored issues
show
Bug Best Practice introduced by
The expression yield 'link' . $ownerNam...tion(...) { /* ... */ } returns the type Generator which is incompatible with the documented return type Application\Api\Field\PermissiveFieldsConfig.
Loading history...
198 1
            'type' => Type::nonNull(_types()->getOutput($ownerClass)),
199 1
            'description' => 'Create a relation between ' . $ownerName . ' and ' . $otherClassName . $semantic . '.' . PHP_EOL . PHP_EOL
200 1
                . 'If the relation already exists, it will have no effect.',
201 1
            'args' => $args,
202 1
            'resolve' => function ($root, array $args) use ($lowerOwnerName, $lowerOtherName, $otherName): AbstractModel {
203
                $owner = $args[$lowerOwnerName]->getEntity();
204
                $other = $args[$lowerOtherName]->getEntity();
205
206
                // Check ACL
207
                Helper::throwIfDenied($owner, 'update');
208
209
                // Do it
210
                $method = 'add' . $otherName;
211
                $owner->$method($other);
212
                _em()->flush();
213
214
                return $owner;
215 1
            },
216 1
        ];
217
218 1
        yield 'unlink' . $ownerName . $otherName => fn () => [
219 1
            'type' => Type::nonNull(_types()->getOutput($ownerClass)),
220 1
            'description' => 'Delete a relation between ' . $ownerName . ' and ' . $otherClassName . $semantic . '.' . PHP_EOL . PHP_EOL
221 1
                . 'If the relation does not exist, it will have no effect.',
222 1
            'args' => $args,
223 1
            'resolve' => function ($root, array $args) use ($lowerOwnerName, $lowerOtherName, $otherName): AbstractModel {
224
                $owner = $args[$lowerOwnerName]->getEntity();
225
                $other = $args[$lowerOtherName]->getEntity();
226
227
                // Check ACL
228
                Helper::throwIfDenied($owner, 'update');
229
230
                // Do it
231
                if ($other) {
232
                    $method = 'remove' . $otherName;
233
                    $owner->$method($other);
234
                    _em()->flush();
235
                }
236
237
                return $owner;
238 1
            },
239 1
        ];
240
    }
241
242
    /**
243
     * Return arguments used for the list.
244
     */
245 1
    private static function getListArguments(ClassMetadata $class): array
246
    {
247 1
        $listArgs = [
248 1
            [
249 1
                'name' => 'filter',
250 1
                'type' => _types()->getFilter($class->getName()),
251 1
            ],
252 1
            [
253 1
                'name' => 'sorting',
254 1
                'type' => _types()->getSorting($class->getName()),
255 1
                'defaultValue' => self::getDefaultSorting($class),
256 1
            ],
257 1
        ];
258
259 1
        $listArgs[] = PaginationInputType::build(_types());
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 1
        $args = [
270 1
            'id' => Type::nonNull(_types()->getId($class)),
271 1
        ];
272
273 1
        return $args;
274
    }
275
276
    /**
277
     * Get default sorting values with some fallback for some special cases.
278
     */
279 4
    private static function getDefaultSorting(ClassMetadata $metadata): array
280
    {
281 4
        $defaultSorting = [];
282
283 4
        $class = $metadata->getName();
284 4
        if ($class === Order::class) {
285 1
            $defaultSorting[] = [
286 1
                'field' => 'creationDate',
287 1
                'order' => 'DESC',
288 1
            ];
289
        }
290
291 4
        return $defaultSorting;
292
    }
293
294
    /**
295
     * Recursively convert custom scalars that don't implement __toString() to their scalar
296
     * representation to injected back into DQL/SQL.
297
     */
298 3
    private static function customTypesToScalar(array $args): array
299
    {
300 3
        foreach ($args as &$p) {
301
            if (is_array($p)) {
302
                $p = self::customTypesToScalar($p);
303
            } elseif ($p instanceof Money) {
304
                $p = $p->getAmount();
305
            }
306
        }
307
308 3
        return $args;
309
    }
310
}
311