Standard   A
last analyzed

Complexity

Total Complexity 17

Size/Duplication

Total Lines 286
Duplicated Lines 0 %

Test Coverage

Coverage 89.53%

Importance

Changes 0
Metric Value
wmc 17
eloc 139
dl 0
loc 286
ccs 154
cts 172
cp 0.8953
rs 10
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A getDefaultSorting() 0 25 4
A getSingleArguments() 0 7 1
A getListArguments() 0 17 1
A buildMutation() 0 80 4
A buildQuery() 0 44 2
B buildRelationMutation() 0 67 5
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 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 ReflectionClass;
16
17
/**
18
 * Provide easy way to build standard fields to query and mutate objects.
19
 *
20
 * @phpstan-import-type PermissiveFieldsConfig from FieldInterface
21
 */
22
abstract class Standard
23
{
24
    /**
25
     * Returns standard fields to query the object.
26
     *
27
     * @return PermissiveFieldsConfig
0 ignored issues
show
Bug introduced by
The type Application\Api\Field\PermissiveFieldsConfig was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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