feat: Setup projet Classeo avec infrastructure Docker et architecture DDD
Configure l'environnement de développement complet avec Docker Compose, structure DDD 4 Bounded Contexts, et pipeline CI/CD GitHub Actions. Corrections compatibilité CI: - Symfony 8 nécessite monolog-bundle ^4.0 (la v3.x ne supporte que jusqu'à Symfony 7) - ESLint v9 nécessite flat config (eslint.config.js) - le format .eslintrc.cjs est obsolète
This commit is contained in:
54
backend/.env
Normal file
54
backend/.env
Normal file
@@ -0,0 +1,54 @@
|
||||
# In all environments, the following files are loaded if they exist,
|
||||
# the latter taking precedence over the former:
|
||||
#
|
||||
# * .env contains default values for the environment variables needed by the app
|
||||
# * .env.local uncommitted file with local overrides
|
||||
# * .env.$APP_ENV committed environment-specific defaults
|
||||
# * .env.$APP_ENV.local uncommitted environment-specific overrides
|
||||
#
|
||||
# Real environment variables win over .env files.
|
||||
#
|
||||
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
|
||||
# https://symfony.com/doc/current/configuration/secrets.html
|
||||
#
|
||||
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_ENV=dev
|
||||
APP_SECRET=change_me_in_production_12345678
|
||||
TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||
TRUSTED_HOSTS='^(localhost|php|127\.0\.0\.1)$'
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
DATABASE_URL="postgresql://classeo:classeo@db:5432/classeo_master?serverVersion=18&charset=utf8"
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
###> symfony/messenger ###
|
||||
MESSENGER_TRANSPORT_DSN=amqp://guest:guest@rabbitmq:5672/%2f/messages
|
||||
###< symfony/messenger ###
|
||||
|
||||
###> lexik/jwt-authentication-bundle ###
|
||||
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
|
||||
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
||||
JWT_PASSPHRASE=classeo_jwt_passphrase_change_me
|
||||
###< lexik/jwt-authentication-bundle ###
|
||||
|
||||
###> redis ###
|
||||
REDIS_URL=redis://redis:6379
|
||||
###< redis ###
|
||||
|
||||
###> mercure ###
|
||||
MERCURE_URL=http://mercure/.well-known/mercure
|
||||
MERCURE_PUBLIC_URL=http://localhost:3000/.well-known/mercure
|
||||
MERCURE_JWT_SECRET=mercure_publisher_secret_change_me_in_production
|
||||
###< mercure ###
|
||||
|
||||
###> meilisearch ###
|
||||
MEILISEARCH_URL=http://meilisearch:7700
|
||||
MEILISEARCH_API_KEY=masterKey
|
||||
###< meilisearch ###
|
||||
|
||||
###> symfony/mailer ###
|
||||
MAILER_DSN=smtp://mailpit:1025
|
||||
###< symfony/mailer ###
|
||||
72
backend/.gitignore
vendored
Normal file
72
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
# =============================================================================
|
||||
# Symfony
|
||||
# =============================================================================
|
||||
/var/
|
||||
/vendor/
|
||||
/.env.local
|
||||
/.env.local.php
|
||||
/.env.*.local
|
||||
/config/secrets/prod/prod.decrypt.private.php
|
||||
/public/bundles/
|
||||
|
||||
# Fichiers auto-générés par Symfony
|
||||
/config/bundles.php
|
||||
/config/preload.php
|
||||
/config/reference.php
|
||||
|
||||
# =============================================================================
|
||||
# Doctrine Fixtures (auto-généré)
|
||||
# =============================================================================
|
||||
/src/DataFixtures/
|
||||
|
||||
# =============================================================================
|
||||
# PHPUnit
|
||||
# =============================================================================
|
||||
/phpunit.xml
|
||||
.phpunit.cache/
|
||||
.phpunit.result.cache
|
||||
|
||||
# =============================================================================
|
||||
# PHPStan
|
||||
# =============================================================================
|
||||
phpstan.neon.dist
|
||||
|
||||
# =============================================================================
|
||||
# PHP CS Fixer
|
||||
# =============================================================================
|
||||
.php-cs-fixer.cache
|
||||
|
||||
# =============================================================================
|
||||
# JWT Keys
|
||||
# =============================================================================
|
||||
/config/jwt/*.pem
|
||||
|
||||
# =============================================================================
|
||||
# Composer
|
||||
# =============================================================================
|
||||
composer.phar
|
||||
composer.lock
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
/.env.local
|
||||
/.env.local.php
|
||||
/.env.*.local
|
||||
/config/secrets/prod/prod.decrypt.private.php
|
||||
/public/bundles/
|
||||
/var/
|
||||
/vendor/
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> friendsofphp/php-cs-fixer ###
|
||||
/.php-cs-fixer.php
|
||||
/.php-cs-fixer.cache
|
||||
###< friendsofphp/php-cs-fixer ###
|
||||
|
||||
###> lexik/jwt-authentication-bundle ###
|
||||
/config/jwt/*.pem
|
||||
###< lexik/jwt-authentication-bundle ###
|
||||
|
||||
###> phpunit/phpunit ###
|
||||
/phpunit.xml
|
||||
/.phpunit.cache/
|
||||
###< phpunit/phpunit ###
|
||||
60
backend/.php-cs-fixer.php
Normal file
60
backend/.php-cs-fixer.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$finder = (new PhpCsFixer\Finder())
|
||||
->in(__DIR__)
|
||||
->exclude('var')
|
||||
->exclude('vendor')
|
||||
// Fichiers auto-générés par Symfony/Doctrine
|
||||
->notPath('config/bundles.php')
|
||||
->notPath('config/preload.php')
|
||||
->notPath('config/reference.php')
|
||||
->notPath('src/DataFixtures/AppFixtures.php')
|
||||
// Exclusions spécifiques
|
||||
->notPath('src/Shared/Domain/AggregateRoot.php')
|
||||
->notPath('src/Shared/Domain/EntityId.php')
|
||||
;
|
||||
|
||||
return (new PhpCsFixer\Config())
|
||||
->setRules([
|
||||
'@Symfony' => true,
|
||||
'@Symfony:risky' => true,
|
||||
'declare_strict_types' => true,
|
||||
'strict_param' => true,
|
||||
'array_syntax' => ['syntax' => 'short'],
|
||||
'ordered_imports' => ['sort_algorithm' => 'alpha'],
|
||||
'no_unused_imports' => true,
|
||||
'not_operator_with_successor_space' => true,
|
||||
'trailing_comma_in_multiline' => true,
|
||||
'phpdoc_order' => true,
|
||||
'phpdoc_separation' => true,
|
||||
'phpdoc_no_empty_return' => true,
|
||||
'native_function_invocation' => [
|
||||
'include' => ['@compiler_optimized'],
|
||||
'scope' => 'namespaced',
|
||||
'strict' => true,
|
||||
],
|
||||
'native_constant_invocation' => true,
|
||||
'global_namespace_import' => [
|
||||
'import_classes' => true,
|
||||
'import_constants' => true,
|
||||
'import_functions' => true,
|
||||
],
|
||||
'final_class' => true,
|
||||
'class_definition' => [
|
||||
'single_line' => true,
|
||||
],
|
||||
'concat_space' => [
|
||||
'spacing' => 'one',
|
||||
],
|
||||
'single_line_throw' => false,
|
||||
// NO Yoda conditions
|
||||
'yoda_style' => false,
|
||||
'blank_line_before_statement' => [
|
||||
'statements' => ['return', 'throw', 'try'],
|
||||
],
|
||||
])
|
||||
->setRiskyAllowed(true)
|
||||
->setFinder($finder)
|
||||
;
|
||||
107
backend/Dockerfile
Normal file
107
backend/Dockerfile
Normal file
@@ -0,0 +1,107 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# =============================================================================
|
||||
# PHP 8.5 + FrankenPHP - Backend Classeo
|
||||
# =============================================================================
|
||||
|
||||
FROM dunglas/frankenphp:1-php8.5-alpine AS base
|
||||
|
||||
# Install system dependencies
|
||||
RUN apk add --no-cache \
|
||||
acl \
|
||||
fcgi \
|
||||
file \
|
||||
gettext \
|
||||
git \
|
||||
icu-dev \
|
||||
libzip-dev \
|
||||
postgresql-dev \
|
||||
rabbitmq-c-dev \
|
||||
linux-headers \
|
||||
$PHPIZE_DEPS
|
||||
|
||||
# Install PHP extensions (opcache is pre-installed in FrankenPHP)
|
||||
RUN docker-php-ext-install intl pdo_pgsql zip sockets
|
||||
|
||||
# Install AMQP extension for RabbitMQ
|
||||
RUN pecl install amqp && docker-php-ext-enable amqp
|
||||
|
||||
# Install Redis extension
|
||||
RUN pecl install redis && docker-php-ext-enable redis
|
||||
|
||||
# Install Composer
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Configure PHP for production
|
||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||
|
||||
# Custom PHP configuration
|
||||
RUN echo "opcache.enable=1" >> "$PHP_INI_DIR/conf.d/opcache.ini" \
|
||||
&& echo "opcache.memory_consumption=256" >> "$PHP_INI_DIR/conf.d/opcache.ini" \
|
||||
&& echo "opcache.interned_strings_buffer=16" >> "$PHP_INI_DIR/conf.d/opcache.ini" \
|
||||
&& echo "opcache.max_accelerated_files=20000" >> "$PHP_INI_DIR/conf.d/opcache.ini" \
|
||||
&& echo "opcache.validate_timestamps=0" >> "$PHP_INI_DIR/conf.d/opcache.ini" \
|
||||
&& echo "realpath_cache_size=4096K" >> "$PHP_INI_DIR/conf.d/opcache.ini" \
|
||||
&& echo "realpath_cache_ttl=600" >> "$PHP_INI_DIR/conf.d/opcache.ini"
|
||||
|
||||
# =============================================================================
|
||||
# Development stage
|
||||
# =============================================================================
|
||||
FROM base AS dev
|
||||
|
||||
# Enable opcache revalidation for dev (zz- prefix loads last alphabetically)
|
||||
RUN echo "opcache.validate_timestamps=1" >> "$PHP_INI_DIR/conf.d/zz-opcache-dev.ini"
|
||||
|
||||
# Enable Xdebug for development
|
||||
RUN pecl install xdebug && docker-php-ext-enable xdebug
|
||||
RUN echo "xdebug.mode=develop,debug,coverage" >> "$PHP_INI_DIR/conf.d/xdebug.ini" \
|
||||
&& echo "xdebug.client_host=host.docker.internal" >> "$PHP_INI_DIR/conf.d/xdebug.ini" \
|
||||
&& echo "xdebug.start_with_request=trigger" >> "$PHP_INI_DIR/conf.d/xdebug.ini"
|
||||
|
||||
# Caddy config for FrankenPHP
|
||||
ENV SERVER_NAME=:8000
|
||||
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
|
||||
|
||||
# Create entrypoint script for dev (installs deps if needed)
|
||||
RUN echo '#!/bin/sh' > /usr/local/bin/docker-entrypoint.sh && \
|
||||
echo 'set -e' >> /usr/local/bin/docker-entrypoint.sh && \
|
||||
echo 'if [ ! -f /app/vendor/autoload.php ]; then' >> /usr/local/bin/docker-entrypoint.sh && \
|
||||
echo ' echo "Installing Composer dependencies..."' >> /usr/local/bin/docker-entrypoint.sh && \
|
||||
echo ' composer install --prefer-dist --no-progress --no-interaction' >> /usr/local/bin/docker-entrypoint.sh && \
|
||||
echo 'fi' >> /usr/local/bin/docker-entrypoint.sh && \
|
||||
echo 'mkdir -p var/cache var/log && chmod -R 777 var' >> /usr/local/bin/docker-entrypoint.sh && \
|
||||
echo 'exec "$@"' >> /usr/local/bin/docker-entrypoint.sh && \
|
||||
chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||
CMD ["frankenphp", "run", "--config", "/etc/caddy/Caddyfile"]
|
||||
|
||||
# =============================================================================
|
||||
# Production stage
|
||||
# =============================================================================
|
||||
FROM base AS prod
|
||||
|
||||
ENV APP_ENV=prod
|
||||
ENV SERVER_NAME=:8000
|
||||
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
|
||||
|
||||
# Copy application files
|
||||
COPY . /app
|
||||
|
||||
# Install dependencies (prod only, optimized)
|
||||
RUN composer install --no-dev --prefer-dist --no-progress --no-interaction --optimize-autoloader
|
||||
|
||||
# Warmup cache
|
||||
RUN php bin/console cache:warmup
|
||||
|
||||
# Ensure var directory exists with proper permissions
|
||||
RUN mkdir -p var/cache var/log && chmod -R 755 var
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["frankenphp", "run", "--config", "/etc/caddy/Caddyfile"]
|
||||
19
backend/bin/console
Executable file
19
backend/bin/console
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Kernel;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
|
||||
if (!is_file(dirname(__DIR__) . '/vendor/autoload_runtime.php')) {
|
||||
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__) . '/vendor/autoload_runtime.php';
|
||||
|
||||
return static function (array $context): Application {
|
||||
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
|
||||
return new Application($kernel);
|
||||
};
|
||||
109
backend/composer.json
Normal file
109
backend/composer.json
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"name": "classeo/backend",
|
||||
"description": "Classeo - Backend API (Symfony 8 + API Platform)",
|
||||
"type": "project",
|
||||
"license": "proprietary",
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.5",
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"ext-intl": "*",
|
||||
"api-platform/core": "^4.0",
|
||||
"doctrine/dbal": "^4.0",
|
||||
"doctrine/doctrine-bundle": "^2.13 || ^3.0@dev",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.4",
|
||||
"doctrine/orm": "^3.3",
|
||||
"lexik/jwt-authentication-bundle": "^3.2",
|
||||
"ramsey/uuid": "^4.7",
|
||||
"symfony/amqp-messenger": "^8.0",
|
||||
"symfony/asset": "^8.0",
|
||||
"symfony/console": "^8.0",
|
||||
"symfony/doctrine-messenger": "^8.0",
|
||||
"symfony/dotenv": "^8.0",
|
||||
"symfony/flex": "^2",
|
||||
"symfony/framework-bundle": "^8.0",
|
||||
"symfony/messenger": "^8.0",
|
||||
"symfony/monolog-bundle": "^4.0",
|
||||
"symfony/property-access": "^8.0",
|
||||
"symfony/property-info": "^8.0",
|
||||
"symfony/runtime": "^8.0",
|
||||
"symfony/security-bundle": "^8.0",
|
||||
"symfony/serializer": "^8.0",
|
||||
"symfony/twig-bundle": "^8.0",
|
||||
"symfony/uid": "^8.0",
|
||||
"symfony/validator": "^8.0",
|
||||
"symfony/yaml": "^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/doctrine-fixtures-bundle": "^4.0",
|
||||
"phpstan/phpstan": "^2.0",
|
||||
"phpstan/phpstan-doctrine": "^2.0",
|
||||
"phpstan/phpstan-symfony": "^2.0",
|
||||
"phpunit/phpunit": "^11.0",
|
||||
"symfony/browser-kit": "^8.0",
|
||||
"symfony/css-selector": "^8.0",
|
||||
"symfony/debug-bundle": "^8.0",
|
||||
"symfony/maker-bundle": "^1.62",
|
||||
"symfony/phpunit-bridge": "^8.0",
|
||||
"symfony/stopwatch": "^8.0",
|
||||
"symfony/web-profiler-bundle": "^8.0",
|
||||
"friendsofphp/php-cs-fixer": "^3.65"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true,
|
||||
"symfony/flex": true,
|
||||
"symfony/runtime": true
|
||||
},
|
||||
"sort-packages": true
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"App\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"replace": {
|
||||
"symfony/polyfill-ctype": "*",
|
||||
"symfony/polyfill-iconv": "*",
|
||||
"symfony/polyfill-php72": "*",
|
||||
"symfony/polyfill-php73": "*",
|
||||
"symfony/polyfill-php74": "*",
|
||||
"symfony/polyfill-php80": "*",
|
||||
"symfony/polyfill-php81": "*",
|
||||
"symfony/polyfill-php82": "*",
|
||||
"symfony/polyfill-php83": "*",
|
||||
"symfony/polyfill-php84": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"auto-scripts": {
|
||||
"cache:clear": "symfony-cmd",
|
||||
"assets:install %PUBLIC_DIR%": "symfony-cmd"
|
||||
},
|
||||
"post-install-cmd": [
|
||||
"@auto-scripts"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@auto-scripts"
|
||||
],
|
||||
"test": "phpunit",
|
||||
"phpstan": "phpstan analyse --memory-limit=512M",
|
||||
"cs-fix": "php-cs-fixer fix",
|
||||
"cs-check": "php-cs-fixer fix --dry-run --diff"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/symfony": "*"
|
||||
},
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "8.0.*"
|
||||
}
|
||||
}
|
||||
}
|
||||
31
backend/config/packages/api_platform.yaml
Normal file
31
backend/config/packages/api_platform.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
api_platform:
|
||||
title: 'Classeo API'
|
||||
description: 'API for Classeo - School Management System'
|
||||
version: '1.0.0'
|
||||
|
||||
# Enable OpenAPI documentation
|
||||
formats:
|
||||
jsonld: ['application/ld+json']
|
||||
json: ['application/json']
|
||||
html: ['text/html']
|
||||
|
||||
docs_formats:
|
||||
jsonld: ['application/ld+json']
|
||||
jsonopenapi: ['application/vnd.openapi+json']
|
||||
html: ['text/html']
|
||||
|
||||
# Defaults
|
||||
defaults:
|
||||
stateless: true
|
||||
cache_headers:
|
||||
vary: ['Content-Type', 'Authorization', 'Origin']
|
||||
extra_properties:
|
||||
standard_put: true
|
||||
rfc_7807_compliant_errors: true
|
||||
pagination_items_per_page: 30
|
||||
|
||||
# Pagination
|
||||
collection:
|
||||
pagination:
|
||||
enabled: true
|
||||
items_per_page_parameter_name: 'itemsPerPage'
|
||||
13
backend/config/packages/cache.yaml
Normal file
13
backend/config/packages/cache.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
framework:
|
||||
cache:
|
||||
# Unique name of your app: used to compute stable namespaces for cache keys.
|
||||
prefix_seed: classeo/backend
|
||||
|
||||
when@prod:
|
||||
framework:
|
||||
cache:
|
||||
pools:
|
||||
doctrine.system_cache_pool:
|
||||
adapter: cache.adapter.system
|
||||
doctrine.result_cache_pool:
|
||||
adapter: cache.adapter.system
|
||||
52
backend/config/packages/doctrine.yaml
Normal file
52
backend/config/packages/doctrine.yaml
Normal file
@@ -0,0 +1,52 @@
|
||||
doctrine:
|
||||
dbal:
|
||||
url: '%env(resolve:DATABASE_URL)%'
|
||||
profiling_collect_backtrace: '%kernel.debug%'
|
||||
|
||||
orm:
|
||||
validate_xml_mapping: true
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
auto_mapping: true
|
||||
mappings:
|
||||
# Infrastructure mappings - keep entities separate from Domain
|
||||
Administration:
|
||||
type: attribute
|
||||
is_bundle: false
|
||||
dir: '%kernel.project_dir%/src/Administration/Infrastructure/Persistence/Mapping'
|
||||
prefix: 'App\Administration\Infrastructure\Persistence\Mapping'
|
||||
alias: Administration
|
||||
Scolarite:
|
||||
type: attribute
|
||||
is_bundle: false
|
||||
dir: '%kernel.project_dir%/src/Scolarite/Infrastructure/Persistence/Mapping'
|
||||
prefix: 'App\Scolarite\Infrastructure\Persistence\Mapping'
|
||||
alias: Scolarite
|
||||
VieScolaire:
|
||||
type: attribute
|
||||
is_bundle: false
|
||||
dir: '%kernel.project_dir%/src/VieScolaire/Infrastructure/Persistence/Mapping'
|
||||
prefix: 'App\VieScolaire\Infrastructure\Persistence\Mapping'
|
||||
alias: VieScolaire
|
||||
Communication:
|
||||
type: attribute
|
||||
is_bundle: false
|
||||
dir: '%kernel.project_dir%/src/Communication/Infrastructure/Persistence/Mapping'
|
||||
prefix: 'App\Communication\Infrastructure\Persistence\Mapping'
|
||||
alias: Communication
|
||||
controller_resolver:
|
||||
auto_mapping: false
|
||||
|
||||
when@test:
|
||||
doctrine:
|
||||
dbal:
|
||||
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
|
||||
|
||||
when@prod:
|
||||
doctrine:
|
||||
orm:
|
||||
query_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.system_cache_pool
|
||||
result_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.result_cache_pool
|
||||
8
backend/config/packages/doctrine_migrations.yaml
Normal file
8
backend/config/packages/doctrine_migrations.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
doctrine_migrations:
|
||||
migrations_paths:
|
||||
'DoctrineMigrations': '%kernel.project_dir%/migrations'
|
||||
enable_profiler: false
|
||||
organize_migrations: none
|
||||
all_or_nothing: true
|
||||
transactional: true
|
||||
check_database_platform: true
|
||||
25
backend/config/packages/framework.yaml
Normal file
25
backend/config/packages/framework.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||
framework:
|
||||
secret: '%env(APP_SECRET)%'
|
||||
csrf_protection: true
|
||||
handle_all_throwables: true
|
||||
http_method_override: false
|
||||
trusted_proxies: '%env(TRUSTED_PROXIES)%'
|
||||
trusted_hosts: '%env(TRUSTED_HOSTS)%'
|
||||
|
||||
# Enables session support
|
||||
session:
|
||||
handler_id: null
|
||||
cookie_secure: auto
|
||||
cookie_samesite: lax
|
||||
storage_factory_id: session.storage.factory.native
|
||||
|
||||
# Enable php_attributes routing
|
||||
php_errors:
|
||||
log: true
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
test: true
|
||||
session:
|
||||
storage_factory_id: session.storage.factory.mock_file
|
||||
17
backend/config/packages/lexik_jwt_authentication.yaml
Normal file
17
backend/config/packages/lexik_jwt_authentication.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
lexik_jwt_authentication:
|
||||
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
|
||||
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
|
||||
pass_phrase: '%env(JWT_PASSPHRASE)%'
|
||||
token_ttl: 3600
|
||||
user_id_claim: username
|
||||
clock_skew: 0
|
||||
|
||||
# Automatically extracts the token from cookies
|
||||
token_extractors:
|
||||
authorization_header:
|
||||
enabled: true
|
||||
prefix: Bearer
|
||||
name: Authorization
|
||||
cookie:
|
||||
enabled: true
|
||||
name: BEARER
|
||||
44
backend/config/packages/messenger.yaml
Normal file
44
backend/config/packages/messenger.yaml
Normal file
@@ -0,0 +1,44 @@
|
||||
framework:
|
||||
messenger:
|
||||
# Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
|
||||
failure_transport: failed
|
||||
|
||||
# Three buses: Command, Query, Event (CQRS + Event-driven)
|
||||
default_bus: command.bus
|
||||
|
||||
buses:
|
||||
command.bus:
|
||||
default_middleware: true
|
||||
middleware:
|
||||
- doctrine_transaction
|
||||
|
||||
query.bus:
|
||||
default_middleware: true
|
||||
|
||||
event.bus:
|
||||
default_middleware:
|
||||
allow_no_handlers: true
|
||||
|
||||
transports:
|
||||
# https://symfony.com/doc/current/messenger.html#transport-configuration
|
||||
async:
|
||||
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
|
||||
options:
|
||||
exchange:
|
||||
name: classeo_messages
|
||||
type: topic
|
||||
queues:
|
||||
messages:
|
||||
binding_keys: ['#']
|
||||
retry_strategy:
|
||||
max_retries: 3
|
||||
delay: 1000
|
||||
multiplier: 2
|
||||
max_delay: 60000
|
||||
|
||||
failed:
|
||||
dsn: 'doctrine://default?queue_name=failed'
|
||||
|
||||
routing:
|
||||
# Route your messages to the transports
|
||||
# 'App\Message\YourMessage': async
|
||||
57
backend/config/packages/monolog.yaml
Normal file
57
backend/config/packages/monolog.yaml
Normal file
@@ -0,0 +1,57 @@
|
||||
monolog:
|
||||
channels:
|
||||
- deprecation
|
||||
- security
|
||||
- audit
|
||||
|
||||
when@dev:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
channels: ["!event"]
|
||||
formatter: monolog.formatter.json
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!console"]
|
||||
|
||||
when@test:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
channels: ["!event"]
|
||||
nested:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
|
||||
when@prod:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
buffer_size: 50
|
||||
nested:
|
||||
type: stream
|
||||
path: php://stderr
|
||||
level: debug
|
||||
formatter: monolog.formatter.json
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine"]
|
||||
deprecation:
|
||||
type: stream
|
||||
channels: [deprecation]
|
||||
path: php://stderr
|
||||
formatter: monolog.formatter.json
|
||||
41
backend/config/packages/security.yaml
Normal file
41
backend/config/packages/security.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
security:
|
||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||
password_hashers:
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||
|
||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||
providers:
|
||||
# used to reload user from session & other features (e.g. switch_user)
|
||||
# Configure user provider when User entity is created
|
||||
users_in_memory:
|
||||
memory:
|
||||
users:
|
||||
admin: { password: 'admin', roles: ['ROLE_ADMIN'] }
|
||||
|
||||
firewalls:
|
||||
dev:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
api:
|
||||
pattern: ^/api
|
||||
stateless: true
|
||||
jwt: ~
|
||||
main:
|
||||
lazy: true
|
||||
provider: users_in_memory
|
||||
|
||||
# Easy way to control access for large sections of your site
|
||||
# Note: Only the *first* access control that matches will be used
|
||||
access_control:
|
||||
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/api/login, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
|
||||
|
||||
when@test:
|
||||
security:
|
||||
password_hashers:
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
||||
algorithm: auto
|
||||
cost: 4 # Lowest possible value for bcrypt
|
||||
time_cost: 3 # Lowest possible value for argon
|
||||
memory_cost: 10 # Lowest possible value for argon
|
||||
6
backend/config/packages/twig.yaml
Normal file
6
backend/config/packages/twig.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
twig:
|
||||
file_name_pattern: '*.twig'
|
||||
|
||||
when@test:
|
||||
twig:
|
||||
strict_variables: true
|
||||
8
backend/config/packages/validator.yaml
Normal file
8
backend/config/packages/validator.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
framework:
|
||||
validation:
|
||||
email_validation_mode: html5
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
validation:
|
||||
not_compromised_password: false
|
||||
5
backend/config/routes.yaml
Normal file
5
backend/config/routes.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
controllers:
|
||||
resource:
|
||||
path: ../src/
|
||||
namespace: App\
|
||||
type: attribute
|
||||
4
backend/config/routes/api_platform.yaml
Normal file
4
backend/config/routes/api_platform.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
api_platform:
|
||||
resource: .
|
||||
type: api_platform
|
||||
prefix: /api
|
||||
27
backend/config/services.yaml
Normal file
27
backend/config/services.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
# This file is the entry point to configure your own services.
|
||||
# Files in the packages/ subdirectory configure your dependencies.
|
||||
|
||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||
parameters:
|
||||
|
||||
services:
|
||||
# default configuration for services in this file
|
||||
_defaults:
|
||||
autowire: true # Automatically injects dependencies in your services.
|
||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||
|
||||
# makes classes in src/ available to be used as services
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
App\:
|
||||
resource: '../src/'
|
||||
exclude:
|
||||
- '../src/DependencyInjection/'
|
||||
- '../src/Entity/'
|
||||
- '../src/Kernel.php'
|
||||
# Exclude Domain layers - they should be pure PHP with no framework deps
|
||||
- '../src/*/Domain/'
|
||||
|
||||
# Domain services need to be registered explicitly to avoid framework coupling
|
||||
# Example: App\Administration\Application\Command\:
|
||||
# resource: '../src/Administration/Application/Command/'
|
||||
12
backend/phpstan.neon
Normal file
12
backend/phpstan.neon
Normal file
@@ -0,0 +1,12 @@
|
||||
parameters:
|
||||
level: 9
|
||||
paths:
|
||||
- src
|
||||
excludePaths:
|
||||
- src/Kernel.php
|
||||
treatPhpDocTypesAsCertain: false
|
||||
reportUnmatchedIgnoredErrors: false
|
||||
|
||||
includes:
|
||||
- vendor/phpstan/phpstan-doctrine/extension.neon
|
||||
- vendor/phpstan/phpstan-symfony/extension.neon
|
||||
11
backend/public/index.php
Normal file
11
backend/public/index.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Kernel;
|
||||
|
||||
require_once dirname(__DIR__) . '/vendor/autoload_runtime.php';
|
||||
|
||||
return static function (array $context): Kernel {
|
||||
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
};
|
||||
0
backend/src/Administration/Domain/Event/.gitkeep
Normal file
0
backend/src/Administration/Domain/Event/.gitkeep
Normal file
0
backend/src/Administration/Domain/Model/.gitkeep
Normal file
0
backend/src/Administration/Domain/Model/.gitkeep
Normal file
0
backend/src/Administration/Domain/Policy/.gitkeep
Normal file
0
backend/src/Administration/Domain/Policy/.gitkeep
Normal file
0
backend/src/Administration/Domain/Service/.gitkeep
Normal file
0
backend/src/Administration/Domain/Service/.gitkeep
Normal file
0
backend/src/Communication/Application/Port/.gitkeep
Normal file
0
backend/src/Communication/Application/Port/.gitkeep
Normal file
0
backend/src/Communication/Domain/Event/.gitkeep
Normal file
0
backend/src/Communication/Domain/Event/.gitkeep
Normal file
0
backend/src/Communication/Domain/Exception/.gitkeep
Normal file
0
backend/src/Communication/Domain/Exception/.gitkeep
Normal file
0
backend/src/Communication/Domain/Model/.gitkeep
Normal file
0
backend/src/Communication/Domain/Model/.gitkeep
Normal file
0
backend/src/Communication/Domain/Policy/.gitkeep
Normal file
0
backend/src/Communication/Domain/Policy/.gitkeep
Normal file
0
backend/src/Communication/Domain/Service/.gitkeep
Normal file
0
backend/src/Communication/Domain/Service/.gitkeep
Normal file
13
backend/src/Kernel.php
Normal file
13
backend/src/Kernel.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||
|
||||
final class Kernel extends BaseKernel
|
||||
{
|
||||
use MicroKernelTrait;
|
||||
}
|
||||
0
backend/src/Scolarite/Application/Command/.gitkeep
Normal file
0
backend/src/Scolarite/Application/Command/.gitkeep
Normal file
0
backend/src/Scolarite/Application/Port/.gitkeep
Normal file
0
backend/src/Scolarite/Application/Port/.gitkeep
Normal file
0
backend/src/Scolarite/Application/Query/.gitkeep
Normal file
0
backend/src/Scolarite/Application/Query/.gitkeep
Normal file
0
backend/src/Scolarite/Domain/Event/.gitkeep
Normal file
0
backend/src/Scolarite/Domain/Event/.gitkeep
Normal file
0
backend/src/Scolarite/Domain/Exception/.gitkeep
Normal file
0
backend/src/Scolarite/Domain/Exception/.gitkeep
Normal file
0
backend/src/Scolarite/Domain/Model/.gitkeep
Normal file
0
backend/src/Scolarite/Domain/Model/.gitkeep
Normal file
0
backend/src/Scolarite/Domain/Policy/.gitkeep
Normal file
0
backend/src/Scolarite/Domain/Policy/.gitkeep
Normal file
0
backend/src/Scolarite/Domain/Repository/.gitkeep
Normal file
0
backend/src/Scolarite/Domain/Repository/.gitkeep
Normal file
0
backend/src/Scolarite/Domain/Service/.gitkeep
Normal file
0
backend/src/Scolarite/Domain/Service/.gitkeep
Normal file
0
backend/src/Scolarite/Infrastructure/Api/.gitkeep
Normal file
0
backend/src/Scolarite/Infrastructure/Api/.gitkeep
Normal file
0
backend/src/Shared/Contracts/.gitkeep
Normal file
0
backend/src/Shared/Contracts/.gitkeep
Normal file
25
backend/src/Shared/Domain/AggregateRoot.php
Normal file
25
backend/src/Shared/Domain/AggregateRoot.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Shared\Domain;
|
||||
|
||||
abstract class AggregateRoot
|
||||
{
|
||||
/** @var DomainEvent[] */
|
||||
private array $domainEvents = [];
|
||||
|
||||
protected function recordEvent(DomainEvent $event): void
|
||||
{
|
||||
$this->domainEvents[] = $event;
|
||||
}
|
||||
|
||||
/** @return DomainEvent[] */
|
||||
public function pullDomainEvents(): array
|
||||
{
|
||||
$events = $this->domainEvents;
|
||||
$this->domainEvents = [];
|
||||
|
||||
return $events;
|
||||
}
|
||||
}
|
||||
12
backend/src/Shared/Domain/Clock.php
Normal file
12
backend/src/Shared/Domain/Clock.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Shared\Domain;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
interface Clock
|
||||
{
|
||||
public function now(): DateTimeImmutable;
|
||||
}
|
||||
35
backend/src/Shared/Domain/CorrelationId.php
Normal file
35
backend/src/Shared/Domain/CorrelationId.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Shared\Domain;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
final readonly class CorrelationId
|
||||
{
|
||||
private function __construct(
|
||||
private string $value,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function generate(): self
|
||||
{
|
||||
return new self(Uuid::uuid4()->toString());
|
||||
}
|
||||
|
||||
public static function fromString(string $value): self
|
||||
{
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
public function value(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
15
backend/src/Shared/Domain/DomainEvent.php
Normal file
15
backend/src/Shared/Domain/DomainEvent.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Shared\Domain;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
interface DomainEvent
|
||||
{
|
||||
public function occurredOn(): DateTimeImmutable;
|
||||
|
||||
public function aggregateId(): UuidInterface;
|
||||
}
|
||||
39
backend/src/Shared/Domain/EntityId.php
Normal file
39
backend/src/Shared/Domain/EntityId.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Shared\Domain;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
/**
|
||||
* @phpstan-consistent-constructor
|
||||
*/
|
||||
abstract readonly class EntityId
|
||||
{
|
||||
protected function __construct(
|
||||
public UuidInterface $value,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function generate(): static
|
||||
{
|
||||
return new static(Uuid::uuid4());
|
||||
}
|
||||
|
||||
public static function fromString(string $value): static
|
||||
{
|
||||
return new static(Uuid::fromString($value));
|
||||
}
|
||||
|
||||
public function equals(EntityId $other): bool
|
||||
{
|
||||
return $this->value->equals($other->value);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value->toString();
|
||||
}
|
||||
}
|
||||
16
backend/src/Shared/Infrastructure/Clock/SystemClock.php
Normal file
16
backend/src/Shared/Infrastructure/Clock/SystemClock.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Shared\Infrastructure\Clock;
|
||||
|
||||
use App\Shared\Domain\Clock;
|
||||
use DateTimeImmutable;
|
||||
|
||||
final readonly class SystemClock implements Clock
|
||||
{
|
||||
public function now(): DateTimeImmutable
|
||||
{
|
||||
return new DateTimeImmutable();
|
||||
}
|
||||
}
|
||||
0
backend/src/Shared/Infrastructure/Tenant/.gitkeep
Normal file
0
backend/src/Shared/Infrastructure/Tenant/.gitkeep
Normal file
0
backend/src/VieScolaire/Application/Port/.gitkeep
Normal file
0
backend/src/VieScolaire/Application/Port/.gitkeep
Normal file
0
backend/src/VieScolaire/Application/Query/.gitkeep
Normal file
0
backend/src/VieScolaire/Application/Query/.gitkeep
Normal file
0
backend/src/VieScolaire/Domain/Event/.gitkeep
Normal file
0
backend/src/VieScolaire/Domain/Event/.gitkeep
Normal file
0
backend/src/VieScolaire/Domain/Exception/.gitkeep
Normal file
0
backend/src/VieScolaire/Domain/Exception/.gitkeep
Normal file
0
backend/src/VieScolaire/Domain/Model/.gitkeep
Normal file
0
backend/src/VieScolaire/Domain/Model/.gitkeep
Normal file
0
backend/src/VieScolaire/Domain/Policy/.gitkeep
Normal file
0
backend/src/VieScolaire/Domain/Policy/.gitkeep
Normal file
0
backend/src/VieScolaire/Domain/Repository/.gitkeep
Normal file
0
backend/src/VieScolaire/Domain/Repository/.gitkeep
Normal file
0
backend/src/VieScolaire/Domain/Service/.gitkeep
Normal file
0
backend/src/VieScolaire/Domain/Service/.gitkeep
Normal file
0
backend/src/VieScolaire/Infrastructure/Api/.gitkeep
Normal file
0
backend/src/VieScolaire/Infrastructure/Api/.gitkeep
Normal file
87
backend/tests/Unit/Shared/Domain/AggregateRootTest.php
Normal file
87
backend/tests/Unit/Shared/Domain/AggregateRootTest.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Shared\Domain;
|
||||
|
||||
use App\Shared\Domain\AggregateRoot;
|
||||
use App\Shared\Domain\DomainEvent;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final class AggregateRootTest extends TestCase
|
||||
{
|
||||
public function testPullDomainEventsReturnsRecordedEvents(): void
|
||||
{
|
||||
$aggregate = new TestAggregate();
|
||||
$event1 = new TestDomainEvent('test1');
|
||||
$event2 = new TestDomainEvent('test2');
|
||||
|
||||
$aggregate->doSomething($event1);
|
||||
$aggregate->doSomething($event2);
|
||||
|
||||
$events = $aggregate->pullDomainEvents();
|
||||
|
||||
$this->assertCount(2, $events);
|
||||
$this->assertSame($event1, $events[0]);
|
||||
$this->assertSame($event2, $events[1]);
|
||||
}
|
||||
|
||||
public function testPullDomainEventsClearsEventsAfterPulling(): void
|
||||
{
|
||||
$aggregate = new TestAggregate();
|
||||
$event = new TestDomainEvent('test');
|
||||
|
||||
$aggregate->doSomething($event);
|
||||
$aggregate->pullDomainEvents();
|
||||
|
||||
$secondPull = $aggregate->pullDomainEvents();
|
||||
|
||||
$this->assertCount(0, $secondPull);
|
||||
}
|
||||
|
||||
public function testRecordEventAddsEventToInternalList(): void
|
||||
{
|
||||
$aggregate = new TestAggregate();
|
||||
$event = new TestDomainEvent('test');
|
||||
|
||||
$aggregate->doSomething($event);
|
||||
$events = $aggregate->pullDomainEvents();
|
||||
|
||||
$this->assertCount(1, $events);
|
||||
$this->assertInstanceOf(TestDomainEvent::class, $events[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Test implementations
|
||||
final class TestAggregate extends AggregateRoot
|
||||
{
|
||||
public function doSomething(DomainEvent $event): void
|
||||
{
|
||||
$this->recordEvent($event);
|
||||
}
|
||||
}
|
||||
|
||||
final readonly class TestDomainEvent implements DomainEvent
|
||||
{
|
||||
private DateTimeImmutable $occurredOn;
|
||||
|
||||
public function __construct(
|
||||
public string $data,
|
||||
private ?UuidInterface $testAggregateId = null,
|
||||
) {
|
||||
$this->occurredOn = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function occurredOn(): DateTimeImmutable
|
||||
{
|
||||
return $this->occurredOn;
|
||||
}
|
||||
|
||||
public function aggregateId(): UuidInterface
|
||||
{
|
||||
return $this->testAggregateId ?? Uuid::uuid4();
|
||||
}
|
||||
}
|
||||
51
backend/tests/Unit/Shared/Domain/CorrelationIdTest.php
Normal file
51
backend/tests/Unit/Shared/Domain/CorrelationIdTest.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Shared\Domain;
|
||||
|
||||
use App\Shared\Domain\CorrelationId;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
final class CorrelationIdTest extends TestCase
|
||||
{
|
||||
public function testGenerateCreatesValidUuid(): void
|
||||
{
|
||||
$correlationId = CorrelationId::generate();
|
||||
|
||||
$this->assertTrue(Uuid::isValid($correlationId->value()));
|
||||
}
|
||||
|
||||
public function testFromStringCreatesCorrelationIdFromValidUuid(): void
|
||||
{
|
||||
$uuid = '550e8400-e29b-41d4-a716-446655440000';
|
||||
$correlationId = CorrelationId::fromString($uuid);
|
||||
|
||||
$this->assertSame($uuid, $correlationId->value());
|
||||
}
|
||||
|
||||
public function testValueReturnsUuidString(): void
|
||||
{
|
||||
$uuid = '550e8400-e29b-41d4-a716-446655440000';
|
||||
$correlationId = CorrelationId::fromString($uuid);
|
||||
|
||||
$this->assertSame($uuid, $correlationId->value());
|
||||
}
|
||||
|
||||
public function testToStringReturnsUuidString(): void
|
||||
{
|
||||
$uuid = '550e8400-e29b-41d4-a716-446655440000';
|
||||
$correlationId = CorrelationId::fromString($uuid);
|
||||
|
||||
$this->assertSame($uuid, (string) $correlationId);
|
||||
}
|
||||
|
||||
public function testGenerateCreatesDifferentIdsEachTime(): void
|
||||
{
|
||||
$id1 = CorrelationId::generate();
|
||||
$id2 = CorrelationId::generate();
|
||||
|
||||
$this->assertNotSame($id1->value(), $id2->value());
|
||||
}
|
||||
}
|
||||
58
backend/tests/Unit/Shared/Domain/EntityIdTest.php
Normal file
58
backend/tests/Unit/Shared/Domain/EntityIdTest.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Shared\Domain;
|
||||
|
||||
use App\Shared\Domain\EntityId;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
final class EntityIdTest extends TestCase
|
||||
{
|
||||
public function testGenerateCreatesValidUuid(): void
|
||||
{
|
||||
$id = TestEntityId::generate();
|
||||
|
||||
$this->assertInstanceOf(TestEntityId::class, $id);
|
||||
$this->assertTrue(Uuid::isValid((string) $id));
|
||||
}
|
||||
|
||||
public function testFromStringCreatesEntityIdFromValidUuid(): void
|
||||
{
|
||||
$uuid = '550e8400-e29b-41d4-a716-446655440000';
|
||||
$id = TestEntityId::fromString($uuid);
|
||||
|
||||
$this->assertSame($uuid, (string) $id);
|
||||
}
|
||||
|
||||
public function testEqualsReturnsTrueForSameValue(): void
|
||||
{
|
||||
$uuid = '550e8400-e29b-41d4-a716-446655440000';
|
||||
$id1 = TestEntityId::fromString($uuid);
|
||||
$id2 = TestEntityId::fromString($uuid);
|
||||
|
||||
$this->assertTrue($id1->equals($id2));
|
||||
}
|
||||
|
||||
public function testEqualsReturnsFalseForDifferentValue(): void
|
||||
{
|
||||
$id1 = TestEntityId::generate();
|
||||
$id2 = TestEntityId::generate();
|
||||
|
||||
$this->assertFalse($id1->equals($id2));
|
||||
}
|
||||
|
||||
public function testToStringReturnsUuidString(): void
|
||||
{
|
||||
$uuid = '550e8400-e29b-41d4-a716-446655440000';
|
||||
$id = TestEntityId::fromString($uuid);
|
||||
|
||||
$this->assertSame($uuid, (string) $id);
|
||||
}
|
||||
}
|
||||
|
||||
// Test concrete implementation
|
||||
final readonly class TestEntityId extends EntityId
|
||||
{
|
||||
}
|
||||
13
backend/tests/bootstrap.php
Normal file
13
backend/tests/bootstrap.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
if (file_exists(dirname(__DIR__) . '/config/bootstrap.php')) {
|
||||
require dirname(__DIR__) . '/config/bootstrap.php';
|
||||
} elseif (method_exists(Dotenv::class, 'bootEnv')) {
|
||||
(new Dotenv())->bootEnv(dirname(__DIR__) . '/.env');
|
||||
}
|
||||
Reference in New Issue
Block a user