feat : mise à jour de la structure du projet

This commit is contained in:
2026-04-09 11:02:19 +02:00
parent bcfecb2281
commit 68d62c31ec
69 changed files with 4782 additions and 408 deletions

View File

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Module\Commercial;
final class CommercialModule
{
public const string ID = 'commercial';
public const string LABEL = 'Commercial';
public const bool REQUIRED = false;
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Module\Core\Application\DTO;
use App\Module\Core\Domain\Entity\User;
use DateTimeImmutable;
final readonly class UserOutput
{
public function __construct(
public int $id,
public string $username,
/** @var list<string> */
public array $roles,
public ?DateTimeImmutable $createdAt,
) {}
public static function fromEntity(User $user): self
{
return new self(
id: $user->getId(),
username: $user->getUsername(),
roles: $user->getRoles(),
createdAt: $user->getCreatedAt(),
);
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Module\Core;
final class CoreModule
{
public const string ID = 'core';
public const string LABEL = 'Core';
public const bool REQUIRED = true;
}

View File

@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Entity;
namespace App\Module\Core\Domain\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
@@ -10,9 +10,9 @@ use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Api\Auth\State\MeProvider;
use App\Api\Auth\State\UserPasswordHasherProcessor;
use App\Repository\UserRepository;
use App\Module\Core\Infrastructure\ApiPlatform\State\Processor\UserPasswordHasherProcessor;
use App\Module\Core\Infrastructure\ApiPlatform\State\Provider\MeProvider;
use App\Module\Core\Infrastructure\Doctrine\DoctrineUserRepository;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
@@ -38,7 +38,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
],
denormalizationContext: ['groups' => ['user:write']],
)]
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Entity(repositoryClass: DoctrineUserRepository::class)]
#[ORM\Table(name: '`user`')]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Module\Core\Domain\Event;
use App\Shared\Domain\Event\DomainEventInterface;
use DateTimeImmutable;
final readonly class UserCreated implements DomainEventInterface
{
public function __construct(
public int $userId,
public string $username,
private DateTimeImmutable $occurredAt = new DateTimeImmutable(),
) {}
public function occurredAt(): DateTimeImmutable
{
return $this->occurredAt;
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Module\Core\Domain\Repository;
use App\Module\Core\Domain\Entity\User;
interface UserRepositoryInterface
{
public function findById(int $id): ?User;
public function findByUsername(string $username): ?User;
public function save(User $user): void;
}

View File

@@ -2,11 +2,11 @@
declare(strict_types=1);
namespace App\Api\Auth\State;
namespace App\Module\Core\Infrastructure\ApiPlatform\State\Processor;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\User;
use App\Module\Core\Domain\Entity\User;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

View File

@@ -2,19 +2,18 @@
declare(strict_types=1);
namespace App\Api\Auth\State;
namespace App\Module\Core\Infrastructure\ApiPlatform\State\Provider;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use Symfony\Bundle\SecurityBundle\Security;
/**
* @implements ProviderInterface<\App\Entity\User>
* @implements ProviderInterface<object>
*/
class MeProvider implements ProviderInterface
{
public function __construct(
private readonly Security $security,
private readonly \Symfony\Bundle\SecurityBundle\Security $security,
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?object

View File

@@ -2,9 +2,9 @@
declare(strict_types=1);
namespace App\Command;
namespace App\Module\Core\Infrastructure\Console;
use App\Entity\User;
use App\Module\Core\Domain\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;

View File

@@ -2,9 +2,9 @@
declare(strict_types=1);
namespace App\DataFixtures;
namespace App\Module\Core\Infrastructure\DataFixtures;
use App\Entity\User;
use App\Module\Core\Domain\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Module\Core\Infrastructure\Doctrine;
use App\Module\Core\Domain\Entity\User;
use App\Module\Core\Domain\Repository\UserRepositoryInterface;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<User>
*/
class DoctrineUserRepository extends ServiceEntityRepository implements UserRepositoryInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, User::class);
}
public function findById(int $id): ?User
{
return $this->find($id);
}
public function findByUsername(string $username): ?User
{
return $this->findOneBy(['username' => $username]);
}
public function save(User $user): void
{
$this->getEntityManager()->persist($user);
$this->getEntityManager()->flush();
}
}

View File

@@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<User>
*/
class UserRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, User::class);
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Shared\Application\Bus;
interface CommandBusInterface
{
public function dispatch(object $command): void;
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Shared\Application\Bus;
interface QueryBusInterface
{
public function ask(object $query): mixed;
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Shared\Domain\Contract;
interface TenantAwareInterface
{
public function getTenantId(): ?string;
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Shared\Domain\Contract;
interface UserResolverInterface
{
public function resolve(int $id): ?object;
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Shared\Domain\Event;
use DateTimeImmutable;
interface DomainEventInterface
{
public function occurredAt(): DateTimeImmutable;
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Shared\Domain\ValueObject;
use InvalidArgumentException;
final readonly class Email
{
public readonly string $value;
public function __construct(string $value)
{
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException(sprintf('"%s" is not a valid email address.', $value));
}
$this->value = $value;
}
public function __toString(): string
{
return $this->value;
}
public function equals(self $other): bool
{
return $this->value === $other->value;
}
}

View File

@@ -2,11 +2,11 @@
declare(strict_types=1);
namespace App\Api\Shared\Resource;
namespace App\Shared\Infrastructure\ApiPlatform\Resource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use App\Api\Shared\State\AppVersionProvider;
use App\Shared\Infrastructure\ApiPlatform\State\AppVersionProvider;
#[ApiResource(
operations: [

View File

@@ -0,0 +1,29 @@
<?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;
#[ApiResource(
operations: [
new Get(
uriTemplate: '/modules',
provider: ModulesProvider::class,
),
],
)]
class ModulesResource
{
/** @var list<string> */
public array $modules = [];
/** @param list<string> $modules */
public function __construct(array $modules = [])
{
$this->modules = $modules;
}
}

View File

@@ -0,0 +1,36 @@
<?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\SidebarProvider;
#[ApiResource(
operations: [
new Get(
uriTemplate: '/sidebar',
provider: SidebarProvider::class,
),
],
)]
class SidebarResource
{
/** @var list<array{label: string, icon: string, items: list<array{label: string, to: string, icon: string}>}> */
public array $sections = [];
/** @var list<string> */
public array $disabledRoutes = [];
/**
* @param list<array{label: string, icon: string, items: list<array{label: string, to: string, icon: string}>}> $sections
* @param list<string> $disabledRoutes
*/
public function __construct(array $sections = [], array $disabledRoutes = [])
{
$this->sections = $sections;
$this->disabledRoutes = $disabledRoutes;
}
}

View File

@@ -2,11 +2,11 @@
declare(strict_types=1);
namespace App\Api\Shared\State;
namespace App\Shared\Infrastructure\ApiPlatform\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Api\Shared\Resource\AppVersion;
use App\Shared\Infrastructure\ApiPlatform\Resource\AppVersion;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
/**

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Shared\Infrastructure\ApiPlatform\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Shared\Infrastructure\ApiPlatform\Resource\ModulesResource;
/**
* @implements ProviderInterface<object>
*/
class ModulesProvider implements ProviderInterface
{
/** @var list<string> */
private readonly array $activeModuleIds;
public function __construct()
{
$configPath = dirname(__DIR__, 5).'/config/modules.php';
$moduleClasses = file_exists($configPath) ? require $configPath : [];
$ids = [];
foreach ($moduleClasses as $moduleClass) {
if (defined($moduleClass.'::ID')) {
$ids[] = $moduleClass::ID;
}
}
$this->activeModuleIds = $ids;
}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object
{
return new ModulesResource($this->activeModuleIds);
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace App\Shared\Infrastructure\ApiPlatform\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Shared\Infrastructure\ApiPlatform\Resource\SidebarResource;
/**
* @implements ProviderInterface<object>
*/
class SidebarProvider implements ProviderInterface
{
/** @var list<string> */
private readonly array $activeModuleIds;
/** @var list<array{label: string, icon: string, items: list<array{label: string, to: string, icon: string, module: string}>}> */
private readonly array $sidebarConfig;
public function __construct()
{
$configDir = dirname(__DIR__, 5).'/config';
// Load active modules
$modulesFile = $configDir.'/modules.php';
$moduleClasses = file_exists($modulesFile) ? require $modulesFile : [];
$ids = [];
foreach ($moduleClasses as $moduleClass) {
if (defined($moduleClass.'::ID')) {
$ids[] = $moduleClass::ID;
}
}
$this->activeModuleIds = $ids;
// Load sidebar config
$sidebarFile = $configDir.'/sidebar.php';
$this->sidebarConfig = file_exists($sidebarFile) ? require $sidebarFile : [];
}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object
{
$sections = [];
$disabledRoutes = [];
foreach ($this->sidebarConfig as $section) {
$items = [];
foreach ($section['items'] ?? [] as $item) {
$isActive = in_array($item['module'] ?? null, $this->activeModuleIds, true);
if (!$isActive) {
if (isset($item['to'])) {
$disabledRoutes[] = $item['to'];
}
continue;
}
$items[] = [
'label' => $item['label'],
'to' => $item['to'],
'icon' => $item['icon'],
];
}
if ([] === $items) {
continue;
}
$sections[] = [
'label' => $section['label'],
'icon' => $section['icon'],
'items' => $items,
];
}
return new SidebarResource($sections, array_values(array_unique($disabledRoutes)));
}
}