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

api-platform / core / 19799301771

30 Nov 2025 01:04PM UTC coverage: 25.229% (-0.03%) from 25.257%
19799301771

push

github

web-flow
fix(metadata): repeatable attribute mutators (#7542)

14557 of 57700 relevant lines covered (25.23%)

28.11 hits per line

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

99.4
/src/Symfony/Bundle/DependencyInjection/Configuration.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\Symfony\Bundle\DependencyInjection;
15

16
use ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface;
17
use ApiPlatform\Metadata\ApiResource;
18
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
19
use ApiPlatform\Metadata\Post;
20
use ApiPlatform\Metadata\Put;
21
use ApiPlatform\Symfony\Controller\MainController;
22
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
23
use Doctrine\Bundle\MongoDBBundle\DoctrineMongoDBBundle;
24
use Doctrine\ORM\EntityManagerInterface;
25
use Doctrine\ORM\OptimisticLockException;
26
use GraphQL\GraphQL;
27
use Symfony\Bundle\FrameworkBundle\Controller\ControllerHelper;
28
use Symfony\Bundle\FullStack;
29
use Symfony\Bundle\MakerBundle\MakerBundle;
30
use Symfony\Bundle\MercureBundle\MercureBundle;
31
use Symfony\Bundle\TwigBundle\TwigBundle;
32
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
33
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
34
use Symfony\Component\Config\Definition\ConfigurationInterface;
35
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
36
use Symfony\Component\HttpFoundation\Response;
37
use Symfony\Component\JsonStreamer\JsonStreamWriter;
38
use Symfony\Component\Messenger\MessageBusInterface;
39
use Symfony\Component\Serializer\Exception\ExceptionInterface as SerializerExceptionInterface;
40
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
41
use Symfony\Component\Yaml\Yaml;
42

43
/**
44
 * The configuration of the bundle.
45
 *
46
 * @author Kévin Dunglas <dunglas@gmail.com>
47
 * @author Baptiste Meyer <baptiste.meyer@gmail.com>
48
 */
49
final class Configuration implements ConfigurationInterface
50
{
51
    /**
52
     * {@inheritdoc}
53
     */
54
    public function getConfigTreeBuilder(): TreeBuilder
55
    {
56
        $treeBuilder = new TreeBuilder('api_platform');
48✔
57
        $rootNode = $treeBuilder->getRootNode();
48✔
58

59
        $rootNode
48✔
60
            ->beforeNormalization()
48✔
61
                ->ifTrue(static function ($v) {
48✔
62
                    return false === ($v['enable_swagger'] ?? null);
48✔
63
                })
48✔
64
                ->then(static function ($v) {
48✔
65
                    $v['swagger']['versions'] = [];
2✔
66

67
                    return $v;
2✔
68
                })
48✔
69
            ->end()
48✔
70
            ->children()
48✔
71
                ->scalarNode('title')
48✔
72
                    ->info('The title of the API.')
48✔
73
                    ->cannotBeEmpty()
48✔
74
                    ->defaultValue('')
48✔
75
                ->end()
48✔
76
                ->scalarNode('description')
48✔
77
                    ->info('The description of the API.')
48✔
78
                    ->cannotBeEmpty()
48✔
79
                    ->defaultValue('')
48✔
80
                ->end()
48✔
81
                ->scalarNode('version')
48✔
82
                    ->info('The version of the API.')
48✔
83
                    ->cannotBeEmpty()
48✔
84
                    ->defaultValue('0.0.0')
48✔
85
                ->end()
48✔
86
                ->booleanNode('show_webby')->defaultTrue()->info('If true, show Webby on the documentation page')->end()
48✔
87
                ->booleanNode('use_symfony_listeners')->defaultFalse()->info(sprintf('Uses Symfony event listeners instead of the %s.', MainController::class))->end()
48✔
88
                ->scalarNode('name_converter')->defaultNull()->info('Specify a name converter to use.')->end()
48✔
89
                ->scalarNode('asset_package')->defaultNull()->info('Specify an asset package name to use.')->end()
48✔
90
                ->scalarNode('path_segment_name_generator')->defaultValue('api_platform.metadata.path_segment_name_generator.underscore')->info('Specify a path name generator to use.')->end()
48✔
91
                ->scalarNode('inflector')->defaultValue('api_platform.metadata.inflector')->info('Specify an inflector to use.')->end()
48✔
92
                ->arrayNode('validator')
48✔
93
                    ->addDefaultsIfNotSet()
48✔
94
                    ->children()
48✔
95
                        ->variableNode('serialize_payload_fields')->defaultValue([])->info('Set to null to serialize all payload fields when a validation error is thrown, or set the fields you want to include explicitly.')->end()
48✔
96
                        ->booleanNode('query_parameter_validation')
48✔
97
                            ->defaultValue(true)
48✔
98
                            ->setDeprecated('api-platform/symfony', '4.2', 'Will be removed in API Platform 5.0.')
48✔
99
                        ->end()
48✔
100
                    ->end()
48✔
101
                ->end()
48✔
102
                ->arrayNode('eager_loading')
48✔
103
                    ->canBeDisabled()
48✔
104
                    ->addDefaultsIfNotSet()
48✔
105
                    ->children()
48✔
106
                        ->booleanNode('fetch_partial')->defaultFalse()->info('Fetch only partial data according to serialization groups. If enabled, Doctrine ORM entities will not work as expected if any of the other fields are used.')->end()
48✔
107
                        ->integerNode('max_joins')->defaultValue(30)->info('Max number of joined relations before EagerLoading throws a RuntimeException')->end()
48✔
108
                        ->booleanNode('force_eager')->defaultTrue()->info('Force join on every relation. If disabled, it will only join relations having the EAGER fetch mode.')->end()
48✔
109
                    ->end()
48✔
110
                ->end()
48✔
111
                ->booleanNode('handle_symfony_errors')->defaultFalse()->info('Allows to handle symfony exceptions.')->end()
48✔
112
                ->booleanNode('enable_swagger')->defaultTrue()->info('Enable the Swagger documentation and export.')->end()
48✔
113
                ->booleanNode('enable_json_streamer')->defaultValue(class_exists(ControllerHelper::class) && class_exists(JsonStreamWriter::class))->info('Enable json streamer.')->end()
48✔
114
                ->booleanNode('enable_swagger_ui')->defaultValue(class_exists(TwigBundle::class))->info('Enable Swagger UI')->end()
48✔
115
                ->booleanNode('enable_re_doc')->defaultValue(class_exists(TwigBundle::class))->info('Enable ReDoc')->end()
48✔
116
                ->booleanNode('enable_entrypoint')->defaultTrue()->info('Enable the entrypoint')->end()
48✔
117
                ->booleanNode('enable_docs')->defaultTrue()->info('Enable the docs')->end()
48✔
118
                ->booleanNode('enable_profiler')->defaultTrue()->info('Enable the data collector and the WebProfilerBundle integration.')->end()
48✔
119
                ->booleanNode('enable_phpdoc_parser')->defaultTrue()->info('Enable resource metadata collector using PHPStan PhpDocParser.')->end()
48✔
120
                ->booleanNode('enable_link_security')->defaultFalse()->info('Enable security for Links (sub resources)')->end()
48✔
121
                ->arrayNode('collection')
48✔
122
                    ->addDefaultsIfNotSet()
48✔
123
                    ->children()
48✔
124
                        ->scalarNode('exists_parameter_name')->defaultValue('exists')->cannotBeEmpty()->info('The name of the query parameter to filter on nullable field values.')->end()
48✔
125
                        ->scalarNode('order')->defaultValue('ASC')->info('The default order of results.')->end() // Default ORDER is required for postgresql and mysql >= 5.7 when using LIMIT/OFFSET request
48✔
126
                        ->scalarNode('order_parameter_name')->defaultValue('order')->cannotBeEmpty()->info('The name of the query parameter to order results.')->end()
48✔
127
                        ->enumNode('order_nulls_comparison')->defaultNull()->values(interface_exists(OrderFilterInterface::class) ? array_merge(array_keys(OrderFilterInterface::NULLS_DIRECTION_MAP), [null]) : [null])->info('The nulls comparison strategy.')->end()
48✔
128
                        ->arrayNode('pagination')
48✔
129
                            ->canBeDisabled()
48✔
130
                            ->addDefaultsIfNotSet()
48✔
131
                            ->children()
48✔
132
                                ->scalarNode('page_parameter_name')->defaultValue('page')->cannotBeEmpty()->info('The default name of the parameter handling the page number.')->end()
48✔
133
                                ->scalarNode('enabled_parameter_name')->defaultValue('pagination')->cannotBeEmpty()->info('The name of the query parameter to enable or disable pagination.')->end()
48✔
134
                                ->scalarNode('items_per_page_parameter_name')->defaultValue('itemsPerPage')->cannotBeEmpty()->info('The name of the query parameter to set the number of items per page.')->end()
48✔
135
                                ->scalarNode('partial_parameter_name')->defaultValue('partial')->cannotBeEmpty()->info('The name of the query parameter to enable or disable partial pagination.')->end()
48✔
136
                            ->end()
48✔
137
                        ->end()
48✔
138
                    ->end()
48✔
139
                ->end()
48✔
140
                ->arrayNode('mapping')
48✔
141
                    ->addDefaultsIfNotSet()
48✔
142
                    ->children()
48✔
143
                        ->arrayNode('imports')
48✔
144
                            ->prototype('scalar')->end()
48✔
145
                        ->end()
48✔
146
                        ->arrayNode('paths')
48✔
147
                            ->prototype('scalar')->end()
48✔
148
                        ->end()
48✔
149
                    ->end()
48✔
150
                ->end()
48✔
151
                ->arrayNode('resource_class_directories')
48✔
152
                    ->prototype('scalar')->end()
48✔
153
                    ->setDeprecated('api-platform/symfony', '4.1', 'The "resource_class_directories" configuration is deprecated, classes using #[ApiResource] attribute are autoconfigured by the dependency injection container.')
48✔
154
                ->end()
48✔
155
                ->arrayNode('serializer')
48✔
156
                    ->addDefaultsIfNotSet()
48✔
157
                    ->children()
48✔
158
                        ->booleanNode('hydra_prefix')->defaultFalse()->info('Use the "hydra:" prefix.')->end()
48✔
159
                    ->end()
48✔
160
                ->end()
48✔
161
            ->end();
48✔
162

163
        $this->addDoctrineOrmSection($rootNode);
48✔
164
        $this->addDoctrineMongoDbOdmSection($rootNode);
48✔
165
        $this->addOAuthSection($rootNode);
48✔
166
        $this->addGraphQlSection($rootNode);
48✔
167
        $this->addSwaggerSection($rootNode);
48✔
168
        $this->addHttpCacheSection($rootNode);
48✔
169
        $this->addMercureSection($rootNode);
48✔
170
        $this->addMessengerSection($rootNode);
48✔
171
        $this->addElasticsearchSection($rootNode);
48✔
172
        $this->addOpenApiSection($rootNode);
48✔
173
        $this->addMakerSection($rootNode);
48✔
174

175
        $this->addExceptionToStatusSection($rootNode);
48✔
176

177
        $this->addFormatSection($rootNode, 'formats', [
48✔
178
            'jsonld' => ['mime_types' => ['application/ld+json']],
48✔
179
        ]);
48✔
180
        $this->addFormatSection($rootNode, 'patch_formats', [
48✔
181
            'json' => ['mime_types' => ['application/merge-patch+json']],
48✔
182
        ]);
48✔
183

184
        $defaultDocFormats = [
48✔
185
            'jsonld' => ['mime_types' => ['application/ld+json']],
48✔
186
            'jsonopenapi' => ['mime_types' => ['application/vnd.openapi+json']],
48✔
187
            'html' => ['mime_types' => ['text/html']],
48✔
188
        ];
48✔
189

190
        if (class_exists(Yaml::class)) {
48✔
191
            $defaultDocFormats['yamlopenapi'] = ['mime_types' => ['application/vnd.openapi+yaml']];
48✔
192
        }
193

194
        $this->addFormatSection($rootNode, 'docs_formats', $defaultDocFormats);
48✔
195

196
        $this->addFormatSection($rootNode, 'error_formats', [
48✔
197
            'jsonld' => ['mime_types' => ['application/ld+json']],
48✔
198
            'jsonproblem' => ['mime_types' => ['application/problem+json']],
48✔
199
            'json' => ['mime_types' => ['application/problem+json', 'application/json']],
48✔
200
        ]);
48✔
201
        $rootNode
48✔
202
            ->children()
48✔
203
                ->arrayNode('jsonschema_formats')
48✔
204
                    ->scalarPrototype()->end()
48✔
205
                    ->defaultValue([])
48✔
206
                    ->info('The JSON formats to compute the JSON Schemas for.')
48✔
207
                ->end()
48✔
208
            ->end();
48✔
209

210
        $this->addDefaultsSection($rootNode);
48✔
211

212
        return $treeBuilder;
48✔
213
    }
214

215
    private function addDoctrineOrmSection(ArrayNodeDefinition $rootNode): void
216
    {
217
        $rootNode
48✔
218
            ->children()
48✔
219
                ->arrayNode('doctrine')
48✔
220
                    ->{class_exists(DoctrineBundle::class) && interface_exists(EntityManagerInterface::class) ? 'canBeDisabled' : 'canBeEnabled'}()
48✔
221
                ->end()
48✔
222
            ->end();
48✔
223
    }
224

225
    private function addDoctrineMongoDbOdmSection(ArrayNodeDefinition $rootNode): void
226
    {
227
        $rootNode
48✔
228
            ->children()
48✔
229
                ->arrayNode('doctrine_mongodb_odm')
48✔
230
                    ->{class_exists(DoctrineMongoDBBundle::class) ? 'canBeDisabled' : 'canBeEnabled'}()
48✔
231
                ->end()
48✔
232
            ->end();
48✔
233
    }
234

235
    private function addOAuthSection(ArrayNodeDefinition $rootNode): void
236
    {
237
        $rootNode
48✔
238
            ->children()
48✔
239
                ->arrayNode('oauth')
48✔
240
                    ->canBeEnabled()
48✔
241
                    ->addDefaultsIfNotSet()
48✔
242
                    ->children()
48✔
243
                        ->scalarNode('clientId')->defaultValue('')->info('The oauth client id.')->end()
48✔
244
                        ->scalarNode('clientSecret')
48✔
245
                            ->defaultValue('')
48✔
246
                            ->info('The OAuth client secret. Never use this parameter in your production environment. It exposes crucial security information. This feature is intended for dev/test environments only. Enable "oauth.pkce" instead')
48✔
247
                        ->end()
48✔
248
                        ->booleanNode('pkce')->defaultFalse()->info('Enable the oauth PKCE.')->end()
48✔
249
                        ->scalarNode('type')->defaultValue('oauth2')->info('The oauth type.')->end()
48✔
250
                        ->scalarNode('flow')->defaultValue('application')->info('The oauth flow grant type.')->end()
48✔
251
                        ->scalarNode('tokenUrl')->defaultValue('')->info('The oauth token url.')->end()
48✔
252
                        ->scalarNode('authorizationUrl')->defaultValue('')->info('The oauth authentication url.')->end()
48✔
253
                        ->scalarNode('refreshUrl')->defaultValue('')->info('The oauth refresh url.')->end()
48✔
254
                        ->arrayNode('scopes')
48✔
255
                            ->prototype('scalar')->end()
48✔
256
                        ->end()
48✔
257
                    ->end()
48✔
258
                ->end()
48✔
259
            ->end();
48✔
260
    }
261

262
    private function addGraphQlSection(ArrayNodeDefinition $rootNode): void
263
    {
264
        $rootNode
48✔
265
            ->children()
48✔
266
                ->arrayNode('graphql')
48✔
267
                    ->{class_exists(GraphQL::class) ? 'canBeDisabled' : 'canBeEnabled'}()
48✔
268
                    ->addDefaultsIfNotSet()
48✔
269
                    ->children()
48✔
270
                        ->scalarNode('default_ide')->defaultValue('graphiql')->end()
48✔
271
                        ->arrayNode('graphiql')
48✔
272
                            ->{class_exists(GraphQL::class) && class_exists(TwigBundle::class) ? 'canBeDisabled' : 'canBeEnabled'}()
48✔
273
                        ->end()
48✔
274
                        ->arrayNode('introspection')
48✔
275
                            ->canBeDisabled()
48✔
276
                        ->end()
48✔
277
                        ->integerNode('max_query_depth')->defaultValue(20)
48✔
278
                        ->end()
48✔
279
                        ->arrayNode('graphql_playground')
48✔
280
                            ->setDeprecated('api-platform/core', '4.0')
48✔
281
                        ->end()
48✔
282
                        ->integerNode('max_query_complexity')->defaultValue(500)
48✔
283
                        ->end()
48✔
284
                        ->scalarNode('nesting_separator')->defaultValue('_')->info('The separator to use to filter nested fields.')->end()
48✔
285
                        ->arrayNode('collection')
48✔
286
                            ->addDefaultsIfNotSet()
48✔
287
                            ->children()
48✔
288
                                ->arrayNode('pagination')
48✔
289
                                    ->canBeDisabled()
48✔
290
                                ->end()
48✔
291
                            ->end()
48✔
292
                        ->end()
48✔
293
                    ->end()
48✔
294
                ->end()
48✔
295
            ->end();
48✔
296
    }
297

298
    private function addSwaggerSection(ArrayNodeDefinition $rootNode): void
299
    {
300
        $supportedVersions = [3];
48✔
301

302
        $rootNode
48✔
303
            ->children()
48✔
304
                ->arrayNode('swagger')
48✔
305
                    ->addDefaultsIfNotSet()
48✔
306
                    ->children()
48✔
307
                        ->booleanNode('persist_authorization')->defaultValue(false)->info('Persist the SwaggerUI Authorization in the localStorage.')->end()
48✔
308
                        ->arrayNode('versions')
48✔
309
                            ->info('The active versions of OpenAPI to be exported or used in Swagger UI. The first value is the default.')
48✔
310
                            ->defaultValue($supportedVersions)
48✔
311
                            ->beforeNormalization()
48✔
312
                                ->always(static function ($v): array {
48✔
313
                                    if (!\is_array($v)) {
4✔
314
                                        $v = [$v];
×
315
                                    }
316

317
                                    foreach ($v as &$version) {
4✔
318
                                        $version = (int) $version;
2✔
319
                                    }
320

321
                                    return $v;
4✔
322
                                })
48✔
323
                            ->end()
48✔
324
                            ->validate()
48✔
325
                                ->ifTrue(static fn ($v): bool => $v !== array_intersect($v, $supportedVersions))
48✔
326
                                ->thenInvalid(sprintf('Only the versions %s are supported. Got %s.', implode(' and ', $supportedVersions), '%s'))
48✔
327
                            ->end()
48✔
328
                            ->prototype('scalar')->end()
48✔
329
                        ->end()
48✔
330
                        ->arrayNode('api_keys')
48✔
331
                            ->useAttributeAsKey('key')
48✔
332
                            ->validate()
48✔
333
                                ->ifTrue(static fn ($v): bool => (bool) array_filter(array_keys($v), fn ($item) => !preg_match('/^[a-zA-Z0-9._-]+$/', $item)))
48✔
334
                                ->thenInvalid('The api keys "key" is not valid according to the pattern enforced by OpenAPI 3.1 ^[a-zA-Z0-9._-]+$.')
48✔
335
                            ->end()
48✔
336
                            ->prototype('array')
48✔
337
                                ->children()
48✔
338
                                    ->scalarNode('name')
48✔
339
                                        ->info('The name of the header or query parameter containing the api key.')
48✔
340
                                    ->end()
48✔
341
                                    ->enumNode('type')
48✔
342
                                        ->info('Whether the api key should be a query parameter or a header.')
48✔
343
                                        ->values(['query', 'header'])
48✔
344
                                    ->end()
48✔
345
                                ->end()
48✔
346
                            ->end()
48✔
347
                        ->end()
48✔
348
                        ->arrayNode('http_auth')
48✔
349
                            ->info('Creates http security schemes for OpenAPI.')
48✔
350
                            ->useAttributeAsKey('key')
48✔
351
                            ->validate()
48✔
352
                                ->ifTrue(static fn ($v): bool => (bool) array_filter(array_keys($v), fn ($item) => !preg_match('/^[a-zA-Z0-9._-]+$/', $item)))
48✔
353
                                ->thenInvalid('The api keys "key" is not valid according to the pattern enforced by OpenAPI 3.1 ^[a-zA-Z0-9._-]+$.')
48✔
354
                            ->end()
48✔
355
                            ->prototype('array')
48✔
356
                                ->children()
48✔
357
                                    ->scalarNode('scheme')
48✔
358
                                        ->info('The OpenAPI HTTP auth scheme, for example "bearer"')
48✔
359
                                    ->end()
48✔
360
                                    ->scalarNode('bearerFormat')
48✔
361
                                        ->info('The OpenAPI HTTP bearer format')
48✔
362
                                    ->end()
48✔
363
                                ->end()
48✔
364
                            ->end()
48✔
365
                        ->end()
48✔
366
                        ->variableNode('swagger_ui_extra_configuration')
48✔
367
                            ->defaultValue([])
48✔
368
                            ->validate()
48✔
369
                                ->ifTrue(static fn ($v): bool => false === \is_array($v))
48✔
370
                                ->thenInvalid('The swagger_ui_extra_configuration parameter must be an array.')
48✔
371
                            ->end()
48✔
372
                            ->info('To pass extra configuration to Swagger UI, like docExpansion or filter.')
48✔
373
                        ->end()
48✔
374
                    ->end()
48✔
375
                ->end()
48✔
376
            ->end();
48✔
377
    }
378

379
    private function addHttpCacheSection(ArrayNodeDefinition $rootNode): void
380
    {
381
        $rootNode
48✔
382
            ->children()
48✔
383
                ->arrayNode('http_cache')
48✔
384
                    ->addDefaultsIfNotSet()
48✔
385
                    ->children()
48✔
386
                        ->booleanNode('public')->defaultNull()->info('To make all responses public by default.')->end()
48✔
387
                        ->arrayNode('invalidation')
48✔
388
                            ->info('Enable the tags-based cache invalidation system.')
48✔
389
                            ->canBeEnabled()
48✔
390
                            ->children()
48✔
391
                                ->arrayNode('varnish_urls')
48✔
392
                                    ->setDeprecated('api-platform/core', '3.0', 'The "varnish_urls" configuration is deprecated, use "urls" or "scoped_clients".')
48✔
393
                                    ->defaultValue([])
48✔
394
                                    ->prototype('scalar')->end()
48✔
395
                                    ->info('URLs of the Varnish servers to purge using cache tags when a resource is updated.')
48✔
396
                                ->end()
48✔
397
                                ->arrayNode('urls')
48✔
398
                                    ->defaultValue([])
48✔
399
                                    ->prototype('scalar')->end()
48✔
400
                                    ->info('URLs of the Varnish servers to purge using cache tags when a resource is updated.')
48✔
401
                                ->end()
48✔
402
                                ->arrayNode('scoped_clients')
48✔
403
                                    ->defaultValue([])
48✔
404
                                    ->prototype('scalar')->end()
48✔
405
                                    ->info('Service names of scoped client to use by the cache purger.')
48✔
406
                                ->end()
48✔
407
                                ->integerNode('max_header_length')
48✔
408
                                    ->defaultValue(7500)
48✔
409
                                    ->info('Max header length supported by the cache server.')
48✔
410
                                ->end()
48✔
411
                                ->variableNode('request_options')
48✔
412
                                    ->defaultValue([])
48✔
413
                                    ->validate()
48✔
414
                                        ->ifTrue(static fn ($v): bool => false === \is_array($v))
48✔
415
                                        ->thenInvalid('The request_options parameter must be an array.')
48✔
416
                                    ->end()
48✔
417
                                    ->info('To pass options to the client charged with the request.')
48✔
418
                                ->end()
48✔
419
                                ->scalarNode('purger')
48✔
420
                                    ->defaultValue('api_platform.http_cache.purger.varnish')
48✔
421
                                    ->info('Specify a purger to use (available values: "api_platform.http_cache.purger.varnish.ban", "api_platform.http_cache.purger.varnish.xkey", "api_platform.http_cache.purger.souin").')
48✔
422
                                ->end()
48✔
423
                                ->arrayNode('xkey')
48✔
424
                                    ->setDeprecated('api-platform/core', '3.0', 'The "xkey" configuration is deprecated, use your own purger to customize surrogate keys or the appropriate paramters.')
48✔
425
                                    ->addDefaultsIfNotSet()
48✔
426
                                    ->children()
48✔
427
                                        ->scalarNode('glue')
48✔
428
                                        ->defaultValue(' ')
48✔
429
                                        ->info('xkey glue between keys')
48✔
430
                                        ->end()
48✔
431
                                    ->end()
48✔
432
                                ->end()
48✔
433
                            ->end()
48✔
434
                        ->end()
48✔
435
                    ->end()
48✔
436
                ->end()
48✔
437
            ->end();
48✔
438
    }
439

440
    private function addMercureSection(ArrayNodeDefinition $rootNode): void
441
    {
442
        $rootNode
48✔
443
            ->children()
48✔
444
                ->arrayNode('mercure')
48✔
445
                    ->{class_exists(MercureBundle::class) ? 'canBeDisabled' : 'canBeEnabled'}()
48✔
446
                    ->children()
48✔
447
                        ->scalarNode('hub_url')
48✔
448
                            ->defaultNull()
48✔
449
                            ->info('The URL sent in the Link HTTP header. If not set, will default to the URL for MercureBundle\'s default hub.')
48✔
450
                        ->end()
48✔
451
                        ->booleanNode('include_type')
48✔
452
                            ->defaultFalse()
48✔
453
                            ->info('Always include @type in updates (including delete ones).')
48✔
454
                        ->end()
48✔
455
                    ->end()
48✔
456
                ->end()
48✔
457
            ->end();
48✔
458
    }
459

460
    private function addMessengerSection(ArrayNodeDefinition $rootNode): void
461
    {
462
        $rootNode
48✔
463
            ->children()
48✔
464
                ->arrayNode('messenger')
48✔
465
                    ->{!class_exists(FullStack::class) && interface_exists(MessageBusInterface::class) ? 'canBeDisabled' : 'canBeEnabled'}()
48✔
466
                ->end()
48✔
467
            ->end();
48✔
468
    }
469

470
    private function addElasticsearchSection(ArrayNodeDefinition $rootNode): void
471
    {
472
        $rootNode
48✔
473
            ->children()
48✔
474
                ->arrayNode('elasticsearch')
48✔
475
                    ->canBeEnabled()
48✔
476
                    ->addDefaultsIfNotSet()
48✔
477
                    ->children()
48✔
478
                        ->booleanNode('enabled')
48✔
479
                            ->defaultFalse()
48✔
480
                            ->validate()
48✔
481
                                ->ifTrue()
48✔
482
                                ->then(static function (bool $v): bool {
48✔
483
                                    if (
484
                                        // ES v7
485
                                        !class_exists(\Elasticsearch\Client::class)
2✔
486
                                        // ES v8 and up
487
                                        && !class_exists(\Elastic\Elasticsearch\Client::class)
2✔
488
                                    ) {
489
                                        throw new InvalidConfigurationException('The elasticsearch/elasticsearch package is required for Elasticsearch support.');
×
490
                                    }
491

492
                                    return $v;
2✔
493
                                })
48✔
494
                            ->end()
48✔
495
                        ->end()
48✔
496
                        ->arrayNode('hosts')
48✔
497
                            ->beforeNormalization()->castToArray()->end()
48✔
498
                            ->defaultValue([])
48✔
499
                            ->prototype('scalar')->end()
48✔
500
                        ->end()
48✔
501
                    ->end()
48✔
502
                ->end()
48✔
503
            ->end();
48✔
504
    }
505

506
    private function addOpenApiSection(ArrayNodeDefinition $rootNode): void
507
    {
508
        $rootNode
48✔
509
            ->children()
48✔
510
                ->arrayNode('openapi')
48✔
511
                    ->addDefaultsIfNotSet()
48✔
512
                        ->children()
48✔
513
                        ->arrayNode('contact')
48✔
514
                        ->addDefaultsIfNotSet()
48✔
515
                            ->children()
48✔
516
                                ->scalarNode('name')->defaultNull()->info('The identifying name of the contact person/organization.')->end()
48✔
517
                                ->scalarNode('url')->defaultNull()->info('The URL pointing to the contact information. MUST be in the format of a URL.')->end()
48✔
518
                                ->scalarNode('email')->defaultNull()->info('The email address of the contact person/organization. MUST be in the format of an email address.')->end()
48✔
519
                            ->end()
48✔
520
                        ->end()
48✔
521
                        ->scalarNode('termsOfService')->defaultNull()->info('A URL to the Terms of Service for the API. MUST be in the format of a URL.')->end()
48✔
522
                        ->arrayNode('tags')
48✔
523
                            ->info('Global OpenApi tags overriding the default computed tags if specified.')
48✔
524
                            ->prototype('array')
48✔
525
                                ->children()
48✔
526
                                    ->scalarNode('name')->isRequired()->end()
48✔
527
                                    ->scalarNode('description')->defaultNull()->end()
48✔
528
                                ->end()
48✔
529
                            ->end()
48✔
530
                        ->end()
48✔
531
                        ->arrayNode('license')
48✔
532
                        ->addDefaultsIfNotSet()
48✔
533
                            ->children()
48✔
534
                                ->scalarNode('name')->defaultNull()->info('The license name used for the API.')->end()
48✔
535
                                ->scalarNode('url')->defaultNull()->info('URL to the license used for the API. MUST be in the format of a URL.')->end()
48✔
536
                                ->scalarNode('identifier')->defaultNull()->info('An SPDX license expression for the API. The identifier field is mutually exclusive of the url field.')->end()
48✔
537
                            ->end()
48✔
538
                        ->end()
48✔
539
                        ->variableNode('swagger_ui_extra_configuration')
48✔
540
                            ->defaultValue([])
48✔
541
                            ->validate()
48✔
542
                                ->ifTrue(static fn ($v): bool => false === \is_array($v))
48✔
543
                                ->thenInvalid('The swagger_ui_extra_configuration parameter must be an array.')
48✔
544
                            ->end()
48✔
545
                            ->info('To pass extra configuration to Swagger UI, like docExpansion or filter.')
48✔
546
                        ->end()
48✔
547
                        ->booleanNode('overrideResponses')->defaultTrue()->info('Whether API Platform adds automatic responses to the OpenAPI documentation.')->end()
48✔
548
                        ->scalarNode('error_resource_class')->defaultNull()->info('The class used to represent errors in the OpenAPI documentation.')->end()
48✔
549
                        ->scalarNode('validation_error_resource_class')->defaultNull()->info('The class used to represent validation errors in the OpenAPI documentation.')->end()
48✔
550
                    ->end()
48✔
551
                ->end()
48✔
552
            ->end();
48✔
553
    }
554

555
    /**
556
     * @throws InvalidConfigurationException
557
     */
558
    private function addExceptionToStatusSection(ArrayNodeDefinition $rootNode): void
559
    {
560
        $rootNode
48✔
561
            ->children()
48✔
562
                ->arrayNode('exception_to_status')
48✔
563
                    ->defaultValue([
48✔
564
                        SerializerExceptionInterface::class => Response::HTTP_BAD_REQUEST,
48✔
565
                        InvalidArgumentException::class => Response::HTTP_BAD_REQUEST,
48✔
566
                        OptimisticLockException::class => Response::HTTP_CONFLICT,
48✔
567
                    ])
48✔
568
                    ->info('The list of exceptions mapped to their HTTP status code.')
48✔
569
                    ->normalizeKeys(false)
48✔
570
                    ->useAttributeAsKey('exception_class')
48✔
571
                    ->prototype('integer')->end()
48✔
572
                    ->validate()
48✔
573
                        ->ifArray()
48✔
574
                        ->then(static function (array $exceptionToStatus): array {
48✔
575
                            foreach ($exceptionToStatus as $httpStatusCode) {
16✔
576
                                if ($httpStatusCode < 100 || $httpStatusCode >= 600) {
16✔
577
                                    throw new InvalidConfigurationException(sprintf('The HTTP status code "%s" is not valid.', $httpStatusCode));
8✔
578
                                }
579
                            }
580

581
                            return $exceptionToStatus;
8✔
582
                        })
48✔
583
                    ->end()
48✔
584
                ->end()
48✔
585
            ->end();
48✔
586
    }
587

588
    private function addFormatSection(ArrayNodeDefinition $rootNode, string $key, array $defaultValue): void
589
    {
590
        $rootNode
48✔
591
            ->children()
48✔
592
                ->arrayNode($key)
48✔
593
                    ->defaultValue($defaultValue)
48✔
594
                    ->info('The list of enabled formats. The first one will be the default.')
48✔
595
                    ->normalizeKeys(false)
48✔
596
                    ->useAttributeAsKey('format')
48✔
597
                    ->beforeNormalization()
48✔
598
                        ->ifArray()
48✔
599
                        ->then(static function ($v) {
48✔
600
                            foreach ($v as $format => $value) {
8✔
601
                                if (isset($value['mime_types'])) {
8✔
602
                                    continue;
×
603
                                }
604

605
                                $v[$format] = ['mime_types' => $value];
8✔
606
                            }
607

608
                            return $v;
8✔
609
                        })
48✔
610
                    ->end()
48✔
611
                    ->prototype('array')
48✔
612
                        ->children()
48✔
613
                            ->arrayNode('mime_types')->prototype('scalar')->end()->end()
48✔
614
                        ->end()
48✔
615
                    ->end()
48✔
616
                ->end()
48✔
617
            ->end();
48✔
618
    }
619

620
    private function addDefaultsSection(ArrayNodeDefinition $rootNode): void
621
    {
622
        $nameConverter = new CamelCaseToSnakeCaseNameConverter();
48✔
623
        $defaultsNode = $rootNode->children()->arrayNode('defaults');
48✔
624

625
        $defaultsNode
48✔
626
            ->ignoreExtraKeys(false)
48✔
627
            ->beforeNormalization()
48✔
628
            ->always(static function (array $defaults) use ($nameConverter): array {
48✔
629
                $normalizedDefaults = [];
8✔
630
                foreach ($defaults as $option => $value) {
8✔
631
                    $option = $nameConverter->normalize($option);
8✔
632
                    $normalizedDefaults[$option] = $value;
8✔
633
                }
634

635
                return $normalizedDefaults;
8✔
636
            });
48✔
637

638
        $this->defineDefault($defaultsNode, new \ReflectionClass(ApiResource::class), $nameConverter);
48✔
639
        $this->defineDefault($defaultsNode, new \ReflectionClass(Put::class), $nameConverter);
48✔
640
        $this->defineDefault($defaultsNode, new \ReflectionClass(Post::class), $nameConverter);
48✔
641
    }
642

643
    private function addMakerSection(ArrayNodeDefinition $rootNode): void
644
    {
645
        $rootNode
48✔
646
            ->children()
48✔
647
                ->arrayNode('maker')
48✔
648
                    ->{class_exists(MakerBundle::class) ? 'canBeDisabled' : 'canBeEnabled'}()
48✔
649
                ->end()
48✔
650
            ->end();
48✔
651
    }
652

653
    private function defineDefault(ArrayNodeDefinition $defaultsNode, \ReflectionClass $reflectionClass, CamelCaseToSnakeCaseNameConverter $nameConverter): void
654
    {
655
        foreach ($reflectionClass->getConstructor()->getParameters() as $parameter) {
48✔
656
            $defaultsNode->children()->variableNode($nameConverter->normalize($parameter->getName()));
48✔
657
        }
658
    }
659
}
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

© 2026 Coveralls, Inc