Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IBX-8335: Initial integration with API Platform #114

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
- uses: ramsey/composer-install@v3
with:
dependency-versions: highest
composer-options: "--ignore-platform-reqs --optimize-autoloader"

- name: Run code style check
run: composer run-script check-cs -- --format=checkstyle | cs2pr
Expand Down
13 changes: 11 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"ext-libxml": "*",
"ext-simplexml": "*",
"ext-xmlwriter": "*",
"api-platform/core": "dev-downgraded-deps",
"hautelook/templated-uri-bundle": "^3.4",
"ibexa/core": "~5.0.x-dev",
"lexik/jwt-authentication-bundle": "^2.8",
Expand Down Expand Up @@ -78,7 +79,15 @@
},
"extra": {
"branch-alias": {
"dev-main": "5.0.x-dev"
"dev-main": "5.0.x-dev",
"dev-IBX-8335-full-api-platform": "5.0.x-dev"
}
}
},
"repositories": {
"api-platform/core": {
"type": "vcs",
"url": "https://github.com/tischsoic/core"
}
},
"minimum-stability": "dev"
}
36 changes: 36 additions & 0 deletions src/bundle/ApiPlatform/ClassNameResourceNameCollectionFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Bundle\Rest\ApiPlatform;

use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\ResourceNameCollection;

/**
* @internal
*/
final class ClassNameResourceNameCollectionFactory implements ResourceNameCollectionFactoryInterface
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it be readonly?

{
/**
* @var array<string>
*/
Comment on lines +19 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We tend to use one-liner here.

private array $resources = [];

public function create(): ResourceNameCollection
{
return new ResourceNameCollection($this->resources);
}

/**
* @param array<string> $newResources
*/
public function addResources(array $newResources): void
{
$this->resources = array_merge($this->resources, $newResources);
}
}
147 changes: 147 additions & 0 deletions src/bundle/ApiPlatform/OpenApiFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Bundle\Rest\ApiPlatform;

use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\OpenApi\Model\Response;
use ApiPlatform\OpenApi\OpenApi;
use Symfony\Component\HttpKernel\KernelInterface;

final class OpenApiFactory implements OpenApiFactoryInterface
{
public function __construct(
private readonly OpenApiFactoryInterface $decorated,
private readonly SchemasCollectionFactory $schemaCollectionFactory,
private readonly KernelInterface $kernel,
Comment on lines +19 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cannot the whole class be readonly instead?

) {
}

/**
* @param array<mixed> $context
*/
public function __invoke(array $context = []): OpenApi
{
$openApi = $this->decorated->__invoke($context);
$openApi = $this->addSchemas($openApi);

$this->insertExampleFilesContent($openApi);

return $openApi;
}

private function addSchemas(OpenApi $openApi): OpenApi
{
$schemasCollection = $this->schemaCollectionFactory->create();
$schemas = iterator_to_array($schemasCollection);

$components = $openApi->getComponents();
$components = $components->withSchemas(new \ArrayObject($schemas));

$openApi = $openApi->withComponents($components);

return $openApi;
}

private function insertExampleFilesContent(OpenApi $openApi): void
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Readability is not great, I will split this method into smaller ones.

{
$paths = $openApi->getPaths();

/** @var \ApiPlatform\OpenApi\Model\PathItem $pathItem */
foreach ($paths->getPaths() as $path => $pathItem) {
$newPathItem = $pathItem;

/** @var array<string, \ApiPlatform\OpenApi\Model\Operation|null> $methods */
$methods = [
'GET' => $pathItem->getGet(),
'PUT' => $pathItem->getPut(),
'POST' => $pathItem->getPost(),
'DELETE' => $pathItem->getDelete(),
'OPTIONS' => $pathItem->getOptions(),
'HEAD' => $pathItem->getHead(),
'PATCH' => $pathItem->getPatch(),
'TRACE' => $pathItem->getTrace(),
];
foreach ($methods as $method => $operation) {
if (empty($operation)) {
continue;
}

$responses = $operation->getResponses();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

if ($responses === null) {
continue;
}

$newOperation = $operation;

/**
* @var int $responseCode
* @var \ApiPlatform\OpenApi\Model\Response|array<string, array<mixed>> $response
*/
foreach ($operation->getResponses() as $responseCode => $response) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foreach ($operation->getResponses() as $responseCode => $response) {
foreach ($responses as $responseCode => $response) {

if (!is_array($response) || !array_key_exists('content', $response)) {
continue;
}

$content = $response['content'];
$newContent = $content;

foreach ($newContent as $mediaType => $responseContent) {
if (array_key_exists('x-ibexa-example-file', $responseContent)) {
$exampleFilePath = $this->kernel->locateResource($responseContent['x-ibexa-example-file']);
$exampleFileContent = file_get_contents($exampleFilePath);
$newContent[$mediaType]['example'] = $exampleFileContent;
unset($newContent[$mediaType]['x-ibexa-example-file']);
}
}

if ($newContent !== $content) {
$newOperation = $newOperation->withResponse(
$responseCode,
new Response((string)$responseCode, new \ArrayObject($newContent)),
);
}
}

if ($newOperation !== $operation) {
switch ($method) {
case 'GET':
$newPathItem = $newPathItem->withGet($newOperation);
break;
case 'PUT':
$newPathItem = $newPathItem->withPut($newOperation);
break;
case 'POST':
$newPathItem = $newPathItem->withPost($newOperation);
break;
case 'DELETE':
$newPathItem = $newPathItem->withDelete($newOperation);
break;
case 'OPTIONS':
$newPathItem = $newPathItem->withOptions($newOperation);
break;
case 'HEAD':
$newPathItem = $newPathItem->withHead($newOperation);
break;
case 'PATCH':
$newPathItem = $newPathItem->withPatch($newOperation);
break;
case 'TRACE':
$newPathItem = $newPathItem->withTrace($newOperation);
break;
}
}
}

if ($newPathItem !== $pathItem) {
$paths->addPath($path, $newPathItem);
}
}
}
}
40 changes: 40 additions & 0 deletions src/bundle/ApiPlatform/SchemasCollectionFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Bundle\Rest\ApiPlatform;

use Ibexa\Rest\ApiPlatform\SchemasCollection;
use Ibexa\Rest\ApiPlatform\SchemasCollectionFactoryInterface;
use Ibexa\Rest\ApiPlatform\SchemasProviderInterface;

/**
* @internal
*/
final class SchemasCollectionFactory implements SchemasCollectionFactoryInterface
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps it can be:

Suggested change
final class SchemasCollectionFactory implements SchemasCollectionFactoryInterface
final readonly class SchemasCollectionFactory implements SchemasCollectionFactoryInterface

{
/**
* @var array<SchemasProviderInterface>
*/
Comment on lines +20 to +22
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One line comment here.

private array $providers = [];

public function create(): SchemasCollection
{
$schemas = [];

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

foreach ($this->providers as $provider) {
$schemas = array_merge($schemas, $provider->getSchemas());
}

return new SchemasCollection($schemas);
}

public function addProvider(SchemasProviderInterface $provider): void
{
$this->providers[] = $provider;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
declare(strict_types=1);

namespace Ibexa\Bundle\Rest\DependencyInjection\Compiler;

use Ibexa\Bundle\Rest\ApiPlatform\ClassNameResourceNameCollectionFactory;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class ClassNameResourceNamePass implements CompilerPassInterface
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class ClassNameResourceNamePass implements CompilerPassInterface
final readonly class ClassNameResourceNamePass implements CompilerPassInterface

{
public const API_PLATFORM_RESOURCE_SERVICE_TAG = 'ibexa.api_platform.resource';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public const API_PLATFORM_RESOURCE_SERVICE_TAG = 'ibexa.api_platform.resource';
public const string API_PLATFORM_RESOURCE_SERVICE_TAG = 'ibexa.api_platform.resource';


public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition(ClassNameResourceNameCollectionFactory::class)) {
return;
}

$definition = $container->getDefinition(ClassNameResourceNameCollectionFactory::class);

$taggedServiceIds = $container->findTaggedServiceIds(self::API_PLATFORM_RESOURCE_SERVICE_TAG);
foreach ($taggedServiceIds as $id => $attributes) {
$taggedServiceDefinition = $container->getDefinition($id);
$definition->addMethodCall(
'addResources',
[[$taggedServiceDefinition->getClass()]]
);
}
}
}
35 changes: 35 additions & 0 deletions src/bundle/DependencyInjection/Compiler/SchemaProviderPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
declare(strict_types=1);

namespace Ibexa\Bundle\Rest\DependencyInjection\Compiler;

use Ibexa\Bundle\Rest\ApiPlatform\SchemasCollectionFactory;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class SchemaProviderPass implements CompilerPassInterface
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class SchemaProviderPass implements CompilerPassInterface
final readonly class SchemaProviderPass implements CompilerPassInterface

{
public const API_PLATFORM_SCHEMA_PROVIDER_SERVICE_TAG = 'ibexa.api_platform.schemas_provider';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public const API_PLATFORM_SCHEMA_PROVIDER_SERVICE_TAG = 'ibexa.api_platform.schemas_provider';
public const string API_PLATFORM_SCHEMA_PROVIDER_SERVICE_TAG = 'ibexa.api_platform.schemas_provider';


public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition(SchemasCollectionFactory::class)) {
return;
}

$definition = $container->getDefinition(SchemasCollectionFactory::class);

$taggedServiceIds = $container->findTaggedServiceIds(self::API_PLATFORM_SCHEMA_PROVIDER_SERVICE_TAG);
foreach ($taggedServiceIds as $serviceId => $attributes) {
$definition->addMethodCall(
'addProvider',
[new Reference($serviceId)]
);
}
}
}
16 changes: 16 additions & 0 deletions src/bundle/DependencyInjection/IbexaRestExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
namespace Ibexa\Bundle\Rest\DependencyInjection;

use Ibexa\Bundle\Core\DependencyInjection\Configuration\SiteAccessAware\ConfigurationProcessor;
use Ibexa\Bundle\Rest\DependencyInjection\Compiler\ClassNameResourceNamePass;
use Ibexa\Bundle\Rest\DependencyInjection\Compiler\SchemaProviderPass;
use Ibexa\Rest\ApiPlatform\SchemasProviderInterface;
use Ibexa\Rest\Server\Controller as RestController;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\DependencyInjection\ContainerBuilder;
Expand Down Expand Up @@ -35,11 +39,14 @@ public function getAlias(): string
*/
public function load(array $configs, ContainerBuilder $container)
{
$this->configureApiPlatformAutotagging($container);

$configuration = $this->getConfiguration($configs, $container);
$config = $this->processConfiguration($configuration, $configs);

$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('services.yml');
$loader->load('api_platform.yml');
$loader->load('value_object_visitors.yml');
$loader->load('input_parsers.yml');
$loader->load('security.yml');
Expand Down Expand Up @@ -83,4 +90,13 @@ private function prependJMSTranslation(ContainerBuilder $container): void
],
]);
}

private function configureApiPlatformAutotagging(ContainerBuilder $container): void
{
$container->registerForAutoconfiguration(RestController::class)
->addTag(ClassNameResourceNamePass::API_PLATFORM_RESOURCE_SERVICE_TAG);

$container->registerForAutoconfiguration(SchemasProviderInterface::class)
->addTag(SchemaProviderPass::API_PLATFORM_SCHEMA_PROVIDER_SERVICE_TAG);
}
}
2 changes: 2 additions & 0 deletions src/bundle/IbexaRestBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public function build(ContainerBuilder $container): void
$container->addCompilerPass(new Compiler\InputParserPass());
$container->addCompilerPass(new Compiler\OutputVisitorPass());
$container->addCompilerPass(new Compiler\ValueObjectVisitorPass());
$container->addCompilerPass(new Compiler\ClassNameResourceNamePass());
$container->addCompilerPass(new Compiler\SchemaProviderPass());

if ($container->hasExtension('lexik_jwt_authentication')) {
$container->addCompilerPass(new Compiler\LexikAuthorizationHeaderBridgePass());
Expand Down
Loading
Loading