core icon indicating copy to clipboard operation
core copied to clipboard

Parsing of XML Resource causes TypeError when optional openapi parameters are not provided

Open MrSrsen opened this issue 5 months ago • 0 comments

API Platform version(s) affected: 3.1+

PHP: PHP 8.1.26 (cli) (built: Nov 27 2023 23:37:27) (NTS) Api platform: v3.3.11

Description
Class vendor/api-platform/core/src/OpenApi/Model/Parameter.php has default parameters so I would expect that providing all the parameters in my XML file is not required. But for example when I do not provide all the parameters in the <openapi> section in my XML resource definition it will lead to the TypeError because the default parameters (from PHP class) are not used.

Stacktrace:

TypeError:
ApiPlatform\OpenApi\Model\Parameter::__construct(): Argument #3 ($description) must be of type string, null given, called in /var/www/vendor/api-platform/core/src/Metadata/Extractor/XmlResourceExtractor.php on line 215

  at vendor/api-platform/core/src/OpenApi/Model/Parameter.php:20
  at ApiPlatform\OpenApi\Model\Parameter->__construct('from', 'query', null, false, null, null, null, null, null, null, '2024-06', null, null)
     (vendor/api-platform/core/src/Metadata/Extractor/XmlResourceExtractor.php:215)
  at ApiPlatform\Metadata\Extractor\XmlResourceExtractor->buildOpenapi(object(SimpleXMLElement))
     (vendor/api-platform/core/src/Metadata/Extractor/XmlResourceExtractor.php:93)
  at ApiPlatform\Metadata\Extractor\XmlResourceExtractor->buildExtendedBase(object(SimpleXMLElement))
     (vendor/api-platform/core/src/Metadata/Extractor/XmlResourceExtractor.php:382)
  at ApiPlatform\Metadata\Extractor\XmlResourceExtractor->buildOperations(object(SimpleXMLElement), array('shortName' => null, 'description' => null, 'urlGenerationStrategy' => null, 'deprecationReason' => null, 'elasticsearch' => null, 'messenger' => null, 'mercure' => null, 'input' => null, 'output' => null, 'fetchPartial' => null, 'forceEager' => null, 'paginationClientEnabled' => null, 'paginationClientItemsPerPage' => null, 'paginationClientPartial' => null, 'paginationEnabled' => null, 'paginationFetchJoinCollection' => null, 'paginationUseOutputWalkers' => null, 'paginationItemsPerPage' => null, 'paginationMaximumItemsPerPage' => null, 'paginationPartial' => null, 'paginationType' => null, 'processor' => null, 'provider' => null, 'security' => null, 'securityMessage' => null, 'securityPostDenormalize' => null, 'securityPostDenormalizeMessage' => null, 'securityPostValidation' => null, 'securityPostValidationMessage' => null, 'normalizationContext' => null, 'denormalizationContext' => null, 'collectDenormalizationErrors' => null, 'validationContext' => null, 'filters' => null, 'order' => null, 'extraProperties' => null, 'read' => null, 'write' => null, 'uriTemplate' => null, 'routePrefix' => null, 'stateless' => null, 'sunset' => null, 'acceptPatch' => null, 'status' => null, 'host' => null, 'condition' => null, 'controller' => null, 'types' => null, 'formats' => null, 'inputFormats' => null, 'outputFormats' => null, 'uriVariables' => null, 'defaults' => null, 'requirements' => null, 'options' => null, 'schemes' => null, 'cacheHeaders' => null, 'hydraContext' => null, 'openapiContext' => null, 'openapi' => null, 'paginationViaCursor' => null, 'exceptionToStatus' => null, 'queryParameterValidationEnabled' => null, 'stateOptions' => null, 'links' => null))
     (vendor/api-platform/core/src/Metadata/Extractor/XmlResourceExtractor.php:63)
  at ApiPlatform\Metadata\Extractor\XmlResourceExtractor->extractPath('/var/www/config/api_platform/Partner/B2BAdmin/MonthRangeStatistics.xml')
     (vendor/api-platform/core/src/Metadata/Extractor/AbstractResourceExtractor.php:47)
  at ApiPlatform\Metadata\Extractor\AbstractResourceExtractor->getResources()
     (vendor/api-platform/core/src/Metadata/Resource/Factory/ExtractorResourceNameCollectionFactory.php:46)
  at ApiPlatform\Metadata\Resource\Factory\ExtractorResourceNameCollectionFactory->create()
     (vendor/api-platform/core/src/Metadata/Resource/Factory/AttributesResourceNameCollectionFactory.php:44)
  at ApiPlatform\Metadata\Resource\Factory\AttributesResourceNameCollectionFactory->create()
     (vendor/api-platform/core/src/Metadata/Resource/Factory/ClassNameResourceNameCollectionFactory.php:37)
  at ApiPlatform\Metadata\Resource\Factory\ClassNameResourceNameCollectionFactory->create()
     (vendor/api-platform/core/src/Metadata/Resource/Factory/ExtractorResourceNameCollectionFactory.php:41)
  at ApiPlatform\Metadata\Resource\Factory\ExtractorResourceNameCollectionFactory->create()
     (vendor/api-platform/core/src/Metadata/Resource/Factory/CachedResourceNameCollectionFactory.php:41)
  at ApiPlatform\Metadata\Resource\Factory\CachedResourceNameCollectionFactory->ApiPlatform\Metadata\Resource\Factory\{closure}()
     (vendor/api-platform/core/src/Metadata/Util/CachedTrait.php:43)
  at ApiPlatform\Metadata\Resource\Factory\CachedResourceNameCollectionFactory->getCached('resource_name_collection', object(Closure))
     (vendor/api-platform/core/src/Metadata/Resource/Factory/CachedResourceNameCollectionFactory.php:41)
  at ApiPlatform\Metadata\Resource\Factory\CachedResourceNameCollectionFactory->create()
     (vendor/api-platform/core/src/Symfony/Routing/ApiLoader.php:57)
  at ApiPlatform\Symfony\Routing\ApiLoader->load('.', 'api_platform')
     (vendor/symfony/config/Loader/FileLoader.php:142)
  at Symfony\Component\Config\Loader\FileLoader->doImport('.', 'api_platform', false, '/var/www/config/routes/api_platform.yaml')
     (vendor/symfony/config/Loader/FileLoader.php:94)
  at Symfony\Component\Config\Loader\FileLoader->import('.', 'api_platform', false, '/var/www/config/routes/api_platform.yaml', null)
     (vendor/symfony/routing/Loader/YamlFileLoader.php:208)
  at Symfony\Component\Routing\Loader\YamlFileLoader->parseImport(object(RouteCollection), array('resource' => '.', 'type' => 'api_platform', 'prefix' => '/api'), '/var/www/config/routes/api_platform.yaml', '/var/www/config/routes/api_platform.yaml')
     (vendor/symfony/routing/Loader/YamlFileLoader.php:99)
  at Symfony\Component\Routing\Loader\YamlFileLoader->load('/var/www/config/routes/api_platform.yaml', null)
     (vendor/symfony/config/Loader/FileLoader.php:163)
  at Symfony\Component\Config\Loader\FileLoader->doImport('/var/www/config/routes/api_platform.yaml', null, false, '/var/www/src/Kernel.php')
     (vendor/symfony/config/Loader/FileLoader.php:94)
  at Symfony\Component\Config\Loader\FileLoader->import('/var/www/config/routes/api_platform.yaml', null, false, '/var/www/src/Kernel.php', null)
     (vendor/symfony/routing/Loader/Configurator/RoutingConfigurator.php:45)
  at Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator->import('/var/www/config/routes/api_platform.yaml')
     (vendor/sulu/sulu/src/Sulu/Component/HttpKernel/SuluKernel.php:186)
  at Sulu\Component\HttpKernel\SuluKernel->import(object(RoutingConfigurator), '/var/www/config', '/{routes}/*')
     (vendor/sulu/sulu/src/Sulu/Component/HttpKernel/SuluKernel.php:152)
  at Sulu\Component\HttpKernel\SuluKernel->configureRoutes(object(RoutingConfigurator))
     (vendor/symfony/framework-bundle/Kernel/MicroKernelTrait.php:202)
  at Sulu\Component\HttpKernel\SuluKernel->loadRoutes(object(ContainerLoader), 'dev')
     (vendor/symfony/routing/Loader/ObjectLoader.php:55)
  at Symfony\Component\Routing\Loader\ObjectLoader->load('kernel::loadRoutes', 'service')
     (vendor/symfony/config/Loader/DelegatingLoader.php:37)
  at Symfony\Component\Config\Loader\DelegatingLoader->load('kernel::loadRoutes', 'service')
     (vendor/symfony/framework-bundle/Routing/DelegatingLoader.php:67)
  at Symfony\Bundle\FrameworkBundle\Routing\DelegatingLoader->load('kernel::loadRoutes', 'service')
     (vendor/symfony/framework-bundle/Routing/Router.php:65)
  at Symfony\Bundle\FrameworkBundle\Routing\Router->getRouteCollection()
     (vendor/symfony/routing/Router.php:317)
  at Symfony\Component\Routing\Router->getMatcherDumperInstance()
     (vendor/symfony/routing/Router.php:255)
  at Symfony\Component\Routing\Router->Symfony\Component\Routing\{closure}(object(ResourceCheckerConfigCache))
     (vendor/symfony/config/ResourceCheckerConfigCacheFactory.php:36)
  at Symfony\Component\Config\ResourceCheckerConfigCacheFactory->cache('/var/www/var/cache/website/dev/url_matching_routes.php', object(Closure))
     (vendor/symfony/routing/Router.php:263)
  at Symfony\Component\Routing\Router->getMatcher()
     (vendor/symfony/routing/Router.php:219)
  at Symfony\Component\Routing\Router->matchRequest(object(Request))
     (vendor/symfony-cmf/routing/src/ChainRouter.php:161)
  at Symfony\Cmf\Component\Routing\ChainRouter->doMatch('/api', object(Request))
     (vendor/symfony-cmf/routing/src/ChainRouter.php:134)
  at Symfony\Cmf\Component\Routing\ChainRouter->matchRequest(object(Request))
     (vendor/symfony/http-kernel/EventListener/RouterListener.php:105)
  at Symfony\Component\HttpKernel\EventListener\RouterListener->onKernelRequest(object(RequestEvent))
     (vendor/sulu/sulu/src/Sulu/Bundle/WebsiteBundle/EventListener/RouterListener.php:57)
  at Sulu\Bundle\WebsiteBundle\EventListener\RouterListener->onKernelRequest(object(RequestEvent), 'kernel.request', object(TraceableEventDispatcher))
     (vendor/symfony/event-dispatcher/Debug/WrappedListener.php:116)
  at Symfony\Component\EventDispatcher\Debug\WrappedListener->__invoke(object(RequestEvent), 'kernel.request', object(TraceableEventDispatcher))
     (vendor/symfony/event-dispatcher/EventDispatcher.php:206)
  at Symfony\Component\EventDispatcher\EventDispatcher->callListeners(array(object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener)), 'kernel.request', object(RequestEvent))
     (vendor/symfony/event-dispatcher/EventDispatcher.php:56)
  at Symfony\Component\EventDispatcher\EventDispatcher->dispatch(object(RequestEvent), 'kernel.request')
     (vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php:127)
  at Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher->dispatch(object(RequestEvent), 'kernel.request')
     (vendor/symfony/http-kernel/HttpKernel.php:139)
  at Symfony\Component\HttpKernel\HttpKernel->handleRaw(object(Request), 1)
     (vendor/symfony/http-kernel/HttpKernel.php:74)
  at Symfony\Component\HttpKernel\HttpKernel->handle(object(Request), 1, true)
     (vendor/symfony/http-kernel/Kernel.php:184)
  at Symfony\Component\HttpKernel\Kernel->handle(object(Request))
     (public/index.php:68)

How to reproduce

  • make Resource definition with openapi section
  • provide just the required parameters
  • visit localhost API docs page
  • you get TypeError

My XML resource definition:

<resources xmlns="https://api-platform.com/schema/metadata/resources-3.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://api-platform.com/schema/metadata/resources-3.0 https://api-platform.com/schema/metadata/resources-3.0.xsd">
    <resource class="App\Partner\Infrastructure\Dto\Statistics\MonthRangeStatistics">
        <operations>
            <operation
                class="ApiPlatform\Metadata\Get"
                uriTemplate="/b2b-admin/month-range-statistics"
                provider="App\Partner\Entrypoint\Provider\B2BAdmin\MonthRangeStatisticsProvider"
                security="is_granted('ROLE_ADMIN')"
                stateless="true">
                <normalizationContext>
                    <values>
                        <value name="skip_null_values">false</value>
                    </values>
                </normalizationContext>
                <openapi>
                    <parameters>
                        <parameter
                            name="from"
                            in="query"
                            example="2024-06"  />
                        <parameter
                            name="to"
                            in="query"
                            example="2024-08" />
                    </parameters>
                </openapi>
            </operation>
        </operations>
    </resource>
</resources>

In the openapi section I used all the required parameters (by PHP class definition).

Possible Solution

I would suggest fixing method buildOpenapi in the XmlResourceExtractor.php file.

Add correct default values on these lines:

description: $this->phpize($parameter, 'description', 'string'),
required: $this->phpize($parameter, 'required', 'bool'),
deprecated: $this->phpize($parameter, 'deprecated', 'bool'),
allowEmptyValue: $this->phpize($parameter, 'allowEmptyValue', 'bool'),
schema: isset($parameter->schema->values) ? $this->buildValues($parameter->schema->values) : null,
// ...
allowReserved: $this->phpize($parameter, 'allowReserved', 'bool'),

and change it to this:

description: $this->phpize($parameter, 'description', 'string', ''),
required: $this->phpize($parameter, 'required', 'bool', false),
deprecated: $this->phpize($parameter, 'deprecated', 'bool', false),
allowEmptyValue: $this->phpize($parameter, 'allowEmptyValue', 'bool', false),
schema: isset($parameter->schema->values) ? $this->buildValues($parameter->schema->values) : [],
// ...
allowReserved: $this->phpize($parameter, 'allowReserved', 'bool', false),

to match default values defined in the PHP Parameter.php class.

The last option is to use deprecated openapiContext that works:

<openapiContext>
    <values>
        <value name="parameters">
            <values>
                <value>
                    <values>
                        <value name="name">from</value>
                        <value name="in">query</value>
                        <value name="type">string</value>
                        <value name="example">2024-04</value>
                        <value name="description">Start month</value>
                        <value name="schema">
                            <values>
                                <value name="type">string</value>
                            </values>
                        </value>
                    </values>
                </value>
                <value>
                    <values>
                        <value name="name">to</value>
                        <value name="in">query</value>
                        <value name="type">string</value>
                        <value name="example">2024-08</value>
                        <value name="description">End month</value>
                        <value name="schema">
                            <values>
                                <value name="type">string</value>
                            </values>
                        </value>
                    </values>
                </value>
            </values>
        </value>
    </values>
</openapiContext>

MrSrsen avatar Aug 29 '24 11:08 MrSrsen