Standard::buildQuery()   A
last analyzed

Complexity

Conditions 2
Paths 1

Size

Total Lines 48
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 34
CRAP Score 2

Importance

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