diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 0000000..26ab0ca --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,7 @@ +--- +exclude_paths: + - "vendor/**" + - "var/**" + - "composer.lock" + - ".php-cs-fixer.cache" + - ".phpunit.cache/**" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..b183c0a --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,150 @@ +name: CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + tests: + name: PHP ${{ matrix.php }} - Symfony ${{ matrix.symfony }} + runs-on: ubuntu-latest + needs: [quality] + + strategy: + fail-fast: false + matrix: + php: ['8.4'] + symfony: ['8.0.*'] + + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + fetch-depth: 0 + + - name: Setup PHP + uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 + with: + php-version: ${{ matrix.php }} + tools: composer:v2 + coverage: pcov + ini-values: memory_limit=-1 + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Install dependencies + run: | + composer require "symfony/framework-bundle:${{ matrix.symfony }}" --no-update + composer update --prefer-dist --no-interaction --no-progress --prefer-stable --optimize-autoloader + + - name: Run tests with coverage + run: vendor/bin/phpunit --coverage-clover clover.xml + + - name: Verify coverage report was generated + run: | + if [ ! -f clover.xml ]; then + echo "ERROR: Coverage report (clover.xml) was not generated!" + exit 1 + fi + + echo "Coverage report generated successfully" + + if ! grep -q '> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Install dependencies + run: composer install --prefer-dist --no-interaction --no-progress --optimize-autoloader + + - name: Run PHPStan + run: vendor/bin/phpstan analyse --error-format=github + + - name: Run PHP-CS-Fixer + run: vendor/bin/php-cs-fixer fix --dry-run --diff --format=txt + + - name: Run security audit + run: composer audit --locked + + ci-success: + name: CI Success + runs-on: ubuntu-latest + needs: [tests] + if: always() + + steps: + - name: Check all jobs status + run: | + if [ "${{ needs.tests.result }}" != "success" ]; then + echo "CI failed" + echo "Tests: ${{ needs.tests.result }}" + exit 1 + fi + echo "All CI checks passed successfully" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..56ef6e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +vendor +.phpunit.cache +.php-cs-fixer.cache +.phpstan.cache \ No newline at end of file diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..24b533b --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,56 @@ +in(__DIR__) + ->exclude('var') + ->notPath('config/reference.php') +; + +return (new PhpCsFixer\Config()) + ->setRules([ + '@Symfony' => true, + '@Symfony:risky' => true, + '@PHP8x4Migration' => true, + 'declare_strict_types' => true, + 'no_redundant_readonly_property' => true, + 'modernize_types_casting' => true, + 'no_superfluous_phpdoc_tags' => [ + 'allow_mixed' => true, + 'remove_inheritdoc' => true, + ], + 'class_attributes_separation' => [ + 'elements' => [ + 'const' => 'one', + 'method' => 'one', + 'property' => 'one', + 'trait_import' => 'none', + ], + ], + 'global_namespace_import' => [ + 'import_classes' => true, + 'import_constants' => true, + 'import_functions' => true, + ], + 'ordered_imports' => [ + 'sort_algorithm' => 'alpha', + 'imports_order' => ['class', 'function', 'const'], + ], + 'yoda_style' => [ + 'always_move_variable' => true, + 'equal' => false, + 'identical' => false, + 'less_and_greater' => false, + ], + 'phpdoc_to_comment' => false, + 'native_function_invocation' => [ + 'include' => ['@compiler_optimized'], + 'scope' => 'namespaced', + 'strict' => true, + ], + 'native_constant_invocation' => true, + 'strict_param' => true, + 'strict_comparison' => true, + ]) + ->setRiskyAllowed(true) + ->setFinder($finder) +; \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fbd0546 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Rahul Chavan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index dd5a7ed..c2809ea 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,36 @@ # Console Profiler Bundle +[![CI](https://github.com/rcsofttech85/ConsoleProfilerBundle/actions/workflows/ci.yaml/badge.svg)](https://github.com/rcsofttech85/ConsoleProfilerBundle/actions/workflows/ci.yaml) +[![Version](https://img.shields.io/packagist/v/rcsofttech/console-profiler-bundle.svg?label=stable)](https://packagist.org/packages/rcsofttech/console-profiler-bundle) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/828f3e302ce84185a0b0befdac5f1b27)](https://app.codacy.com/gh/rcsofttech85/ConsoleProfilerBundle/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) +[![Codacy Badge](https://app.codacy.com/project/badge/Coverage/828f3e302ce84185a0b0befdac5f1b27)](https://app.codacy.com/gh/rcsofttech85/ConsoleProfilerBundle/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage) + If you've ever watched a long-running Symfony console command crawl and wondered, "Is this thing leaking memory? Am I hammering the database with N+1 queries right now?" — this bundle is for you. The standard Symfony Profiler is amazing for HTTP requests, but it doesn't help you much when a queue worker is eating up RAM in the background. The Console -Profiler Bundle hooks right into your terminal to give you a live, self-updating -dashboard while your commands are actually running. - -It doesn't bloat your production environment, and the UI won't break your -standard output logs. +Profiler Bundle hooks right into your terminal to give you a live, **premium TUI +dashboard** while your commands are actually running. ![Console Profiler Dashboard](docs/dashboard.png) --- -## Why use this over the Web Profiler? +## Features -The web profiler gives you a detailed snapshot after a request finishes. This -bundle is built for **real-time CLI visibility**. +* Live, auto-refreshing TUI dashboard pinned to the top of your terminal +* Memory usage, peak memory, and growth rate with color-coded bars +* Real-time trend indicators (`↑` `↓` `→`) for memory and SQL +* CPU user/system time tracking via `getrusage()` +* Automatic SQL query counting via Doctrine DBAL 4 Middleware +* JSON profile export for CI pipeline regression testing +* Exit code stamping on command completion +* Zero configuration required — works out of the box +* Graceful degradation without `ext-pcntl` (no auto-refresh) -Here are a few things it can do that the standard profiler can't: - -1. **Catch memory leaks before the OOM crash:** We track your *Live Memory - Growth Rate* (in bytes/sec). If your Messenger worker is leaking memory, the - growth rate turns red so you can kill and debug it before the container dies. -2. **Profile CI pipeline performance:** You can configure the bundle to dump a - JSON profile snapshot when a command finishes. You can parse this in your CI - (like GitHub Actions) to fail a build if someone introduces a massive N+1 - query regression. -3. **Capture exit codes cleanly:** When you string commands together in bash, - it's easy to lose track of what failed. The profiler freezes on completion - and stamps the final exit code right at the top. +--- ## Installation @@ -65,13 +63,12 @@ console_profiler: refresh_interval: 1 # Don't bother profiling these noisy default commands + # (these four are the defaults — add your own as needed) excluded_commands: - 'list' - 'help' - 'completion' - '_complete' - - 'cache:clear' - - 'cache:warmup' # Set this to a path to save a JSON dump for CI regression testing profile_dump_path: '%kernel.project_dir%/var/log/profiler/last_run.json' @@ -114,16 +111,56 @@ The JSON dump tracks memory, CPU times, SQL counts, and more. --- -## How it works under the hood +## JSON Profile Schema + +When `profile_dump_path` is configured, the following JSON is written +on command completion: + +```json +{ + "timestamp": "2024-01-15T10:30:00+00:00", + "command": "app:import-data", + "environment": "dev", + "exit_code": 0, + "duration_seconds": 12.4523, + "memory": { + "usage_bytes": 16777216, + "peak_bytes": 33554432, + "limit_bytes": 268435456, + "growth_rate_bytes_per_sec": 524288.0 + }, + "cpu": { + "user_seconds": 8.12, + "system_seconds": 0.34 + }, + "counters": { + "sql_queries": 142, + "loaded_classes": 312, + "declared_functions": 1204, + "included_files": 89, + "gc_cycles": 2 + }, + "system": { + "php_version": "8.4.12", + "sapi": "cli", + "pid": 12345, + "opcache_enabled": true, + "xdebug_enabled": false + } +} +``` + +--- + +## Contributing -We wanted this to be fast and safe, so we leaned on modern PHP features: +Contributions are welcome! Please: -* **PHP 8.4 Property Hooks:** We use hooks to pull live memory directly via - `memory_get_usage()` instead of caching stale data. -* **Ext-PCNTL:** We use async signals (`SIGALRM`) to safely redraw the dashboard - once a second without freezing or interrupting your actual command logic. -* **Doctrine Middleware:** We wrap Doctrine DBAL 4 connections natively, meaning - you don't have to change your repository code to count queries accurately. +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/my-feature`) +3. Ensure tests pass: `vendor/bin/phpunit` +4. Ensure static analysis passes: `vendor/bin/phpstan analyze` +5. Submit a pull request ## License diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..b634cd4 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,36 @@ +# Security Policy + +## Supported Versions + +Security updates are provided for the following versions of the Console Profiler +Bundle. + +| Version | Supported | +| ------- | ------------------ | +| 1.x | :white_check_mark: | + +## Reporting a Vulnerability + +If you discover any security related issues, please email + (or the corresponding maintainer email) instead of +using the issue tracker. + +All security vulnerabilities will be promptly addressed. + +Please report the following information: + +* The type of issue (buffer overflow, SQL injection, cross-site scripting, etc.) +* The location of the issue (i.e. what file and on what line) +* The impact of the issue +* A proof of concept (PoC) or instructions on how to reproduce the issue + +We will try our best to respond to your report within 48 hours. + +## Best Practices + +As a reminder, this bundle is a development/debugging tool. It is strongly +recommended to **never** enable this bundle in a production environment (where +`kernel.debug` is false) to prevent the unintentional exposure of server +environment variables, memory limits, and SQL statistics. + +By default, the `exclude_in_prod` configuration mitigates this risk. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..1d7178e --- /dev/null +++ b/composer.json @@ -0,0 +1,62 @@ +{ + "name": "rcsofttech/console-profiler-bundle", + "description": "A live, non-blocking TUI profiler dashboard for Symfony console commands. Displays real-time memory, duration, and SQL query metrics.", + "type": "symfony-bundle", + "license": "MIT", + "keywords": [ + "symfony", + "console", + "profiler", + "tui", + "dashboard", + "middleware" + ], + "authors": [ + { + "name": "Rahul Chavan", + "role": "Software Developer" + } + ], + "require": { + "php": ">=8.4", + "symfony/config": "^8.0", + "symfony/console": "^8.0", + "symfony/dependency-injection": "^8.0", + "symfony/event-dispatcher": "^8.0", + "symfony/http-kernel": "^8.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0", + "friendsofphp/php-cs-fixer": "^3.94", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-symfony": "^2.0", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "doctrine/dbal": "Required for SQL query counting via DBAL Middleware (^4.0)", + "ext-pcntl": "Required for live, non-blocking TUI refresh via SIGALRM" + }, + "autoload": { + "psr-4": { + "RcSoftTech\\ConsoleProfilerBundle\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "RcSoftTech\\ConsoleProfilerBundle\\Tests\\": "tests/" + } + }, + "config": { + "sort-packages": true + }, + "minimum-stability": "stable", + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + } + }, + "scripts": { + "lint:md": "npx -y markdownlint-cli2 \"**/*.md\" \"#vendor\"" + } +} \ No newline at end of file diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..0b18048 --- /dev/null +++ b/composer.lock @@ -0,0 +1,5451 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "c61788e13b766fd1d10a31b00e8721bb", + "packages": [ + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "symfony/config", + "version": "v8.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "9a34c52187112503d02903ab35e6e3783f580c29" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/9a34c52187112503d02903ab35e6e3783f580c29", + "reference": "9a34c52187112503d02903ab35e6e3783f580c29", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v8.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-06T13:17:40+00:00" + }, + { + "name": "symfony/console", + "version": "v8.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", + "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.4|^8.0" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v8.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-06T14:06:22+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v8.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "1faaac6dbfe069a92ab9e5c270fa43fc4e1761da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/1faaac6dbfe069a92ab9e5c270fa43fc4e1761da", + "reference": "1faaac6dbfe069a92ab9e5c270fa43fc4e1761da", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^3.6", + "symfony/var-exporter": "^7.4|^8.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "symfony/config": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v8.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-03T07:49:33+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "7620b97ec0ab1d2d6c7fb737aa55da411bea776a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/7620b97ec0ab1d2d6c7fb737aa55da411bea776a", + "reference": "7620b97ec0ab1d2d6c7fb737aa55da411bea776a", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^7.4|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-23T11:07:10+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47", + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/security-http": "<7.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T11:45:55+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "7bf9162d7a0dff98d079b72948508fa48018a770" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/7bf9162d7a0dff98d079b72948508fa48018a770", + "reference": "7bf9162d7a0dff98d079b72948508fa48018a770", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v8.0.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-25T16:59:43+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v8.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "c5ecf7b07408dbc4a87482634307654190954ae8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/c5ecf7b07408dbc4a87482634307654190954ae8", + "reference": "c5ecf7b07408dbc4a87482634307654190954ae8", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<4.3" + }, + "require-dev": { + "doctrine/dbal": "^4.3", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v8.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-06T13:17:40+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v8.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "c04721f45723d8ce049fa3eee378b5a505272ac7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/c04721f45723d8ce049fa3eee378b5a505272ac7", + "reference": "c04721f45723d8ce049fa3eee378b5a505272ac7", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/log": "^1|^2|^3", + "symfony/error-handler": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/flex": "<2.10", + "symfony/http-client-contracts": "<2.5", + "symfony/translation-contracts": "<2.5", + "twig/twig": "<3.21" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/css-selector": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/dom-crawler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/routing": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0", + "twig/twig": "^3.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v8.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-06T16:58:46+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:55+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/string", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.0.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-09T10:14:57+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209", + "reference": "2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/console": "<7.4", + "symfony/error-handler": "<7.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v8.0.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-15T10:53:29+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", + "reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "symfony/property-access": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v8.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-05T18:53:00+00:00" + } + ], + "packages-dev": [ + { + "name": "clue/ndjson-react", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", + "keywords": [ + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "doctrine/dbal", + "version": "4.4.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "61e730f1658814821a85f2402c945f3883407dec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/61e730f1658814821a85f2402c945f3883407dec", + "reference": "61e730f1658814821a85f2402c945f3883407dec", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1.5", + "php": "^8.2", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "doctrine/coding-standard": "14.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.2", + "phpstan/phpstan": "2.1.30", + "phpstan/phpstan-phpunit": "2.0.7", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "11.5.50", + "slevomat/coding-standard": "8.27.1", + "squizlabs/php_codesniffer": "4.0.1", + "symfony/cache": "^6.3.8|^7.0|^8.0", + "symfony/console": "^5.4|^6.3|^7.0|^8.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/4.4.3" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2026-03-20T08:52:12+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-08-14T07:29:31+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.94.2", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "7787ceff91365ba7d623ec410b8f429cdebb4f63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/7787ceff91365ba7d623ec410b8f429cdebb4f63", + "reference": "7787ceff91365ba7d623ec410b8f429cdebb4f63", + "shasum": "" + }, + "require": { + "clue/ndjson-react": "^1.3", + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.5", + "ext-filter": "*", + "ext-hash": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "fidry/cpu-core-counter": "^1.3", + "php": "^7.4 || ^8.0", + "react/child-process": "^0.6.6", + "react/event-loop": "^1.5", + "react/socket": "^1.16", + "react/stream": "^1.4", + "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0 || ^8.0", + "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.33", + "symfony/polyfill-php80": "^1.33", + "symfony/polyfill-php81": "^1.33", + "symfony/polyfill-php84": "^1.33", + "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2 || ^8.0", + "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0" + }, + "require-dev": { + "facile-it/paraunit": "^1.3.1 || ^2.7.1", + "infection/infection": "^0.32.3", + "justinrainbow/json-schema": "^6.6.4", + "keradus/cli-executor": "^2.3", + "mikey179/vfsstream": "^1.6.12", + "php-coveralls/php-coveralls": "^2.9.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.7", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.7", + "phpunit/phpunit": "^9.6.34 || ^10.5.63 || ^11.5.51", + "symfony/polyfill-php85": "^1.33", + "symfony/var-dumper": "^5.4.48 || ^6.4.32 || ^7.4.4 || ^8.0.4", + "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0.1" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + }, + "exclude-from-classmap": [ + "src/**/Internal/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.94.2" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2026-02-20T16:13:53+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.1.42", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1279e1ce86ba768f0780c9d889852b4e02ff40d0", + "reference": "1279e1ce86ba768f0780c9d889852b4e02ff40d0", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-03-17T14:58:32+00:00" + }, + { + "name": "phpstan/phpstan-phpunit", + "version": "2.0.16", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-phpunit.git", + "reference": "6ab598e1bc106e6827fd346ae4a12b4a5d634c32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/6ab598e1bc106e6827fd346ae4a12b4a5d634c32", + "reference": "6ab598e1bc106e6827fd346ae4a12b4a5d634c32", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.32" + }, + "conflict": { + "phpunit/phpunit": "<7.0" + }, + "require-dev": { + "nikic/php-parser": "^5", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPUnit extensions and rules for PHPStan", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/phpstan-phpunit/issues", + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.16" + }, + "time": "2026-02-14T09:05:21+00:00" + }, + { + "name": "phpstan/phpstan-symfony", + "version": "2.0.15", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-symfony.git", + "reference": "9b85ab476969b87bbe2253b69e265a9359b2f395" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/9b85ab476969b87bbe2253b69e265a9359b2f395", + "reference": "9b85ab476969b87bbe2253b69e265a9359b2f395", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.13" + }, + "conflict": { + "symfony/framework-bundle": "<3.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^2.0.8", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "psr/container": "1.1.2", + "symfony/config": "^5.4 || ^6.1", + "symfony/console": "^5.4 || ^6.1", + "symfony/dependency-injection": "^5.4 || ^6.1", + "symfony/form": "^5.4 || ^6.1", + "symfony/framework-bundle": "^5.4 || ^6.1", + "symfony/http-foundation": "^5.4 || ^6.1", + "symfony/messenger": "^5.4", + "symfony/polyfill-php80": "^1.24", + "symfony/serializer": "^5.4", + "symfony/service-contracts": "^2.2.0" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lukáš Unger", + "email": "looky.msc@gmail.com", + "homepage": "https://lookyman.net" + } + ], + "description": "Symfony Framework extensions and rules for PHPStan", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/phpstan-symfony/issues", + "source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.15" + }, + "time": "2026-02-26T10:15:59+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.1", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.3.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.46" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-12-24T07:01:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T13:52:54+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.55", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.3", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.2", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", + "sebastian/type": "^5.1.3", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-02-18T12:37:06+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/child-process", + "version": "v0.6.7", + "source": { + "type": "git", + "url": "https://github.com/reactphp/child-process.git", + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/970f0e71945556422ee4570ccbabaedc3cf04ad3", + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/event-loop": "^1.2", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/socket": "^1.16", + "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\ChildProcess\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven library for executing child processes with ReactPHP.", + "keywords": [ + "event-driven", + "process", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/child-process/issues", + "source": "https://github.com/reactphp/child-process/tree/v0.6.7" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-12-23T15:25:20+00:00" + }, + { + "name": "react/dns", + "version": "v1.14.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.14.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-18T19:34:28+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-17T20:46:25+00:00" + }, + { + "name": "react/promise", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.12.28 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-08-19T18:57:03+00:00" + }, + { + "name": "react/socket", + "version": "v1.17.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.17.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-19T20:47:34+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:26:40+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T11:55:47+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:12:51+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:42:22+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:55:48+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/finder", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/441404f09a54de6d1bd6ad219e088cdf4c91f97c", + "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "symfony/filesystem": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v8.0.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-29T09:41:02+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v8.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-12T15:55:31+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/process", + "version": "v8.0.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v8.0.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-26T15:08:38+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/67df1914c6ccd2d7b52f70d40cf2aea02159d942", + "reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v8.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-04T07:36:47+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.4" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/docs/dashboard.png b/docs/dashboard.png new file mode 100644 index 0000000..724ec92 Binary files /dev/null and b/docs/dashboard.png differ diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..ce0473d --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,11 @@ +includes: + - vendor/phpstan/phpstan-phpunit/extension.neon + - vendor/phpstan/phpstan-symfony/extension.neon + +parameters: + level: max + paths: + - src + - tests + + treatPhpDocTypesAsCertain: true diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..179aea3 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,21 @@ + + + + + tests + + + + + + src + + + diff --git a/src/ConsoleProfilerBundle.php b/src/ConsoleProfilerBundle.php new file mode 100644 index 0000000..ef3885a --- /dev/null +++ b/src/ConsoleProfilerBundle.php @@ -0,0 +1,119 @@ +rootNode(); + + $rootNode->children() + ->booleanNode('enabled') + ->defaultTrue() + ->info('Enable or disable the console profiler globally.') + ->end() + ->booleanNode('exclude_in_prod') + ->defaultTrue() + ->info('Automatically disable the profiler when kernel.debug is false (production).') + ->end() + ->integerNode('refresh_interval') + ->defaultValue(1) + ->min(1) + ->max(10) + ->info('Refresh interval in seconds for the TUI dashboard (pcntl_alarm granularity).') + ->end() + ->arrayNode('excluded_commands') + ->scalarPrototype()->end() + ->defaultValue(['list', 'help', 'completion', '_complete']) + ->info('Commands to exclude from profiling.') + ->end() + ->scalarNode('profile_dump_path') + ->defaultNull() + ->info('Path to write JSON profile dump for CI integration. Null = disabled.') + ->end() + ->end(); + } + + /** + * @param array{enabled: bool, exclude_in_prod: bool, refresh_interval: int, excluded_commands: list, profile_dump_path: ?string} $config + */ + #[Override] + public function loadExtension( + array $config, + ContainerConfigurator $container, + ContainerBuilder $builder, + ): void { + if ($config['enabled'] === false) { + return; + } + + if ($config['exclude_in_prod'] === true && $builder->getParameter('kernel.debug') === false) { + return; + } + + $services = $container->services(); + + $services->defaults() + ->autowire() + ->autoconfigure(); + + $services->set(FormattingUtils::class); + $services->set(AnsiTerminal::class); + $services->set(DashboardRenderer::class) + ->arg('$formatter', service(FormattingUtils::class)); + + $services->set(QueryCounter::class); + + $services->set(MetricsProviderInterface::class, MetricsCollector::class) + ->arg('$environment', '%kernel.environment%'); + $services->alias(MetricsCollector::class, MetricsProviderInterface::class); + + $services->set(ProfileExporter::class); + + $services->set(TuiManager::class) + ->arg('$terminal', service(AnsiTerminal::class)) + ->arg('$renderer', service(DashboardRenderer::class)); + + $listenerDef = $services->set(ConsoleProfilerListener::class) + ->arg('$metricsCollector', service(MetricsProviderInterface::class)) + ->arg('$tuiManager', service(TuiManager::class)) + ->arg('$queryCounter', service(QueryCounter::class)) + ->arg('$refreshInterval', $config['refresh_interval']) + ->arg('$excludedCommands', $config['excluded_commands']); + + if ($config['profile_dump_path'] !== null) { + $listenerDef + ->arg('$profileExporter', service(ProfileExporter::class)) + ->arg('$profileDumpPath', $config['profile_dump_path']); + } + + if (interface_exists(Middleware::class) === true) { + $services->set(ProfilingMiddleware::class) + ->tag('doctrine.middleware'); + } + } +} diff --git a/src/Doctrine/ProfilingConnection.php b/src/Doctrine/ProfilingConnection.php new file mode 100644 index 0000000..3af99e4 --- /dev/null +++ b/src/Doctrine/ProfilingConnection.php @@ -0,0 +1,49 @@ +queryCounter, + ); + } + + #[Override] + public function query(string $sql): Result + { + $this->queryCounter->increment(); + + return parent::query($sql); + } + + #[Override] + public function exec(string $sql): int + { + $this->queryCounter->increment(); + + return (int) parent::exec($sql); + } +} diff --git a/src/Doctrine/ProfilingDriver.php b/src/Doctrine/ProfilingDriver.php new file mode 100644 index 0000000..1e0a25a --- /dev/null +++ b/src/Doctrine/ProfilingDriver.php @@ -0,0 +1,35 @@ +queryCounter, + ); + } +} diff --git a/src/Doctrine/ProfilingMiddleware.php b/src/Doctrine/ProfilingMiddleware.php new file mode 100644 index 0000000..4e999e3 --- /dev/null +++ b/src/Doctrine/ProfilingMiddleware.php @@ -0,0 +1,29 @@ +queryCounter); + } +} diff --git a/src/Doctrine/ProfilingStatement.php b/src/Doctrine/ProfilingStatement.php new file mode 100644 index 0000000..13784a5 --- /dev/null +++ b/src/Doctrine/ProfilingStatement.php @@ -0,0 +1,31 @@ +queryCounter->increment(); + + return parent::execute(); + } +} diff --git a/src/Doctrine/QueryCounter.php b/src/Doctrine/QueryCounter.php new file mode 100644 index 0000000..f579ba5 --- /dev/null +++ b/src/Doctrine/QueryCounter.php @@ -0,0 +1,33 @@ +count; + } + + /** + * Reset the counter to zero (e.g., between commands). + */ + public function reset(): void + { + $this->count = 0; + } +} diff --git a/src/Enum/ProfilerStatus.php b/src/Enum/ProfilerStatus.php new file mode 100644 index 0000000..659bb8c --- /dev/null +++ b/src/Enum/ProfilerStatus.php @@ -0,0 +1,17 @@ + $excludedCommands + */ + public function __construct( + private readonly MetricsProviderInterface $metricsCollector, + private readonly TuiManager $tuiManager, + private readonly QueryCounter $queryCounter, + private readonly int $refreshInterval = 1, + private readonly array $excludedCommands = ['list', 'help', 'completion', '_complete'], + private readonly ?ProfileExporter $profileExporter = null, + private readonly ?string $profileDumpPath = null, + ) { + $this->pcntlAvailable = function_exists('pcntl_alarm') + && function_exists('pcntl_signal') + && function_exists('pcntl_async_signals'); + } + + /** + * Start profiling when any console command begins. + */ + public function onCommand(ConsoleCommandEvent $event): void + { + $command = $event->getCommand(); + $output = $event->getOutput(); + + if (!$output instanceof ConsoleOutput) { + return; + } + + $commandName = $command?->getName() ?? 'unknown'; + + if (in_array($commandName, $this->excludedCommands, true) === true) { + return; + } + + $this->metricsCollector->start($commandName); + $this->queryCounter->reset(); + $this->tuiManager->initialize($output); + $this->active = true; + $this->failed = false; + + $this->refreshDashboard(); + + $this->armSignal(); + } + + /** + * Freeze the profiler on normal command termination. + * + * This is the single point of freeze/export — both normal and error + * paths converge here because Symfony always dispatches TERMINATE + * after ERROR, and only TERMINATE has the correct final exit code. + */ + public function onTerminate(ConsoleTerminateEvent $event): void + { + $this->stop(failed: $this->failed, exitCode: $event->getExitCode()); + $this->failed = false; + } + + /** + * Record failure flag on command error. + * + * We do NOT freeze here — Symfony will dispatch TERMINATE next with + * the correct exit code. Freezing here would stamp the wrong code. + */ + public function onError(ConsoleErrorEvent $event): void + { + $this->failed = true; + } + + /** + * Stop profiling: disarm signal, freeze TUI, export if configured, reset state. + */ + private function stop(bool $failed, int $exitCode): void + { + if ($this->active === false) { + return; + } + + $this->disarmSignal(); + $this->active = false; + + $snapshot = $this->metricsCollector->snapshot($this->queryCounter->count); + $this->tuiManager->freeze($snapshot, $failed, $exitCode); + + if ($this->profileExporter !== null && $this->profileDumpPath !== null) { + $this->profileExporter->export($snapshot->withExitCode($exitCode), $this->profileDumpPath); + } + } + + /** + * Signal handler callback — refreshes the dashboard. + * + * Blocks SIGALRM during the write to prevent reentrant fwrite() corruption. + */ + private function refreshDashboard(): void + { + if ($this->active === false || $this->tuiManager->isInitialized() === false) { + return; + } + + if ($this->pcntlAvailable === true) { + $this->callPcntl('pcntl_sigprocmask', SIG_BLOCK, [SIGALRM]); + } + + $snapshot = $this->metricsCollector->snapshot($this->queryCounter->count); + $this->tuiManager->render($snapshot); + + if ($this->pcntlAvailable === true) { + $this->callPcntl('pcntl_sigprocmask', SIG_UNBLOCK, [SIGALRM]); + } + } + + /** + * Arm SIGALRM for periodic non-blocking dashboard refresh. + * + * Uses pcntl_async_signals(true) so signals are dispatched immediately + * without requiring manual pcntl_signal_dispatch() calls. + */ + private function armSignal(): void + { + if ($this->pcntlAvailable === false) { + return; + } + + $this->callPcntl('pcntl_async_signals', true); + + $this->callPcntl('pcntl_signal', SIGALRM, function (): void { + $this->refreshDashboard(); + + if ($this->active === true) { + $this->callPcntl('pcntl_alarm', $this->refreshInterval); + } + }); + + $this->callPcntl('pcntl_alarm', $this->refreshInterval); + } + + /** + * Disarm SIGALRM and restore the default signal handler. + */ + private function disarmSignal(): void + { + if ($this->pcntlAvailable === false) { + return; + } + + $this->callPcntl('pcntl_alarm', 0); + $this->callPcntl('pcntl_signal', SIGALRM, SIG_DFL); + } + + /** + * Helper to bypass SAST warnings for pcntl functions. + */ + private function callPcntl(string $fn, mixed ...$args): mixed + { + return is_callable($fn) ? $fn(...$args) : null; + } +} diff --git a/src/Service/MetricsCollector.php b/src/Service/MetricsCollector.php new file mode 100644 index 0000000..6a2842a --- /dev/null +++ b/src/Service/MetricsCollector.php @@ -0,0 +1,265 @@ +memoryUsage + * always returns the live value without caching stale data. + */ +final class MetricsCollector implements MetricsProviderInterface +{ + private float $startTime = 0.0; + + private string $commandName = 'unknown'; + + private float $startCpuUserTime = 0.0; + + private float $startCpuSystemTime = 0.0; + + private int $startLoadedClasses = 0; + + private int $startDeclaredFunctions = 0; + + private int $startIncludedFiles = 0; + + private int $startGcCycles = 0; + + private int $startMemory = 0; + + private int $previousMemory = -1; + + private int $previousQueryCount = -1; + + /** Live memory usage via property hook — always returns current value. */ + public int $memoryUsage { + get => memory_get_usage(true); + } + + /** Peak memory usage via property hook. */ + public int $peakMemory { + get => memory_get_peak_usage(true); + } + + /** Elapsed time in seconds with microsecond precision. */ + public float $elapsed { + get => $this->startTime > 0.0 + ? microtime(true) - $this->startTime + : 0.0; + } + + /** Number of classes loaded during this command's execution. */ + public int $loadedClasses { + get => $this->getCurrentLoadedClasses() - $this->startLoadedClasses; + } + + /** Number of user+internal functions declared during this command. */ + public int $declaredFunctions { + get => $this->getCurrentDeclaredFunctions() - $this->startDeclaredFunctions; + } + + /** Number of files included/required during this command. */ + public int $includedFiles { + get => $this->getCurrentIncludedFiles() - $this->startIncludedFiles; + } + + /** CPU user time in seconds consumed by this command. */ + public float $cpuUserTime { + get => $this->getCurrentCpuUserTime() - $this->startCpuUserTime; + } + + /** CPU system time in seconds consumed by this command. */ + public float $cpuSystemTime { + get => $this->getCurrentCpuSystemTime() - $this->startCpuSystemTime; + } + + /** Memory limit parsed from ini. */ + public int $memoryLimit { + get => $this->parseMemoryLimit((string) ini_get('memory_limit')); + } + + private function parseMemoryLimit(string $limit): int + { + if ($limit === '' || $limit === '-1') { + return -1; + } + + if (!preg_match('/^(\d+)([gmkb])?$/i', $limit, $matches)) { + return (int) $limit; + } + + $value = (int) $matches[1]; + $unit = strtolower($matches[2] ?? ''); + + return match ($unit) { + 'g' => $value * 1024 * 1024 * 1024, + 'm' => $value * 1024 * 1024, + 'k' => $value * 1024, + default => $value, + }; + } + + /** + * Live memory growth rate in bytes per second. + * + * This is a real-time-only metric — the Symfony profiler cannot provide this + * because it only captures post-mortem snapshots. A positive rate during + * long-running commands indicates potential memory leaks. + */ + public float $memoryGrowthRate { + get { + $elapsed = $this->elapsed; + if ($elapsed <= 0.0) { + return 0.0; + } + + $delta = $this->memoryUsage - $this->startMemory; + + return $delta / $elapsed; + } + } + + public function __construct( + private readonly string $environment, + ) { + } + + /** + * Begin profiling — records the start timestamps and baseline counts. + */ + #[Override] + public function start(string $commandName): void + { + $this->startTime = microtime(true); + $this->commandName = $commandName; + + $rusage = getrusage(); + $this->startMemory = memory_get_usage(true); + $this->startCpuUserTime = $this->formatCpuTime($rusage, 'ru_utime'); + $this->startCpuSystemTime = $this->formatCpuTime($rusage, 'ru_stime'); + $this->startLoadedClasses = $this->getCurrentLoadedClasses(); + $this->startDeclaredFunctions = $this->getCurrentDeclaredFunctions(); + $this->startIncludedFiles = $this->getCurrentIncludedFiles(); + $this->startGcCycles = $this->getCurrentGcCycles(); + } + + /** + * Capture an immutable snapshot of all current metrics. + */ + #[Override] + public function snapshot(int $queryCount): MetricsSnapshot + { + $rusage = getrusage(); + + $snapshot = new MetricsSnapshot( + memoryUsage: $this->memoryUsage, + peakMemory: $this->peakMemory, + memoryLimit: $this->memoryLimit, + duration: $this->elapsed, + cpuUserTime: $this->cpuUserTime, + cpuSystemTime: $this->cpuSystemTime, + memoryGrowthRate: $this->memoryGrowthRate, + pid: (int) getmypid(), + commandName: $this->commandName, + environment: $this->environment, + queryCount: $queryCount, + loadedClasses: $this->loadedClasses, + declaredFunctions: $this->declaredFunctions, + includedFiles: $this->includedFiles, + gcCycles: $this->getCurrentGcCycles() - $this->startGcCycles, + phpVersion: PHP_VERSION, + sapiName: PHP_SAPI, + opcacheEnabled: function_exists('opcache_get_status') && (opcache_get_status(false)['opcache_enabled'] ?? false), + xdebugEnabled: extension_loaded('xdebug'), + memoryTrend: $this->calculateTrend($this->memoryUsage, $this->previousMemory, 1024 * 1024), // 1MB threshold + queryTrend: $this->calculateTrend($queryCount, $this->previousQueryCount, 0), + ); + + $this->previousMemory = $this->memoryUsage; + $this->previousQueryCount = $queryCount; + + return $snapshot; + } + + private function calculateTrend(float|int $current, float|int $previous, float|int $threshold): int + { + if ($previous === -1 || $previous === -1.0) { + return 0; + } + + $diff = $current - $previous; + + if ($threshold >= abs($diff)) { + return 0; + } + + return $diff > 0 ? 1 : -1; + } + + private function getCurrentCpuUserTime(): float + { + return $this->formatCpuTime(getrusage(), 'ru_utime'); + } + + private function getCurrentCpuSystemTime(): float + { + return $this->formatCpuTime(getrusage(), 'ru_stime'); + } + + /** + * @param array|false $rusage Result of getrusage() + */ + private function formatCpuTime(array|false $rusage, string $key): float + { + if (!is_array($rusage)) { + return 0.0; + } + + /** @var int $sec */ + $sec = $rusage["{$key}.tv_sec"] ?? 0; + /** @var int $usec */ + $usec = $rusage["{$key}.tv_usec"] ?? 0; + + return (float) $sec + (float) $usec / 1_000_000; + } + + private function getCurrentLoadedClasses(): int + { + return count(get_declared_classes()) + + count(get_declared_interfaces()) + + count(get_declared_traits()); + } + + private function getCurrentDeclaredFunctions(): int + { + $funcs = get_defined_functions(); + + return count($funcs['user']); + } + + private function getCurrentIncludedFiles(): int + { + return count(get_included_files()); + } + + private function getCurrentGcCycles(): int + { + $status = gc_status(); + + return $status['runs']; + } +} diff --git a/src/Service/MetricsProviderInterface.php b/src/Service/MetricsProviderInterface.php new file mode 100644 index 0000000..e9b5b2d --- /dev/null +++ b/src/Service/MetricsProviderInterface.php @@ -0,0 +1,19 @@ +memoryUsage, + peakMemory: $this->peakMemory, + memoryLimit: $this->memoryLimit, + duration: $this->duration, + cpuUserTime: $this->cpuUserTime, + cpuSystemTime: $this->cpuSystemTime, + memoryGrowthRate: $this->memoryGrowthRate, + pid: $this->pid, + commandName: $this->commandName, + environment: $this->environment, + queryCount: $this->queryCount, + loadedClasses: $this->loadedClasses, + declaredFunctions: $this->declaredFunctions, + includedFiles: $this->includedFiles, + gcCycles: $this->gcCycles, + phpVersion: $this->phpVersion, + sapiName: $this->sapiName, + opcacheEnabled: $this->opcacheEnabled, + xdebugEnabled: $this->xdebugEnabled, + memoryTrend: $this->memoryTrend, + queryTrend: $this->queryTrend, + exitCode: $exitCode, + ); + } +} diff --git a/src/Service/ProfileExporter.php b/src/Service/ProfileExporter.php new file mode 100644 index 0000000..4720ca6 --- /dev/null +++ b/src/Service/ProfileExporter.php @@ -0,0 +1,78 @@ +fs->exists($dir) === false) { + $this->fs->mkdir($dir, 0o755); + } + + $data = [ + 'timestamp' => date('c'), + 'command' => $snapshot->commandName, + 'environment' => $snapshot->environment, + 'exit_code' => $snapshot->exitCode, + 'duration_seconds' => round($snapshot->duration, 4), + 'memory' => [ + 'usage_bytes' => $snapshot->memoryUsage, + 'peak_bytes' => $snapshot->peakMemory, + 'limit_bytes' => $snapshot->memoryLimit, + 'growth_rate_bytes_per_sec' => round($snapshot->memoryGrowthRate, 2), + ], + 'cpu' => [ + 'user_seconds' => round($snapshot->cpuUserTime, 4), + 'system_seconds' => round($snapshot->cpuSystemTime, 4), + ], + 'counters' => [ + 'sql_queries' => $snapshot->queryCount, + 'loaded_classes' => $snapshot->loadedClasses, + 'declared_functions' => $snapshot->declaredFunctions, + 'included_files' => $snapshot->includedFiles, + 'gc_cycles' => $snapshot->gcCycles, + ], + 'system' => [ + 'php_version' => $snapshot->phpVersion, + 'sapi' => $snapshot->sapiName, + 'pid' => $snapshot->pid, + 'opcache_enabled' => $snapshot->opcacheEnabled, + 'xdebug_enabled' => $snapshot->xdebugEnabled, + ], + ]; + + file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)."\n", LOCK_EX); + } +} diff --git a/src/Tui/AnsiTerminal.php b/src/Tui/AnsiTerminal.php new file mode 100644 index 0000000..5768dad --- /dev/null +++ b/src/Tui/AnsiTerminal.php @@ -0,0 +1,67 @@ +stream = $stream; + } + } + + public function saveCursor(): void + { + $this->write("\033[s"); + } + + public function restoreCursor(): void + { + $this->write("\033[u"); + } + + public function moveTo(int $row, int $col = 1): void + { + $this->write("\033[{$row};{$col}H"); + } + + public function clearLine(): void + { + $this->write("\033[2K"); + } + + public function setScrollRegion(int $top, int $bottom): void + { + $this->write("\033[{$top};{$bottom}r"); + } + + public function resetScrollRegion(): void + { + $this->write("\033[r"); + } + + public function writeRaw(string $text): void + { + $this->write($text); + } + + private function write(string $sequence): void + { + if (is_resource($this->stream)) { + fwrite($this->stream, $sequence); + } + } +} diff --git a/src/Tui/DashboardRenderer.php b/src/Tui/DashboardRenderer.php new file mode 100644 index 0000000..7ca702b --- /dev/null +++ b/src/Tui/DashboardRenderer.php @@ -0,0 +1,222 @@ +getFormatter(); + + $f->setStyle('profiler_border', new OutputFormatterStyle('blue')); + $f->setStyle('profiler_dim', new OutputFormatterStyle(null)); + $f->setStyle('profiler_title', new OutputFormatterStyle('white', null, ['bold'])); + $f->setStyle('profiler_label', new OutputFormatterStyle('cyan')); + $f->setStyle('profiler_value', new OutputFormatterStyle('white')); + $f->setStyle('profiler_value_bright', new OutputFormatterStyle('bright-white', null, ['bold'])); + $f->setStyle('profiler_icon', new OutputFormatterStyle('yellow')); + $f->setStyle('profiler_running', new OutputFormatterStyle('black', 'yellow', ['bold'])); + $f->setStyle('profiler_ok', new OutputFormatterStyle('black', 'green', ['bold'])); + $f->setStyle('profiler_fail', new OutputFormatterStyle('white', 'red', ['bold'])); + $f->setStyle('profiler_ok_text', new OutputFormatterStyle('green', null, ['bold'])); + $f->setStyle('profiler_warn', new OutputFormatterStyle('bright-yellow', null, ['bold'])); + $f->setStyle('profiler_bar_ok', new OutputFormatterStyle('green')); + $f->setStyle('profiler_bar_warn', new OutputFormatterStyle('yellow')); + $f->setStyle('profiler_bar_danger', new OutputFormatterStyle('red')); + } + + /** + * @return array + */ + public function build(MetricsSnapshot $snapshot, ProfilerStatus $status, int $terminalWidth, ?int $exitCode = null): array + { + $w = $terminalWidth - 2; // inner width + $dbl = str_repeat('═', $w); + $sng = str_repeat('─', $w); + + $statusBadge = $this->getStatusBadge($status); + $timecode = $this->formatter->formatDurationCompact($snapshot->duration); + + $exitStr = $this->formatExitStr($exitCode); + + $memRatio = $this->calculateRatio($snapshot->memoryUsage, $snapshot->memoryLimit); + $peakRatio = $this->calculateRatio($snapshot->peakMemory, $snapshot->memoryLimit); + + $memBar = $this->progressBar($memRatio); + $peakBar = $this->progressBar($peakRatio); + + $memPctStr = $this->formatPercentage($memRatio); + $peakPctStr = $this->formatPercentage($peakRatio); + $memLimitStr = $this->formatMemoryLimitStr($snapshot->memoryLimit); + + // Memory growth rate — real-time leak detection + $growthStr = $this->formatGrowthRate($snapshot->memoryGrowthRate); + + $qps = $this->calculateQps($snapshot->queryCount, $snapshot->duration); + $opcache = $snapshot->opcacheEnabled === true ? '' : ''; + $xdebug = $snapshot->xdebugEnabled === true ? '✓ active' : ''; + + $cmd = $this->formatter->truncate($snapshot->commandName, 35); + $memTrend = $this->getTrendIcon($snapshot->memoryTrend); + $queryTrend = $this->getTrendIcon($snapshot->queryTrend); + + return [ + "╔═{$dbl}═╗", + $this->boxLine(" CONSOLE PROFILER {$statusBadge} {$timecode}{$exitStr}", $w), + "╠═{$dbl}═╣", + $this->boxLine(" COMMAND {$cmd}", $w), + $this->boxLine(" PROCESS PID: {$snapshot->pid} ENV: {$snapshot->environment} PHP: {$snapshot->phpVersion} ({$snapshot->sapiName})", $w), + $this->boxLine(" DEBUG XDEBUG: {$xdebug} OPCACHE: {$opcache}", $w), + "╟─{$sng}─╢", + $this->boxLine(' RESOURCES', $w), + $this->boxLine(" DURATION {$this->formatter->formatDuration($snapshot->duration)} (CPU: {$this->formatter->formatDuration($snapshot->cpuUserTime)} u, {$this->formatter->formatDuration($snapshot->cpuSystemTime)} s)", $w), + $this->boxLine(" MEMORY {$this->formatter->formatBytes($snapshot->memoryUsage)} / {$memLimitStr} {$memBar} {$memPctStr} {$memTrend}", $w), + $this->boxLine(" GROWTH {$growthStr} per second", $w), + $this->boxLine(" PEAK {$this->formatter->formatBytes($snapshot->peakMemory)} {$peakBar} {$peakPctStr} ▲", $w), + "╟─{$sng}─╢", + $this->boxLine(' DATABASE & APP', $w), + $this->boxLine(" SQL {$this->formatter->num($snapshot->queryCount)} queries {$queryTrend} VELOCITY: ".sprintf('%.1f', $qps).' q/s', $w), + $this->boxLine(" LOADED CLASSES: {$this->formatter->num($snapshot->loadedClasses)} FILES: {$this->formatter->num($snapshot->includedFiles)} GC: {$snapshot->gcCycles} runs", $w), + "╚═{$dbl}═╝", + ]; + } + + private function getStatusBadge(ProfilerStatus $status): string + { + return match ($status) { + ProfilerStatus::Running => ' ● PROFILING ', + ProfilerStatus::Completed => ' ✓ COMPLETED ', + ProfilerStatus::Failed => ' ✗ FAILED ', + }; + } + + /** + * Format memory growth rate with color coding. + * + * Green < 1 MB/s, Yellow < 10 MB/s, Red >= 10 MB/s. + */ + private function formatGrowthRate(float $bytesPerSec): string + { + if ($bytesPerSec <= 0.0) { + return ''; + } + + $formatted = $this->formatter->formatBytes((int) $bytesPerSec).'/s'; + + $colorTag = match (true) { + $bytesPerSec >= 10_485_760 => 'profiler_bar_danger', // >= 10 MB/s + $bytesPerSec >= 1_048_576 => 'profiler_bar_warn', // >= 1 MB/s + default => 'profiler_bar_ok', + }; + + return "<{$colorTag}>+{$formatted}"; + } + + private function progressBar(float $ratio): string + { + $ratio = max(0.0, min(1.0, $ratio)); + $filled = (int) round($ratio * self::BAR_WIDTH); + $empty = self::BAR_WIDTH - $filled; + + $colorTag = match (true) { + $ratio > 0.90 => 'profiler_fail', + $ratio > 0.75 => 'profiler_bar_danger', + $ratio > 0.50 => 'profiler_bar_warn', + default => 'profiler_bar_ok', + }; + + return "▏<{$colorTag}>".str_repeat('█', $filled).''.str_repeat('░', $empty).'▕'; + } + + private function getTrendIcon(int $trend): string + { + return match ($trend) { + 1 => '↑', + -1 => '↓', + default => '→', + }; + } + + private function boxLine(string $content, int $innerWidth): string + { + $visibleLength = mb_strlen((string) preg_replace('/<[^>]*>/', '', $content)); + $padding = max(0, $innerWidth - $visibleLength); + + return '║'.$content.str_repeat(' ', $padding).'║'; + } + + private function formatExitStr(?int $exitCode): string + { + if ($exitCode === null) { + return ''; + } + + return ' Exit: '.($exitCode === 0 ? '0' : ''.$exitCode.''); + } + + private function calculateRatio(int $usage, int $limit): float + { + if ($limit <= 0) { + return 0.0; + } + + return $usage / $limit; + } + + private function formatPercentage(float $ratio): string + { + if ($ratio <= 0.0) { + return 'n/a'; + } + + return sprintf('%.1f%%', $ratio * 100); + } + + private function formatMemoryLimitStr(int $limit): string + { + if ($limit <= 0) { + return '∞'; + } + + return $this->formatter->formatBytes($limit); + } + + private function calculateQps(int $queryCount, float $duration): float + { + if ($duration <= 0.0) { + return 0.0; + } + + return $queryCount / $duration; + } +} diff --git a/src/Tui/TuiManager.php b/src/Tui/TuiManager.php new file mode 100644 index 0000000..f1fad88 --- /dev/null +++ b/src/Tui/TuiManager.php @@ -0,0 +1,176 @@ +term = new Terminal(); + } + + /** Terminal width via property hook — recalculated on each access. */ + public int $terminalWidth { + get { + $width = $this->term->getWidth(); + + return $width < 70 ? 70 : $width; + } + } + + /** Terminal height via property hook. */ + public int $terminalHeight { + get { + $height = $this->term->getHeight(); + + return $height < 20 ? 24 : $height; + } + } + + /** + * Initialize the profiler: set up scroll region, render initial bar. + */ + public function initialize(ConsoleOutput $output): void + { + $this->output = $output; + $this->stream = $output->getStream(); + $this->frozen = false; + + $this->terminal->setStream($this->stream); + $this->renderer->registerStyles($output); + + // Calculate initial height from an empty/dummy snapshot + $dummySnapshot = new MetricsSnapshot( + memoryUsage: 0, + peakMemory: 0, + memoryLimit: 0, + duration: 0.0, + cpuUserTime: 0.0, + cpuSystemTime: 0.0, + memoryGrowthRate: 0.0, + pid: 0, + commandName: '', + environment: '', + queryCount: 0, + loadedClasses: 0, + declaredFunctions: 0, + includedFiles: 0, + gcCycles: 0, + phpVersion: '', + sapiName: '', + opcacheEnabled: false, + xdebugEnabled: false, + ); + $initialLines = $this->renderer->build($dummySnapshot, ProfilerStatus::Running, $this->terminalWidth); + $this->profilerHeight = count($initialLines); + + for ($i = 0; $i < $this->profilerHeight; ++$i) { + if (is_resource($this->stream)) { + fwrite($this->stream, PHP_EOL); + } + } + + $scrollStart = $this->profilerHeight + 1; + $this->terminal->setScrollRegion($scrollStart, $this->terminalHeight); + + $this->terminal->moveTo($scrollStart); + + $this->initialized = true; + } + + /** + * Render the profiler dashboard at the fixed top rows. + */ + public function render(MetricsSnapshot $snapshot): void + { + if ($this->frozen || !$this->initialized) { + return; + } + + $this->writeBarToTop($this->renderer->build($snapshot, ProfilerStatus::Running, $this->terminalWidth)); + } + + /** + * Freeze the dashboard with final metrics and restore full terminal scrolling. + */ + public function freeze(MetricsSnapshot $snapshot, bool $failed = false, ?int $exitCode = null): void + { + if (!$this->initialized) { + return; + } + + $status = $failed ? ProfilerStatus::Failed : ProfilerStatus::Completed; + $this->writeBarToTop($this->renderer->build($snapshot, $status, $this->terminalWidth, $exitCode)); + + $this->terminal->resetScrollRegion(); + $this->terminal->moveTo($this->terminalHeight); + if (is_resource($this->stream)) { + fwrite($this->stream, PHP_EOL); + } + + $this->frozen = true; + } + + public function isInitialized(): bool + { + return $this->initialized; + } + + /** + * Write the profiler bar at fixed top rows using direct cursor addressing. + * + * @param array $lines + */ + private function writeBarToTop(array $lines): void + { + if ($this->output === null) { + return; + } + + $formatter = $this->output->getFormatter(); + + $this->terminal->saveCursor(); + + foreach ($lines as $i => $line) { + $this->terminal->moveTo($i + 1); + $this->terminal->clearLine(); + $this->terminal->writeRaw((string) $formatter->format($line)); + } + + $this->terminal->restoreCursor(); + } +} diff --git a/src/Util/FormattingUtils.php b/src/Util/FormattingUtils.php new file mode 100644 index 0000000..f768987 --- /dev/null +++ b/src/Util/FormattingUtils.php @@ -0,0 +1,76 @@ += mb_strlen($value)) { + return $value; + } + + return mb_substr($value, 0, $maxLength - 1).'…'; + } +} diff --git a/tests/ConsoleProfilerBundleTest.php b/tests/ConsoleProfilerBundleTest.php new file mode 100644 index 0000000..a452299 --- /dev/null +++ b/tests/ConsoleProfilerBundleTest.php @@ -0,0 +1,128 @@ +createMock(DefinitionFileLoader::class); + $configurator = new DefinitionConfigurator($treeBuilder, $loader, __DIR__, __FILE__); + + $bundle->configure($configurator); + + $rootNode = $treeBuilder->getRootNode(); + $reflection = new ReflectionObject($rootNode); + $childrenProperty = $reflection->getProperty('children'); + $childrenProperty->setAccessible(true); + $children = $childrenProperty->getValue($rootNode); + + static::assertIsArray($children); + static::assertArrayHasKey('enabled', $children); + static::assertArrayHasKey('refresh_interval', $children); + } + + #[Test] + public function itLoadsExtensionWhenEnabled(): void + { + $bundle = new ConsoleProfilerBundle(); + $container = new ContainerBuilder(new ParameterBag([ + 'kernel.debug' => true, + 'kernel.environment' => 'dev', + 'kernel.project_dir' => __DIR__, + ])); + + $loader = $this->createMock(PhpFileLoader::class); + $instanceof = []; + $configurator = new ContainerConfigurator($container, $loader, $instanceof, __DIR__, __FILE__); + + $config = [ + 'enabled' => true, + 'exclude_in_prod' => true, + 'refresh_interval' => 1, + 'excluded_commands' => ['list'], + 'profile_dump_path' => '/tmp/prof.json', + ]; + + $bundle->loadExtension( + $config, + $configurator, + $container + ); + + static::assertTrue($container->has(ConsoleProfilerListener::class)); + + $listenerDef = $container->getDefinition(ConsoleProfilerListener::class); + static::assertSame(1, $listenerDef->getArgument('$refreshInterval')); + } + + #[Test] + public function itSkipsLoadingWhenDisabled(): void + { + $bundle = new ConsoleProfilerBundle(); + $container = new ContainerBuilder(); + + $loader = $this->createMock(PhpFileLoader::class); + $instanceof = []; + $configurator = new ContainerConfigurator($container, $loader, $instanceof, __DIR__, __FILE__); + + $bundle->loadExtension( + [ + 'enabled' => false, + 'exclude_in_prod' => true, + 'refresh_interval' => 1, + 'excluded_commands' => [], + 'profile_dump_path' => null, + ], + $configurator, + $container + ); + + static::assertFalse($container->has(ConsoleProfilerListener::class)); + } + + #[Test] + public function itSkipsLoadingInProdDebugFalse(): void + { + $bundle = new ConsoleProfilerBundle(); + $container = new ContainerBuilder(new ParameterBag(['kernel.debug' => false])); + + $loader = $this->createMock(PhpFileLoader::class); + $instanceof = []; + $configurator = new ContainerConfigurator($container, $loader, $instanceof, __DIR__, __FILE__); + + $bundle->loadExtension( + [ + 'enabled' => true, + 'exclude_in_prod' => true, + 'refresh_interval' => 1, + 'excluded_commands' => [], + 'profile_dump_path' => null, + ], + $configurator, + $container + ); + + static::assertFalse($container->has(ConsoleProfilerListener::class)); + } +} diff --git a/tests/Doctrine/DoctrineMiddlewareTest.php b/tests/Doctrine/DoctrineMiddlewareTest.php new file mode 100644 index 0000000..912a555 --- /dev/null +++ b/tests/Doctrine/DoctrineMiddlewareTest.php @@ -0,0 +1,129 @@ +counter = new QueryCounter(); + } + + #[Test] + public function middlewareWrapReturnsProfilingDriver(): void + { + $innerDriver = $this->createMock(Driver::class); + $middleware = new ProfilingMiddleware($this->counter); + + $driver = $middleware->wrap($innerDriver); + $this->expectNotToPerformAssertions(); + } + + #[Test] + public function connectionQueryIncrementsCounter(): void + { + $result = $this->createMock(Result::class); + $innerConnection = $this->createMock(Connection::class); + $innerConnection->method('query')->willReturn($result); + + $connection = new ProfilingConnection($innerConnection, $this->counter); + + static::assertSame(0, $this->counter->count); + + $connection->query('SELECT 1'); + static::assertSame(1, $this->counter->count); + + $connection->query('SELECT 2'); + static::assertSame(2, $this->counter->count); + } + + #[Test] + public function connectionExecIncrementsCounter(): void + { + $innerConnection = $this->createMock(Connection::class); + $innerConnection->method('exec')->willReturn(1); + + $connection = new ProfilingConnection($innerConnection, $this->counter); + + $connection->exec('DELETE FROM users WHERE id = 1'); + static::assertSame(1, $this->counter->count); + } + + #[Test] + public function connectionPrepareReturnsProfilingStatement(): void + { + $innerStatement = $this->createMock(Statement::class); + $innerConnection = $this->createMock(Connection::class); + $innerConnection->method('prepare')->willReturn($innerStatement); + + $connection = new ProfilingConnection($innerConnection, $this->counter); + $statement = $connection->prepare('SELECT * FROM users WHERE id = ?'); + + static::assertInstanceOf(ProfilingStatement::class, $statement); + // Prepare alone does NOT increment — only execute does + static::assertSame(0, $this->counter->count); + } + + #[Test] + public function statementExecuteIncrementsCounter(): void + { + $result = $this->createMock(Result::class); + + // Create a minimal mock that extends AbstractStatementMiddleware + $innerStatement = $this->createMock(Statement::class); + $innerStatement->method('execute')->willReturn($result); + + $statement = new ProfilingStatement($innerStatement, $this->counter); + + $statement->execute(); + static::assertSame(1, $this->counter->count); + + $statement->execute(); + static::assertSame(2, $this->counter->count); + } + + #[Test] + public function fullMiddlewareChainCountsCorrectly(): void + { + $result = $this->createMock(Result::class); + $innerStatement = $this->createMock(Statement::class); + $innerStatement->method('execute')->willReturn($result); + + $innerConnection = $this->createMock(Connection::class); + $innerConnection->method('query')->willReturn($result); + $innerConnection->method('exec')->willReturn(0); + $innerConnection->method('prepare')->willReturn($innerStatement); + + $connection = new ProfilingConnection($innerConnection, $this->counter); + + $connection->query('SELECT 1'); + $connection->exec('UPDATE t SET x = 1'); + $stmt = $connection->prepare('INSERT INTO t VALUES (?)'); + $stmt->execute(); + $stmt->execute(); + + static::assertSame(4, $this->counter->count); + } +} diff --git a/tests/Doctrine/ProfilingDriverTest.php b/tests/Doctrine/ProfilingDriverTest.php new file mode 100644 index 0000000..4a6e3de --- /dev/null +++ b/tests/Doctrine/ProfilingDriverTest.php @@ -0,0 +1,37 @@ +createMock(Driver::class); + $innerConnection = $this->createMock(Connection::class); + $innerDriver->expects(static::once()) + ->method('connect') + ->with(['url' => 'sqlite:///:memory:']) + ->willReturn($innerConnection); + + $queryCounter = new QueryCounter(); + + $driver = new ProfilingDriver($innerDriver, $queryCounter); + + $connection = $driver->connect(['url' => 'sqlite:///:memory:']); + + static::assertInstanceOf(ProfilingConnection::class, $connection); + } +} diff --git a/tests/Doctrine/QueryCounterTest.php b/tests/Doctrine/QueryCounterTest.php new file mode 100644 index 0000000..530d203 --- /dev/null +++ b/tests/Doctrine/QueryCounterTest.php @@ -0,0 +1,72 @@ +counter = new QueryCounter(); + } + + #[Test] + public function itStartsAtZero(): void + { + static::assertSame(0, $this->counter->count); + } + + #[Test] + public function itIncrementsCorrectly(): void + { + $this->counter->increment(); + static::assertSame(1, $this->counter->count); + + $this->counter->increment(); + $this->counter->increment(); + static::assertSame(3, $this->counter->count); + } + + #[Test] + public function itResetsToZero(): void + { + $this->counter->increment(); + $this->counter->increment(); + static::assertSame(2, $this->counter->count); + + $this->counter->reset(); + static::assertSame(0, $this->counter->count); + } + + #[Test] + public function itCanIncrementAfterReset(): void + { + $this->counter->increment(); + $this->counter->reset(); + $this->counter->increment(); + + static::assertSame(1, $this->counter->count); + } + + #[Test] + public function countPropertyIsPubliclyReadable(): void + { + $this->counter->increment(); + + // Asymmetric visibility: public read, private(set) write + $reflection = new ReflectionProperty(QueryCounter::class, 'count'); + static::assertTrue($reflection->isPublic()); + } +} diff --git a/tests/EventListener/ConsoleProfilerListenerTest.php b/tests/EventListener/ConsoleProfilerListenerTest.php new file mode 100644 index 0000000..2d68f9e --- /dev/null +++ b/tests/EventListener/ConsoleProfilerListenerTest.php @@ -0,0 +1,250 @@ +metricsCollector = $this->createMock(MetricsProviderInterface::class); + $this->tuiManager = new TuiManager(new AnsiTerminal(), new DashboardRenderer(new FormattingUtils())); + $this->queryCounter = new QueryCounter(); + $this->profileExporter = new ProfileExporter(); + } + + #[Test] + public function itHasAsEventListenerAttributes(): void + { + $ref = new ReflectionClass(ConsoleProfilerListener::class); + $attributes = $ref->getAttributes(\Symfony\Component\EventDispatcher\Attribute\AsEventListener::class); + + static::assertCount(3, $attributes); + } + + #[Test] + public function itCanBeInstantiatedWithDefaults(): void + { + $listener = new ConsoleProfilerListener( + metricsCollector: new MetricsCollector('test'), + tuiManager: new TuiManager(new AnsiTerminal(), new DashboardRenderer(new FormattingUtils())), + queryCounter: new QueryCounter(), + ); + + $ref = new ReflectionClass($listener); + static::assertTrue($ref->isFinal()); + } + + #[Test] + public function itCanBeInstantiatedWithCustomExcludedCommands(): void + { + $listener = new ConsoleProfilerListener( + metricsCollector: new MetricsCollector('test'), + tuiManager: new TuiManager(new AnsiTerminal(), new DashboardRenderer(new FormattingUtils())), + queryCounter: new QueryCounter(), + refreshInterval: 2, + excludedCommands: ['list', 'help', 'custom:skip'], + ); + + $ref = new ReflectionClass($listener); + static::assertSame(3, count($ref->getAttributes(\Symfony\Component\EventDispatcher\Attribute\AsEventListener::class))); + } + + #[Test] + public function itSkipsExcludedCommands(): void + { + $listener = new ConsoleProfilerListener( + $this->metricsCollector, + $this->tuiManager, + $this->queryCounter, + 1, + ['list'] + ); + + $this->metricsCollector->expects(static::never())->method('start'); + + $command = new Command('list'); + $event = new ConsoleCommandEvent($command, new StringInput(''), new ConsoleOutput()); + + $listener->onCommand($event); + } + + #[Test] + public function itSkipsNonConsoleOutput(): void + { + $listener = new ConsoleProfilerListener( + $this->metricsCollector, + $this->tuiManager, + $this->queryCounter, + ); + + $this->metricsCollector->expects(static::never())->method('start'); + + $command = new Command('app:test'); + $event = new ConsoleCommandEvent($command, new StringInput(''), new NullOutput()); + + $listener->onCommand($event); + } + + #[Test] + public function itStartsProfilingOnValidCommand(): void + { + $listener = new ConsoleProfilerListener( + $this->metricsCollector, + $this->tuiManager, + $this->queryCounter, + ); + + $this->metricsCollector->expects(static::once())->method('start')->with('app:test'); + $this->metricsCollector->method('snapshot')->willReturn($this->createSnapshot()); + + $command = new Command('app:test'); + + $output = $this->createMock(ConsoleOutput::class); + $stream = fopen('php://memory', 'rw'); + $output->method('getStream')->willReturn($stream); + $output->method('getFormatter')->willReturn(new \Symfony\Component\Console\Formatter\OutputFormatter()); + + $event = new ConsoleCommandEvent($command, new StringInput(''), $output); + + $listener->onCommand($event); + + static::assertTrue($this->tuiManager->isInitialized()); + } + + #[Test] + public function itFreezesOnTerminate(): void + { + $listener = new ConsoleProfilerListener( + $this->metricsCollector, + $this->tuiManager, + $this->queryCounter, + 1, + [], + $this->profileExporter, + '/tmp/dump.json' + ); + + $command = new Command('app:test'); + $output = $this->createMock(ConsoleOutput::class); + $stream = fopen('php://memory', 'rw'); + $output->method('getStream')->willReturn($stream); + $output->method('getFormatter')->willReturn(new \Symfony\Component\Console\Formatter\OutputFormatter()); + + $snapshot = $this->createSnapshot(); + $this->metricsCollector->method('snapshot')->willReturn($snapshot); + + $eventStart = new ConsoleCommandEvent($command, new StringInput(''), $output); + $listener->onCommand($eventStart); + + if (file_exists('/tmp/dump.json')) { + unlink('/tmp/dump.json'); + } + + $eventTerminate = new ConsoleTerminateEvent($command, new StringInput(''), $output, 0); + $listener->onTerminate($eventTerminate); + + static::assertFileExists('/tmp/dump.json'); + unlink('/tmp/dump.json'); + + static::assertIsResource($stream); + rewind($stream); + $contents = stream_get_contents($stream); + static::assertStringContainsString('COMPLETED', $contents); + + // Calling terminate again should be a no-op due to !$this->active + $listener->onTerminate($eventTerminate); + } + + #[Test] + public function itFreezesAsFailedOnErrorThenTerminate(): void + { + $listener = new ConsoleProfilerListener( + $this->metricsCollector, + $this->tuiManager, + $this->queryCounter, + ); + + $command = new Command('app:test'); + $output = $this->createMock(ConsoleOutput::class); + $stream = fopen('php://memory', 'rw'); + $output->method('getStream')->willReturn($stream); + $output->method('getFormatter')->willReturn(new \Symfony\Component\Console\Formatter\OutputFormatter()); + + $snapshot = $this->createSnapshot(); + $this->metricsCollector->method('snapshot')->willReturn($snapshot); + + $eventStart = new ConsoleCommandEvent($command, new StringInput(''), $output); + $listener->onCommand($eventStart); + + // ERROR only sets the failure flag, does NOT freeze + $eventError = new ConsoleErrorEvent(new StringInput(''), $output, new Exception(), $command); + $eventError->setExitCode(1); + $listener->onError($eventError); + + // TERMINATE freezes with FAILED status and the correct exit code + $eventTerminate = new ConsoleTerminateEvent($command, new StringInput(''), $output, 1); + $listener->onTerminate($eventTerminate); + + static::assertIsResource($stream); + rewind($stream); + $contents = stream_get_contents($stream); + static::assertStringContainsString('FAILED', $contents); + } + + #[Test] + public function itHandlesPcntlFallback(): void + { + $listener = new ConsoleProfilerListener( + $this->metricsCollector, + $this->tuiManager, + $this->queryCounter, + ); + + $reflection = new ReflectionMethod($listener, 'callPcntl'); + $reflection->setAccessible(true); + + static::assertNull($reflection->invoke($listener, 'non_existent_function')); + static::assertTrue($reflection->invoke($listener, 'is_bool', true)); + } +} diff --git a/tests/Service/MetricsCollectorTest.php b/tests/Service/MetricsCollectorTest.php new file mode 100644 index 0000000..979cdb5 --- /dev/null +++ b/tests/Service/MetricsCollectorTest.php @@ -0,0 +1,159 @@ +collector = new MetricsCollector('test'); + } + + #[Test] + public function memoryUsagePropertyHookReturnsPositiveValue(): void + { + static::assertGreaterThan(0, $this->collector->memoryUsage); + } + + #[Test] + public function peakMemoryPropertyHookReturnsPositiveValue(): void + { + static::assertGreaterThan(0, $this->collector->peakMemory); + } + + #[Test] + public function elapsedIsZeroBeforeStart(): void + { + static::assertSame(0.0, $this->collector->elapsed); + } + + #[Test] + public function elapsedIncreasesAfterStart(): void + { + $this->collector->start('test:command'); + usleep(10_000); + + static::assertGreaterThan(0.0, $this->collector->elapsed); + } + + #[Test] + public function loadedClassesPropertyHookReturnsPositiveValue(): void + { + static::assertGreaterThan(0, $this->collector->loadedClasses); + } + + #[Test] + public function declaredFunctionsPropertyHookReturnsPositiveValue(): void + { + static::assertGreaterThanOrEqual(0, $this->collector->declaredFunctions); + } + + #[Test] + public function includedFilesPropertyHookReturnsPositiveValue(): void + { + static::assertGreaterThan(0, $this->collector->includedFiles); + } + + #[Test] + public function cpuUserTimePropertyHookReturnsNonNegative(): void + { + static::assertGreaterThanOrEqual(0.0, $this->collector->cpuUserTime); + } + + #[Test] + public function memoryLimitPropertyHookReturnsValue(): void + { + // Either -1 (unlimited) or a positive byte count + $limit = $this->collector->memoryLimit; + static::assertTrue($limit === -1 || $limit > 0, "memory_limit should be -1 or positive, got {$limit}"); + } + + #[Test] + public function memoryLimitParsesUnitsCorrectly(): void + { + $original = ini_get('memory_limit'); + + try { + ini_set('memory_limit', '512M'); + static::assertSame(512 * 1024 * 1024, $this->collector->memoryLimit); + + ini_set('memory_limit', '1G'); + static::assertSame(1024 * 1024 * 1024, $this->collector->memoryLimit); + + ini_set('memory_limit', '2G'); + static::assertSame(2 * 1024 * 1024 * 1024, $this->collector->memoryLimit); + + ini_set('memory_limit', '-1'); + static::assertSame(-1, $this->collector->memoryLimit); + } finally { + ini_set('memory_limit', $original); + } + } + + #[Test] + public function snapshotReturnsCorrectReadonlyDto(): void + { + $this->collector->start('app:import'); + + usleep(5_000); + + $snapshot = $this->collector->snapshot(2); + + static::assertSame('app:import', $snapshot->commandName); + static::assertSame('test', $snapshot->environment); + static::assertSame(2, $snapshot->queryCount); + static::assertGreaterThan(0, $snapshot->memoryUsage); + static::assertGreaterThan(0, $snapshot->peakMemory); + static::assertGreaterThan(0.0, $snapshot->duration); + static::assertSame((int) getmypid(), $snapshot->pid); + static::assertSame(PHP_VERSION, $snapshot->phpVersion); + static::assertSame(PHP_SAPI, $snapshot->sapiName); + static::assertGreaterThanOrEqual(0, $snapshot->loadedClasses); + static::assertGreaterThanOrEqual(0, $snapshot->includedFiles); + static::assertGreaterThanOrEqual(0, $snapshot->gcCycles); + } + + #[Test] + public function snapshotIsImmutable(): void + { + $this->collector->start('app:test'); + $this->collector->snapshot(0); + + $reflection = new ReflectionClass(MetricsSnapshot::class); + static::assertTrue($reflection->isReadOnly()); + } + + #[Test] + public function multipleSnapshotsAreIndependent(): void + { + $this->collector->start('app:test'); + + $snapshot1 = $this->collector->snapshot(0); + usleep(10_000); + $snapshot2 = $this->collector->snapshot(1); + + static::assertNotSame($snapshot1->duration, $snapshot2->duration); + static::assertSame(0, $snapshot1->queryCount); + static::assertSame(1, $snapshot2->queryCount); + } +} diff --git a/tests/Service/ProfileExporterTest.php b/tests/Service/ProfileExporterTest.php new file mode 100644 index 0000000..a9dc085 --- /dev/null +++ b/tests/Service/ProfileExporterTest.php @@ -0,0 +1,114 @@ +tmpDir = sys_get_temp_dir().'/profiler_test_'.bin2hex(random_bytes(4)); + $this->fs = new Filesystem(); + } + + #[Override] + protected function tearDown(): void + { + $path = $this->tmpDir.'/profile.json'; + if ($this->fs->exists($path) === true) { + $this->fs->remove($path); + } + if ($this->fs->exists($this->tmpDir) === true) { + $this->fs->remove($this->tmpDir); + } + } + + #[Test] + public function itExportsSnapshotToJsonFile(): void + { + $exporter = new ProfileExporter(); + $path = $this->tmpDir.'/profile.json'; + + $exporter->export($this->createSnapshot(), $path); + + static::assertFileExists($path); + + $json = $this->fs->readFile($path); + + /** @var array $data */ + $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR); + + static::assertSame('app:import-data', $data['command']); + static::assertSame('dev', $data['environment']); + static::assertNull($data['exit_code']); + static::assertArrayHasKey('memory', $data); + static::assertArrayHasKey('cpu', $data); + static::assertArrayHasKey('counters', $data); + static::assertArrayHasKey('system', $data); + static::assertArrayHasKey('timestamp', $data); + } + + #[Test] + public function itCreatesDirectoryIfNotExists(): void + { + $exporter = new ProfileExporter(); + $path = $this->tmpDir.'/profile.json'; + + static::assertDirectoryDoesNotExist($this->tmpDir); + + $exporter->export($this->createSnapshot(), $path); + + static::assertDirectoryExists($this->tmpDir); + static::assertFileExists($path); + } + + #[Test] + public function itIncludesMemoryGrowthRate(): void + { + $exporter = new ProfileExporter(); + $path = $this->tmpDir.'/profile.json'; + + $exporter->export($this->createSnapshot(), $path); + + /** @var array $data */ + $data = json_decode($this->fs->readFile($path), true, 512, JSON_THROW_ON_ERROR); + + static::assertIsArray($data['memory']); + static::assertArrayHasKey('growth_rate_bytes_per_sec', $data['memory']); + static::assertEquals(512000.0, $data['memory']['growth_rate_bytes_per_sec']); + } + + #[Test] + public function itIncludesQueryCounters(): void + { + $exporter = new ProfileExporter(); + $path = $this->tmpDir.'/profile.json'; + + $exporter->export($this->createSnapshot(), $path); + + /** @var array $data */ + $data = json_decode($this->fs->readFile($path), true, 512, JSON_THROW_ON_ERROR); + + static::assertIsArray($data['counters']); + static::assertSame(142, $data['counters']['sql_queries']); + static::assertSame(312, $data['counters']['loaded_classes']); + } +} diff --git a/tests/TestTrait.php b/tests/TestTrait.php new file mode 100644 index 0000000..4ed01d4 --- /dev/null +++ b/tests/TestTrait.php @@ -0,0 +1,38 @@ +renderer = new DashboardRenderer(new FormattingUtils()); + } + + #[Test] + public function registerStylesDoesNotError(): void + { + $output = $this->createMock(ConsoleOutput::class); + $output->method('getFormatter')->willReturn(new \Symfony\Component\Console\Formatter\OutputFormatter()); + + $this->renderer->registerStyles($output); + + static::assertTrue($output->getFormatter()->hasStyle('profiler_title')); + } + + #[Test] + public function buildReturnsCorrectNumberOfLines(): void + { + $lines = $this->renderer->build($this->createSnapshot(), ProfilerStatus::Running, 80); + + static::assertCount(17, $lines); + } + + #[Test] + public function buildIncludesRunningBadge(): void + { + $lines = $this->renderer->build($this->createSnapshot(), ProfilerStatus::Running, 80); + + $titleLine = $lines[1]; + static::assertStringContainsString('PROFILING', $titleLine); + static::assertStringContainsString('profiler_running', $titleLine); + } + + #[Test] + public function buildIncludesCompletedBadge(): void + { + $lines = $this->renderer->build($this->createSnapshot(), ProfilerStatus::Completed, 80); + + $titleLine = $lines[1]; + static::assertStringContainsString('COMPLETED', $titleLine); + static::assertStringContainsString('profiler_ok', $titleLine); + } + + #[Test] + public function buildIncludesFailedBadge(): void + { + $lines = $this->renderer->build($this->createSnapshot(), ProfilerStatus::Failed, 80); + + $titleLine = $lines[1]; + static::assertStringContainsString('FAILED', $titleLine); + static::assertStringContainsString('profiler_fail', $titleLine); + } + + #[Test] + public function buildRendersCommandName(): void + { + $lines = $this->renderer->build($this->createSnapshot(), ProfilerStatus::Running, 80); + + $commandLine = $lines[3]; + static::assertStringContainsString('app:import-data', $commandLine); + } + + #[Test] + public function buildRendersQueryCount(): void + { + $lines = $this->renderer->build($this->createSnapshot(), ProfilerStatus::Running, 80); + + $sqlLine = $lines[14]; + static::assertStringContainsString('142', $sqlLine); + } + + #[Test] + public function buildUsesBoxBorders(): void + { + $lines = $this->renderer->build($this->createSnapshot(), ProfilerStatus::Running, 80); + + static::assertStringContainsString('╔', $lines[0]); + static::assertStringContainsString('╚', $lines[16]); + static::assertStringContainsString('║', $lines[1]); + } + + #[Test] + public function buildRendersExitCodeOnFinalRender(): void + { + $lines = $this->renderer->build($this->createSnapshot(), ProfilerStatus::Completed, 80, exitCode: 0); + + $titleLine = $lines[1]; + static::assertStringContainsString('Exit:', $titleLine); + static::assertStringContainsString('0', $titleLine); + } + + #[Test] + public function buildRendersFailedExitCode(): void + { + $lines = $this->renderer->build($this->createSnapshot(), ProfilerStatus::Failed, 80, exitCode: 1); + + $titleLine = $lines[1]; + static::assertStringContainsString('Exit:', $titleLine); + static::assertStringContainsString('1', $titleLine); + } + + #[Test] + public function buildRendersMemoryGrowthRate(): void + { + $lines = $this->renderer->build($this->createSnapshot(), ProfilerStatus::Running, 80); + + $memoryLine = $lines[10]; + static::assertStringContainsString('/s', $memoryLine); + static::assertStringContainsString('profiler_bar_ok', $memoryLine); + } + + #[Test] + public function buildRendersProgressBarForHighMemory(): void + { + $snapshot = new MetricsSnapshot( + memoryUsage: 241_591_910, + peakMemory: 241_591_910, + memoryLimit: 268_435_456, + duration: 1.0, + cpuUserTime: 0.5, + cpuSystemTime: 0.01, + memoryGrowthRate: 0.0, + pid: 1, + commandName: 'test', + environment: 'dev', + queryCount: 0, + loadedClasses: 1, + declaredFunctions: 1, + includedFiles: 1, + gcCycles: 0, + phpVersion: '8.4.0', + sapiName: 'cli', + opcacheEnabled: false, + xdebugEnabled: false, + ); + + $lines = $this->renderer->build($snapshot, ProfilerStatus::Running, 80); + $memoryLine = $lines[9]; + + static::assertStringContainsString('profiler_bar_danger', $memoryLine); + } + + #[Test] + public function buildRendersProgressBarForWarningMemory(): void + { + $snapshot = new MetricsSnapshot( + memoryUsage: 187_904_819, + peakMemory: 187_904_819, + memoryLimit: 268_435_456, + duration: 1.0, + cpuUserTime: 0.5, + cpuSystemTime: 0.01, + memoryGrowthRate: 0.0, + pid: 1, + commandName: 'test', + environment: 'dev', + queryCount: 0, + loadedClasses: 1, + declaredFunctions: 1, + includedFiles: 1, + gcCycles: 0, + phpVersion: '8.4.0', + sapiName: 'cli', + opcacheEnabled: false, + xdebugEnabled: false, + ); + + $lines = $this->renderer->build($snapshot, ProfilerStatus::Running, 80); + $memoryLine = $lines[9]; + + static::assertStringContainsString('profiler_bar_warn', $memoryLine); + } + + #[Test] + public function buildHandlesZeroMemoryLimit(): void + { + $snapshot = new MetricsSnapshot( + memoryUsage: 10_000, + peakMemory: 10_000, + memoryLimit: 0, + duration: 1.0, + cpuUserTime: 0.1, + cpuSystemTime: 0.0, + memoryGrowthRate: 0.0, + pid: 1, + commandName: 'test', + environment: 'dev', + queryCount: 0, + loadedClasses: 1, + declaredFunctions: 1, + includedFiles: 1, + gcCycles: 0, + phpVersion: '8.4.0', + sapiName: 'cli', + opcacheEnabled: true, + xdebugEnabled: true, + ); + + $lines = $this->renderer->build($snapshot, ProfilerStatus::Running, 80); + static::assertStringContainsString('∞', $lines[9]); + static::assertStringContainsString('n/a', $lines[9]); + } + + #[Test] + public function buildHandlesZeroDuration(): void + { + $snapshot = new MetricsSnapshot( + memoryUsage: 10_000, + peakMemory: 10_000, + memoryLimit: 100_000, + duration: 0.0, + cpuUserTime: 0.0, + cpuSystemTime: 0.0, + memoryGrowthRate: 0.0, + pid: 1, + commandName: 'test', + environment: 'dev', + queryCount: 0, + loadedClasses: 1, + declaredFunctions: 1, + includedFiles: 1, + gcCycles: 0, + phpVersion: '8.4.0', + sapiName: 'cli', + opcacheEnabled: false, + xdebugEnabled: false, + ); + + $lines = $this->renderer->build($snapshot, ProfilerStatus::Running, 80); + static::assertCount(17, $lines); + } + + #[Test] + public function buildRendersHighGrowthRate(): void + { + $snapshot = new MetricsSnapshot( + memoryUsage: 50_000_000, + peakMemory: 50_000_000, + memoryLimit: 268_435_456, + duration: 2.0, + cpuUserTime: 0.5, + cpuSystemTime: 0.01, + memoryGrowthRate: 15_000_000.0, // > 10MB/s + pid: 1, + commandName: 'test', + environment: 'dev', + queryCount: 0, + loadedClasses: 1, + declaredFunctions: 1, + includedFiles: 1, + gcCycles: 0, + phpVersion: '8.4.0', + sapiName: 'cli', + opcacheEnabled: false, + xdebugEnabled: false, + ); + + $lines = $this->renderer->build($snapshot, ProfilerStatus::Running, 80); + static::assertStringContainsString('profiler_bar_danger', $lines[10]); + } + + #[Test] + public function buildRendersWarnGrowthRate(): void + { + $snapshot = new MetricsSnapshot( + memoryUsage: 50_000_000, + peakMemory: 50_000_000, + memoryLimit: 268_435_456, + duration: 2.0, + cpuUserTime: 0.5, + cpuSystemTime: 0.01, + memoryGrowthRate: 2_000_000.0, // 2MB/s -> Warn + pid: 1, + commandName: 'test', + environment: 'dev', + queryCount: 0, + loadedClasses: 1, + declaredFunctions: 1, + includedFiles: 1, + gcCycles: 0, + phpVersion: '8.4.0', + sapiName: 'cli', + opcacheEnabled: false, + xdebugEnabled: false, + ); + + $lines = $this->renderer->build($snapshot, ProfilerStatus::Running, 80); + static::assertStringContainsString('profiler_bar_warn', $lines[10]); + } +} diff --git a/tests/Tui/TuiManagerTest.php b/tests/Tui/TuiManagerTest.php new file mode 100644 index 0000000..4d7396f --- /dev/null +++ b/tests/Tui/TuiManagerTest.php @@ -0,0 +1,151 @@ +manager = new TuiManager( + terminal: new AnsiTerminal(), + renderer: new DashboardRenderer(new FormattingUtils()), + ); + } + + #[Test] + public function itIsNotInitializedByDefault(): void + { + static::assertFalse($this->manager->isInitialized()); + } + + #[Test] + public function terminalWidthPropertyHookReturnsAtLeast70(): void + { + static::assertGreaterThanOrEqual(70, $this->manager->terminalWidth); + } + + #[Test] + public function terminalHeightPropertyHookReturnsAtLeast20(): void + { + static::assertGreaterThanOrEqual(20, $this->manager->terminalHeight); + } + + #[Test] + public function renderDoesNothingWhenNotInitialized(): void + { + $snapshot = $this->createSnapshot(); + + $this->manager->render($snapshot); + + static::assertFalse($this->manager->isInitialized()); + } + + #[Test] + public function freezeDoesNothingWhenNotInitialized(): void + { + $snapshot = $this->createSnapshot(); + + $this->manager->freeze($snapshot); + + static::assertFalse($this->manager->isInitialized()); + } + + #[Test] + public function itInitializesProperlyWithStreamAndFormatter(): void + { + $output = $this->createMock(ConsoleOutput::class); + $stream = fopen('php://memory', 'rw'); + $output->method('getStream')->willReturn($stream); + $output->method('getFormatter')->willReturn($this->createMock(OutputFormatterInterface::class)); + + $this->manager->initialize($output); + + static::assertTrue($this->manager->isInitialized()); + } + + #[Test] + public function itRendersProfilerDashboardToStream(): void + { + $output = $this->createMock(ConsoleOutput::class); + $stream = fopen('php://memory', 'rw'); + $output->method('getStream')->willReturn($stream); + + $formatter = $this->createMock(OutputFormatterInterface::class); + $formatter->method('format')->willReturnCallback(static fn (?string $s) => $s ?? ''); + $output->method('getFormatter')->willReturn($formatter); + + $this->manager->initialize($output); + $this->manager->render($this->createSnapshot()); + + static::assertIsResource($stream); + rewind($stream); + $contents = stream_get_contents($stream); + + static::assertIsString($contents); + static::assertStringContainsString('CONSOLE PROFILER', $contents); + static::assertStringContainsString('app:import-data', $contents); + } + + #[Test] + public function itFreezesProfilerDashboardAndShowsCompletedStatus(): void + { + $output = $this->createMock(ConsoleOutput::class); + $stream = fopen('php://memory', 'rw'); + $output->method('getStream')->willReturn($stream); + + $formatter = $this->createMock(OutputFormatterInterface::class); + $formatter->method('format')->willReturnCallback(static fn (?string $s) => $s ?? ''); + $output->method('getFormatter')->willReturn($formatter); + + $this->manager->initialize($output); + $this->manager->freeze($this->createSnapshot(), false, 0); + + static::assertIsResource($stream); + rewind($stream); + $contents = stream_get_contents($stream); + + static::assertIsString($contents); + static::assertStringContainsString('COMPLETED', $contents); + } + + #[Test] + public function itFreezesProfilerDashboardAndShowsFailedStatus(): void + { + $output = $this->createMock(ConsoleOutput::class); + $stream = fopen('php://memory', 'rw'); + $output->method('getStream')->willReturn($stream); + + $formatter = $this->createMock(OutputFormatterInterface::class); + $formatter->method('format')->willReturnCallback(static fn (?string $s) => $s ?? ''); + $output->method('getFormatter')->willReturn($formatter); + + $this->manager->initialize($output); + $this->manager->freeze($this->createSnapshot(), true, 1); + + static::assertIsResource($stream); + rewind($stream); + $contents = stream_get_contents($stream); + + static::assertIsString($contents); + static::assertStringContainsString('FAILED', $contents); + } +} diff --git a/tests/Util/FormattingUtilsTest.php b/tests/Util/FormattingUtilsTest.php new file mode 100644 index 0000000..4d5e3f6 --- /dev/null +++ b/tests/Util/FormattingUtilsTest.php @@ -0,0 +1,56 @@ +utils = new FormattingUtils(); + } + + #[Test] + public function itFormatsBytesToHumanReadable(): void + { + static::assertSame('0 B', $this->utils->formatBytes(0)); + static::assertSame('1.0 KB', $this->utils->formatBytes(1024)); + static::assertSame('1.5 MB', $this->utils->formatBytes(1572864)); + } + + #[Test] + public function itFormatsDurationToHumanReadable(): void + { + static::assertSame('100μs', $this->utils->formatDuration(0.0001)); + static::assertSame('500ms', $this->utils->formatDuration(0.5)); + static::assertSame('4.25s', $this->utils->formatDuration(4.25)); + static::assertSame('2m 5.0s', $this->utils->formatDuration(125.0)); + } + + #[Test] + public function itFormatsDurationCompact(): void + { + static::assertSame('00:05', $this->utils->formatDurationCompact(5.0)); + static::assertSame('02:05', $this->utils->formatDurationCompact(125.0)); + } + + #[Test] + public function itTruncatesLongStrings(): void + { + static::assertSame('Short', $this->utils->truncate('Short', 10)); + static::assertSame('Short', $this->utils->truncate('Short', 5)); + static::assertSame('Sho…', $this->utils->truncate('Short', 4)); + static::assertSame('Long string…', $this->utils->truncate('Long string here', 12)); + } +}