• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

api-platform / core / 10739011304

06 Sep 2024 01:11PM UTC coverage: 7.159% (-0.5%) from 7.645%
10739011304

push

github

web-flow
 feat(laravel): eloquent filters (search, date, equals, or) (#6593)

* feat(laravel): Eloquent filters search date or

* feat(laravel): eloquent filters order range equals afterdate

* fix(laravel): order afterDate filters

* temp

* test(laravel): filter with eloquent

---------

Co-authored-by: Nathan <nathan@les-tilleuls.coop>

0 of 144 new or added lines in 16 files covered. (0.0%)

4792 existing lines in 155 files now uncovered.

11736 of 163930 relevant lines covered (7.16%)

22.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.93
/src/JsonSchema/SchemaFactory.php
1
<?php
2

3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <dunglas@gmail.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
declare(strict_types=1);
13

14
namespace ApiPlatform\JsonSchema;
15

16
use ApiPlatform\JsonSchema\Metadata\Property\Factory\SchemaPropertyMetadataFactory;
17
use ApiPlatform\Metadata\ApiProperty;
18
use ApiPlatform\Metadata\CollectionOperationInterface;
19
use ApiPlatform\Metadata\HttpOperation;
20
use ApiPlatform\Metadata\Operation;
21
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
22
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
23
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
24
use ApiPlatform\Metadata\ResourceClassResolverInterface;
25
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
26
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
27

28
/**
29
 * {@inheritdoc}
30
 *
31
 * @author Kévin Dunglas <dunglas@gmail.com>
32
 */
33
final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface
34
{
35
    use ResourceMetadataTrait;
36

37
    private ?SchemaFactoryInterface $schemaFactory = null;
38
    // Edge case where the related resource is not readable (for example: NotExposed) but we have groups to read the whole related object
39
    public const FORCE_SUBSCHEMA = '_api_subschema_force_readable_link';
40
    public const OPENAPI_DEFINITION_NAME = 'openapi_definition_name';
41

42
    public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ?NameConverterInterface $nameConverter = null, ?ResourceClassResolverInterface $resourceClassResolver = null, private readonly ?array $distinctFormats = null, private ?DefinitionNameFactoryInterface $definitionNameFactory = null)
43
    {
UNCOV
44
        if (!$definitionNameFactory) {
1,850✔
45
            $this->definitionNameFactory = new DefinitionNameFactory($this->distinctFormats);
×
46
        }
47

UNCOV
48
        $this->resourceMetadataFactory = $resourceMetadataFactory;
1,850✔
UNCOV
49
        $this->resourceClassResolver = $resourceClassResolver;
1,850✔
50
    }
51

52
    /**
53
     * {@inheritdoc}
54
     */
55
    public function buildSchema(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?Operation $operation = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
56
    {
UNCOV
57
        $schema = $schema ? clone $schema : new Schema();
36✔
58

UNCOV
59
        if (!$this->isResourceClass($className)) {
36✔
UNCOV
60
            $operation = null;
36✔
UNCOV
61
            $inputOrOutputClass = $className;
36✔
UNCOV
62
            $serializerContext ??= [];
36✔
63
        } else {
UNCOV
64
            $operation = $this->findOperation($className, $type, $operation, $serializerContext);
36✔
UNCOV
65
            $inputOrOutputClass = $this->findOutputClass($className, $type, $operation, $serializerContext);
36✔
UNCOV
66
            $serializerContext ??= $this->getSerializerContext($operation, $type);
36✔
67
        }
68

UNCOV
69
        if (null === $inputOrOutputClass) {
36✔
70
            // input or output disabled
UNCOV
71
            return $schema;
36✔
72
        }
73

UNCOV
74
        $validationGroups = $operation ? $this->getValidationGroups($operation) : [];
36✔
UNCOV
75
        $version = $schema->getVersion();
36✔
UNCOV
76
        $definitionName = $this->definitionNameFactory->create($className, $format, $inputOrOutputClass, $operation, $serializerContext);
36✔
77

UNCOV
78
        $method = $operation instanceof HttpOperation ? $operation->getMethod() : 'GET';
36✔
UNCOV
79
        if (!$operation) {
36✔
UNCOV
80
            $method = Schema::TYPE_INPUT === $type ? 'POST' : 'GET';
36✔
81
        }
82

83
        // In case of FORCE_SUBSCHEMA an object can be writable through another class even though it has no POST operation
UNCOV
84
        if (!($serializerContext[self::FORCE_SUBSCHEMA] ?? false) && Schema::TYPE_OUTPUT !== $type && !\in_array($method, ['POST', 'PATCH', 'PUT'], true)) {
36✔
85
            return $schema;
14✔
86
        }
87

UNCOV
88
        if (!isset($schema['$ref']) && !isset($schema['type'])) {
36✔
UNCOV
89
            $ref = Schema::VERSION_OPENAPI === $version ? '#/components/schemas/'.$definitionName : '#/definitions/'.$definitionName;
36✔
UNCOV
90
            if ($forceCollection || ('POST' !== $method && $operation instanceof CollectionOperationInterface)) {
36✔
UNCOV
91
                $schema['type'] = 'array';
36✔
UNCOV
92
                $schema['items'] = ['$ref' => $ref];
36✔
93
            } else {
UNCOV
94
                $schema['$ref'] = $ref;
36✔
95
            }
96
        }
97

UNCOV
98
        $definitions = $schema->getDefinitions();
36✔
UNCOV
99
        if (isset($definitions[$definitionName])) {
36✔
100
            // Already computed
UNCOV
101
            return $schema;
36✔
102
        }
103

104
        /** @var \ArrayObject<string, mixed> $definition */
UNCOV
105
        $definition = new \ArrayObject(['type' => 'object']);
36✔
UNCOV
106
        $definitions[$definitionName] = $definition;
36✔
UNCOV
107
        $definition['description'] = $operation ? ($operation->getDescription() ?? '') : '';
36✔
108

109
        // additionalProperties are allowed by default, so it does not need to be set explicitly, unless allow_extra_attributes is false
110
        // See https://json-schema.org/understanding-json-schema/reference/object.html#properties
UNCOV
111
        if (false === ($serializerContext[AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES] ?? true)) {
36✔
112
            $definition['additionalProperties'] = false;
×
113
        }
114

115
        // see https://github.com/json-schema-org/json-schema-spec/pull/737
UNCOV
116
        if (Schema::VERSION_SWAGGER !== $version && $operation && $operation->getDeprecationReason()) {
36✔
UNCOV
117
            $definition['deprecated'] = true;
36✔
118
        } else {
UNCOV
119
            $definition['deprecated'] = false;
36✔
120
        }
121

122
        // externalDocs is an OpenAPI specific extension, but JSON Schema allows additional keys, so we always add it
123
        // See https://json-schema.org/latest/json-schema-core.html#rfc.section.6.4
UNCOV
124
        if ($operation instanceof HttpOperation && ($operation->getTypes()[0] ?? null)) {
36✔
UNCOV
125
            $definition['externalDocs'] = ['url' => $operation->getTypes()[0]];
36✔
126
        }
127

UNCOV
128
        $options = ['schema_type' => $type] + $this->getFactoryOptions($serializerContext, $validationGroups, $operation instanceof HttpOperation ? $operation : null);
36✔
UNCOV
129
        foreach ($this->propertyNameCollectionFactory->create($inputOrOutputClass, $options) as $propertyName) {
36✔
UNCOV
130
            $propertyMetadata = $this->propertyMetadataFactory->create($inputOrOutputClass, $propertyName, $options);
36✔
UNCOV
131
            if (!$propertyMetadata->isReadable() && !$propertyMetadata->isWritable()) {
36✔
UNCOV
132
                continue;
36✔
133
            }
134

UNCOV
135
            $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName, $inputOrOutputClass, $format, $serializerContext) : $propertyName;
36✔
UNCOV
136
            if ($propertyMetadata->isRequired()) {
36✔
UNCOV
137
                $definition['required'][] = $normalizedPropertyName;
36✔
138
            }
139

UNCOV
140
            $this->buildPropertySchema($schema, $definitionName, $normalizedPropertyName, $propertyMetadata, $serializerContext, $format, $type);
36✔
141
        }
142

UNCOV
143
        return $schema;
36✔
144
    }
145

146
    private function buildPropertySchema(Schema $schema, string $definitionName, string $normalizedPropertyName, ApiProperty $propertyMetadata, array $serializerContext, string $format, string $parentType): void
147
    {
UNCOV
148
        $version = $schema->getVersion();
36✔
UNCOV
149
        if (Schema::VERSION_SWAGGER === $version || Schema::VERSION_OPENAPI === $version) {
36✔
UNCOV
150
            $additionalPropertySchema = $propertyMetadata->getOpenapiContext();
36✔
151
        } else {
152
            $additionalPropertySchema = $propertyMetadata->getJsonSchemaContext();
×
153
        }
154

UNCOV
155
        $propertySchema = array_merge(
36✔
UNCOV
156
            $propertyMetadata->getSchema() ?? [],
36✔
UNCOV
157
            $additionalPropertySchema ?? []
36✔
UNCOV
158
        );
36✔
159

160
        // @see https://github.com/api-platform/core/issues/6299
UNCOV
161
        if (Schema::UNKNOWN_TYPE === ($propertySchema['type'] ?? null) && isset($propertySchema['$ref'])) {
36✔
162
            unset($propertySchema['type']);
×
163
        }
164

UNCOV
165
        $extraProperties = $propertyMetadata->getExtraProperties() ?? [];
36✔
166
        // see AttributePropertyMetadataFactory
UNCOV
167
        if (true === ($extraProperties[SchemaPropertyMetadataFactory::JSON_SCHEMA_USER_DEFINED] ?? false)) {
36✔
168
            // schema seems to have been declared by the user: do not override nor complete user value
169
            $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema);
14✔
170

171
            return;
14✔
172
        }
173

UNCOV
174
        $types = $propertyMetadata->getBuiltinTypes() ?? [];
36✔
175

176
        // never override the following keys if at least one is already set
177
        // or if property has no type(s) defined
178
        // or if property schema is already fully defined (type=string + format || enum)
UNCOV
179
        $propertySchemaType = $propertySchema['type'] ?? false;
36✔
180

UNCOV
181
        $isUnknown = Schema::UNKNOWN_TYPE === $propertySchemaType
36✔
UNCOV
182
            || ('array' === $propertySchemaType && Schema::UNKNOWN_TYPE === ($propertySchema['items']['type'] ?? null));
36✔
183

184
        if (
UNCOV
185
            !$isUnknown && (
36✔
UNCOV
186
                [] === $types
36✔
UNCOV
187
                || ($propertySchema['$ref'] ?? $propertySchema['anyOf'] ?? $propertySchema['allOf'] ?? $propertySchema['oneOf'] ?? false)
36✔
UNCOV
188
                || (\is_array($propertySchemaType) ? \array_key_exists('string', $propertySchemaType) : 'string' !== $propertySchemaType)
36✔
UNCOV
189
                || ($propertySchema['format'] ?? $propertySchema['enum'] ?? false)
36✔
190
            )
191
        ) {
UNCOV
192
            $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema);
36✔
193

UNCOV
194
            return;
36✔
195
        }
196

197
        // property schema is created in SchemaPropertyMetadataFactory, but it cannot build resource reference ($ref)
198
        // complete property schema with resource reference ($ref) only if it's related to an object
UNCOV
199
        $version = $schema->getVersion();
36✔
UNCOV
200
        $refs = [];
36✔
UNCOV
201
        $isNullable = null;
36✔
202

UNCOV
203
        foreach ($types as $type) {
36✔
UNCOV
204
            $subSchema = new Schema($version);
36✔
UNCOV
205
            $subSchema->setDefinitions($schema->getDefinitions()); // Populate definitions of the main schema
36✔
206

UNCOV
207
            $isCollection = $type->isCollection();
36✔
UNCOV
208
            if ($isCollection) {
36✔
UNCOV
209
                $valueType = $type->getCollectionValueTypes()[0] ?? null;
36✔
210
            } else {
UNCOV
211
                $valueType = $type;
36✔
212
            }
213

UNCOV
214
            $className = $valueType?->getClassName();
36✔
UNCOV
215
            if (null === $className) {
36✔
UNCOV
216
                continue;
36✔
217
            }
218

UNCOV
219
            $subSchemaFactory = $this->schemaFactory ?: $this;
36✔
UNCOV
220
            $subSchema = $subSchemaFactory->buildSchema($className, $format, $parentType, null, $subSchema, $serializerContext + [self::FORCE_SUBSCHEMA => true], false);
36✔
UNCOV
221
            if (!isset($subSchema['$ref'])) {
36✔
UNCOV
222
                continue;
36✔
223
            }
224

UNCOV
225
            if ($isCollection) {
36✔
UNCOV
226
                $propertySchema['items']['$ref'] = $subSchema['$ref'];
36✔
UNCOV
227
                unset($propertySchema['items']['type']);
36✔
UNCOV
228
                break;
36✔
229
            }
230

UNCOV
231
            $refs[] = ['$ref' => $subSchema['$ref']];
36✔
UNCOV
232
            $isNullable = $isNullable ?? $type->isNullable();
36✔
233
        }
234

UNCOV
235
        if ($isNullable) {
36✔
UNCOV
236
            $refs[] = ['type' => 'null'];
36✔
237
        }
238

UNCOV
239
        if (($c = \count($refs)) > 1) {
36✔
UNCOV
240
            $propertySchema['anyOf'] = $refs;
36✔
UNCOV
241
            unset($propertySchema['type']);
36✔
UNCOV
242
        } elseif (1 === $c) {
36✔
UNCOV
243
            $propertySchema['$ref'] = $refs[0]['$ref'];
36✔
UNCOV
244
            unset($propertySchema['type']);
36✔
245
        }
246

UNCOV
247
        $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema);
36✔
248
    }
249

250
    private function getValidationGroups(Operation $operation): array
251
    {
UNCOV
252
        $groups = $operation->getValidationContext()['groups'] ?? [];
36✔
253

UNCOV
254
        return \is_array($groups) ? $groups : [$groups];
36✔
255
    }
256

257
    /**
258
     * Gets the options for the property name collection / property metadata factories.
259
     */
260
    private function getFactoryOptions(array $serializerContext, array $validationGroups, ?HttpOperation $operation = null): array
261
    {
UNCOV
262
        $options = [
36✔
263
            /* @see https://github.com/symfony/symfony/blob/v5.1.0/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php */
UNCOV
264
            'enable_getter_setter_extraction' => true,
36✔
UNCOV
265
        ];
36✔
266

UNCOV
267
        if (isset($serializerContext[AbstractNormalizer::GROUPS])) {
36✔
268
            /* @see https://github.com/symfony/symfony/blob/v4.2.6/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php */
UNCOV
269
            $options['serializer_groups'] = (array) $serializerContext[AbstractNormalizer::GROUPS];
36✔
270
        }
271

UNCOV
272
        if ($operation && ($normalizationGroups = $operation->getNormalizationContext()['groups'] ?? null)) {
36✔
UNCOV
273
            $options['normalization_groups'] = $normalizationGroups;
36✔
274
        }
275

UNCOV
276
        if ($operation && ($denormalizationGroups = $operation->getDenormalizationContext()['groups'] ?? null)) {
36✔
UNCOV
277
            $options['denormalization_groups'] = $denormalizationGroups;
36✔
278
        }
279

UNCOV
280
        if ($validationGroups) {
36✔
281
            $options['validation_groups'] = $validationGroups;
×
282
        }
283

UNCOV
284
        return $options;
36✔
285
    }
286

287
    public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
288
    {
UNCOV
289
        $this->schemaFactory = $schemaFactory;
1,850✔
290
    }
291
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2024 Coveralls, Inc