Failed Conditions
Push — master ( 4bd139...a041a8 )
by Adrien
40:22 queued 27:02
created

Standard::customTypesToScalar()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 6.9849

Importance

Changes 0
Metric Value
cc 4
eloc 6
nc 4
nop 1
dl 0
loc 11
ccs 3
cts 7
cp 0.4286
crap 6.9849
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\Model\AbstractModel;
9
use Application\Model\Order;
10
use Doctrine\ORM\Mapping\ClassMetadata;
11
use Ecodev\Felix\Api\Input\PaginationInputType;
12
use Ecodev\Felix\Api\Plural;
13
use GraphQL\Type\Definition\Type;
14
use Money\Money;
15
use ReflectionClass;
16
17
/**
18
 * Provide easy way to build standard fields to query and mutate objects.
19
 */
20
abstract class Standard
21
{
22
    /**
23
     * Returns standard fields to query the object.
24
     */
25 4
    public static function buildQuery(string $class): array
26
    {
27 1
        $metadata = _em()->getClassMetadata($class);
28 1
        $reflect = $metadata->getReflectionClass();
29 1
        $name = lcfirst($reflect->getShortName());
30 1
        $shortName = $reflect->getShortName();
31 1
        $plural = Plural::make($name);
32
33 1
        $listArgs = self::getListArguments($metadata);
34 1
        $singleArgs = self::getSingleArguments($class);
35
36 4
        return [
37 4
            [
38 4
                'name' => $plural,
39 4
                'type' => Type::nonNull(_types()->get($shortName . 'Pagination')),
40 4
                'args' => $listArgs,
41 4
                'resolve' => function ($root, array $args) use ($class, $metadata): array {
42 3
                    $filters = self::customTypesToScalar($args['filter'] ?? []);
43
44
                    // If null or empty list is provided by client, fallback on default sorting
45 3
                    $sorting = $args['sorting'] ?? [];
46 3
                    if (!$sorting) {
47 3
                        $sorting = self::getDefaultSorting($metadata);
48
                    }
49
50
                    // And **always** sort by ID
51 3
                    $sorting[] = [
52 3
                        'field' => 'id',
53 3
                        'order' => 'ASC',
54 3
                    ];
55
56 3
                    $qb = _types()->createFilteredQueryBuilder($class, $filters, $sorting);
57
58 3
                    $items = Helper::paginate($args['pagination'], $qb);
59 3
                    $aggregatedFields = Helper::aggregatedFields($class, $qb);
60 3
                    $result = array_merge($aggregatedFields, $items);
61
62 3
                    return $result;
63 4
                },
64 4
            ],
65 4
            [
66 4
                'name' => $name,
67 4
                'type' => Type::nonNull(_types()->getOutput($class)),
68 4
                'args' => $singleArgs,
69 4
                'resolve' => function ($root, array $args): ?AbstractModel {
70 3
                    $object = $args['id']->getEntity();
71
72 2
                    Helper::throwIfDenied($object, 'read');
73
74 2
                    return $object;
75 4
                },
76 4
            ],
77 4
        ];
78
    }
79
80
    /**
81
     * Returns standard fields to mutate the object.
82
     */
83 3
    public static function buildMutation(string $class): array
84
    {
85 1
        $reflect = new ReflectionClass($class);
86 1
        $name = $reflect->getShortName();
87 1
        $plural = Plural::make($name);
88
89 3
        return [
90 3
            [
91 3
                'name' => 'create' . $name,
92 3
                'type' => Type::nonNull(_types()->getOutput($class)),
93 3
                'description' => 'Create a new ' . $name,
94 3
                'args' => [
95 3
                    'input' => Type::nonNull(_types()->getInput($class)),
96 3
                ],
97 3
                'resolve' => function ($root, array $args) use ($class): AbstractModel {
98
                    // Do it
99 2
                    $object = new $class();
100 2
                    $input = $args['input'];
101 2
                    Helper::hydrate($object, $input);
102
103
                    // Check ACL
104 2
                    Helper::throwIfDenied($object, 'create');
105
106 2
                    _em()->persist($object);
107 2
                    _em()->flush();
108
109 2
                    return $object;
110 3
                },
111 3
            ],
112 3
            [
113 3
                'name' => 'update' . $name,
114 3
                'type' => Type::nonNull(_types()->getOutput($class)),
115 3
                'description' => 'Update an existing ' . $name,
116 3
                'args' => [
117 3
                    'id' => Type::nonNull(_types()->getId($class)),
118 3
                    'input' => Type::nonNull(_types()->getPartialInput($class)),
119 3
                ],
120 3
                'resolve' => function ($root, array $args): AbstractModel {
121
                    $object = $args['id']->getEntity();
122
123
                    // Check ACL
124
                    Helper::throwIfDenied($object, 'update');
125
126
                    // Do it
127
                    $input = $args['input'];
128
                    Helper::hydrate($object, $input);
129
130
                    _em()->flush();
131
132
                    return $object;
133 3
                },
134 3
            ],
135 3
            [
136 3
                'name' => 'delete' . $plural,
137 3
                'type' => Type::nonNull(Type::boolean()),
138 3
                'description' => 'Delete one or several existing ' . $name,
139 3
                'args' => [
140 3
                    'ids' => Type::nonNull(Type::listOf(Type::nonNull(_types()->getId($class)))),
141 3
                ],
142 3
                'resolve' => function ($root, array $args): bool {
143
                    foreach ($args['ids'] as $id) {
144
                        $object = $id->getEntity();
145
146
                        // Check ACL
147
                        Helper::throwIfDenied($object, 'delete');
148
149
                        // Do it
150
                        _em()->remove($object);
151
                    }
152
153
                    _em()->flush();
154
155
                    return true;
156 3
                },
157 3
            ],
158 3
        ];
159
    }
160
161
    /**
162
     * Returns standard mutations to manage many-to-many relations between two given class.
163
     *
164
     * @param string $ownerClass The class owning the relation
165
     * @param string $otherClass The other class, not-owning the relation
166
     * @param null|string $otherName a specific semantic, if needed, to be use as adder. If `$otherName = 'Parent'`, then we will call `addParent()`
167
     */
168 1
    public static function buildRelationMutation(string $ownerClass, string $otherClass, ?string $otherName = null): array
169
    {
170 1
        $ownerReflect = new ReflectionClass($ownerClass);
171 1
        $ownerName = $ownerReflect->getShortName();
172 1
        $lowerOwnerName = lcfirst($ownerName);
173
174 1
        $otherReflect = new ReflectionClass($otherClass);
175 1
        $otherClassName = $otherReflect->getShortName();
176 1
        if ($otherName) {
177 1
            $semantic = ' as ' . $otherName;
178
        } else {
179 1
            $semantic = '';
180 1
            $otherName = $otherClassName;
181
        }
182 1
        $lowerOtherName = lcfirst($otherName);
183
184 1
        if ($lowerOwnerName === $lowerOtherName) {
185
            $lowerOwnerName .= 1;
186
            $lowerOtherName .= 2;
187
        }
188
189 1
        $args = [
190 1
            $lowerOwnerName => Type::nonNull(_types()->getId($ownerClass)),
191 1
            $lowerOtherName => Type::nonNull(_types()->getId($otherClass)),
192 1
        ];
193
194 1
        return [
195 1
            [
196 1
                'name' => 'link' . $ownerName . $otherName,
197 1
                'type' => Type::nonNull(_types()->getOutput($ownerClass)),
198 1
                'description' => 'Create a relation between ' . $ownerName . ' and ' . $otherClassName . $semantic . '.' . PHP_EOL . PHP_EOL
199 1
                    . 'If the relation already exists, it will have no effect.',
200 1
                'args' => $args,
201 1
                'resolve' => function ($root, array $args) use ($lowerOwnerName, $lowerOtherName, $otherName): AbstractModel {
202
                    $owner = $args[$lowerOwnerName]->getEntity();
203
                    $other = $args[$lowerOtherName]->getEntity();
204
205
                    // Check ACL
206
                    Helper::throwIfDenied($owner, 'update');
207
208
                    // Do it
209
                    $method = 'add' . $otherName;
210
                    $owner->$method($other);
211
                    _em()->flush();
212
213
                    return $owner;
214 1
                },
215 1
            ],
216 1
            [
217 1
                'name' => 'unlink' . $ownerName . $otherName,
218 1
                'type' => Type::nonNull(_types()->getOutput($ownerClass)),
219 1
                'description' => 'Delete a relation between ' . $ownerName . ' and ' . $otherClassName . $semantic . '.' . PHP_EOL . PHP_EOL
220 1
                    . 'If the relation does not exist, it will have no effect.',
221 1
                'args' => $args,
222 1
                'resolve' => function ($root, array $args) use ($lowerOwnerName, $lowerOtherName, $otherName): AbstractModel {
223
                    $owner = $args[$lowerOwnerName]->getEntity();
224
                    $other = $args[$lowerOtherName]->getEntity();
225
226
                    // Check ACL
227
                    Helper::throwIfDenied($owner, 'update');
228
229
                    // Do it
230
                    if ($other) {
231
                        $method = 'remove' . $otherName;
232
                        $owner->$method($other);
233
                        _em()->flush();
234
                    }
235
236
                    return $owner;
237 1
                },
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