diff --git a/.env b/.env index 40b1e9c..7a9fbac 100644 --- a/.env +++ b/.env @@ -40,3 +40,13 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 ###> symfony/mailer ### MAILER_DSN=null://null ###< symfony/mailer ### + +###> symfony/mercure-bundle ### +# See https://symfony.com/doc/current/mercure.html#configuration +# The URL of the Mercure hub, used by the app to publish updates (can be a local URL) +MERCURE_URL=http://mercure/.well-known/mercure +# The public URL of the Mercure hub, used by the browser to connect +MERCURE_PUBLIC_URL=http://localhost:3000/.well-known/mercure +# The secret used to sign the JWTs +MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!" +###< symfony/mercure-bundle ### diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..4b05943 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,92 @@ +name: Code Quality + +on: + push: + branches: [ main, develop, chore/*, feature/*, fix/* ] + pull_request: + branches: [ main, develop ] + +jobs: + quality-checks: + name: Quality Checks (PHP 8.4) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP 8.4 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, dom, filter, json + coverage: none + tools: composer:v2 + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Cache Composer dependencies + uses: actions/cache@v3 + with: + path: vendor + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress --no-interaction + + - name: Run PHPStan (Level Max) + run: vendor/bin/phpstan analyse --no-progress --error-format=github --memory-limit=512M + + - name: Run PHP-CS-Fixer (Check only) + run: vendor/bin/php-cs-fixer fix --dry-run --diff --verbose + + - name: Run PHPUnit Tests + run: vendor/bin/phpunit --testdox + + - name: Run PHPMD + run: vendor/bin/phpmd src github rulesets.xml + continue-on-error: true + + - name: Run PHPArkitect (Architecture validation) + run: vendor/bin/phparkitect check + + phpunit-coverage: + name: PHPUnit with Coverage + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP 8.4 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, dom, filter, json + coverage: xdebug + tools: composer:v2 + + - name: Cache Composer dependencies + uses: actions/cache@v3 + with: + path: vendor + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress --no-interaction + + - name: Run PHPUnit with Coverage + run: vendor/bin/phpunit --coverage-text --coverage-clover=coverage.xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + files: ./coverage.xml + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..be37840 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,52 @@ +# Quick Reference for Coding Agents + +## Commands +```bash +# Database & Fixtures +make db.reset # Drop, create, migrate database (empty) +make db.refresh # Reset database + load YAML fixtures +make fixtures # Load benchmarks from fixtures/benchmarks/*.yaml + +# Tests +docker-compose run --rm main vendor/bin/phpunit # All tests +docker-compose run --rm main vendor/bin/phpunit tests/Path/To/SpecificTest.php # Single test + +# Code Quality +make phpcsfixer-fix # Fix code style (PSR-12) +make phpstan # Static analysis (level 9) +make quality # Run all checks + fixes + +# Assets (CSS/JS) +make assets.refresh # Force refresh assets: compile, regenerate hashes, clear cache, restart + # Use when CSS/JS changes don't appear in browser + # Don't forget to hard refresh browser (Ctrl+Shift+R) + +# Benchmarks +make run test=Loop iterations=10 # Run specific benchmark +docker-compose run --rm main php bin/console benchmark:run --test=Loop --php-version=php84 + +# Mercure (Real-Time) +./scripts/mercure-verify.sh # Verify Mercure setup +./scripts/mercure-listen.sh # Watch real-time events +./scripts/mercure-test.sh 5 Loop php84 # End-to-end test +``` + +## Code Style +- **PHP 8.4+** with `declare(strict_types=1)` at top of every file +- **PSR-12** + Symfony style enforced by PHP-CS-Fixer +- **Imports**: Fully qualified, alphabetically sorted (classes, functions, constants) +- **Arrays**: Short syntax `[]`, trailing commas in multiline +- **Strings**: Concatenation with ` . ` (spaces around dot) +- **Comparison**: Strict (`===`, `!==`) always, Yoda style (`null === $var`) +- **Classes**: `final readonly` by default, ordered elements (constants → properties → constructor → methods) +- **Types**: Full type hints everywhere (params, returns, properties with asymmetric visibility `public private(set)`) +- **PHPDoc**: Only for interfaces, complex return types (`@return Type[]`), or "why" explanations + +## Architecture (Clean + DDD + Hexagonal) +- **Namespace**: `Jblairy\PhpBenchmark\{Domain|Application|Infrastructure}\...` +- **Dependencies flow INWARD**: Infrastructure → Application → Domain +- **Domain**: Pure PHP, no framework (Symfony/Doctrine), defines Ports (interfaces) +- **Infrastructure**: Implements Adapters for Domain Ports, uses any library +- **Naming**: Ports end with `Port`, Value Objects are nouns, Services are verbs + +See [docs/architecture/01-overview.md](docs/architecture/01-overview.md) and [CLAUDE.md](CLAUDE.md) for details. diff --git a/CLAUDE.md b/CLAUDE.md index 2545ca2..80f0404 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,9 +37,6 @@ This includes: │ ├── 03-ports-adapters.md │ └── 04-execution-flow.md ├── concepts/ - │ ├── clean-architecture.md - │ ├── ddd-patterns.md - │ └── value-objects-vs-entities.md └── guides/ ├── creating-benchmarks.md ├── testing.md @@ -54,6 +51,29 @@ make up # Start all Docker containers make start # Build Docker images ``` +### Database & Fixtures +```bash +# Database management +make db.reset # Drop, create, and migrate database (empty) +make db.refresh # Reset database and load fixtures +make fixtures # Load benchmark fixtures from YAML files + +# Manual commands +docker-compose run --rm main php bin/console doctrine:database:drop --force --if-exists +docker-compose run --rm main php bin/console doctrine:database:create +docker-compose run --rm main php bin/console doctrine:migrations:migrate --no-interaction +docker-compose run --rm main php bin/console doctrine:fixtures:load --no-interaction +``` + +### Mercure Real-Time (Debugging & Testing) +```bash +./scripts/mercure-verify.sh # Verify Mercure configuration +./scripts/mercure-listen.sh # Listen to real-time events (pretty format) +./scripts/mercure-listen.sh "" json # Listen with JSON output +./scripts/mercure-listen.sh "" stats # Show event statistics +./scripts/mercure-test.sh 5 Loop php84 # End-to-end test (5 iterations) +``` + ### Running Benchmarks ```bash # Run all benchmarks across all PHP versions @@ -80,210 +100,79 @@ make quality # Run all quality checks and fixes ### Testing ```bash -docker-compose run --rm main vendor/bin/phpunit -``` - -## Architecture - -This project follows **Clean Architecture** with **Domain-Driven Design (DDD)** and **Hexagonal Architecture (Ports & Adapters)** patterns. - -### 🎯 Core Principles - -1. **Clean Architecture**: Dependencies point inward (Infrastructure → Application → Domain) -2. **DDD**: Business logic is in the Domain layer, isolated from technical details -3. **Hexagonal**: Domain defines Ports (interfaces), Infrastructure provides Adapters (implementations) - -### 📁 Project Structure (Clean Architecture Layers) - -``` -src/ -├── Application/ # Use Cases (orchestration) -│ ├── Service/ -│ │ └── ChartBuilder.php -│ └── UseCase/ -│ ├── AsyncBenchmarkRunner.php -│ └── BenchmarkOrchestrator.php -│ -├── Domain/ # Business Logic (core) -│ ├── Benchmark/ -│ │ ├── Contract/ # Abstractions -│ │ │ ├── AbstractBenchmark.php -│ │ │ └── Benchmark.php (interface) -│ │ ├── Exception/ # Domain exceptions -│ │ ├── Model/ # Value Objects & Domain Models -│ │ │ ├── BenchmarkConfiguration.php -│ │ │ ├── BenchmarkResult.php -│ │ │ └── ExecutionContext.php -│ │ ├── Port/ # Interfaces (Hexagonal Ports) -│ │ │ ├── BenchmarkExecutorPort.php -│ │ │ ├── BenchmarkRepositoryPort.php -│ │ │ ├── CodeExtractorPort.php -│ │ │ ├── ResultPersisterPort.php -│ │ │ └── ScriptExecutorPort.php -│ │ ├── Service/ # Domain Services -│ │ │ └── SingleBenchmarkExecutor.php -│ │ └── Test/ # Benchmark implementations -│ │ ├── Loop.php -│ │ ├── ArrayMap/ -│ │ ├── StringConcatenation/ -│ │ └── ... (40+ benchmarks) -│ └── PhpVersion/ -│ ├── Attribute/ # PHP version targeting (#[Php84], #[All]) -│ └── Enum/ -│ └── PhpVersion.php -│ -└── Infrastructure/ # Technical implementations (adapters) - ├── Cli/ - │ └── BenchmarkCommand.php - ├── Execution/ - │ ├── CodeExtraction/ - │ │ └── ReflectionCodeExtractor.php - │ ├── Docker/ - │ │ └── DockerScriptExecutor.php - │ └── ScriptBuilding/ - │ └── InstrumentedScriptBuilder.php - ├── Persistence/ - │ ├── Doctrine/ - │ │ ├── Entity/ # Doctrine entities - │ │ │ └── Pulse.php - │ │ ├── Repository/ # Doctrine repositories - │ │ │ └── PulseRepository.php - │ │ └── DoctrinePulseResultPersister.php - │ └── InMemory/ - │ └── InMemoryBenchmarkRepository.php - └── Web/ - └── Controller/ - └── DashboardController.php -``` - -### 🔄 Dependency Flow (Hexagonal Architecture) - -**Port (Domain) → Adapter (Infrastructure)** +# Unit tests +make test # Run all tests +docker-compose run --rm main vendor/bin/phpunit tests/Path/To/SpecificTest.php # Single test -| Port (Interface in Domain) | Adapter (Implementation in Infrastructure) | -|----------------------------|---------------------------------------------| -| `CodeExtractorPort` | `ReflectionCodeExtractor` | -| `BenchmarkRepositoryPort` | `InMemoryBenchmarkRepository` | -| `ScriptExecutorPort` | `DockerScriptExecutor` | -| `ResultPersisterPort` | `DoctrinePulseResultPersister` | -| `BenchmarkExecutorPort` | `SingleBenchmarkExecutor` (Domain Service) | +# Mutation testing (tests the quality of your tests) +make infection-report # Run Infection (no threshold, for exploration) +make infection # Run Infection with strict thresholds (MSI >= 80%) -**Configuration in `config/services.yaml`:** -```yaml -Jblairy\PhpBenchmark\Domain\Benchmark\Port\CodeExtractorPort: - class: Jblairy\PhpBenchmark\Infrastructure\Execution\CodeExtraction\ReflectionCodeExtractor +# Manual mutation testing +docker-compose run --rm main phpdbg -qrr vendor/bin/infection --threads=4 --show-mutations ``` -### 🚀 Execution Flow - -``` -1. CLI Command (Infrastructure/Cli/BenchmarkCommand) - ↓ Receives: php bin/console benchmark:run --test=Loop - ↓ Parses options and calls Application layer - -2. Use Case (Application/UseCase/BenchmarkOrchestrator) - ↓ Orchestrates execution - ↓ Creates BenchmarkConfiguration (Domain Model) - ↓ Delegates to AsyncBenchmarkRunner - -3. AsyncBenchmarkRunner (Application/UseCase) - ↓ Uses BenchmarkExecutorPort (Domain Port) - ↓ Runs benchmarks in parallel (Spatie\Async\Pool) - -4. SingleBenchmarkExecutor (Domain/Benchmark/Service) - ↓ Implements BenchmarkExecutorPort - ↓ Uses CodeExtractorPort to extract code - ↓ Uses ScriptExecutorPort to execute - ↓ Returns BenchmarkResult (Value Object) - -5. DockerScriptExecutor (Infrastructure/Execution/Docker) - ↓ Implements ScriptExecutorPort - ↓ Executes in Docker container via docker-compose exec - ↓ Returns execution metrics - -6. DoctrinePulseResultPersister (Infrastructure/Persistence/Doctrine) - ↓ Implements ResultPersisterPort - ↓ Converts BenchmarkResult (Domain) → Pulse (Doctrine Entity) - ↓ Persists to MariaDB via Doctrine ORM -``` +**Note**: Infection requires a code coverage driver (phpdbg or PCOV). The `make` commands use phpdbg which is already available in the container. -### 🏗️ Domain-Driven Design (DDD) Concepts +See [docs/guides/mutation-testing.md](docs/guides/mutation-testing.md) for complete Infection guide. -**Value Objects (Domain/Benchmark/Model/):** -- `BenchmarkConfiguration`: Immutable configuration (benchmark + PHP version + iterations) -- `BenchmarkResult`: Immutable result (execution time + memory usage) -- `ExecutionContext`: Immutable execution context - -**Entities (Infrastructure/Persistence/Doctrine/Entity/):** -- `Pulse`: Doctrine entity with ID, persisted to database +## Architecture -**Domain Services (Domain/Benchmark/Service/):** -- `SingleBenchmarkExecutor`: Coordinates benchmark execution +This project follows **Clean Architecture + DDD + Hexagonal Architecture** (Ports & Adapters). -**Ports (Domain/Benchmark/Port/):** -- Interfaces that define contracts for Infrastructure +📖 **Full documentation**: [docs/architecture/01-overview.md](docs/architecture/01-overview.md) -**Adapters (Infrastructure/):** -- Concrete implementations of Ports +## Database & Fixtures -### 📝 Creating Benchmarks +The project uses MariaDB 10.11 via Docker with Doctrine ORM for entity management. -Benchmarks live in `src/Domain/Benchmark/Test/` and must: +### Database Structure -1. **Extend `AbstractBenchmark`** (implements `Benchmark` interface) -2. **Use PHP version attributes** to specify compatibility: - - `#[All]` - Run on all PHP versions - - `#[Php73]`, `#[Php74]`, `#[Php84]`, etc. - Run on specific versions - - Multiple attributes can be used on different methods +**Entities:** +- **Benchmark** (`benchmarks` table) - Stores benchmark code and metadata loaded from YAML fixtures + - Fields: slug, name, category, description, code, phpVersions, tags, icon + - Loaded from `fixtures/benchmarks/*.yaml` files + +- **Pulse** (`pulse` table) - Stores benchmark execution results + - Fields: benchId, name, phpVersion, executionTimeMs, memoryUsedBytes, memoryPeakByte + - Created during benchmark execution -**Example:** -```php -namespace Jblairy\PhpBenchmark\Domain\Benchmark\Test; +### Fixtures System -use Jblairy\PhpBenchmark\Domain\Benchmark\Contract\AbstractBenchmark; -use Jblairy\PhpBenchmark\Domain\PhpVersion\Attribute\All; +Benchmarks are now defined as YAML files in `fixtures/benchmarks/` and loaded into the database: -final class Loop extends AbstractBenchmark -{ - #[All] - public function execute(): void - { - $x = []; - for ($i = 0; 100000 > $i; ++$i) { - $x[] = $i * 2; - } - } -} +**Example fixture** (`fixtures/benchmarks/array-fill-benchmark.yaml`): +```yaml +slug: array-fill +name: 'Array Fill' +category: 'Array Operations' +description: 'Fill array using array_fill() function' +icon: 📦 +tags: + - array + - fill +phpVersions: + - php56 + - php70 + - php84 + - php85 +code: | + for ($i = 0; $i < 10000; $i++) { + $array = array_fill(0, 100, 'value'); + } ``` -### 🐳 PHP Version System - -- **PhpVersion Enum** (`src/Domain/PhpVersion/Enum/PhpVersion.php`) - Defines available PHP versions -- **Version Attributes** (`src/Domain/PhpVersion/Attribute/`) - PHP 5.6 through 8.5, plus `All.php` -- **Docker Services** (`docker-compose.yml`) - Each PHP version runs as isolated container: - - Shared volume mount at `/srv/php_benchmark` - - 512MB memory limit - - 1 CPU limit - - `tail -f /dev/null` to keep running - -### 📊 Data Layer - -**Domain Models (Value Objects):** -- `BenchmarkResult` - Immutable result object - -**Infrastructure Entities:** -- `Pulse` (src/Infrastructure/Persistence/Doctrine/Entity/Pulse.php) - Doctrine entity for database - - Fields: `id`, `benchId`, `name`, `phpVersion`, `executionTimeMs`, `memoryUsedBytes`, `memoryPeakByte` - -**Dashboard:** -- `DashboardController` (src/Infrastructure/Web/Controller/DashboardController.php) - Web UI at `/dashboard` - - Aggregates benchmark results by test and PHP version - - Calculates percentiles (P50, P80, P90, P95, P99) and averages - - Generates charts via `ChartBuilder` +**Loading fixtures:** +```bash +make fixtures # Load all YAML files from fixtures/benchmarks/ +make db.refresh # Reset database + load fixtures +``` -## Database +### Migrations -The project uses MariaDB 10.11 via Docker. Doctrine ORM is configured for entity management and migrations are in `migrations/`. +Doctrine migrations are in `migrations/`. Current migrations: +- `Version20251103175019` - Creates `benchmarks` table +- Earlier versions - Create `pulse` table and related schema ## Code Standards @@ -301,6 +190,107 @@ The project uses MariaDB 10.11 via Docker. Doctrine ORM is configured for entity - **Docker** for PHP version isolation - **MariaDB 10.11** database +### Code Comments and Documentation + +**Principle: Self-documenting code over comments** + +#### ✅ DO: Write expressive, well-named code + +```php +// ✅ GOOD - Self-explanatory +final readonly class StatisticsCalculator +{ + public function calculatePercentile(array $sortedValues, int $percentile): float + { + $index = (int) ceil(($percentile / 100) * count($sortedValues)) - 1; + return $sortedValues[max(0, $index)]; + } +} +``` + +#### ❌ DON'T: Use comments to explain what code does + +```php +// ❌ BAD - Comment explains poor naming +// Calculate the p value from the arr +public function calc(array $arr, int $p): float +{ + // Get the index + $i = (int) ceil(($p / 100) * count($arr)) - 1; + // Return the value + return $arr[max(0, $i)]; +} +``` + +#### ✅ WHEN to use comments + +Comments are **acceptable and encouraged** for: + +1. **"Why" not "What"** - Explain business decisions or complex algorithms + ```php + // Using P90 instead of average to avoid outliers skewing benchmark results + $p90 = $this->calculatePercentile($times, 90); + ``` + +2. **API documentation** - Public interfaces and contracts (PHPDoc) + ```php + /** + * Port for accessing dashboard data + */ + interface DashboardRepositoryPort + { + /** + * @return BenchmarkMetrics[] Grouped by benchmark ID and PHP version + */ + public function getAllBenchmarkMetrics(): array; + } + ``` + +3. **Class-level documentation** - Purpose and responsibility + ```php + /** + * Doctrine adapter implementing DashboardRepositoryPort + * + * Follows Dependency Inversion Principle: implements interface from Domain + */ + final readonly class DoctrineDashboardRepository implements DashboardRepositoryPort + ``` + +4. **Non-obvious workarounds** - Technical constraints or edge cases + ```php + // PHP 5.6 doesn't support ** operator, must use pow() + if ($phpVersion === 'php56') { + return pow($base, $exponent); + } + ``` + +#### ❌ AVOID these comments + +- **Obvious comments**: `// Set the name` above `$this->name = $name;` +- **Commented-out code**: Delete it (use git history if needed) +- **Redundant documentation**: Repeating method signature in PHPDoc + ```php + // ❌ BAD + /** + * Get dashboard data + * @return DashboardData The dashboard data + */ + public function getDashboardData(): DashboardData + ``` + +- **TODO/FIXME without context**: Always add reason and date + ```php + // ❌ BAD: TODO fix this + // ✅ GOOD: TODO (2024-10-15): Refactor to use async for performance + ``` + +#### Best Practices + +1. **Prefer refactoring over commenting** - If code needs a comment to be understood, consider refactoring +2. **Use meaningful names** - `calculatePercentile()` > `calc()` + comment +3. **Extract complex logic** - Into well-named private methods or Value Objects +4. **Keep comments up-to-date** - Outdated comments are worse than no comments + ### Quality Commands ```bash @@ -367,7 +357,6 @@ Jblairy\PhpBenchmark\ - [Ports & Adapters](docs/architecture/03-ports-adapters.md) - Hexagonal architecture **Concepts:** -- [Value Objects vs Entities](docs/concepts/value-objects-vs-entities.md) - DDD patterns explained **Guides:** - [Creating Benchmarks](docs/guides/creating-benchmarks.md) - Step-by-step tutorial diff --git a/Dockerfile.php85 b/Dockerfile.php85 index be7b175..4010900 100644 --- a/Dockerfile.php85 +++ b/Dockerfile.php85 @@ -1,6 +1,6 @@ FROM debian:bullseye -# 📦 Installer les dépendances nécessaires +# Install required dependencies RUN apt update && apt install -y \ build-essential \ libxml2-dev \ @@ -20,15 +20,15 @@ RUN apt update && apt install -y \ bzip2 \ git -# 📥 Télécharger et extraire PHP 8.5.0 Alpha 1 +# Download and extract PHP 8.5.0 WORKDIR /usr/src -RUN wget https://downloads.php.net/~daniels/php-8.5.0alpha1.tar.xz && \ - tar -xf php-8.5.0alpha1.tar.xz && \ - rm php-8.5.0alpha1.tar.xz +RUN wget https://downloads.php.net/~daniels/php-8.5.0RC3.tar.xz && \ + tar -xf php-8.5.0RC3.tar.xz && \ + rm php-8.5.0RC3.tar.xz -WORKDIR /usr/src/php-8.5.0alpha1 +WORKDIR /usr/src/php-8.5.0RC3 -# ⚙️ Configurer, compiler et installer +# Configure, compile and install RUN ./configure --prefix=/usr/local/php8.5 \ --with-openssl \ --enable-mbstring \ @@ -37,8 +37,8 @@ RUN ./configure --prefix=/usr/local/php8.5 \ make -j$(nproc) && \ make install -# 🧪 Ajouter PHP au PATH +# Add PHP to PATH ENV PATH="/usr/local/php8.5/bin:${PATH}" -# 🔍 Vérification +# Verification CMD ["php", "-v"] diff --git a/Makefile b/Makefile index 889b35e..58312b3 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: up start run phpcsfixer phpcsfixer-fix phpstan quality phpmd phparkitect +.PHONY: up start run fixtures db.reset db.refresh phpcsfixer phpcsfixer-fix phpstan quality phpmd phparkitect infection assets.refresh up: docker-compose up -d --remove-orphans @@ -22,6 +22,26 @@ run: docker-compose run --rm main php bin/console benchmark:run --test=$(test) --iterations=$(or $(iterations),1) --php-version=$(version); \ fi +# Load fixtures into database from YAML files +fixtures: + @echo "🔄 Loading fixtures from fixtures/benchmarks/*.yaml..." + @docker-compose exec main php bin/console doctrine:fixtures:load --no-interaction + @echo "✅ Fixtures loaded successfully" + +# Reset database (drop, create, migrate) - without fixtures +db.reset: + @echo "🗑️ Dropping database..." + @docker-compose run --rm main php bin/console d:d:d --force --if-exists + @echo "📦 Creating database..." + @docker-compose run --rm main php bin/console d:d:c + @echo "🔄 Running migrations..." + @docker-compose run --rm main php bin/console d:m:m --no-interaction + @echo "✅ Database reset complete" + +# Reset database and load fixtures (full refresh) +db.refresh: db.reset fixtures + @echo "✅ Database refreshed with fixtures" + phpcsfixer: docker-compose run --rm main vendor/bin/php-cs-fixer fix --dry-run --diff @@ -29,7 +49,7 @@ phpcsfixer-fix: docker-compose run --rm main vendor/bin/php-cs-fixer fix phpstan: - docker-compose run --rm main vendor/bin/phpstan analyse + docker-compose run --rm main vendor/bin/phpstan analyse --memory-limit=512M phpmd: docker-compose run --rm main vendor/bin/phpmd ./src ansi rulesets.xml @@ -37,4 +57,48 @@ phpmd: phparkitect: docker-compose run --rm main vendor/bin/phparkitect check +rector: + docker-compose run --rm main vendor/bin/rector + +test: + docker-compose run --rm main vendor/bin/phpunit + +test-coverage: + @echo "📊 Generating code coverage..." + docker-compose run --rm main phpdbg -qrr vendor/bin/phpunit --coverage-xml=var/coverage/coverage-xml --log-junit=var/coverage/junit.xml + +infection: + @echo "🧬 Running Infection mutation testing..." + @echo "⚠️ This may take several minutes..." + @echo "📊 Step 1/2: Generating code coverage with PHPUnit..." + @docker-compose run --rm main phpdbg -qrr vendor/bin/phpunit --coverage-xml=var/coverage/coverage-xml --log-junit=var/coverage/junit.xml + @echo "🧬 Step 2/2: Running mutations..." + docker-compose run --rm main vendor/bin/infection --coverage=var/coverage --threads=4 --show-mutations --min-msi=80 --min-covered-msi=85 + +infection-report: + @echo "🧬 Running Infection mutation testing (report only, no MSI threshold)..." + @echo "📊 Step 1/2: Generating code coverage with PHPUnit..." + @docker-compose run --rm main phpdbg -qrr vendor/bin/phpunit --coverage-xml=var/coverage/coverage-xml --log-junit=var/coverage/junit.xml + @echo "🧬 Step 2/2: Running mutations..." + docker-compose run --rm main vendor/bin/infection --coverage=var/coverage --threads=4 --show-mutations + quality: phpcsfixer-fix phpstan phpmd phparkitect + +# Force refresh assets (CSS/JS) and invalidate browser cache +# Useful when CSS/JS changes are not reflected in the browser +# This command: +# 1. Compiles all assets (SCSS → CSS, etc.) +# 2. Deletes compiled assets to force new hash generation +# 3. Clears Symfony cache +# 4. Restarts the main container for a clean state +assets.refresh: + @echo "🎨 Compiling assets..." + @docker-compose exec main php bin/console asset-map:compile + @echo "🗑️ Removing compiled assets to force hash regeneration..." + @rm -rf public/assets + @echo "🔄 Clearing Symfony cache..." + @docker-compose exec main php bin/console cache:clear + @echo "🔄 Restarting main container..." + @docker-compose restart main + @echo "✅ Assets refreshed! New CSS/JS hashes generated." + @echo "💡 Hard refresh your browser (Ctrl+Shift+R or Cmd+Shift+R) to see changes." diff --git a/README.md b/README.md index 9399bd2..2073dc0 100644 --- a/README.md +++ b/README.md @@ -1,170 +1,458 @@ -# PHP Benchmark Suite - -A modern, benchmarking framework for PHP that allows testing performance of different implementations and evaluating performance evolution across PHP versions (5.6 through 8.5). - -## Features - -- **Performance Testing**: 40+ automated benchmarks covering arrays, strings, loops, OOP, and more -- **Version Comparison**: Test performance across different PHP versions (5.6 to 8.5) -- **Parallel Execution**: Concurrent benchmark execution using Spatie\Async (100 parallel tasks) -- **Docker Isolation**: Each PHP version runs in isolated Docker containers -- **Web Dashboard**: Visual charts and statistics at `/dashboard` -- **Clean Architecture**: Domain-driven design with hexagonal ports & adapters -- **Modular Architecture**: Easily add your own test cases - -## Requirements - -- docker -- docker-compose - -## Installation +
+ 🎯 Performance Testing+
|
+
+ 🔄 Version Comparison+
|
+
+ ⚡ High Performance+
|
+
+ 🏗️ Modern Architecture+
|
+
+ 📊 Web Dashboard+
|
+
+ 🎨 Easy Customization+
|
+
| 📝 | +Code Style | +Follow PSR-12 standards (enforced by PHP-CS-Fixer) | +
| 🔍 | +Static Analysis | +Pass PHPStan level max (strictest level) | +
| 🏗️ | +Architecture | +Respect Clean Architecture principles (validated by PHPArkitect) | +
| ✅ | +Testing | +Write PHPUnit tests for new features | +
| 🌐 | +Language | +Write all code, comments, docs, commits, PRs in English | +
| 📚 | +Documentation | +Document your benchmarks and architectural decisions | +
|
+ + PHP 8.4+ + |
+
+ + Symfony 7.2 + |
+
+ + Doctrine ORM + |
+
+ + Docker + |
+
|
+ + MariaDB + |
+
+ + Mercure + |
+
+ + Chart.js + |
+
+ + Stimulus + |
+
| 🐛 Issues | +Report bugs or request features | +
| 💬 Discussions | +Ask questions and share ideas | +
| 📧 Contact | +Reach out via GitHub issues or discussions | +
{{ this.sourceCode }}
+ | Métrique | + {% for phpVersion in this.data.phpVersions|keys %} ++ PHP {{ phpVersion|replace({'php': ''}) }} + | + {% endfor %} +
|---|---|
| Échantillons | + {% for stats in this.data.phpVersions %} +{{ stats.count }} | + {% endfor %} +
| p50 (ms) | + {% set best_p50 = 999999 %} + {% for stats in this.data.phpVersions %} + {% if stats.getP50() < best_p50 %} + {% set best_p50 = stats.getP50() %} + {% endif %} + {% endfor %} + + {% for stats in this.data.phpVersions %} ++ {{ stats.getP50()|number_format(5) }} + | + {% endfor %} +
| p80 (ms) | + {% set best_p80 = 999999 %} + {% for stats in this.data.phpVersions %} + {% if stats.getP80() < best_p80 %} + {% set best_p80 = stats.getP80() %} + {% endif %} + {% endfor %} + + {% for stats in this.data.phpVersions %} ++ {{ stats.getP80()|number_format(5) }} + | + {% endfor %} +
| p90 (ms) | + {% set best_p90 = 999999 %} + {% for stats in this.data.phpVersions %} + {% if stats.getP90() < best_p90 %} + {% set best_p90 = stats.getP90() %} + {% endif %} + {% endfor %} + + {% for stats in this.data.phpVersions %} ++ {{ stats.getP90()|number_format(5) }} + | + {% endfor %} +
| p95 (ms) | + {% set best_p95 = 999999 %} + {% for stats in this.data.phpVersions %} + {% if stats.getP95() < best_p95 %} + {% set best_p95 = stats.getP95() %} + {% endif %} + {% endfor %} + + {% for stats in this.data.phpVersions %} ++ {{ stats.getP95()|number_format(5) }} + | + {% endfor %} +
| p99 (ms) | + {% set best_p99 = 999999 %} + {% for stats in this.data.phpVersions %} + {% if stats.getP99() < best_p99 %} + {% set best_p99 = stats.getP99() %} + {% endif %} + {% endfor %} + + {% for stats in this.data.phpVersions %} ++ {{ stats.getP99()|number_format(5) }} + | + {% endfor %} +
| Moyenne (ms) | + {% set best_avg = 999999 %} + {% for stats in this.data.phpVersions %} + {% if stats.avg < best_avg %} + {% set best_avg = stats.avg %} + {% endif %} + {% endfor %} + + {% for stats in this.data.phpVersions %} ++ {{ stats.avg|number_format(5) }} + | + {% endfor %} +
| Mémoire utilisée (Mo) | + {% set best_memory = 999999999999 %} + {% for stats in this.data.phpVersions %} + {% if stats.memoryUsed < best_memory %} + {% set best_memory = stats.memoryUsed %} + {% endif %} + {% endfor %} + + {% for stats in this.data.phpVersions %} ++ {{ (stats.memoryUsed / 1024 / 1024)|number_format(2) }} + | + {% endfor %} +
| Pic de mémoire (Mo) | + {% set best_memory_peak = 999999999999 %} + {% for stats in this.data.phpVersions %} + {% if stats.memoryPeak < best_memory_peak %} + {% set best_memory_peak = stats.memoryPeak %} + {% endif %} + {% endfor %} + + {% for stats in this.data.phpVersions %} ++ {{ (stats.memoryPeak / 1024 / 1024)|number_format(2) }} + | + {% endfor %} +
⏳ En attente de démarrage d'un benchmark...
+Nombre total de benchmarks: {{ stats|length }}
-Versions PHP comparées: {{ allPhpVersions|join(', ') }}
-| Métrique | - {% for version in allPhpVersions %} -- PHP {{ version|replace({'php': ''}) }} | - {% endfor %} -
|---|---|
| Échantillons | - {% for version in allPhpVersions %} -- {% if benchmark.phpVersions[version] is defined %} - {{ benchmark.phpVersions[version].count }} - {% else %} - - - {% endif %} - | - {% endfor %} -
| p50 (ms) | - {% set best_p50 = 999999 %} - {% for version in allPhpVersions %} - {% if benchmark.phpVersions[version] is defined and benchmark.phpVersions[version].p50 < best_p50 %} - {% set best_p50 = benchmark.phpVersions[version].p50 %} - {% endif %} - {% endfor %} - - {% for version in allPhpVersions %} -- {% if benchmark.phpVersions[version] is defined %} - {{ benchmark.phpVersions[version].p50|number_format(5) }} - {% else %} - - - {% endif %} - | - {% endfor %} -
| p80 (ms) | - {% set best_p80 = 999999 %} - {% for version in allPhpVersions %} - {% if benchmark.phpVersions[version] is defined and benchmark.phpVersions[version].p80 < best_p80 %} - {% set best_p80 = benchmark.phpVersions[version].p80 %} - {% endif %} - {% endfor %} - - {% for version in allPhpVersions %} -- {% if benchmark.phpVersions[version] is defined %} - {{ benchmark.phpVersions[version].p80|number_format(5) }} - {% else %} - - - {% endif %} - | - {% endfor %} -
| p90 (ms) | - {% set best_p90 = 999999 %} - {% for version in allPhpVersions %} - {% if benchmark.phpVersions[version] is defined and benchmark.phpVersions[version].p90 < best_p90 %} - {% set best_p90 = benchmark.phpVersions[version].p90 %} - {% endif %} - {% endfor %} +- {% if benchmark.phpVersions[version] is defined %} - {{ benchmark.phpVersions[version].p90|number_format(5) }} - {% else %} - - - {% endif %} - | - {% endfor %} -
| p95 (ms) | - {% set best_p95 = 999999 %} - {% for version in allPhpVersions %} - {% if benchmark.phpVersions[version] is defined and benchmark.phpVersions[version].p95 < best_p95 %} - {% set best_p95 = benchmark.phpVersions[version].p95 %} - {% endif %} - {% endfor %} - - {% for version in allPhpVersions %} -- {% if benchmark.phpVersions[version] is defined %} - {{ benchmark.phpVersions[version].p95|number_format(5) }} - {% else %} - - - {% endif %} - | - {% endfor %} -
| p99 (ms) | - {% set best_p99 = 999999 %} - {% for version in allPhpVersions %} - {% if benchmark.phpVersions[version] is defined and benchmark.phpVersions[version].p99 < best_p99 %} - {% set best_p99 = benchmark.phpVersions[version].p99 %} - {% endif %} - {% endfor %} + {# Statistics Overview #} +- {% if benchmark.phpVersions[version] is defined %} - {{ benchmark.phpVersions[version].p99|number_format(5) }} - {% else %} - - - {% endif %} - | - {% endfor %} -
| Moyenne (ms) | - {% set best_avg = 999999 %} - {% for version in allPhpVersions %} - {% if benchmark.phpVersions[version] is defined and benchmark.phpVersions[version].avg < best_avg %} - {% set best_avg = benchmark.phpVersions[version].avg %} - {% endif %} - {% endfor %} + {# Filter and Sort Section #} +- {% if benchmark.phpVersions[version] is defined %} - {{ benchmark.phpVersions[version].avg|number_format(5) }} - {% else %} - - - {% endif %} - | - {% endfor %} -
| Mémoire utilisée (Mo) | - {% set best_memory = 999999999999 %} - {% for version in allPhpVersions %} - {% if benchmark.phpVersions[version] is defined and benchmark.phpVersions[version].memoryUsed < best_memory %} - {% set best_memory = benchmark.phpVersions[version].memoryUsed %} - {% endif %} - {% endfor %} + {# Sort and Filter Buttons #} +- {% if benchmark.phpVersions[version] is defined %} - {{ (benchmark.phpVersions[version].memoryUsed / 1024 / 1024)|number_format(2) }} - {% else %} - - - {% endif %} - | - {% endfor %} -
| Pic de mémoire (Mo) | - {% set best_memory_peak = 999999999999 %} - {% for version in allPhpVersions %} - {% if benchmark.phpVersions[version] is defined and benchmark.phpVersions[version].memoryPeak < best_memory_peak %} - {% set best_memory_peak = benchmark.phpVersions[version].memoryPeak %} - {% endif %} - {% endfor %} +- {% if benchmark.phpVersions[version] is defined %} - {{ (benchmark.phpVersions[version].memoryPeak / 1024 / 1024)|number_format(2) }} - {% else %} - - - {% endif %} - | - {% endfor %} -