feat(modules) : expose GET /api/modules and module registry
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Liste ordonnée des modules actifs (classes implémentant App\Shared\Domain\Module\ModuleInterface).
|
||||||
|
* Activer/désactiver un module = ajouter/commenter sa ligne. Exposé par GET /api/modules.
|
||||||
|
*/
|
||||||
|
return [
|
||||||
|
// Aucun module pour l'instant — les modules arrivent à partir du ticket 1.1 (Core).
|
||||||
|
];
|
||||||
@@ -62,6 +62,8 @@ security:
|
|||||||
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
||||||
# Version de l'application en public
|
# Version de l'application en public
|
||||||
- { path: ^/api/version, roles: PUBLIC_ACCESS, methods: [ GET ] }
|
- { path: ^/api/version, roles: PUBLIC_ACCESS, methods: [ GET ] }
|
||||||
|
# Liste des modules actifs en public (consommée au boot du front)
|
||||||
|
- { path: ^/api/modules, roles: PUBLIC_ACCESS, methods: [ GET ] }
|
||||||
- { path: ^/_mcp, roles: PUBLIC_ACCESS, methods: [ GET ] }
|
- { path: ^/_mcp, roles: PUBLIC_ACCESS, methods: [ GET ] }
|
||||||
- { path: ^/_mcp, roles: IS_AUTHENTICATED_FULLY }
|
- { path: ^/_mcp, roles: IS_AUTHENTICATED_FULLY }
|
||||||
# Mail : requiert authentification (le check ROLE_USER est dans MailAccessChecker)
|
# Mail : requiert authentification (le check ROLE_USER est dans MailAccessChecker)
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Shared\Domain\Module;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implemented by every `*Module` declaration class. The set of active modules
|
||||||
|
* is listed in config/modules.php and exposed via GET /api/modules.
|
||||||
|
*/
|
||||||
|
interface ModuleInterface
|
||||||
|
{
|
||||||
|
public static function id(): string;
|
||||||
|
|
||||||
|
public static function label(): string;
|
||||||
|
|
||||||
|
public static function isRequired(): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{code: string, label: string}>
|
||||||
|
*/
|
||||||
|
public static function permissions(): array;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Shared\Domain\Module;
|
||||||
|
|
||||||
|
final class ModuleRegistry
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param list<class-string> $moduleClasses
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function ids(array $moduleClasses): array
|
||||||
|
{
|
||||||
|
$ids = [];
|
||||||
|
foreach ($moduleClasses as $moduleClass) {
|
||||||
|
if (is_a($moduleClass, ModuleInterface::class, true)) {
|
||||||
|
$ids[] = $moduleClass::id();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ids;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Shared\Infrastructure\ApiPlatform\Resource;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use App\Shared\Infrastructure\ApiPlatform\State\ModulesProvider;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(
|
||||||
|
uriTemplate: '/modules',
|
||||||
|
normalizationContext: ['groups' => ['modules:read']],
|
||||||
|
provider: ModulesProvider::class,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)]
|
||||||
|
final class ModulesResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
#[Groups(['modules:read'])]
|
||||||
|
public array $modules = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Shared\Infrastructure\ApiPlatform\State;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use ApiPlatform\State\ProviderInterface;
|
||||||
|
use App\Shared\Domain\Module\ModuleRegistry;
|
||||||
|
use App\Shared\Infrastructure\ApiPlatform\Resource\ModulesResource;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
|
||||||
|
final readonly class ModulesProvider implements ProviderInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[Autowire('%kernel.project_dir%')]
|
||||||
|
private string $projectDir,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ModulesResource
|
||||||
|
{
|
||||||
|
/** @var list<class-string> $classes */
|
||||||
|
$classes = require $this->projectDir.'/config/modules.php';
|
||||||
|
|
||||||
|
$dto = new ModulesResource();
|
||||||
|
$dto->modules = ModuleRegistry::ids($classes);
|
||||||
|
|
||||||
|
return $dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Functional\Shared;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class ModulesEndpointTest extends WebTestCase
|
||||||
|
{
|
||||||
|
public function testModulesEndpointIsPublicAndReturnsModulesKey(): void
|
||||||
|
{
|
||||||
|
$client = self::createClient();
|
||||||
|
$client->request('GET', '/api/modules');
|
||||||
|
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
$data = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
self::assertArrayHasKey('modules', $data);
|
||||||
|
self::assertIsArray($data['modules']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Unit\Shared\Module;
|
||||||
|
|
||||||
|
use App\Shared\Domain\Module\ModuleInterface;
|
||||||
|
use App\Shared\Domain\Module\ModuleRegistry;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class ModuleRegistryTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testIdsExtractsDeclaredModuleIds(): void
|
||||||
|
{
|
||||||
|
$classes = [FakeAlphaModule::class, FakeBetaModule::class];
|
||||||
|
|
||||||
|
self::assertSame(['alpha', 'beta'], ModuleRegistry::ids($classes));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIdsIgnoresClassesNotImplementingModuleInterface(): void
|
||||||
|
{
|
||||||
|
$classes = [FakeAlphaModule::class, stdClass::class];
|
||||||
|
|
||||||
|
self::assertSame(['alpha'], ModuleRegistry::ids($classes));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIdsReturnsEmptyArrayForNoModules(): void
|
||||||
|
{
|
||||||
|
self::assertSame([], ModuleRegistry::ids([]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class FakeAlphaModule implements ModuleInterface
|
||||||
|
{
|
||||||
|
public static function id(): string
|
||||||
|
{
|
||||||
|
return 'alpha';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function label(): string
|
||||||
|
{
|
||||||
|
return 'Alpha';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isRequired(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function permissions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class FakeBetaModule implements ModuleInterface
|
||||||
|
{
|
||||||
|
public static function id(): string
|
||||||
|
{
|
||||||
|
return 'beta';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function label(): string
|
||||||
|
{
|
||||||
|
return 'Beta';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isRequired(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function permissions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user