Passed
Push — master ( 72f34a...34d54d )
by Sam
09:21 queued 05:21
created

Standard::makePlural()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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