Passed
Pull Request — main (#93)
by Tom
02:43
created

src/Criteria/CriteriaFactory.php (1 issue)

Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ApiSkeletons\Doctrine\GraphQL\Criteria;
6
7
use ApiSkeletons\Doctrine\GraphQL\Config;
8
use ApiSkeletons\Doctrine\GraphQL\Criteria\Type\FiltersInputType;
9
use ApiSkeletons\Doctrine\GraphQL\Type\Entity;
10
use ApiSkeletons\Doctrine\GraphQL\Type\TypeManager;
11
use Doctrine\ORM\EntityManager;
12
use Doctrine\ORM\Mapping\ClassMetadataInfo;
13
use GraphQL\Type\Definition\InputObjectType;
14
use GraphQL\Type\Definition\Type;
15
use League\Event\EventDispatcher;
16
17
use function array_filter;
18
use function array_keys;
19
use function assert;
20
use function in_array;
21
22
class CriteriaFactory
23
{
24
    public function __construct(
25
        protected Config $config,
26
        protected EntityManager $entityManager,
27
        protected TypeManager $typeManager,
28
        protected EventDispatcher $eventDispatcher,
29
    ) {
30
    }
31
32
    /** @param mixed[]|null $associationMetadata */
33
    public function get(
34
        Entity $targetEntity,
35
        Entity|null $owningEntity = null,
36
        string|null $associationName = null,
37
        array|null $associationMetadata = null,
38
    ): InputObjectType {
39
        if ($owningEntity) {
40
            $typeName = $owningEntity->getTypeName() . '_' . $associationName . '_filter';
41
        } else {
42
            $typeName = $targetEntity->getTypeName() . '_filter';
43
        }
44
45
        if ($this->typeManager->has($typeName)) {
46
            return $this->typeManager->get($typeName);
47
        }
48
49
        $fields         = [];
50
        $classMetadata  = $this->entityManager->getClassMetadata($targetEntity->getEntityClass());
51
        $entityMetadata = $targetEntity->getMetadataConfig();
52
53
        $allowedFilters = Filters::toArray();
54
55
        // Limit entity filters
56
        if ($entityMetadata['excludeCriteria']) {
57
            $excludeCriteria = $entityMetadata['excludeCriteria'];
58
            $allowedFilters  = array_filter($allowedFilters, static function ($value) use ($excludeCriteria) {
59
                return ! in_array($value, $excludeCriteria);
60
            });
61
        }
62
63
        // Limit association filters
64
        if ($associationName) {
65
            $excludeCriteria = $associationMetadata['excludeCriteria'];
66
            $allowedFilters  = array_filter($allowedFilters, static function ($value) use ($excludeCriteria) {
67
                return ! in_array($value, $excludeCriteria);
68
            });
69
        }
70
71
        foreach ($classMetadata->getFieldNames() as $fieldName) {
72
            $graphQLType = null;
73
74
            // Only process fields which are in the graphql metadata
75
            if (! in_array($fieldName, array_keys($entityMetadata['fields']))) {
76
                continue;
77
            }
78
79
            /** @psalm-suppress UndefinedDocblockClass */
80
            $fieldMetadata = $classMetadata->getFieldMapping($fieldName);
81
82
            $graphQLType = $this->typeManager
83
                ->get($entityMetadata['fields'][$fieldName]['type']);
84
85
            if ($graphQLType && $classMetadata->isIdentifier($fieldName)) {
86
                $graphQLType = Type::id();
87
            }
88
89
            assert($graphQLType, 'GraphQL type not found for ' . $fieldMetadata['type']);
90
91
            $fields[$fieldName] = [
92
                'name' => $fieldName,
93
                'type' => new FiltersInputType($typeName, $fieldName, $graphQLType, $allowedFilters),
94
                'description' => 'Filters for ' . $fieldName,
95
            ];
96
97
            if (in_array(Filters::SORT, $allowedFilters)) {
0 ignored issues
show
Use early exit to reduce code nesting.
Loading history...
98
                $fields[$fieldName . '_sort'] = [
99
                    'name' => $fieldName . '_sort',
100
                    'type' => Type::string(),
101
                    'description' => 'Sort the result either ASC or DESC',
102
                ];
103
            }
104
        }
105
106
        // Add eq filter for to-one associations
107
        foreach ($classMetadata->getAssociationNames() as $associationName) {
108
            // Only process fields which are in the graphql metadata
109
            if (! in_array($associationName, array_keys($entityMetadata['fields']))) {
110
                continue;
111
            }
112
113
            /** @psalm-suppress UndefinedDocblockClass */
114
            $associationMetadata = $classMetadata->getAssociationMapping($associationName);
115
            $graphQLType         = Type::id();
116
            switch ($associationMetadata['type']) {
117
                case ClassMetadataInfo::ONE_TO_ONE:
118
                case ClassMetadataInfo::MANY_TO_ONE:
119
                case ClassMetadataInfo::TO_ONE:
120
                    // eq filter is for association:value
121
                    if (in_array(Filters::EQ, $allowedFilters)) {
122
                        $fields[$associationName] = [
123
                            'name' => $associationName,
124
                            'type' => new FiltersInputType($typeName, $associationName, $graphQLType, ['eq']),
125
                            'description' => 'Filters for ' . $associationName,
126
                        ];
127
                    }
128
            }
129
        }
130
131
        /** @psalm-suppress InvalidArgument */
132
        $inputObject = new InputObjectType([
133
            'name' => $typeName,
134
            'fields' => static function () use ($fields) {
135
                return $fields;
136
            },
137
        ]);
138
139
        $this->typeManager->set($typeName, $inputObject);
140
141
        return $inputObject;
142
    }
143
}
144