[API] Replace Swagger with OpenAPI

This commit is contained in:
Kevin Kaniaburka 2023-07-11 20:35:39 +02:00
parent da94d80e76
commit 2554fd64f5
No known key found for this signature in database
GPG key ID: 8DB4C54474F3FD72
30 changed files with 827 additions and 1067 deletions

View file

@ -41,5 +41,30 @@
- `GET /admin/product-variant-translation/{id}` -> `GET /admin/product-variant-translations/{id}`
- `GET /shop/product-variant-translation/{id}` -> `GET /shop/product-variant-translations/{id}`
2. Typo in the constraint validator's alias returned by `Sylius\Bundle\ApiBundle\Validator\Constraints\ChangedItemQuantityInCartValidator::validatedBy` has been fixed.
Previously it was `sylius_api_validator_changed_item_guantity_in_cart` and now it is `sylius_api_validator_changed_item_quantity_in_cart`.
1. Typo in the constraint validator's alias returned by `Sylius\Bundle\ApiBundle\Validator\Constraints\ChangedItemQuantityInCartValidator::validatedBy` has been fixed.
Previously it was `sylius_api_validator_changed_item_guantity_in_cart` and now it is `sylius_api_validator_changed_item_quantity_in_cart`.
1. The `ApiPlatform\Core\Bridge\Symfony\Bundle\Action\SwaggerUiAction` controller has been removed.
Therefore, the `api_platform.swagger.action.ui` service ID points to the API Platform's `SwaggerUiAction` controller.
1. The following services have been removed:
* `Sylius\Bundle\ApiBundle\Swagger\AdminAuthenticationTokenDocumentationNormalizer`
* `Sylius\Bundle\ApiBundle\Swagger\ShopAuthenticationTokenDocumentationNormalizer`
* `Sylius\Bundle\ApiBundle\Swagger\ProductDocumentationNormalizer`
* `Sylius\Bundle\ApiBundle\Swagger\ProductImageDocumentationNormalizer`
* `Sylius\Bundle\ApiBundle\Swagger\ProductSlugDocumentationNormalizer`
* `Sylius\Bundle\ApiBundle\Swagger\ProductVariantDocumentationNormalizer`
* `Sylius\Bundle\ApiBundle\Swagger\ShippingMethodDocumentationNormalizer`
* `Sylius\Bundle\ApiBundle\Swagger\PathHiderDocumentationNormalizer`
* `Sylius\Bundle\ApiBundle\Swagger\AcceptLanguageHeaderDocumentationNormalizer`
Responsibility of these services has been moved to the corresponding services tagged with `sylius.open_api.modifier`:
* `Sylius\Bundle\ApiBundle\OpenApi\Documentation\AdminAuthenticationTokenDocumentationModifier`
* `Sylius\Bundle\ApiBundle\OpenApi\Documentation\ShopAuthenticationTokenDocumentationModifier`
* `Sylius\Bundle\ApiBundle\OpenApi\Documentation\ProductDocumentationModifier`
* `Sylius\Bundle\ApiBundle\OpenApi\Documentation\ProductImageDocumentationModifier`
* `Sylius\Bundle\ApiBundle\OpenApi\Documentation\ProductSlugDocumentationModifier`
* `Sylius\Bundle\ApiBundle\OpenApi\Documentation\ProductVariantDocumentationModifier`
* `Sylius\Bundle\ApiBundle\OpenApi\Documentation\ShippingMethodDocumentationModifier`
* `Sylius\Bundle\ApiBundle\OpenApi\Documentation\PathHiderDocumentationModifier`
* `Sylius\Bundle\ApiBundle\OpenApi\Documentation\AcceptLanguageHeaderDocumentationModifier`

View file

@ -280,81 +280,6 @@ parameters:
count: 1
path: src/Sylius/Bundle/ApiBundle/ApiPlatform/ApiResourceConfigurationMergerInterface.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:__construct\\(\\) has parameter \\$formats with no type specified\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:__construct\\(\\) has parameter \\$oauthAuthorizationUrl with no type specified\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:__construct\\(\\) has parameter \\$oauthClientId with no type specified\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:__construct\\(\\) has parameter \\$oauthClientSecret with no type specified\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:__construct\\(\\) has parameter \\$oauthEnabled with no type specified\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:__construct\\(\\) has parameter \\$oauthFlow with no type specified\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:__construct\\(\\) has parameter \\$oauthScopes with no type specified\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:__construct\\(\\) has parameter \\$oauthTokenUrl with no type specified\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:__construct\\(\\) has parameter \\$oauthType with no type specified\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:__invoke\\(\\) has no return type specified\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:getContext\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:getPathAndMethod\\(\\) has parameter \\$swaggerData with no value type specified in iterable type array\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:getPathAndMethod\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
-
message: "#^Property Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:\\$formats type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
-
message: "#^Property Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:\\$formatsProvider has no type specified\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Routing\\\\RouteNameResolver\\:\\:isSameSubresource\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#"
count: 1
@ -1920,16 +1845,6 @@ parameters:
count: 1
path: src/Sylius/Bundle/ApiBundle/SerializerContextBuilder/LocaleContextBuilder.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Swagger\\\\AcceptLanguageHeaderDocumentationNormalizer\\:\\:__construct\\(\\) has parameter \\$localeRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/Swagger/AcceptLanguageHeaderDocumentationNormalizer.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Swagger\\\\PathHiderDocumentationNormalizer\\:\\:__construct\\(\\) has parameter \\$apiRoutes with no value type specified in iterable type array\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/Swagger/PathHiderDocumentationNormalizer.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\CatalogPromotion\\\\FixedDiscountActionValidator\\:\\:isChannelAmountValid\\(\\) has parameter \\$channelConfiguration with no value type specified in iterable type array\\.$#"
count: 1

View file

@ -12,7 +12,6 @@
<directory name="src" />
<ignoreFiles>
<file name="src/Sylius/Bundle/CoreBundle/Application/Kernel.php" />
<file name="src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php" />
<directory name="src/Sylius/Behat" />

View file

@ -1,173 +0,0 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\ApiPlatform\Bridge\Symfony\Bundle\Action;
use ApiPlatform\Core\Api\FormatsProviderInterface;
use ApiPlatform\Core\Documentation\Documentation;
use ApiPlatform\Core\Exception\RuntimeException;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
use ApiPlatform\Core\Util\RequestAttributesExtractor;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Twig\Environment as TwigEnvironment;
/**
* Displays the documentation.
*
* @internal
*
* This class will be probably removed in Sylius 1.9
*/
final class SwaggerUiAction
{
private ?array $formats;
private $formatsProvider;
/**
* @param int[] $swaggerVersions
*/
public function __construct(
private ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory,
private ResourceMetadataFactoryInterface $resourceMetadataFactory,
private NormalizerInterface $normalizer,
private TwigEnvironment $twig,
private UrlGeneratorInterface $urlGenerator,
private string $title = '',
private string $description = '',
private string $version = '',
$formats = [],
private $oauthEnabled = false,
private $oauthClientId = '',
private $oauthClientSecret = '',
private $oauthType = '',
private $oauthFlow = '',
private $oauthTokenUrl = '',
private $oauthAuthorizationUrl = '',
private $oauthScopes = [],
private bool $showWebby = true,
private bool $swaggerUiEnabled = false,
private bool $reDocEnabled = false,
private bool $graphqlEnabled = false,
private bool $graphiQlEnabled = false,
private bool $graphQlPlaygroundEnabled = false,
private array $swaggerVersions = [2, 3],
) {
if (\is_array($formats)) {
$this->formats = $formats;
return;
}
@trigger_error(sprintf(
'Passing an array or an instance of "%s" as 5th parameter of the constructor of "%s" is deprecated since API Platform 2.5, pass an array instead',
FormatsProviderInterface::class,
__CLASS__,
), \E_USER_DEPRECATED);
$this->formatsProvider = $formats;
}
public function __invoke(Request $request)
{
$attributes = RequestAttributesExtractor::extractAttributes($request);
// BC check to be removed in 3.0
if (null === $this->formatsProvider) {
$formats = $attributes ? $this
->resourceMetadataFactory
->create($attributes['resource_class'])
->getOperationAttribute($attributes, 'output_formats', [], true) : $this->formats;
} else {
$formats = $this->formatsProvider->getFormatsFromAttributes($attributes);
}
$documentation = new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version);
return new Response($this->twig->render('@SyliusApi/SwaggerUi/index.html.twig', $this->getContext($request, $documentation) + ['formats' => $formats]));
}
/**
* Gets the base Twig context.
*/
private function getContext(Request $request, Documentation $documentation): array
{
$context = [
'title' => $this->title,
'description' => $this->description,
'showWebby' => $this->showWebby,
'swaggerUiEnabled' => $this->swaggerUiEnabled,
'reDocEnabled' => $this->reDocEnabled,
'graphqlEnabled' => $this->graphqlEnabled,
'graphiQlEnabled' => $this->graphiQlEnabled,
'graphQlPlaygroundEnabled' => $this->graphQlPlaygroundEnabled,
];
$swaggerContext = ['spec_version' => $request->query->getInt('spec_version', $this->swaggerVersions[0] ?? 2)];
if ('' !== $baseUrl = $request->getBaseUrl()) {
$swaggerContext['base_url'] = $baseUrl;
}
$swaggerData = [
'url' => $this->urlGenerator->generate('api_doc', ['format' => 'json']),
'spec' => $this->normalizer->normalize($documentation, 'json', $swaggerContext),
];
$swaggerData['oauth'] = [
'enabled' => $this->oauthEnabled,
'clientId' => $this->oauthClientId,
'clientSecret' => $this->oauthClientSecret,
'type' => $this->oauthType,
'flow' => $this->oauthFlow,
'tokenUrl' => $this->oauthTokenUrl,
'authorizationUrl' => $this->oauthAuthorizationUrl,
'scopes' => $this->oauthScopes,
];
if ($request->isMethodSafe() && null !== $resourceClass = $request->attributes->get('_api_resource_class')) {
$swaggerData['id'] = $request->attributes->get('id');
$swaggerData['queryParameters'] = $request->query->all();
$metadata = $this->resourceMetadataFactory->create($resourceClass);
$swaggerData['shortName'] = $metadata->getShortName();
if (null !== $collectionOperationName = $request->attributes->get('_api_collection_operation_name')) {
$swaggerData['operationId'] = sprintf('%s%sCollection', $collectionOperationName, ucfirst($swaggerData['shortName']));
} elseif (null !== $itemOperationName = $request->attributes->get('_api_item_operation_name')) {
$swaggerData['operationId'] = sprintf('%s%sItem', $itemOperationName, ucfirst($swaggerData['shortName']));
} elseif (null !== $subresourceOperationContext = $request->attributes->get('_api_subresource_context')) {
$swaggerData['operationId'] = $subresourceOperationContext['operationId'];
}
[$swaggerData['path'], $swaggerData['method']] = $this->getPathAndMethod($swaggerData);
}
return $context + ['swagger_data' => $swaggerData];
}
private function getPathAndMethod(array $swaggerData): array
{
foreach ($swaggerData['spec']['paths'] as $path => $operations) {
foreach ($operations as $method => $operation) {
if ($operation['operationId'] === $swaggerData['operationId']) {
return [$path, $method];
}
}
}
throw new RuntimeException(sprintf('The operation "%s" cannot be found in the Swagger specification.', $swaggerData['operationId']));
}
}

View file

@ -37,7 +37,10 @@ final class SyliusApiExtension extends Extension implements PrependExtensionInte
$loader->load('services.xml');
if ($container->hasParameter('api_platform.enable_swagger_ui') && $container->getParameter('api_platform.enable_swagger_ui')) {
// If parameter is not set, it means that Swagger is not enabled (api_platform.enable_swagger set to false)
$swaggerEnabled = $container->hasParameter('api_platform.swagger.api_keys');
if ($swaggerEnabled) {
$loader->load('integrations/swagger.xml');
}
}

View file

@ -0,0 +1,73 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\OpenApi\Documentation;
use ApiPlatform\Core\OpenApi\Model\Parameter;
use ApiPlatform\OpenApi\Model\PathItem;
use ApiPlatform\OpenApi\OpenApi;
use Sylius\Component\Locale\Model\LocaleInterface;
use Sylius\Component\Resource\Repository\RepositoryInterface;
/** @experimental */
final class AcceptLanguageHeaderDocumentationModifier implements DocumentationModifierInterface
{
/**
* @param RepositoryInterface<LocaleInterface> $localeRepository
*/
public function __construct(private RepositoryInterface $localeRepository)
{
}
public function modify(OpenApi $docs): OpenApi
{
$acceptLanguageHeaderParameter = new Parameter(
name: 'Accept-Language',
in: 'header',
description: 'Locales in this enum are all locales defined in the shop and only enabled ones will work in the given channel in the shop.',
required: false,
schema: [
'type' => 'string',
'enum' => array_map(
fn (LocaleInterface $locale): string => $locale->getCode(),
$this->localeRepository->findAll(),
),
],
);
$pathItems = [];
/** @var PathItem $pathItem */
foreach ($docs->getPaths()->getPaths() as $path => $pathItem) {
foreach (PathItem::$methods as $method) {
$operation = $pathItem->{'get' . ucfirst($method)}();
if (null === $operation) {
continue;
}
$parameters = $operation->getParameters();
$parameters[] = $acceptLanguageHeaderParameter;
$operation = $operation->withParameters($parameters);
$pathItems[$path] = $pathItem->{'with' . ucfirst($method)}($operation);
}
}
foreach ($pathItems as $path => $pathItem) {
$docs->getPaths()->addPath($path, $pathItem);
}
return $docs;
}
}

View file

@ -0,0 +1,100 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\OpenApi\Documentation;
use ApiPlatform\OpenApi\Model\Operation;
use ApiPlatform\OpenApi\Model\PathItem;
use ApiPlatform\OpenApi\Model\RequestBody;
use ApiPlatform\OpenApi\OpenApi;
use Symfony\Component\HttpFoundation\Response;
/** @experimental */
final class AdminAuthenticationTokenDocumentationModifier implements DocumentationModifierInterface
{
public function __construct(private string $apiRoute)
{
}
public function modify(OpenApi $docs): OpenApi
{
$components = $docs->getComponents();
$schemas = $components->getSchemas();
$schemas['AdminUserToken'] = [
'type' => 'object',
'properties' => [
'token' => [
'type' => 'string',
'readOnly' => true,
],
'adminUser' => [
'type' => 'string',
'readOnly' => true,
],
],
];
$schemas['AdminUserCredentials'] = [
'type' => 'object',
'properties' => [
'email' => [
'type' => 'string',
'example' => 'api@example.com',
],
'password' => [
'type' => 'string',
'example' => 'sylius-api',
],
],
];
$components = $components->withSchemas($schemas);
$docs = $docs->withComponents($components);
$docs->getPaths()->addPath(
$this->apiRoute . '/admin/authentication-token',
new PathItem(
post: new Operation(
operationId: 'postCredentialsItem',
tags: ['AdminUserToken'],
responses: [
Response::HTTP_OK => [
'description' => 'Get JWT token',
'content' => [
'application/json' => [
'schema' => [
'$ref' => '#/components/schemas/AdminUserToken',
],
],
],
],
],
summary: 'Get JWT token to login.',
requestBody: new RequestBody(
description: 'Create new JWT Token',
content: new \ArrayObject([
'application/json' => [
'schema' => [
'$ref' => '#/components/schemas/AdminUserCredentials',
],
],
]),
),
),
),
);
return $docs;
}
}

View file

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\OpenApi\Documentation;
use ApiPlatform\OpenApi\OpenApi;
/** @experimental */
interface DocumentationModifierInterface
{
public function modify(OpenApi $docs): OpenApi;
}

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\OpenApi\Documentation;
use ApiPlatform\OpenApi\Model\PathItem;
use ApiPlatform\OpenApi\Model\Paths;
use ApiPlatform\OpenApi\OpenApi;
/** @experimental */
final class PathHiderDocumentationModifier implements DocumentationModifierInterface
{
/**
* @param string[] $apiRoutes
*/
public function __construct(private array $apiRoutes)
{
}
public function modify(OpenApi $docs): OpenApi
{
/** @var array<string, PathItem> $pathItems */
$pathItems = $docs->getPaths()->getPaths();
foreach ($this->apiRoutes as $apiRoute) {
if (array_key_exists($apiRoute, $pathItems)) {
unset($pathItems[$apiRoute]);
}
}
$paths = new Paths();
foreach ($pathItems as $path => $pathItem) {
$paths->addPath($path, $pathItem);
}
return $docs->withPaths($paths);
}
}

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\OpenApi\Documentation;
use ApiPlatform\OpenApi\Model\PathItem;
use ApiPlatform\OpenApi\Model\Paths;
use ApiPlatform\OpenApi\OpenApi;
/** @experimental */
final class ProductDocumentationModifier implements DocumentationModifierInterface
{
public function modify(OpenApi $docs): OpenApi
{
$components = $docs->getComponents();
$schemas = $components->getSchemas();
$schemas['Product.jsonld-shop.product.read']['properties']['defaultVariant'] = [
'type' => 'string',
'format' => 'iri-reference',
'nullable' => true,
'readOnly' => true,
];
return $docs->withComponents(
$components->withSchemas($schemas)
);
}
}

View file

@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\OpenApi\Documentation;
use ApiPlatform\OpenApi\Model\Parameter;
use ApiPlatform\OpenApi\OpenApi;
use Sylius\Bundle\ApiBundle\Provider\ProductImageFilterProviderInterface;
/** @experimental */
final class ProductImageDocumentationModifier implements DocumentationModifierInterface
{
public function __construct(
private ProductImageFilterProviderInterface $filterProvider,
private string $apiRoute,
) {
}
public function modify(OpenApi $docs): OpenApi
{
$enums = array_keys($this->filterProvider->provideShopFilters());
$path = sprintf('%s/shop/product-images/{id}', $this->apiRoute);
$paths = $docs->getPaths();
$pathItem = $paths->getPath($path);
$operation = $pathItem->getGet();
/** @var Parameter[] $parameters */
$parameters = $operation->getParameters();
foreach ($parameters as &$parameter) {
if ($parameter->getIn() === 'query' && $parameter->getName() === 'filter') {
$schema = $parameter->getSchema();
$schema['enum'] = $enums;
$parameter = $parameter->withSchema($schema);
}
}
$operation = $operation->withParameters($parameters);
$pathItem = $pathItem->withGet($operation);
$paths->addPath($path, $pathItem);
return $docs;
}
}

View file

@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\OpenApi\Documentation;
use ApiPlatform\OpenApi\Model\Parameter;
use ApiPlatform\OpenApi\OpenApi;
use Sylius\Bundle\ApiBundle\Provider\ProductImageFilterProviderInterface;
/** @experimental */
final class ProductSlugDocumentationModifier implements DocumentationModifierInterface
{
public function __construct(private string $apiRoute)
{
}
public function modify(OpenApi $docs): OpenApi
{
$path = sprintf('%s/shop/products-by-slug/{slug}', $this->apiRoute);
$paths = $docs->getPaths();
$pathItem = $paths->getPath($path);
$operation = $pathItem->getGet();
/** @var Parameter[] $parameters */
$parameters = $operation->getParameters();
foreach ($parameters as $key => $parameter) {
if ($parameter->getName() === 'code') {
unset($parameters[$key]);
}
}
$operation = $operation->withParameters(array_values($parameters));
$pathItem = $pathItem->withGet($operation);
$paths->addPath($path, $pathItem);
return $docs;
}
}

View file

@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\OpenApi\Documentation;
use ApiPlatform\OpenApi\Model\PathItem;
use ApiPlatform\OpenApi\Model\Paths;
use ApiPlatform\OpenApi\OpenApi;
/** @experimental */
final class ProductVariantDocumentationModifier implements DocumentationModifierInterface
{
public function modify(OpenApi $docs): OpenApi
{
$components = $docs->getComponents();
$schemas = $components->getSchemas();
$schemas['ProductVariant.jsonld-shop.product_variant.read']['properties']['price'] = [
'type' => 'integer',
'readOnly' => true,
'default' => 0,
];
$schemas['ProductVariant.jsonld-shop.product_variant.read']['properties']['inStock'] = [
'type' => 'boolean',
'readOnly' => true,
];
$schemas['ProductVariant.jsonld-shop.product_variant.read']['properties']['originalPrice'] = [
'type' => 'integer',
'readOnly' => true,
'default' => 0,
];
return $docs->withComponents(
$components->withSchemas($schemas)
);
}
}

View file

@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\OpenApi\Documentation;
use ApiPlatform\OpenApi\Model\PathItem;
use ApiPlatform\OpenApi\Model\Paths;
use ApiPlatform\OpenApi\OpenApi;
/** @experimental */
final class ShippingMethodDocumentationModifier implements DocumentationModifierInterface
{
public function modify(OpenApi $docs): OpenApi
{
$components = $docs->getComponents();
$schemas = $components->getSchemas();
$schemas['ShippingMethod.jsonld-shop.shipping_method.read']['properties']['price'] = [
'type' => 'integer',
'readOnly' => true,
'default' => 0,
];
return $docs->withComponents(
$components->withSchemas($schemas)
);
}
}

View file

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\OpenApi\Documentation;
use ApiPlatform\OpenApi\Model\Operation;
use ApiPlatform\OpenApi\Model\PathItem;
use ApiPlatform\OpenApi\Model\RequestBody;
use ApiPlatform\OpenApi\OpenApi;
use Symfony\Component\HttpFoundation\Response;
/** @experimental */
final class ShopAuthenticationTokenDocumentationModifier implements DocumentationModifierInterface
{
public function __construct(private string $apiRoute)
{
}
public function modify(OpenApi $docs): OpenApi
{
$components = $docs->getComponents();
$schemas = $components->getSchemas();
$schemas['ShopUserToken'] = [
'type' => 'object',
'properties' => [
'token' => [
'type' => 'string',
'readOnly' => true,
],
'customer' => [
'type' => 'string',
'readOnly' => true,
],
],
];
$schemas['ShopUserCredentials'] = [
'type' => 'object',
'properties' => [
'email' => [
'type' => 'string',
'example' => 'shop@example.com',
],
'password' => [
'type' => 'string',
'example' => 'sylius',
],
],
];
$components = $components->withSchemas($schemas);
$docs = $docs->withComponents($components);
$docs->getPaths()->addPath(
$this->apiRoute . '/shop/authentication-token',
new PathItem(
post: new Operation(
operationId: 'postCredentialsItem',
tags: ['ShopUserToken'],
responses: [
Response::HTTP_OK => [
'description' => 'Get JWT token',
'content' => [
'application/json' => [
'schema' => [
'$ref' => '#/components/schemas/ShopUserToken',
],
],
],
],
],
summary: 'Get JWT token to login.',
requestBody: new RequestBody(
description: 'Create new JWT Token',
content: new \ArrayObject([
'application/json' => [
'schema' => [
'$ref' => '#/components/schemas/ShopUserCredentials',
],
],
]),
),
),
),
);
return $docs;
}
}

View file

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\OpenApi\Factory;
use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\OpenApi\OpenApi;
use Sylius\Bundle\ApiBundle\OpenApi\Documentation\DocumentationModifierInterface;
use Webmozart\Assert\Assert;
/** @experimental */
final class OpenApiFactory implements OpenApiFactoryInterface
{
public function __construct(
private OpenApiFactoryInterface $decorated,
/** @var iterable<DocumentationModifierInterface> */
private iterable $openApiModifiers,
) {
Assert::allIsInstanceOf($openApiModifiers, DocumentationModifierInterface::class);
}
/**
* @param array<string, mixed> $context
*/
public function __invoke(array $context = []): OpenApi
{
$openApi = ($this->decorated)($context);
foreach ($this->openApiModifiers as $openApiModifier) {
$openApi = $openApiModifier->modify($openApi);
}
return $openApi;
}
}

View file

@ -16,121 +16,54 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"
>
<services>
<defaults public="true" />
<defaults public="false" />
<service id="api_platform.swagger.action.ui" class="Sylius\Bundle\ApiBundle\ApiPlatform\Bridge\Symfony\Bundle\Action\SwaggerUiAction">
<argument type="service" id="api_platform.metadata.resource.name_collection_factory" />
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="api_platform.serializer" />
<argument type="service" id="twig" />
<argument type="service" id="router" />
<argument>%api_platform.title%</argument>
<argument>%api_platform.description%</argument>
<argument>%api_platform.version%</argument>
<argument>%api_platform.formats%</argument>
<argument>%api_platform.oauth.enabled%</argument>
<argument>%api_platform.oauth.clientId%</argument>
<argument>%api_platform.oauth.clientSecret%</argument>
<argument>%api_platform.oauth.type%</argument>
<argument>%api_platform.oauth.flow%</argument>
<argument>%api_platform.oauth.tokenUrl%</argument>
<argument>%api_platform.oauth.authorizationUrl%</argument>
<argument>%api_platform.oauth.scopes%</argument>
<argument>%api_platform.show_webby%</argument>
<argument>%api_platform.enable_swagger_ui%</argument>
<argument>%api_platform.enable_re_doc%</argument>
<argument>%api_platform.graphql.enabled%</argument>
<argument>%api_platform.graphql.graphiql.enabled%</argument>
<argument>%api_platform.graphql.graphql_playground.enabled%</argument>
<argument>%api_platform.swagger.versions%</argument>
<service id="Sylius\Bundle\ApiBundle\OpenApi\Factory\OpenApiFactory" decorates="api_platform.openapi.factory">
<argument type="service" id="Sylius\Bundle\ApiBundle\OpenApi\Factory\OpenApiFactory.inner" />
<argument type="tagged_iterator" tag="sylius.open_api.modifier" />
</service>
<service
id="Sylius\Bundle\ApiBundle\Swagger\AdminAuthenticationTokenDocumentationNormalizer"
decorates="api_platform.swagger.normalizer.documentation"
autoconfigure="false"
decoration-priority="20"
>
<argument type="service" id="Sylius\Bundle\ApiBundle\Swagger\AdminAuthenticationTokenDocumentationNormalizer.inner" />
<service id="Sylius\Bundle\ApiBundle\OpenApi\Documentation\AcceptLanguageHeaderDocumentationModifier">
<argument type="service" id="sylius.repository.locale" />
<tag name="sylius.open_api.modifier" />
</service>
<service id="Sylius\Bundle\ApiBundle\OpenApi\Documentation\AdminAuthenticationTokenDocumentationModifier">
<argument>%sylius.security.new_api_route%</argument>
<tag name="sylius.open_api.modifier" />
</service>
<service
id="Sylius\Bundle\ApiBundle\Swagger\ShopAuthenticationTokenDocumentationNormalizer"
decorates="api_platform.swagger.normalizer.documentation"
autoconfigure="false"
decoration-priority="10"
>
<argument type="service" id="Sylius\Bundle\ApiBundle\Swagger\ShopAuthenticationTokenDocumentationNormalizer.inner" />
<argument>%sylius.security.new_api_route%</argument>
<service id="Sylius\Bundle\ApiBundle\OpenApi\Documentation\PathHiderDocumentationModifier">
<argument>%sylius.api.paths_to_hide%</argument>
<tag name="sylius.open_api.modifier" />
</service>
<service
id="Sylius\Bundle\ApiBundle\Swagger\ProductDocumentationNormalizer"
decorates="api_platform.swagger.normalizer.documentation"
autoconfigure="false"
decoration-priority="20"
>
<argument type="service" id="Sylius\Bundle\ApiBundle\Swagger\ProductDocumentationNormalizer.inner" />
<service id="Sylius\Bundle\ApiBundle\OpenApi\Documentation\ProductDocumentationModifier">
<tag name="sylius.open_api.modifier" />
</service>
<service
id="Sylius\Bundle\ApiBundle\Swagger\ProductImageDocumentationNormalizer"
decorates="api_platform.swagger.normalizer.documentation"
autoconfigure="false"
decoration-priority="20"
>
<argument type="service" id="Sylius\Bundle\ApiBundle\Swagger\ProductImageDocumentationNormalizer.inner" />
<service id="Sylius\Bundle\ApiBundle\OpenApi\Documentation\ProductImageDocumentationModifier">
<argument type="service" id="Sylius\Bundle\ApiBundle\Provider\ProductImageFilterProviderInterface" />
<argument>%sylius.security.new_api_route%</argument>
<tag name="sylius.open_api.modifier" />
</service>
<service
id="Sylius\Bundle\ApiBundle\Swagger\ProductSlugDocumentationNormalizer"
decorates="api_platform.swagger.normalizer.documentation"
autoconfigure="false"
decoration-priority="20"
>
<argument type="service" id="Sylius\Bundle\ApiBundle\Swagger\ProductSlugDocumentationNormalizer.inner" />
<service id="Sylius\Bundle\ApiBundle\OpenApi\Documentation\ProductSlugDocumentationModifier">
<argument>%sylius.security.new_api_route%</argument>
<tag name="sylius.open_api.modifier" />
</service>
<service
id="Sylius\Bundle\ApiBundle\Swagger\ProductVariantDocumentationNormalizer"
decorates="api_platform.swagger.normalizer.documentation"
autoconfigure="false"
decoration-priority="20"
>
<argument type="service" id="Sylius\Bundle\ApiBundle\Swagger\ProductVariantDocumentationNormalizer.inner" />
<service id="Sylius\Bundle\ApiBundle\OpenApi\Documentation\ProductVariantDocumentationModifier">
<tag name="sylius.open_api.modifier" />
</service>
<service
id="Sylius\Bundle\ApiBundle\Swagger\ShippingMethodDocumentationNormalizer"
decorates="api_platform.swagger.normalizer.documentation"
autoconfigure="false"
decoration-priority="20"
>
<argument type="service" id="Sylius\Bundle\ApiBundle\Swagger\ShippingMethodDocumentationNormalizer.inner" />
<service id="Sylius\Bundle\ApiBundle\OpenApi\Documentation\ShippingMethodDocumentationModifier">
<tag name="sylius.open_api.modifier" />
</service>
<service
id="Sylius\Bundle\ApiBundle\Swagger\PathHiderDocumentationNormalizer"
decorates="api_platform.swagger.normalizer.documentation"
autoconfigure="false"
decoration-priority="20"
>
<argument type="service" id="Sylius\Bundle\ApiBundle\Swagger\PathHiderDocumentationNormalizer.inner" />
<argument>%sylius.api.paths_to_hide%</argument>
</service>
<service
id="Sylius\Bundle\ApiBundle\Swagger\AcceptLanguageHeaderDocumentationNormalizer"
decorates="api_platform.swagger.normalizer.documentation"
autoconfigure="false"
decoration-priority="20"
>
<argument type="service" id="Sylius\Bundle\ApiBundle\Swagger\AcceptLanguageHeaderDocumentationNormalizer.inner" />
<argument type="service" id="sylius.repository.locale" />
<service id="Sylius\Bundle\ApiBundle\OpenApi\Documentation\ShopAuthenticationTokenDocumentationModifier">
<argument>%sylius.security.new_api_route%</argument>
<tag name="sylius.open_api.modifier" />
</service>
</services>
</container>

View file

@ -1,65 +0,0 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Swagger;
use Sylius\Component\Locale\Model\LocaleInterface;
use Sylius\Component\Resource\Repository\RepositoryInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/** @experimental */
final class AcceptLanguageHeaderDocumentationNormalizer implements NormalizerInterface
{
public function __construct(
private NormalizerInterface $decoratedNormalizer,
private RepositoryInterface $localeRepository,
) {
}
public function supportsNormalization($data, $format = null): bool
{
return $this->decoratedNormalizer->supportsNormalization($data, $format);
}
public function normalize($object, $format = null, array $context = [])
{
$docs = $this->decoratedNormalizer->normalize($object, $format, $context);
$acceptLanguageHeader = [
'name' => 'Accept-Language',
'in' => 'header',
'required' => false,
'description' => 'Locales in this enum are all locales defined in the shop and only enabled ones will work in the given channel in the shop.',
'schema' => [
'type' => 'string',
'enum' => array_map(
fn (LocaleInterface $locale): string => $locale->getCode(),
$this->localeRepository->findAll(),
),
],
];
foreach ($docs['paths'] as $path => $methods) {
foreach ($methods as $methodName => $methodBody) {
if (is_object($methodBody)) {
$methodBody = $methodBody->getArrayCopy();
$methodBody['parameters'][] = $acceptLanguageHeader;
$docs['paths'][$path][$methodName] = $methodBody;
}
}
}
return $docs;
}
}

View file

@ -1,101 +0,0 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Swagger;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/** @experimental */
final class AdminAuthenticationTokenDocumentationNormalizer implements NormalizerInterface
{
public function __construct(
private NormalizerInterface $decoratedNormalizer,
private string $apiRoute,
) {
}
public function supportsNormalization($data, $format = null): bool
{
return $this->decoratedNormalizer->supportsNormalization($data, $format);
}
public function normalize($object, $format = null, array $context = [])
{
$docs = $this->decoratedNormalizer->normalize($object, $format, $context);
$docs['components']['schemas']['AdminUserToken'] = [
'type' => 'object',
'properties' => [
'token' => [
'type' => 'string',
'readOnly' => true,
],
'adminUser' => [
'type' => 'string',
'readOnly' => true,
],
],
];
$docs['components']['schemas']['AdminUserCredentials'] = [
'type' => 'object',
'properties' => [
'email' => [
'type' => 'string',
'example' => 'api@example.com',
],
'password' => [
'type' => 'string',
'example' => 'sylius-api',
],
],
];
$tokenDocumentation = [
'paths' => [
$this->apiRoute . '/admin/authentication-token' => [
'post' => [
'tags' => ['AdminUserToken'],
'operationId' => 'postCredentialsItem',
'summary' => 'Get JWT token to login.',
'requestBody' => [
'description' => 'Create new JWT Token',
'content' => [
'application/json' => [
'schema' => [
'$ref' => '#/components/schemas/AdminUserCredentials',
],
],
],
],
'responses' => [
Response::HTTP_OK => [
'description' => 'Get JWT token',
'content' => [
'application/json' => [
'schema' => [
'$ref' => '#/components/schemas/AdminUserToken',
],
],
],
],
],
],
],
],
];
return array_merge_recursive($tokenDocumentation, $docs);
}
}

View file

@ -1,46 +0,0 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Swagger;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class PathHiderDocumentationNormalizer implements NormalizerInterface
{
public function __construct(
private NormalizerInterface $decoratedNormalizer,
private array $apiRoutes,
) {
}
public function supportsNormalization($data, $format = null): bool
{
return $this->decoratedNormalizer->supportsNormalization($data, $format);
}
public function normalize($object, $format = null, array $context = [])
{
$docs = $this->decoratedNormalizer->normalize($object, $format, $context);
$paths = (array) $docs['paths'];
foreach ($this->apiRoutes as $apiRoute) {
if (array_key_exists($apiRoute, $paths)) {
unset($paths[$apiRoute]);
}
}
$docs['paths'] = $paths;
return $docs;
}
}

View file

@ -1,45 +0,0 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Swagger;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/** @experimental */
final class ProductDocumentationNormalizer implements NormalizerInterface
{
public function __construct(private NormalizerInterface $decoratedNormalizer)
{
}
public function supportsNormalization($data, $format = null)
{
return $this->decoratedNormalizer->supportsNormalization($data, $format);
}
public function normalize($object, $format = null, array $context = [])
{
$docs = $this->decoratedNormalizer->normalize($object, $format, $context);
$defaultVariantSchema = [
'type' => 'string',
'format' => 'iri-reference',
'nullable' => true,
'readOnly' => true,
];
$docs['components']['schemas']['Product.jsonld-shop.product.read']['properties']['defaultVariant'] = $defaultVariantSchema;
return $docs;
}
}

View file

@ -1,51 +0,0 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Swagger;
use Sylius\Bundle\ApiBundle\Provider\ProductImageFilterProviderInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/** @experimental */
final class ProductImageDocumentationNormalizer implements NormalizerInterface
{
public function __construct(
private NormalizerInterface $decoratedNormalizer,
private ProductImageFilterProviderInterface $filterProvider,
private string $apiRoute,
) {
}
public function supportsNormalization($data, $format = null)
{
return $this->decoratedNormalizer->supportsNormalization($data, $format);
}
public function normalize($object, $format = null, array $context = [])
{
$docs = $this->decoratedNormalizer->normalize($object, $format, $context);
$enums = $this->filterProvider->provideShopFilters();
$enums = array_keys($enums);
$shopProductImagePath = $this->apiRoute . '/shop/product-images/{id}';
foreach ($docs['paths'][$shopProductImagePath]['get']['parameters'] as &$param) {
if ($param['in'] === 'query' && $param['name'] === 'filter') {
$param['schema']['enum'] = $enums;
}
}
return $docs;
}
}

View file

@ -1,50 +0,0 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Swagger;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/** @experimental */
final class ProductSlugDocumentationNormalizer implements NormalizerInterface
{
public function __construct(
private NormalizerInterface $decoratedNormalizer,
private string $apiRoute,
) {
}
public function supportsNormalization($data, $format = null): bool
{
return $this->decoratedNormalizer->supportsNormalization($data, $format);
}
public function normalize($object, $format = null, array $context = [])
{
$docs = $this->decoratedNormalizer->normalize($object, $format, $context);
$shopProductBySlugPath = $this->apiRoute . '/shop/products-by-slug/{slug}';
$params = $docs['paths'][$shopProductBySlugPath]['get']['parameters'];
foreach ($params as $index => $param) {
if ($param['name'] === 'code') {
unset($docs['paths'][$shopProductBySlugPath]['get']['parameters'][$index]);
}
}
// reset key index after unset
$docs['paths'][$shopProductBySlugPath]['get']['parameters'] = array_values($docs['paths'][$shopProductBySlugPath]['get']['parameters']);
return $docs;
}
}

View file

@ -1,53 +0,0 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Swagger;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/** @experimental */
final class ProductVariantDocumentationNormalizer implements NormalizerInterface
{
public function __construct(private NormalizerInterface $decoratedNormalizer)
{
}
public function supportsNormalization($data, $format = null)
{
return $this->decoratedNormalizer->supportsNormalization($data, $format);
}
public function normalize($object, $format = null, array $context = [])
{
$docs = $this->decoratedNormalizer->normalize($object, $format, $context);
$docs['components']['schemas']['ProductVariant.jsonld-shop.product_variant.read']['properties']['price'] = [
'type' => 'integer',
'readOnly' => true,
'default' => 0,
];
$docs['components']['schemas']['ProductVariant.jsonld-shop.product_variant.read']['properties']['inStock'] = [
'type' => 'boolean',
'readOnly' => true,
];
$docs['components']['schemas']['ProductVariant.jsonld-shop.product_variant.read']['properties']['originalPrice'] = [
'type' => 'integer',
'readOnly' => true,
'default' => 0,
];
return $docs;
}
}

View file

@ -1,42 +0,0 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Swagger;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/** @experimental */
final class ShippingMethodDocumentationNormalizer implements NormalizerInterface
{
public function __construct(private NormalizerInterface $decoratedNormalizer)
{
}
public function supportsNormalization($data, $format = null): bool
{
return $this->decoratedNormalizer->supportsNormalization($data, $format);
}
public function normalize($object, $format = null, array $context = [])
{
$docs = $this->decoratedNormalizer->normalize($object, $format, $context);
$docs['components']['schemas']['ShippingMethod.jsonld-shop.shipping_method.read']['properties']['price'] = [
'type' => 'integer',
'readOnly' => true,
'default' => 0,
];
return $docs;
}
}

View file

@ -1,101 +0,0 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Swagger;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/** @experimental */
final class ShopAuthenticationTokenDocumentationNormalizer implements NormalizerInterface
{
public function __construct(
private NormalizerInterface $decoratedNormalizer,
private string $apiRoute,
) {
}
public function supportsNormalization($data, $format = null): bool
{
return $this->decoratedNormalizer->supportsNormalization($data, $format);
}
public function normalize($object, $format = null, array $context = [])
{
$docs = $this->decoratedNormalizer->normalize($object, $format, $context);
$docs['components']['schemas']['ShopUserToken'] = [
'type' => 'object',
'properties' => [
'token' => [
'type' => 'string',
'readOnly' => true,
],
'customer' => [
'type' => 'string',
'readOnly' => true,
],
],
];
$docs['components']['schemas']['ShopUserCredentials'] = [
'type' => 'object',
'properties' => [
'email' => [
'type' => 'string',
'example' => 'shop@example.com',
],
'password' => [
'type' => 'string',
'example' => 'sylius',
],
],
];
$tokenDocumentation = [
'paths' => [
$this->apiRoute . '/shop/authentication-token' => [
'post' => [
'tags' => ['ShopUserToken'],
'operationId' => 'postCredentialsItem',
'summary' => 'Get JWT token to login.',
'requestBody' => [
'description' => 'Create new JWT Token',
'content' => [
'application/json' => [
'schema' => [
'$ref' => '#/components/schemas/ShopUserCredentials',
],
],
],
],
'responses' => [
Response::HTTP_OK => [
'description' => 'Get JWT token',
'content' => [
'application/json' => [
'schema' => [
'$ref' => '#/components/schemas/ShopUserToken',
],
],
],
],
],
],
],
],
];
return array_merge_recursive($docs, $tokenDocumentation);
}
}

View file

@ -14,7 +14,6 @@ declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Tests\DependencyInjection;
use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase;
use Sylius\Bundle\ApiBundle\ApiPlatform\Bridge\Symfony\Bundle\Action\SwaggerUiAction;
use Sylius\Bundle\ApiBundle\DependencyInjection\SyliusApiExtension;
final class SyliusApiExtensionTest extends AbstractExtensionTestCase
@ -24,10 +23,10 @@ final class SyliusApiExtensionTest extends AbstractExtensionTestCase
{
$this->container->setParameter('kernel.bundles_metadata', ['SyliusApiBundle' => ['path' => __DIR__ . '../..']]);
$this->setParameter('api_platform.enable_swagger_ui', true);
$this->setParameter('api_platform.swagger.api_keys', []);
$this->load();
$this->assertContainerBuilderHasService('api_platform.swagger.action.ui', SwaggerUiAction::class);
$this->assertContainerBuilderHasService('Sylius\Bundle\ApiBundle\OpenApi\Documentation\AcceptLanguageHeaderDocumentationModifier');
}
/** @test */
@ -35,20 +34,9 @@ final class SyliusApiExtensionTest extends AbstractExtensionTestCase
{
$this->container->setParameter('kernel.bundles_metadata', ['SyliusApiBundle' => ['path' => __DIR__ . '../..']]);
$this->setParameter('api_platform.enable_swagger_ui', false);
$this->load();
$this->assertContainerBuilderNotHasService('api_platform.swagger.action.ui');
}
/** @test */
public function it_does_not_load_swagger_integration_if_it_does_not_exists(): void
{
$this->container->setParameter('kernel.bundles_metadata', ['SyliusApiBundle' => ['path' => __DIR__ . '../..']]);
$this->load();
$this->assertContainerBuilderNotHasService('api_platform.swagger.action.ui');
$this->assertContainerBuilderNotHasService('Sylius\Bundle\ApiBundle\OpenApi\Documentation\AcceptLanguageHeaderDocumentationModifier');
}
/** @test */

View file

@ -1,142 +0,0 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\ApiBundle\Swagger;
use ApiPlatform\Core\Documentation\Documentation;
use ApiPlatform\Core\Metadata\Resource\ResourceNameCollection;
use PhpSpec\ObjectBehavior;
use Sylius\Component\Locale\Model\LocaleInterface;
use Sylius\Component\Resource\Repository\RepositoryInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final class AcceptLanguageHeaderDocumentationNormalizerSpec extends ObjectBehavior
{
function let(NormalizerInterface $decoratedNormalizer, RepositoryInterface $localeRepository): void
{
$this->beConstructedWith($decoratedNormalizer, $localeRepository);
}
function it_supports_normalization(NormalizerInterface $decoratedNormalizer): void
{
$documentation = new Documentation(new ResourceNameCollection());
$decoratedNormalizer->supportsNormalization($documentation, null)->willReturn(true);
$this->supportsNormalization($documentation)->shouldReturn(true);
}
function it_does_not_support_normalization(NormalizerInterface $decoratedNormalizer): void
{
$decoratedNormalizer->supportsNormalization(null, null)->willReturn(false);
$this->supportsNormalization(null)->shouldReturn(false);
}
function it_does_not_add_accept_language_header_to_paths_which_are_not_objects(
NormalizerInterface $decoratedNormalizer,
RepositoryInterface $localeRepository,
LocaleInterface $locale1,
LocaleInterface $locale2,
): void {
$docs = [
'paths' => [
'/api/v2/admin/addresses/{id}' => [
'get' => [
'parameters' => [],
],
],
],
];
$documentation = new Documentation(new ResourceNameCollection());
$localeRepository->findAll()->willReturn([$locale1, $locale2]);
$locale1->getCode()->willReturn('en_US');
$locale2->getCode()->willReturn('de_DE');
$decoratedNormalizer
->normalize($documentation, null, ['spec_version' => 3])
->willReturn($docs)
;
$this
->normalize($documentation, null, ['spec_version' => 3])
->shouldReturn([
'paths' => [
'/api/v2/admin/addresses/{id}' => [
'get' => [
'parameters' => [],
],
],
],
])
;
}
function it_adds_accept_language_header_to_paths_which_are_objects(
NormalizerInterface $decoratedNormalizer,
RepositoryInterface $localeRepository,
LocaleInterface $locale1,
LocaleInterface $locale2,
): void {
$docs = [
'paths' => [
'/api/v2/admin/addresses/{id}' => [
'get' => new \ArrayObject(
[
'parameters' => [],
],
),
],
],
];
$documentation = new Documentation(new ResourceNameCollection());
$localeRepository->findAll()->willReturn([$locale1, $locale2]);
$locale1->getCode()->willReturn('en_US');
$locale2->getCode()->willReturn('de_DE');
$decoratedNormalizer
->normalize($documentation, null, ['spec_version' => 3])
->willReturn($docs)
;
$this
->normalize($documentation, null, ['spec_version' => 3])
->shouldReturn([
'paths' => [
'/api/v2/admin/addresses/{id}' => [
'get' => [
'parameters' => [
[
'name' => 'Accept-Language',
'in' => 'header',
'required' => false,
'description' => 'Locales in this enum are all locales defined in the shop and only enabled ones will work in the given channel in the shop.',
'schema' => [
'type' => 'string',
'enum' => ['en_US', 'de_DE'],
],
],
],
],
],
],
])
;
}
}

View file

@ -0,0 +1,192 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% if title %}{{ title }} - {% endif %}API Platform</title>
{% block stylesheet %}
<link rel="stylesheet" href="{{ asset('bundles/apiplatform/fonts/open-sans/400.css', assetPackage) }}">
<link rel="stylesheet" href="{{ asset('bundles/apiplatform/fonts/open-sans/700.css', assetPackage) }}">
<link rel="stylesheet" href="{{ asset('bundles/apiplatform/swagger-ui/swagger-ui.css', assetPackage) }}">
<link rel="stylesheet" href="{{ asset('bundles/apiplatform/style.css', assetPackage) }}">
{% endblock %}
{% set oauth_data = {'oauth': swagger_data.oauth|merge({'redirectUrl' : absolute_url(asset('bundles/apiplatform/swagger-ui/oauth2-redirect.html', assetPackage)) })} %}
{# json_encode(65) is for JSON_UNESCAPED_SLASHES|JSON_HEX_TAG to avoid JS XSS #}
<script id="swagger-data" type="application/json">{{ swagger_data|merge(oauth_data)|json_encode(65)|raw }}</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="position:absolute;width:0;height:0">
<defs>
<symbol viewBox="0 0 20 20" id="unlocked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
</symbol>
<symbol viewBox="0 0 20 20" id="locked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"></path>
</symbol>
<symbol viewBox="0 0 20 20" id="close">
<path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"></path>
</symbol>
<symbol viewBox="0 0 20 20" id="large-arrow">
<path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"></path>
</symbol>
<symbol viewBox="0 0 20 20" id="large-arrow-down">
<path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"></path>
</symbol>
<symbol viewBox="0 0 24 24" id="jump-to">
<path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"></path>
</symbol>
<symbol viewBox="0 0 24 24" id="expand">
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"></path>
</symbol>
</defs>
</svg>
<header>
<a id="logo" href="https://api-platform.com"><img src="{{ asset('bundles/apiplatform/logo-header.svg', assetPackage) }}" alt="API Platform"></a>
{# > Sylius #}
<a href="https://docs.sylius.com/en/latest/book/api/introduction.html">
<div style="align-items: center; display: flex; height: 70px; font-size: 14px; color: #fff; font-weight: 700; font-family: sans-serif; padding: 10px 40px; position: fixed; top: 0; left: 0; background: #ed6a5a; z-index: 103;"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-circle" style="margin-right: 10px;"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>EXPERIMENTAL</div>
</a>
{# Sylius < #}
</header>
{% if showWebby %}
<div class="web"><img src="{{ asset('bundles/apiplatform/web.png', assetPackage) }}"></div>
<div class="webby"><img src="{{ asset('bundles/apiplatform/webby.png', assetPackage) }}"></div>
{% endif %}
<div id="swagger-ui" class="api-platform"></div>
<div class="swagger-ui" id="formats">
<div class="information-container wrapper">
<div class="info">
Available formats:
{% for format in formats|keys %}
<a href="{{ path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')|merge({'_format': format})) }}">{{ format }}</a>
{% endfor %}
<br>
Other API docs:
{% set active_ui = app.request.get('ui', 'swagger_ui') %}
{% if swaggerUiEnabled and active_ui != 'swagger_ui' %}<a href="{{ path('api_doc') }}">Swagger UI</a>{% endif %}
{% if reDocEnabled and active_ui != 're_doc' %}<a href="{{ path('api_doc', {'ui': 're_doc'}) }}">ReDoc</a>{% endif %}
{# FIXME: Typo in graphql => graphQl in SwaggerUiAction #}
{% if not graphqlEnabled %}<a href="javascript:alert('GraphQL support is not enabled, see https://api-platform.com/docs/core/graphql/')">GraphiQL</a>{% endif %}
{% if graphiQlEnabled %}<a href="{{ path('api_graphql_graphiql') }}">GraphiQL</a>{% endif %}
{% if graphQlPlaygroundEnabled %}<a href="{{ path('api_graphql_graphql_playground') }}">GraphQL Playground</a>{% endif %}
</div>
</div>
</div>
{% block javascript %}
{% if (reDocEnabled and not swaggerUiEnabled) or (reDocEnabled and 're_doc' == active_ui) %}
<script src="{{ asset('bundles/apiplatform/redoc/redoc.standalone.js', assetPackage) }}"></script>
<script src="{{ asset('bundles/apiplatform/init-redoc-ui.js', assetPackage) }}"></script>
{% else %}
<script src="{{ asset('bundles/apiplatform/swagger-ui/swagger-ui-bundle.js', assetPackage) }}"></script>
<script src="{{ asset('bundles/apiplatform/swagger-ui/swagger-ui-standalone-preset.js', assetPackage) }}"></script>
<script src="{{ asset('bundles/apiplatform/init-swagger-ui.js', assetPackage) }}"></script>
{% endif %}
{# > Sylius #}
<script>
class ClassWatcher {
constructor(targetNode, classToWatch, classAddedCallback, classRemovedCallback) {
this.targetNode = targetNode
this.classToWatch = classToWatch
this.classAddedCallback = classAddedCallback
this.classRemovedCallback = classRemovedCallback
this.observer = null
this.lastClassState = targetNode.classList.contains(this.classToWatch)
this.init()
}
init() {
this.observer = new MutationObserver(this.mutationCallback)
this.observe()
}
observe() {
this.observer.observe(this.targetNode, { attributes: true })
}
disconnect() {
this.observer.disconnect()
}
mutationCallback = mutationsList => {
for(let mutation of mutationsList) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
let currentClassState = mutation.target.classList.contains(this.classToWatch)
if(this.lastClassState !== currentClassState) {
this.lastClassState = currentClassState
if(currentClassState) {
this.classAddedCallback()
}
else {
this.classRemovedCallback()
}
}
}
}
}
}
let tokenRequestContainer = null;
function credentialsRequestReady() {
const tokenRequestContainer2 = document.querySelector('#operations-AdminUserToken-postCredentialsItem').getElementsByClassName('opblock-body')[0];
const executeWrapper = tokenRequestContainer2.getElementsByClassName('execute-wrapper')[0];
new ClassWatcher(executeWrapper, 'btn-group', credentialsRequestFinished, () => {});
}
function credentialsRequestFinished() {
const tokenElement = JSON.parse(tokenRequestContainer.querySelector('.highlight-code').getElementsByTagName('code')[0].innerText)
const buttonLogin = document.querySelector("#swagger-ui > section > div.swagger-ui > div:nth-child(2) > div.scheme-container > section > div > button");
const mouseEvent = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
buttonLogin.dispatchEvent(mouseEvent);
const authFormField = document.querySelector("#swagger-ui > section > div.swagger-ui > div:nth-child(2) > div.scheme-container > section > div > div > div.modal-ux > div > div > div.modal-ux-content > div > form > div:nth-child(1) > div > div:nth-child(5) > section > input[type=text]");
const authorizeSubmitButton = document.querySelector("#swagger-ui > section > div.swagger-ui > div:nth-child(2) > div.scheme-container > section > div > div > div.modal-ux > div > div > div.modal-ux-content > div > form > div.auth-btn-wrapper > button.btn.modal-btn.auth.authorize.button");
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
nativeInputValueSetter.call(authFormField, `Bearer ${tokenElement.token}`);
authFormField.dispatchEvent(new Event('input', { bubbles: true }));
authorizeSubmitButton.dispatchEvent(mouseEvent);
const closeAuthFormButton = document.querySelector("#swagger-ui > section > div.swagger-ui > div:nth-child(2) > div.scheme-container > section > div > div > div.modal-ux > div > div > div.modal-ux-content > div > form > div.auth-btn-wrapper > button.btn.modal-btn.auth.btn-done.button");
closeAuthFormButton.dispatchEvent(mouseEvent);
}
window.addEventListener('load', event => {
tokenRequestContainer = document.querySelector('#operations-AdminUserToken-postCredentialsItem');
new ClassWatcher(tokenRequestContainer, 'is-open', credentialsRequestReady, function(){});
});
</script>
{# Sylius < #}
{% endblock %}
</body>
</html>