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:
2026-01-30 09:55:58 +01:00
parent ddefa927c7
commit 6da5996340
125 changed files with 10032 additions and 0 deletions

186
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,186 @@
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
# =============================================================================
# Backend Tests - PHP 8.5, PHPStan, PHPUnit
# =============================================================================
test-backend:
name: Backend Tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
services:
postgres:
image: postgres:18.1-alpine
env:
POSTGRES_DB: classeo_test
POSTGRES_USER: classeo
POSTGRES_PASSWORD: classeo
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7.4-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.5'
extensions: intl, pdo_pgsql, amqp, redis, zip
coverage: xdebug
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Run PHP CS Fixer (check)
run: composer cs-check
- name: Run PHPStan
run: composer phpstan
- name: Run PHPUnit
run: composer test
env:
DATABASE_URL: postgresql://classeo:classeo@localhost:5432/classeo_test?serverVersion=18
REDIS_URL: redis://localhost:6379
- name: Run BC Isolation Check
working-directory: .
run: ./scripts/check-bc-isolation.sh
# =============================================================================
# Frontend Tests - Vitest, Playwright
# =============================================================================
test-frontend:
name: Frontend Tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
- name: Get pnpm store directory
id: pnpm-cache
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Cache pnpm dependencies
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: ${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run linter
run: pnpm run lint
- name: Run TypeScript check
run: pnpm run check
- name: Run unit tests
run: pnpm run test
- name: Install Playwright browsers
run: pnpm exec playwright install --with-deps
- name: Run E2E tests
run: pnpm run test:e2e
- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: frontend/playwright-report/
retention-days: 7
# =============================================================================
# Naming Conventions Check
# =============================================================================
check-naming:
name: Naming Conventions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Naming Check
run: ./scripts/check-naming.sh
# =============================================================================
# Build Check
# =============================================================================
build:
name: Build Check
runs-on: ubuntu-latest
needs: [test-backend, test-frontend]
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build backend image
uses: docker/build-push-action@v6
with:
context: ./backend
push: false
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build frontend image
uses: docker/build-push-action@v6
with:
context: ./frontend
push: false
cache-from: type=gha
cache-to: type=gha,mode=max

15
.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
# =============================================================================
# Environment files
# =============================================================================
.env.local
.env.*.local
*.env.local
# =============================================================================
# Logs
# =============================================================================
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

145
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,145 @@
# Guide de Contribution - Classeo
## Pre-requis
- Docker Desktop 24+
- Git
## Setup Developpeur
### 1. Cloner et lancer
```bash
git clone https://github.com/ClasseoEdu/classeo.git
cd classeo
docker compose up -d
```
### 2. Verifier le setup
```bash
# Tous les services doivent etre "healthy"
docker compose ps
# Backend repond (port 18000 pour eviter conflit avec services locaux)
curl http://localhost:18000/api
# Frontend repond (port 5174 pour eviter conflit avec services locaux)
curl http://localhost:5174
```
## Regles de Code
> **Important** : Lire le fichier `CLAUDE.md` a la racine du projet pour les conventions
> specifiques (style d'imports PHP, format des messages de commit, etc.).
### PHP Backend
1. **`declare(strict_types=1);`** sur la premiere ligne de chaque fichier
2. **PHPStan level 9** - Zero erreur toleree
3. **Domain = PHP pur** - Aucune dependance Symfony/Doctrine dans `src/*/Domain/`
4. **Value Objects immutables** - `final readonly class`
5. **No null returns** - Utiliser exceptions ou Null Object
### TypeScript Frontend
1. **Strict mode** active
2. **Svelte 5 Runes uniquement** - `$state`, `$derived`, `$effect`
3. **Jamais** `writable()`, `on:click`, `export let` (Svelte 4)
4. **Composants PascalCase** - `MyComponent.svelte`
### Conventions Nommage
| Element | Convention | Exemple |
|---------|-----------|---------|
| Classes PHP | PascalCase | `NoteRepository` |
| Methodes | camelCase | `findByStudent()` |
| Events | FR nom + EN verbe passe | `NoteRecorded` |
| Value Objects | `final readonly class` | `NoteId` |
| Composants Svelte | PascalCase.svelte | `GradeCard.svelte` |
## Workflow Git
### Branches
- `main` - Production
- `develop` - Integration
- `feature/XXX` - Nouvelles fonctionnalites
- `fix/XXX` - Corrections de bugs
### Commits
Format : `type(scope): description`
Types :
- `feat` - Nouvelle fonctionnalite
- `fix` - Correction de bug
- `refactor` - Refactoring
- `docs` - Documentation
- `test` - Ajout de tests
- `chore` - Maintenance
Exemples :
```
feat(auth): add JWT authentication
fix(notes): correct average calculation
refactor(admin): extract user service
```
## Tests
### Avant de commit
```bash
# Backend
docker compose exec php composer phpstan
docker compose exec php composer test
# Frontend
docker compose exec frontend pnpm run lint
docker compose exec frontend pnpm run check
docker compose exec frontend pnpm run test
```
### CI/CD
GitHub Actions execute automatiquement :
- PHPStan level 9
- PHPUnit tests
- ESLint
- TypeScript check
- Vitest
- Playwright E2E
- BC isolation check
- Naming conventions check
## Architecture
### Bounded Contexts
Ne pas creer de dependances directes entre BC. Utiliser :
- **Contracts** pour les interfaces partagees
- **Domain Events** pour la communication async
### Domain Layer
```php
// ✅ CORRECT - Pure PHP
namespace App\Scolarite\Domain\Model;
final readonly class NoteId extends EntityId {}
// ❌ INCORRECT - Framework dependency
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Note {}
```
### Infrastructure Layer
Les mappings Doctrine vont dans `Infrastructure/Persistence/Mapping/`.
## Questions ?
Ouvrir une issue sur GitHub ou contacter l'equipe sur le canal #classeo-dev.

99
Makefile Normal file
View File

@@ -0,0 +1,99 @@
.PHONY: help up down build logs ps test lint phpstan cs-fix frontend-lint frontend-test e2e clean
# Default target
help:
@echo "Classeo - Commandes disponibles"
@echo ""
@echo "Docker:"
@echo " make up - Lancer tous les services"
@echo " make down - Arreter tous les services"
@echo " make build - Reconstruire les images"
@echo " make logs - Voir les logs (Ctrl+C pour quitter)"
@echo " make ps - Statut des services"
@echo " make clean - Supprimer volumes et images"
@echo ""
@echo "Backend:"
@echo " make phpstan - Analyse statique PHPStan"
@echo " make cs-fix - Correction code style PHP"
@echo " make test-php - Tests PHPUnit"
@echo ""
@echo "Frontend:"
@echo " make lint - ESLint frontend"
@echo " make test-js - Tests Vitest"
@echo " make e2e - Tests Playwright"
@echo ""
@echo "All:"
@echo " make test - Tous les tests"
@echo " make check - Tous les linters"
# =============================================================================
# Docker
# =============================================================================
up:
docker compose up -d
down:
docker compose down
build:
docker compose build --no-cache
logs:
docker compose logs -f
ps:
docker compose ps
clean:
docker compose down -v --rmi local
# =============================================================================
# Backend
# =============================================================================
phpstan:
docker compose exec php composer phpstan
cs-fix:
docker compose exec php composer cs-fix
cs-check:
docker compose exec php composer cs-check
test-php:
docker compose exec php composer test
# =============================================================================
# Frontend
# =============================================================================
lint:
docker compose exec frontend pnpm run lint
check-types:
docker compose exec frontend pnpm run check
test-js:
docker compose exec frontend pnpm run test
e2e:
docker compose exec frontend pnpm run test:e2e
# =============================================================================
# All
# =============================================================================
test: test-php test-js
check: phpstan cs-check lint check-types
# =============================================================================
# Scripts
# =============================================================================
check-bc:
./scripts/check-bc-isolation.sh
check-naming:
./scripts/check-naming.sh

140
README.md Normal file
View File

@@ -0,0 +1,140 @@
# Classeo
Application de gestion scolaire moderne - Backend Symfony 8 + Frontend SvelteKit 2.
## Quick Start
### Prerequis
- Docker Desktop 24+ avec Docker Compose 2.20+
- Git
### Lancement
```bash
# Cloner le repo
git clone https://github.com/ClasseoEdu/classeo.git
cd classeo
# Lancer tous les services
docker compose up -d
# Verifier le statut
docker compose ps
```
### URLs
| Service | URL | Description |
|---------|-----|-------------|
| Frontend | http://localhost:5174 | Application SvelteKit |
| Backend API | http://localhost:18000/api | API REST (API Platform) |
| API Docs | http://localhost:18000/api/docs | Documentation OpenAPI |
| RabbitMQ | http://localhost:15672 | Admin (guest/guest) |
| Meilisearch | http://localhost:7700 | Dashboard recherche |
| Mailpit | http://localhost:8025 | Emails de test |
| Mercure | http://localhost:3000/.well-known/mercure | SSE Hub |
## Stack Technique
### Backend
- **PHP 8.5** avec property hooks et asymmetric visibility
- **Symfony 8.0** - Framework DDD-friendly
- **API Platform 4.x** - API REST auto-generee
- **Doctrine ORM 3.x** - Persistence avec mappings separes
- **PHPStan level 9** - Analyse statique stricte
### Frontend
- **SvelteKit 2.x** - SSR, routing, PWA
- **Svelte 5** - Runes (`$state`, `$derived`, `$effect`)
- **TypeScript strict** - Typage fort
- **TanStack Query 5** - Server state management
- **Tailwind CSS 3** - Utility-first CSS
### Infrastructure
- **PostgreSQL 18.1** - Base de donnees
- **Redis 7.4** - Cache + Sessions
- **RabbitMQ 4.2** - Message queue
- **Mercure** - Real-time SSE
- **Meilisearch 1.12** - Full-text search
- **Mailpit** - Email testing
## Architecture
### Bounded Contexts
```
backend/src/
├── Administration/ # Gestion etablissement, utilisateurs
├── Scolarite/ # Notes, classes, emploi du temps
├── VieScolaire/ # Absences, retards, sanctions
├── Communication/ # Messages, notifications
└── Shared/ # Kernel partage (EntityId, DomainEvent, etc.)
```
### Structure DDD
Chaque Bounded Context suit la meme structure :
```
{BC}/
├── Domain/ # Pure PHP - ZERO dependance framework
│ ├── Model/ # Aggregates, Entities, Value Objects
│ ├── Event/ # Domain Events
│ ├── Repository/ # Interfaces repository
│ └── Service/ # Domain Services
├── Application/ # Use cases
│ ├── Command/ # Write operations
│ ├── Query/ # Read operations
│ └── EventHandler/ # Domain event handlers
└── Infrastructure/ # Implementations framework
├── Persistence/ # Doctrine repositories
├── Api/ # API Platform resources
└── Messaging/ # RabbitMQ handlers
```
## Developpement
### Commandes utiles
```bash
# Backend
docker compose exec php composer phpstan # Analyse statique
docker compose exec php composer test # Tests PHPUnit
docker compose exec php composer cs-fix # Correction code style
# Frontend
docker compose exec frontend pnpm run lint # ESLint
docker compose exec frontend pnpm run check # TypeScript check
docker compose exec frontend pnpm run test # Vitest
docker compose exec frontend pnpm run test:e2e # Playwright
```
### Makefile (raccourcis)
```bash
make up # docker compose up -d
make down # docker compose down
make logs # docker compose logs -f
make test # Run all tests
make lint # Run all linters
```
## Tests
- **PHPUnit** - Tests unitaires et integration backend
- **Vitest** - Tests unitaires frontend
- **Playwright** - Tests E2E
## Documentation
- [Architecture Decision Records](./docs/adr/)
- [Contributing Guide](./CONTRIBUTING.md)
- [API Documentation](http://localhost:8000/api/docs)
## Licence
Proprietary - ClasseoEdu

54
backend/.env Normal file
View 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
View 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
View 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
View 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
View 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
View 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.*"
}
}
}

View 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'

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,6 @@
twig:
file_name_pattern: '*.twig'
when@test:
twig:
strict_variables: true

View File

@@ -0,0 +1,8 @@
framework:
validation:
email_validation_mode: html5
when@test:
framework:
validation:
not_compromised_password: false

View File

@@ -0,0 +1,5 @@
controllers:
resource:
path: ../src/
namespace: App\
type: attribute

View File

@@ -0,0 +1,4 @@
api_platform:
resource: .
type: api_platform
prefix: /api

View 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
View 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
View 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']);
};

13
backend/src/Kernel.php Normal file
View 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;
}

View File

View 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;
}
}

View File

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

View 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;
}
}

View 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;
}

View 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();
}
}

View 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();
}
}

View 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();
}
}

View 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());
}
}

View 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
{
}

View 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');
}

199
compose.yaml Normal file
View File

@@ -0,0 +1,199 @@
services:
# =============================================================================
# BACKEND API - PHP 8.5 + FrankenPHP
# =============================================================================
php:
build:
context: ./backend
dockerfile: Dockerfile
target: dev
container_name: classeo_php
environment:
APP_ENV: dev
APP_DEBUG: 1
DATABASE_URL: postgresql://classeo:classeo@db:5432/classeo_master?serverVersion=18&charset=utf8
REDIS_URL: redis://redis:6379
MESSENGER_TRANSPORT_DSN: amqp://guest:guest@rabbitmq:5672/%2f/messages
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
MEILISEARCH_URL: http://meilisearch:7700
MEILISEARCH_API_KEY: masterKey
MAILER_DSN: smtp://mailpit:1025
ports:
- "18000:8000" # Port externe 18000 pour eviter conflit
volumes:
- ./backend:/app:cached
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
rabbitmq:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
# =============================================================================
# FRONTEND - SvelteKit + Node.js
# =============================================================================
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
target: dev
container_name: classeo_frontend
environment:
PUBLIC_API_URL: http://localhost:18000/api
PUBLIC_MERCURE_URL: http://localhost:3000/.well-known/mercure
ports:
- "5174:5173" # Port externe 5174 pour eviter conflit
volumes:
- ./frontend:/app:cached
- frontend_node_modules:/app/node_modules
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:5173/"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
# =============================================================================
# DATABASE - PostgreSQL 18.1
# =============================================================================
db:
image: postgres:18.1-alpine
container_name: classeo_db
environment:
POSTGRES_DB: classeo_master
POSTGRES_USER: classeo
POSTGRES_PASSWORD: classeo
ports:
- "5433:5432" # Port externe 5433 pour eviter conflit avec PostgreSQL local
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U classeo -d classeo_master"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: unless-stopped
# =============================================================================
# CACHE & SESSIONS - Redis 7.4
# =============================================================================
redis:
image: redis:7.4-alpine
container_name: classeo_redis
command: redis-server --appendonly yes
ports:
- "6380:6379" # Port externe 6380 pour eviter conflit avec Redis local
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
restart: unless-stopped
# =============================================================================
# MESSAGE QUEUE - RabbitMQ 4.2
# =============================================================================
rabbitmq:
image: rabbitmq:4.2-management-alpine
container_name: classeo_rabbitmq
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
ports:
- "5672:5672"
- "15672:15672"
volumes:
- rabbitmq_data:/var/lib/rabbitmq
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
restart: unless-stopped
# =============================================================================
# REAL-TIME SSE - Mercure
# =============================================================================
mercure:
image: dunglas/mercure:latest
container_name: classeo_mercure
environment:
MERCURE_PUBLISHER_JWT_KEY: "mercure_publisher_secret_change_me_in_production"
MERCURE_SUBSCRIBER_JWT_KEY: "mercure_subscriber_secret_change_me_in_production"
SERVER_NAME: ":80"
MERCURE_EXTRA_DIRECTIVES: |
cors_origins http://localhost:5174
anonymous
ports:
- "3000:80"
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost/.well-known/mercure"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: unless-stopped
# =============================================================================
# FULL-TEXT SEARCH - Meilisearch 1.12
# =============================================================================
meilisearch:
image: getmeili/meilisearch:v1.12
container_name: classeo_meilisearch
environment:
MEILI_MASTER_KEY: "masterKey"
MEILI_ENV: "development"
ports:
- "7700:7700"
volumes:
- meilisearch_data:/meili_data
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:7700/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: unless-stopped
# =============================================================================
# EMAIL TESTING - Mailpit
# =============================================================================
mailpit:
image: axllent/mailpit:latest
container_name: classeo_mailpit
ports:
- "1025:1025"
- "8025:8025"
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8025"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
restart: unless-stopped
# =============================================================================
# VOLUMES PERSISTANTS
# =============================================================================
volumes:
postgres_data:
redis_data:
rabbitmq_data:
meilisearch_data:
frontend_node_modules:

40
docs/adr/index.md Normal file
View File

@@ -0,0 +1,40 @@
# Architecture Decision Records (ADR)
Ce dossier contient les decisions architecturales du projet Classeo.
## Format
Chaque ADR suit le template :
```markdown
# ADR-XXX: Titre
## Status
Proposed | Accepted | Deprecated | Superseded by [ADR-YYY]
## Context
Description du probleme ou de la situation.
## Decision
La decision prise et pourquoi.
## Consequences
Impact positif et negatif de cette decision.
```
## Index
| # | Titre | Status | Date |
|---|-------|--------|------|
| 001 | [Architecture DDD avec Bounded Contexts](./001-ddd-bounded-contexts.md) | Accepted | 2026-01 |
| 002 | [Svelte 5 Runes Only](./002-svelte5-runes-only.md) | Accepted | 2026-01 |
| 003 | [PHP 8.5 Property Hooks](./003-php85-property-hooks.md) | Accepted | 2026-01 |
## Comment proposer une nouvelle ADR
1. Copier le template `template.md`
2. Nommer `XXX-titre-court.md`
3. Remplir les sections
4. Soumettre en Pull Request
5. Discuter en equipe
6. Merger = Accepted

37
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,37 @@
# =============================================================================
# Dependencies
# =============================================================================
node_modules/
.pnpm-store/
# =============================================================================
# Build output
# =============================================================================
/.svelte-kit/
/build/
dist/
# =============================================================================
# Environment files
# =============================================================================
.env
.env.*
!.env.example
# =============================================================================
# Testing
# =============================================================================
/coverage/
/playwright-report/
/test-results/
# =============================================================================
# PWA
# =============================================================================
dev-dist/
# =============================================================================
# Misc
# =============================================================================
*.local
*.tsbuildinfo

15
frontend/.prettierrc Normal file
View File

@@ -0,0 +1,15 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

Some files were not shown because too many files have changed in this diff Show More