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:
186
.github/workflows/ci.yml
vendored
Normal file
186
.github/workflows/ci.yml
vendored
Normal 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
15
.gitignore
vendored
Normal 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
145
CONTRIBUTING.md
Normal 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
99
Makefile
Normal 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
140
README.md
Normal 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
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');
|
||||
}
|
||||
199
compose.yaml
Normal file
199
compose.yaml
Normal 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
40
docs/adr/index.md
Normal 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
37
frontend/.gitignore
vendored
Normal 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
15
frontend/.prettierrc
Normal 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
Reference in New Issue
Block a user