From ab42397c0210fcfbc3686cec521dc5cee74e0bcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Tue, 15 Sep 2026 20:18:04 -0300 Subject: [PATCH 1/4] Modernize PHP support and verify real error handling failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- .dockerignore | 10 +- .github/workflows/ci.yml | 62 + .github/workflows/pages.yml | 31 + .gitignore | 1 + .php-cs-fixer.dist.php | 25 +- .php_cs | 236 -- .scrutinizer.yml | 28 - CHANGELOG.md | 25 + Dockerfile | 41 +- Makefile | 31 +- README.md | 66 +- composer.json | 31 +- composer.lock | 3302 ++++++++++------- docker-compose.override.yml | 9 - docker-compose.yml | 18 +- docs/.nojekyll | 0 docs/api.md | 62 + docs/index.html | 81 + docs/migration.md | 14 + docs/testing.md | 78 + examples/cli.php | 17 + phpcs.xml | 10 +- phpstan.neon | 10 +- phpunit.xml | 25 +- src/Container/ConfigProvider.php | 10 +- src/Container/ErrorHandlerContainer.php | 14 +- src/ErrorHandler.php | 73 +- src/ErrorHandlerInterface.php | 2 +- .../ErrorEventListenerProvider.php | 2 +- .../ThrowableListenerProvider.php | 2 +- src/Handler/PhpLastErrorShutdownHandler.php | 6 +- src/Http/Message/ThrowableResponseFactory.php | 16 +- src/Http/Message/ThrowableStreamFactory.php | 17 +- tests/Fire/bootstrap.php | 16 + tests/Fire/compile-fatal.inc | 3 + tests/Fire/parse-error.inc | 2 + tests/Fire/run.php | 101 + tests/Fire/scenario.php | 171 + .../Container/ConfigProviderTest.php | 25 +- .../Container/ErrorHandlerContainerTest.php | 20 +- .../Middleware/ErrorHandlerCleanupTest.php | 62 + .../Middleware/ErrorHandlerMiddlewareTest.php | 2 +- tests/Http/router.php | 40 + tests/Http/run.php | 67 + .../Container/ErrorHandlerContainerTest.php | 20 +- .../Message/ThrowableResponseFactoryTest.php | 88 + 46 files changed, 3165 insertions(+), 1807 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/pages.yml delete mode 100644 .php_cs delete mode 100644 .scrutinizer.yml create mode 100644 CHANGELOG.md delete mode 100644 docker-compose.override.yml create mode 100644 docs/.nojekyll create mode 100644 docs/api.md create mode 100644 docs/index.html create mode 100644 docs/migration.md create mode 100644 docs/testing.md create mode 100644 examples/cli.php create mode 100644 tests/Fire/bootstrap.php create mode 100644 tests/Fire/compile-fatal.inc create mode 100644 tests/Fire/parse-error.inc create mode 100644 tests/Fire/run.php create mode 100644 tests/Fire/scenario.php create mode 100644 tests/Functional/Http/Middleware/ErrorHandlerCleanupTest.php create mode 100644 tests/Http/router.php create mode 100644 tests/Http/run.php create mode 100644 tests/Unit/Http/Message/ThrowableResponseFactoryTest.php diff --git a/.dockerignore b/.dockerignore index 75e5dd5..4d8ad36 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,11 +1,7 @@ .composer/ -.git* +.git/ +.github/ .idea/ vendor/ -.php_cs +tmp/ *.cache -*.yml -phpcs.xml -phpstan.neon -Dockerfile -README.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d66e826 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: PHP ${{ matrix.php }} / ${{ matrix.dependencies }} + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + include: + - php: '8.3' + dependencies: lowest + - php: '8.3' + dependencies: highest + - php: '8.4' + dependencies: highest + - php: '8.5' + dependencies: locked + - php: '8.5' + dependencies: highest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: shivammathur/setup-php@b604ade2a87db23f8871b7182e69ec5e75effb45 # v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, mbstring, xml, xmlwriter, pdo + coverage: none + tools: composer:v2 + - run: composer validate --strict --no-plugins + - if: matrix.dependencies == 'locked' + run: composer install --no-plugins --no-scripts --no-interaction --prefer-dist --no-progress + - if: matrix.dependencies == 'highest' + run: composer update --no-plugins --no-scripts --no-interaction --prefer-dist --no-progress + - if: matrix.dependencies == 'lowest' + run: composer update --prefer-lowest --prefer-stable --no-plugins --no-scripts --no-interaction --prefer-dist --no-progress + - run: composer --no-plugins check + - name: Verify the documented CLI example + run: | + python3 - <<'PY' + import subprocess + result = subprocess.run(['php', 'examples/cli.php'], capture_output=True, text=True, timeout=5) + assert result.returncode == 1 and result.stdout == '' + assert 'Example warning' in result.stderr and 'Example uncaught exception' in result.stderr + PY + - name: Verify production-only autoload + run: | + composer install --no-dev --no-plugins --no-scripts --no-interaction --no-progress + php -r 'require "vendor/autoload.php"; new CoiSA\ErrorHandler\Container\ErrorHandlerContainer();' diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..9a85376 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,31 @@ +name: Documentation + +on: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + publish: + if: github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 + with: + path: docs + - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 + id: deployment diff --git a/.gitignore b/.gitignore index de94220..fbd4ac1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ tests/html/ tests/clover.xml tests/testdox.html .*.cache +tmp/ diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 6ee3752..75f1c3d 100755 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -14,25 +14,6 @@ * @license https://opensource.org/licenses/MIT MIT License */ -use CoiSA\PhpCsFixer\PhpCsFixer; - -/** - * This file is part of coisa/logger. - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - * - * @see https://github.com/coisa/factory - * @see https://12factor.net/logs - * - * @copyright Copyright (c) 2022 Felipe Sayão Lobato Abreu - * @license https://opensource.org/licenses/MIT MIT License - */ -$paths = [ - __FILE__, - __DIR__, -]; - -$header = file_get_contents(__DIR__ . '/.docheader'); - -return PhpCsFixer::create($paths, $header); +return (new PhpCsFixer\Config()) + ->setRules(['@PSR12' => true]) + ->setFinder(PhpCsFixer\Finder::create()->in([__DIR__ . '/src', __DIR__ . '/tests'])->name('*.php')); diff --git a/.php_cs b/.php_cs deleted file mode 100644 index 0da66c8..0000000 --- a/.php_cs +++ /dev/null @@ -1,236 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -declare(strict_types=1); - -$header = <<<'EOF' -This file is part of coisa/error-handler. - -(c) Felipe Sayão Lobato Abreu - -This source file is subject to the license that is bundled -with this source code in the file LICENSE. -EOF; - -$finder = PhpCsFixer\Finder::create() - ->files() - ->in([__DIR__]) - ->name('*.php') - ->append([__FILE__]) - ->notPath('vendor/'); - -return PhpCsFixer\Config::create() - ->setRiskyAllowed(true) - ->setRules([ - 'align_multiline_comment' => true, - 'array_syntax' => [ - 'syntax' => 'short' - ], - 'binary_operator_spaces' => [ - 'align_double_arrow' => true, - 'align_equals' => true - ], - 'blank_line_after_namespace' => true, - 'blank_line_after_opening_tag' => true, - 'blank_line_before_statement' => [ - 'statements' => [ - 'break', - 'continue', - 'declare', - 'return', - 'throw', - 'try', - 'yield', - ], - ], - 'braces' => true, - 'cast_spaces' => true, - 'class_attributes_separation' => [ - 'elements' => [ - 'const', - 'method', - 'property' - ] - ], - 'class_definition' => [ - 'multi_line_extends_each_single_line' => false, - 'single_item_single_line' => false - ], - 'class_keyword_remove' => false, - 'combine_consecutive_issets' => true, - 'combine_consecutive_unsets' => true, - 'combine_nested_dirname' => true, - 'compact_nullable_typehint' => true, - 'concat_space' => [ - 'spacing' => 'one' - ], - 'date_time_immutable' => false, - 'declare_equal_normalize' => [ - 'space' => 'none' - ], - 'dir_constant' => true, - 'declare_strict_types' => true, - 'elseif' => true, - 'encoding' => true, - 'ereg_to_preg' => true, - 'error_suppression' => [ - 'mute_deprecation_error' => false, - 'noise_remaining_usages' => false, - 'noise_remaining_usages_exclude' => [] - ], - 'escape_implicit_backslashes' => [ - 'double_quoted' => true, - 'heredoc_syntax' => true, - 'single_quoted' => false, - ], - 'explicit_indirect_variable' => true, - 'explicit_string_variable' => true, - 'final_internal_class' => false, - 'fopen_flag_order' => true, - 'fopen_flags' => [ - 'b_mode' => true, - ], - 'full_opening_tag' => true, - 'fully_qualified_strict_types' => true, - 'function_declaration' => [ - 'closure_function_spacing' => 'one' - ], - 'function_to_constant' => [ - 'functions' => [ - 'get_called_class', - 'get_class', - 'php_sapi_name', - 'phpversion', - 'pi' - ] - ], - 'function_typehint_space' => true, - 'general_phpdoc_annotation_remove' => [ - 'annotations' => [ - 'author' - ] - ], - 'header_comment' => [ - 'commentType' => 'PHPDoc', - 'header' => $header, - 'location' => 'after_open', - 'separate' => 'both' - ], - 'heredoc_indentation' => false, - 'heredoc_to_nowdoc' => true, - 'indentation_type' => true, - 'line_ending' => true, - 'list_syntax' => [ - 'syntax' => 'short' - ], - 'lowercase_cast' => true, - 'lowercase_constants' => true, - 'lowercase_keywords' => true, - 'magic_constant_casing' => true, - 'method_argument_space' => [ - 'ensure_fully_multiline' => true - ], - 'modernize_types_casting' => true, - 'native_function_casing' => true, - 'native_function_invocation' => true, - 'no_alias_functions' => true, - 'no_blank_lines_after_class_opening' => true, - 'no_blank_lines_after_phpdoc' => true, - 'no_closing_tag' => true, - 'no_empty_comment' => true, - 'no_empty_phpdoc' => true, - 'no_empty_statement' => true, - 'no_extra_consecutive_blank_lines' => true, - 'no_homoglyph_names' => true, - 'no_leading_import_slash' => true, - 'no_leading_namespace_whitespace' => true, - 'no_mixed_echo_print' => [ - 'use' => 'echo' - ], - 'no_null_property_initialization' => true, - 'no_short_bool_cast' => true, - 'no_short_echo_tag' => true, - 'no_singleline_whitespace_before_semicolons' => true, - 'no_spaces_after_function_name' => true, - 'no_spaces_inside_parenthesis' => true, - 'no_superfluous_elseif' => true, - 'no_trailing_comma_in_list_call' => true, - 'no_trailing_comma_in_singleline_array' => true, - 'no_trailing_whitespace' => true, - 'no_trailing_whitespace_in_comment' => true, - 'no_unneeded_control_parentheses' => true, - 'no_unneeded_curly_braces' => true, - 'no_unneeded_final_method' => true, - 'no_unreachable_default_argument_value' => true, - 'no_unused_imports' => true, - 'no_useless_else' => true, - 'no_whitespace_before_comma_in_array' => true, - 'no_whitespace_in_blank_line' => true, - 'non_printable_character' => true, - 'normalize_index_brace' => true, - 'object_operator_without_whitespace' => true, - 'ordered_class_elements' => [ - 'order' => [ - 'use_trait', - 'constant_public', - 'constant_protected', - 'constant_private', - 'property_public', - 'property_protected', - 'property_private', - 'construct', - 'destruct', - 'magic', - 'phpunit', - 'method_public', - 'method_protected', - 'method_private', - ], - ], - 'ordered_imports' => true, - 'phpdoc_add_missing_param_annotation' => true, - 'phpdoc_align' => true, - 'phpdoc_annotation_without_dot' => true, - 'phpdoc_indent' => true, - 'phpdoc_no_access' => true, - 'phpdoc_no_empty_return' => true, - 'phpdoc_no_package' => false, - 'phpdoc_order' => true, - 'phpdoc_return_self_reference' => true, - 'phpdoc_scalar' => true, - 'phpdoc_separation' => true, - 'phpdoc_single_line_var_spacing' => true, - 'phpdoc_to_comment' => true, - 'phpdoc_trim' => true, - 'phpdoc_types' => true, - 'phpdoc_types_order' => true, - 'phpdoc_var_without_name' => true, - 'pow_to_exponentiation' => true, - 'protected_to_private' => true, - 'return_type_declaration' => [ - 'space_before' => 'none' - ], - 'self_accessor' => true, - 'short_scalar_cast' => true, - 'simplified_null_return' => true, - 'single_blank_line_at_eof' => true, - 'single_import_per_statement' => true, - 'single_line_after_imports' => true, - 'single_quote' => true, - 'standardize_not_equals' => true, - 'ternary_to_null_coalescing' => true, - 'trim_array_spaces' => true, - 'unary_operator_spaces' => true, - 'visibility_required' => true, - 'void_return' => true, - 'whitespace_after_comma_in_array' => true, - ]) - ->setFinder($finder); diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index 46cd6b4..0000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,28 +0,0 @@ -build: - environment: - php: - version: '7.2' - - nodes: - analysis: - project_setup: - override: true - tests: - override: - - php-scrutinizer-run --enable-security-analysis - - phpcs-run - - coverage: - tests: - override: - - command: vendor/bin/phpunit - coverage: - file: tests/clover.xml - format: clover - -checks: - php: true - -filter: - excluded_paths: - - tests/* diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d7e486e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +## Unreleased — next major + +### Changed + +- Require PHP 8.3 or newer and test PHP 8.3–8.5. PHP 7.x and PHP 8.0–8.2 are outside the support contract. +- Exit with status 1 when the registered uncaught-exception handler completes, preserving failure signals in CLI jobs. Direct `handleThrowable()` calls still return normally. +- Reject out-of-order `unregister()` calls without removing another handler. Only the active registration handles shutdown events. +- Return a generic HTTP body when a reporter emits nothing; use standard reason phrases instead of exception messages. Preserve valid error status codes through 599. +- Update PHPUnit, PHPStan, PHP CS Fixer, PHP_CodeSniffer, Docker and CI. Declare PSR-11 as a runtime dependency. + +### Fixed + +- Re-registering an instance no longer adds duplicate shutdown callbacks. +- Failing or recursive reporters are not invoked repeatedly during exception/shutdown handling. +- Throwable rendering cleans its output buffers even when the renderer throws, preserves the body `0`, and accepts SQLSTATE exception codes without raising a secondary type error. +- Shutdown reporting excludes nonfatal core and compile warnings. + +### Added + +- Bounded subprocess fire tests, HTTP loopback tests, lifecycle and response regression tests. +- English usage, API, migration and testing documentation, plus a static documentation site with manual default-branch Pages deployment. + +The historical 2.0.0 and 1.0.0 releases are available in [GitHub Releases](https://github.com/coisa/error-handler/releases). diff --git a/Dockerfile b/Dockerfile index 667fead..3ceeecd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,10 @@ -FROM php:7.2-alpine - -ARG APP_DIR="/app" -ARG APP_VERSION="latest" - -ENV APP_DIR="${APP_DIR}" \ - APP_VERSION="${APP_VERSION}" - -RUN apk add --update \ - make \ - git \ - zip \ - unzip \ - ; - -RUN apk add --no-cache $PHPIZE_DEPS && \ - pecl install xdebug-2.6.0 && \ - docker-php-ext-enable xdebug - -COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer - -RUN addgroup app && \ - adduser -D -h ${APP_DIR} -G app app && \ - mkdir -p ${APP_DIR}/.composer - -WORKDIR ${APP_DIR} -USER app - -COPY --chown=app . ${APP_DIR}/ - -RUN make install +ARG PHP_VERSION=8.3 +FROM php:${PHP_VERSION}-cli-alpine + +RUN apk add --no-cache git unzip +COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer +WORKDIR /app +COPY . . +# Resolve for this PHP version; the checked-in lock is the PHP 8.5 reference set. +RUN composer update --no-plugins --no-scripts --no-interaction --prefer-dist --no-progress +CMD ["composer", "--no-plugins", "check"] diff --git a/Makefile b/Makefile index 20b2c34..4ca7434 100644 --- a/Makefile +++ b/Makefile @@ -1,25 +1,18 @@ -COMPOSER_BIN := composer +COMPOSER_BIN ?= composer -COMPOSER_FLAGS := --prefer-dist \ - --no-scripts \ - --no-progress \ - --no-suggest \ - --optimize-autoloader \ - --no-ansi \ - --no-interaction \ - --no-plugins +.PHONY: install check test fire-test http-test -.PHONY: install uninstall reinstall clean vendor +install: + $(COMPOSER_BIN) install --no-plugins --no-scripts --no-interaction --prefer-dist -install: vendor -uninstall: clean -reinstall: clean vendor +check: + $(COMPOSER_BIN) --no-plugins check -clean: - rm -rf vendor +test: + $(COMPOSER_BIN) --no-plugins test -/usr/local/bin/composer: - curl -s https://getcomposer.org/installer | sudo php -- --install-dir=/usr/local/bin --filename=composer +fire-test: + php tests/Fire/run.php -vendor: /usr/local/bin/composer - $(COMPOSER_BIN) install $(COMPOSER_FLAGS) +http-test: + php tests/Http/run.php diff --git a/README.md b/README.md index c3de0a7..8bc1ff7 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,67 @@ [![PHP Version][ico-php]][link-packagist] [![Latest Version on Packagist][ico-version]][link-packagist] [![Software License][ico-license]](LICENSE) -[![Build Status][ico-build]][link-build] -[![Coverage Status][ico-scrutinizer]][link-scrutinizer] -[![Quality Score][ico-code-quality]][link-code-quality] +[![CI](https://github.com/coisa/error-handler/actions/workflows/ci.yml/badge.svg)](https://github.com/coisa/error-handler/actions/workflows/ci.yml) [![Total Downloads][ico-downloads]][link-downloads] -## Install +Small, composable PHP error handling with PSR-11 containers, PSR-14 events and PSR-15 middleware. Report warnings, handle uncaught throwables and inspect fatal errors at shutdown through the same reporter interface. + +**Development documentation for the next major release.** This branch requires PHP 8.3–8.5 and changes lifecycle, CLI exit and HTTP response behavior. These changes are not part of the published 2.0.0 release. Read the [migration guide](docs/migration.md) before adopting this branch. + +## Install and run + +To reproduce this branch with PHP 8.5 and Composer 2: ```sh -$ composer require coisa/error-handler +git clone https://github.com/coisa/error-handler.git +cd error-handler +composer install --no-plugins --no-scripts --no-interaction --prefer-dist +composer --no-plugins check +php examples/cli.php +``` + +While reviewing an unmerged PR, check out its branch before installing. On PHP 8.3/8.4 use `composer update --no-plugins --no-scripts` to resolve compatible development tools. The CLI example deliberately exits **1** after reporting a warning and an uncaught exception. + +For the currently published release, `composer require coisa/error-handler` installs the version available on Packagist; it does not select this unreleased work. + +## Quick start + +```php +use CoiSA\ErrorHandler\ErrorHandler; +use CoiSA\ErrorHandler\Handler\CallableThrowableHandler; + +$reporter = new CallableThrowableHandler(static function (Throwable $error): void { + // Route diagnostics to your application's private logger. + error_log($error->getMessage()); +}); +$errors = new ErrorHandler($reporter); +$errors->register(); + +trigger_error('A reported warning; execution continues.', E_USER_WARNING); +throw new RuntimeException('An uncaught exception; the process exits 1.'); ``` +Load Composer's autoloader before the snippet. A normal warning is reported and execution continues. Masked/suppressed warnings are ignored. Direct `handleThrowable()` calls do not terminate the process; only the registered uncaught-exception callback does. Reporters are responsible for their output and privacy. + +## HTTP integration + +Compose `ErrorHandlerMiddleware` with a `ThrowableResponseFactory` and a PSR-17 implementation. The complete, tested [HTTP example](tests/Http/router.php) uses Nyholm PSR-7. Run its bounded smoke test with `php tests/Http/run.php`; it starts and cleans up a loopback-only server automatically. + +The middleware restores handlers in `finally`, including when response rendering fails. Reporters that emit no content produce the generic body `Internal Server Error`. A custom renderer may emit a safe response body. Exception text is never used as a reason phrase. Integer exception codes 400–599 become the response status; other codes become 500. Native PHP warnings are reported separately and do not automatically turn a successful request into an error response. + +## Documentation + +- [API and composition](docs/api.md): lifecycle, factories, handlers, containers and events. +- [Testing and limitations](docs/testing.md): reproducible fire tests, exact observed platforms, isolation and failure boundaries. +- [Migration from 2.0](docs/migration.md): breaking behavior and compatibility changes. +- [Static documentation site](docs/index.html): install, composition and evidence at a glance. Open the file locally, or preview the `docs/` directory with a loopback static server. + +GitHub Pages is prepared as a **manual** workflow restricted to `master`. After review and merge, an owner can enable Pages with GitHub Actions as the source and run the Documentation workflow from `master`. Nothing in the pull-request CI publishes documentation or executes PHP on Pages. + +## Confidence and boundaries + +The test suite includes isolated real PHP failures rather than only calls to mocked handlers. Child processes have memory, time and output bounds; tests check termination, delivery counts and restoration. This is evidence for the exercised cases, not a reliability guarantee for every fatal error, PHP SAPI or downstream reporter. See the [failure matrix](docs/testing.md). + ## Credits - [Felipe Sayão Lobato Abreu][link-author] @@ -25,14 +75,8 @@ Please see [License File](LICENSE) for more information. [ico-version]: https://img.shields.io/packagist/v/coisa/error-handler.svg?style=flat-square [ico-php]: https://img.shields.io/packagist/php-v/coisa/error-handler.svg?style=flat-square [ico-license]: https://img.shields.io/github/license/coisa/error-handler.svg?style=flat-square -[ico-build]: https://img.shields.io/scrutinizer/build/g/coisa/error-handler/master.svg?style=flat-square -[ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/coisa/error-handler.svg?style=flat-square -[ico-code-quality]: https://img.shields.io/scrutinizer/g/coisa/error-handler.svg?style=flat-square [ico-downloads]: https://img.shields.io/packagist/dt/coisa/error-handler.svg?style=flat-square [link-packagist]: https://packagist.org/packages/coisa/error-handler -[link-build]: https://scrutinizer-ci.com/g/coisa/error-handler -[link-scrutinizer]: https://scrutinizer-ci.com/g/coisa/error-handler/code-structure -[link-code-quality]: https://scrutinizer-ci.com/g/coisa/error-handler [link-downloads]: https://packagist.org/packages/coisa/error-handler [link-author]: https://github.com/coisa diff --git a/composer.json b/composer.json index 1b106bb..bb24d70 100644 --- a/composer.json +++ b/composer.json @@ -12,10 +12,7 @@ "config": { "sort-packages": true, "prefer-stable": true, - "optimize-autoloader": true, - "allow-plugins": { - "ocramius/package-versions": true - } + "optimize-autoloader": true }, "extra": { "laminas": { @@ -23,7 +20,8 @@ } }, "require": { - "php": "^7.4 || ^8.0", + "php": "^8.3", + "psr/container": "^1.1 || ^2.0", "psr/http-server-middleware": "^1.0", "psr/http-factory": "^1.1", "psr/event-dispatcher": "^1.0", @@ -35,37 +33,38 @@ } }, "autoload-dev": { + "exclude-from-classmap": ["tests/Fire/", "tests/Http/"], "psr-4": { "CoiSA\\ErrorHandler\\Test\\": "tests/" } }, "require-dev": { - "coisa/php-cs-fixer": "^2.1", + "friendsofphp/php-cs-fixer": "^3.95", "laminas/laminas-servicemanager": "^4.3", "nyholm/psr7": "^1.8", - "nyholm/psr7-server": "^1.1", "phly/phly-event-dispatcher": "^1.0", - "phpspec/prophecy": "^1.20", - "phpstan/phpstan": "^1.12", - "phpstan/phpstan-strict-rules": "^1.6", - "phpunit/phpunit": "^8.5", - "psr/container": "^1.0 || ^2.0", - "squizlabs/php_codesniffer": "^3.11" + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^12.5 || ^13.0", + "squizlabs/php_codesniffer": "^4.0" }, "scripts": { "check": [ "@cs-check", "@sniff", "@test", - "@analyze" + "@analyze", + "@fire-test", + "@http-test" ], - "analyze": "phpstan analyze --level max ./src ./tests", + "analyze": "phpstan analyze --no-progress", "cs-check": "php-cs-fixer fix --dry-run --diff", "cs-fix": [ "php-cs-fixer fix", "phpcbf" ], "sniff": "phpcs", - "test": "phpunit --colors=always" + "test": "phpunit", + "fire-test": "@php tests/Fire/run.php", + "http-test": "@php tests/Http/run.php" } } diff --git a/composer.lock b/composer.lock index 206e4cb..8da8111 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,61 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "59661b496ce23170066e2e0589421c59", + "content-hash": "3fb4b5cf36d726d5fac54be222e04d90", "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", @@ -331,26 +384,26 @@ "packages-dev": [ { "name": "brick/varexporter", - "version": "0.5.0", + "version": "0.6.0", "source": { "type": "git", "url": "https://github.com/brick/varexporter.git", - "reference": "84b2a7a91f69aa5d079aec5a0a7256ebf2dceb6b" + "reference": "af98bfc2b702a312abbcaff37656dbe419cec5bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/varexporter/zipball/84b2a7a91f69aa5d079aec5a0a7256ebf2dceb6b", - "reference": "84b2a7a91f69aa5d079aec5a0a7256ebf2dceb6b", + "url": "https://api.github.com/repos/brick/varexporter/zipball/af98bfc2b702a312abbcaff37656dbe419cec5bc", + "reference": "af98bfc2b702a312abbcaff37656dbe419cec5bc", "shasum": "" }, "require": { "nikic/php-parser": "^5.0", - "php": "^7.4 || ^8.0" + "php": "^8.1" }, "require-dev": { "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^9.3", - "psalm/phar": "5.21.1" + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "6.8.4" }, "type": "library", "autoload": { @@ -368,7 +421,7 @@ ], "support": { "issues": "https://github.com/brick/varexporter/issues", - "source": "https://github.com/brick/varexporter/tree/0.5.0" + "source": "https://github.com/brick/varexporter/tree/0.6.0" }, "funding": [ { @@ -376,30 +429,34 @@ "type": "github" } ], - "time": "2024-05-10T17:15:19+00:00" + "time": "2025-02-20T17:42:39+00:00" }, { - "name": "coisa/php-cs-fixer", - "version": "2.1.0", + "name": "clue/ndjson-react", + "version": "v1.3.0", "source": { "type": "git", - "url": "https://github.com/coisa/php-cs-fixer.git", - "reference": "60d871f30ef99c67897cfdb371440cfc0932cf02" + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/coisa/php-cs-fixer/zipball/60d871f30ef99c67897cfdb371440cfc0932cf02", - "reference": "60d871f30ef99c67897cfdb371440cfc0932cf02", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", "shasum": "" }, "require": { - "friendsofphp/php-cs-fixer": "^3.8", - "php": "^7.4 || ^8.0" + "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": { - "CoiSA\\PhpCsFixer\\": "src" + "Clue\\React\\NDJson\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -408,41 +465,61 @@ ], "authors": [ { - "name": "Felipe Sayão Lobato Abreu", - "email": "github@mentor.dev.br" + "name": "Christian Lück", + "email": "christian@clue.engineering" } ], - "description": "Custom PHP CS Fixer Config", + "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/coisa/php-cs-fixer/issues", - "source": "https://github.com/coisa/php-cs-fixer/tree/2.1.0" + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" }, - "time": "2022-05-28T01:44:24+00:00" + "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", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, "conflict": { - "phpstan/phpstan": "<1.11.10" + "phpstan/phpstan": "<2.2.2" }, "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" }, "type": "library", "extra": { @@ -480,7 +557,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" + "source": "https://github.com/composer/pcre/tree/3.4.0" }, "funding": [ { @@ -490,26 +567,22 @@ { "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" + "time": "2026-06-07T11:47:49+00:00" }, { "name": "composer/semver", - "version": "3.4.3", + "version": "3.4.4", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", "shasum": "" }, "require": { @@ -561,7 +634,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.3" + "source": "https://github.com/composer/semver/tree/3.4.4" }, "funding": [ { @@ -571,13 +644,9 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-09-19T14:15:21+00:00" + "time": "2025-08-20T19:15:30+00:00" }, { "name": "composer/xdebug-handler", @@ -646,40 +715,52 @@ "time": "2024-05-06T16:37:16+00:00" }, { - "name": "doctrine/annotations", - "version": "1.14.4", + "name": "ergebnis/agent-detector", + "version": "1.2.0", "source": { "type": "git", - "url": "https://github.com/doctrine/annotations.git", - "reference": "253dca476f70808a5aeed3a47cc2cc88c5cab915" + "url": "https://github.com/ergebnis/agent-detector.git", + "reference": "e211f17928c8b95a51e06040792d57f5462fb271" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/253dca476f70808a5aeed3a47cc2cc88c5cab915", - "reference": "253dca476f70808a5aeed3a47cc2cc88c5cab915", + "url": "https://api.github.com/repos/ergebnis/agent-detector/zipball/e211f17928c8b95a51e06040792d57f5462fb271", + "reference": "e211f17928c8b95a51e06040792d57f5462fb271", "shasum": "" }, "require": { - "doctrine/lexer": "^1 || ^2", - "ext-tokenizer": "*", - "php": "^7.1 || ^8.0", - "psr/cache": "^1 || ^2 || ^3" + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0" }, "require-dev": { - "doctrine/cache": "^1.11 || ^2.0", - "doctrine/coding-standard": "^9 || ^12", - "phpstan/phpstan": "~1.4.10 || ^1.10.28", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "symfony/cache": "^4.4 || ^5.4 || ^6.4 || ^7", - "vimeo/psalm": "^4.30 || ^5.14" - }, - "suggest": { - "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" + "ergebnis/composer-normalize": "^2.51.0", + "ergebnis/license": "^2.7.0", + "ergebnis/php-cs-fixer-config": "^6.60.2", + "ergebnis/phpstan-rules": "^2.13.1", + "ergebnis/phpunit-slow-test-detector": "^2.24.0", + "ergebnis/rector-rules": "^1.18.1", + "fakerphp/faker": "^1.24.1", + "infection/infection": "^0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.54", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "phpunit/phpunit": "^9.6.34", + "rector/rector": "^2.4.2" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, "autoload": { "psr-4": { - "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + "Ergebnis\\AgentDetector\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -688,115 +769,44 @@ ], "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" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Docblock Annotations Parser", - "homepage": "https://www.doctrine-project.org/projects/annotations.html", - "keywords": [ - "annotations", - "docblock", - "parser" - ], - "support": { - "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/1.14.4" - }, - "time": "2024-09-05T10:15:52+00:00" - }, - { - "name": "doctrine/deprecations", - "version": "1.1.4", - "source": { - "type": "git", - "url": "https://github.com/doctrine/deprecations.git", - "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/31610dbb31faa98e6b5447b62340826f54fbc4e9", - "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9 || ^12", - "phpstan/phpstan": "1.4.10 || 2.0.3", - "phpstan/phpstan-phpunit": "^1.0 || ^2", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "psr/log": "^1 || ^2 || ^3" - }, - "suggest": { - "psr/log": "Allows logging deprecations via PSR-3 logger implementation" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Deprecations\\": "src" + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" } - }, - "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/", + "description": "Provides a detector for detecting the presence of an agent.", + "homepage": "https://github.com/ergebnis/agent-detector", "support": { - "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.4" + "issues": "https://github.com/ergebnis/agent-detector/issues", + "security": "https://github.com/ergebnis/agent-detector/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/agent-detector" }, - "time": "2024-12-07T21:18:45+00:00" + "time": "2026-05-07T08:19:07+00:00" }, { - "name": "doctrine/instantiator", - "version": "1.5.0", + "name": "evenement/evenement", + "version": "v3.0.2", "source": { "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": ">=7.0" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^11", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^0.16 || ^1", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.30 || ^5.4" + "phpunit/phpunit": "^9 || ^6" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + "Evenement\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -805,66 +815,53 @@ ], "authors": [ { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "https://ocramius.github.io/" + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" } ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "description": "Événement is a very simple event dispatching library for PHP", "keywords": [ - "constructor", - "instantiate" + "event-dispatcher", + "event-emitter" ], "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" }, - "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%2Finstantiator", - "type": "tidelift" - } - ], - "time": "2022-12-30T00:15:36+00:00" + "time": "2023-08-08T05:53:35+00:00" }, { - "name": "doctrine/lexer", - "version": "2.1.1", + "name": "fidry/cpu-core-counter", + "version": "1.3.0", "source": { "type": "git", - "url": "https://github.com/doctrine/lexer.git", - "reference": "861c870e8b75f7c8f69c146c7f89cc1c0f1b49b6" + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/861c870e8b75f7c8f69c146c7f89cc1c0f1b49b6", - "reference": "861c870e8b75f7c8f69c146c7f89cc1c0f1b49b6", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", "shasum": "" }, "require": { - "doctrine/deprecations": "^1.0", - "php": "^7.1 || ^8.0" + "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^12", - "phpstan/phpstan": "^1.3", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", - "psalm/plugin-phpunit": "^0.18.3", - "vimeo/psalm": "^4.11 || ^5.21" + "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": { - "Doctrine\\Common\\Lexer\\": "src" + "Fidry\\CpuCoreCounter\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -873,95 +870,81 @@ ], "authors": [ { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" } ], - "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", - "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "description": "Tiny utility to get the number of CPU cores.", "keywords": [ - "annotations", - "docblock", - "lexer", - "parser", - "php" + "CPU", + "core" ], "support": { - "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/2.1.1" + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" }, "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%2Flexer", - "type": "tidelift" + "url": "https://github.com/theofidry", + "type": "github" } ], - "time": "2024-02-05T11:35:39+00:00" + "time": "2025-08-14T07:29:31+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.9.5", + "version": "v3.95.25", "source": { "type": "git", - "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "4465d70ba776806857a1ac2a6f877e582445ff36" + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "2cdfc1f3daf173d83a1ebb6177949b58c95de6ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/4465d70ba776806857a1ac2a6f877e582445ff36", - "reference": "4465d70ba776806857a1ac2a6f877e582445ff36", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/2cdfc1f3daf173d83a1ebb6177949b58c95de6ff", + "reference": "2cdfc1f3daf173d83a1ebb6177949b58c95de6ff", "shasum": "" }, "require": { - "composer/semver": "^3.2", - "composer/xdebug-handler": "^3.0.3", - "doctrine/annotations": "^1.13", + "clue/ndjson-react": "^1.3", + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.5", + "ergebnis/agent-detector": "^1.2", + "ext-filter": "*", + "ext-hash": "*", "ext-json": "*", "ext-tokenizer": "*", + "fidry/cpu-core-counter": "^1.3", "php": "^7.4 || ^8.0", - "php-cs-fixer/diff": "^2.0", - "symfony/console": "^5.4 || ^6.0", - "symfony/event-dispatcher": "^5.4 || ^6.0", - "symfony/filesystem": "^5.4 || ^6.0", - "symfony/finder": "^5.4 || ^6.0", - "symfony/options-resolver": "^5.4 || ^6.0", - "symfony/polyfill-mbstring": "^1.23", - "symfony/polyfill-php80": "^1.25", - "symfony/polyfill-php81": "^1.25", - "symfony/process": "^5.4 || ^6.0", - "symfony/stopwatch": "^5.4 || ^6.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 || ^9.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.37", + "symfony/polyfill-php80": "^1.37", + "symfony/polyfill-php81": "^1.37", + "symfony/polyfill-php84": "^1.37", + "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": { - "justinrainbow/json-schema": "^5.2", - "keradus/cli-executor": "^1.5", - "mikey179/vfsstream": "^1.6.10", - "php-coveralls/php-coveralls": "^2.5.2", - "php-cs-fixer/accessible-object": "^1.1", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", - "phpspec/prophecy": "^1.15", - "phpspec/prophecy-phpunit": "^2.0", - "phpunit/phpunit": "^9.5", - "phpunitgoodpractices/polyfill": "^1.5", - "phpunitgoodpractices/traits": "^1.9.1", - "symfony/phpunit-bridge": "^6.0", - "symfony/yaml": "^5.4 || ^6.0" + "facile-it/paraunit": "^1.3.1 || ^2.11.0", + "infection/infection": "^0.32.7", + "justinrainbow/json-schema": "^6.10.0", + "keradus/cli-executor": "^2.3", + "php-coveralls/php-coveralls": "^2.9.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.8", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.8", + "phpunit/phpunit": "^9.6.35 || ^10.5.64 || ^11.5.56 || ^12.5.31 || ^13.0.6", + "symfony/polyfill-php85": "^1.38", + "symfony/var-dumper": "^5.4.48 || ^6.4.36 || ^7.4.8 || ^8.1.1", + "symfony/yaml": "^5.4.53 || ^6.4.41 || ^7.4.13 || ^8.1.1" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -974,7 +957,10 @@ "autoload": { "psr-4": { "PhpCsFixer\\": "src/" - } + }, + "exclude-from-classmap": [ + "src/**/Internal/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -991,9 +977,15 @@ } ], "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], "support": { - "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", - "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.9.5" + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.25" }, "funding": [ { @@ -1001,26 +993,26 @@ "type": "github" } ], - "time": "2022-07-22T08:43:51+00:00" + "time": "2026-09-08T11:11:37+00:00" }, { "name": "laminas/laminas-servicemanager", - "version": "4.3.0", + "version": "4.5.1", "source": { "type": "git", "url": "https://github.com/laminas/laminas-servicemanager.git", - "reference": "b87a792d232c006eec9787230f611c74cc57b8b6" + "reference": "11192d588876ad04ba2988984c77b4ecb5c771c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-servicemanager/zipball/b87a792d232c006eec9787230f611c74cc57b8b6", - "reference": "b87a792d232c006eec9787230f611c74cc57b8b6", + "url": "https://api.github.com/repos/laminas/laminas-servicemanager/zipball/11192d588876ad04ba2988984c77b4ecb5c771c2", + "reference": "11192d588876ad04ba2988984c77b4ecb5c771c2", "shasum": "" }, "require": { - "brick/varexporter": "^0.3.8 || ^0.4.0 || ^0.5.0", + "brick/varexporter": "^0.3.8 || ^0.4.0 || ^0.5.0 || ^0.6.0", "laminas/laminas-stdlib": "^3.19", - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", "psr/container": "^1.1 || ^2.0" }, "conflict": { @@ -1031,19 +1023,17 @@ "psr/container-implementation": "^1.0 || ^2.0" }, "require-dev": { - "boesing/psalm-plugin-stringf": "^1.4", "composer/package-versions-deprecated": "^1.11.99.5", "friendsofphp/proxy-manager-lts": "^1.0.18", - "laminas/laminas-cli": "^1.8", - "laminas/laminas-coding-standard": "~3.0.0", - "laminas/laminas-container-config-test": "^1.0", - "lctrs/psalm-psr-container-plugin": "^1.9", + "laminas/laminas-cli": "^1.11", + "laminas/laminas-coding-standard": "~3.1.0", + "laminas/laminas-container-config-test": "^1.1", "mikey179/vfsstream": "^1.6.12", - "phpbench/phpbench": "^1.3.1", - "phpunit/phpunit": "^10.5.36", - "psalm/plugin-phpunit": "^0.19.0", - "symfony/console": "^6.0", - "vimeo/psalm": "^5.26.1" + "phpbench/phpbench": "^1.4.1", + "phpunit/phpunit": "^10.5.58", + "psalm/plugin-phpunit": "^0.19.5", + "symfony/console": "^6.4.17 || ^7.3.4", + "vimeo/psalm": "^6.13.1" }, "suggest": { "friendsofphp/proxy-manager-lts": "To handle lazy initialization of services", @@ -1080,7 +1070,7 @@ "chat": "https://laminas.dev/chat", "forum": "https://discourse.laminas.dev", "issues": "https://github.com/laminas/laminas-servicemanager/issues", - "source": "https://github.com/laminas/laminas-servicemanager/tree/4.3.0" + "source": "https://github.com/laminas/laminas-servicemanager/tree/4.5.1" }, "funding": [ { @@ -1088,34 +1078,34 @@ "type": "community_bridge" } ], - "time": "2024-11-05T00:35:06+00:00" + "time": "2026-05-12T09:53:32+00:00" }, { "name": "laminas/laminas-stdlib", - "version": "3.20.0", + "version": "3.21.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-stdlib.git", - "reference": "8974a1213be42c3e2f70b2c27b17f910291ab2f4" + "reference": "b1c81514cfe158aadf724c42b34d3d0a8164c096" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-stdlib/zipball/8974a1213be42c3e2f70b2c27b17f910291ab2f4", - "reference": "8974a1213be42c3e2f70b2c27b17f910291ab2f4", + "url": "https://api.github.com/repos/laminas/laminas-stdlib/zipball/b1c81514cfe158aadf724c42b34d3d0a8164c096", + "reference": "b1c81514cfe158aadf724c42b34d3d0a8164c096", "shasum": "" }, "require": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" }, "conflict": { "zendframework/zend-stdlib": "*" }, "require-dev": { - "laminas/laminas-coding-standard": "^3.0", - "phpbench/phpbench": "^1.3.1", - "phpunit/phpunit": "^10.5.38", - "psalm/plugin-phpunit": "^0.19.0", - "vimeo/psalm": "^5.26.1" + "laminas/laminas-coding-standard": "^3.1.0", + "phpbench/phpbench": "^1.4.1", + "phpunit/phpunit": "^11.5.42", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13.1" }, "type": "library", "autoload": { @@ -1147,24 +1137,24 @@ "type": "community_bridge" } ], - "time": "2024-10-29T13:46:07+00:00" + "time": "2025-10-11T18:13:12+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.12.1", + "version": "1.14.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.0" }, "conflict": { "doctrine/collections": "<1.6.8", @@ -1199,32 +1189,31 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0" }, "funding": [ { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" + "url": "https://github.com/mnapoli", + "type": "github" } ], - "time": "2024-11-08T17:47:46+00:00" + "time": "2026-08-11T10:17:44+00:00" }, { "name": "nikic/php-parser", - "version": "v5.3.1", + "version": "v5.9.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def", "shasum": "" }, "require": { - "ext-ctype": "*", "ext-json": "*", "ext-tokenizer": "*", "php": ">=7.4" @@ -1239,7 +1228,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -1263,9 +1252,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0" }, - "time": "2024-10-08T18:51:32+00:00" + "time": "2026-09-13T18:51:52+00:00" }, { "name": "nyholm/psr7", @@ -1345,72 +1334,6 @@ ], "time": "2024-09-09T07:06:30+00:00" }, - { - "name": "nyholm/psr7-server", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/Nyholm/psr7-server.git", - "reference": "4335801d851f554ca43fa6e7d2602141538854dc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Nyholm/psr7-server/zipball/4335801d851f554ca43fa6e7d2602141538854dc", - "reference": "4335801d851f554ca43fa6e7d2602141538854dc", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.0 || ^2.0" - }, - "require-dev": { - "nyholm/nsa": "^1.1", - "nyholm/psr7": "^1.3", - "phpunit/phpunit": "^7.0 || ^8.5 || ^9.3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Nyholm\\Psr7Server\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com" - }, - { - "name": "Martijn van der Ven", - "email": "martijn@vanderven.se" - } - ], - "description": "Helper classes to handle PSR-7 server requests", - "homepage": "http://tnyholm.se", - "keywords": [ - "psr-17", - "psr-7" - ], - "support": { - "issues": "https://github.com/Nyholm/psr7-server/issues", - "source": "https://github.com/Nyholm/psr7-server/tree/1.1.0" - }, - "funding": [ - { - "url": "https://github.com/Zegnat", - "type": "github" - }, - { - "url": "https://github.com/nyholm", - "type": "github" - } - ], - "time": "2023-11-08T09:30:43+00:00" - }, { "name": "phar-io/manifest", "version": "2.0.4", @@ -1531,20 +1454,20 @@ }, { "name": "phly/phly-event-dispatcher", - "version": "1.5.0", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/phly/phly-event-dispatcher.git", - "reference": "0a1b0aa21ace0f6f6736db71cdd3a3580fd9f481" + "reference": "09b86c00d16e34b5d33123191cb613307ff54be7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phly/phly-event-dispatcher/zipball/0a1b0aa21ace0f6f6736db71cdd3a3580fd9f481", - "reference": "0a1b0aa21ace0f6f6736db71cdd3a3580fd9f481", + "url": "https://api.github.com/repos/phly/phly-event-dispatcher/zipball/09b86c00d16e34b5d33123191cb613307ff54be7", + "reference": "09b86c00d16e34b5d33123191cb613307ff54be7", "shasum": "" }, "require": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", "psr/container": "^1.0 || ^2.0", "psr/event-dispatcher": "^1.0" }, @@ -1588,809 +1511,1097 @@ "rss": "https://github.com/phly/phly-event-dispatcher/releases.atom", "source": "https://github.com/phly/phly-event-dispatcher" }, - "time": "2024-11-25T21:57:14+00:00" + "time": "2026-09-09T16:22:59+00:00" }, { - "name": "php-cs-fixer/diff", - "version": "v2.0.2", - "source": { - "type": "git", - "url": "https://github.com/PHP-CS-Fixer/diff.git", - "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3" - }, + "name": "phpstan/phpstan", + "version": "2.2.14", "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/29dc0d507e838c4580d018bd8b5cb412474f7ec3", - "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9c672e7a8e791dfc3d30e55f683e73fc0b63a3ac", + "reference": "9c672e7a8e791dfc3d30e55f683e73fc0b63a3ac", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0 || ^8.0" + "php": "^7.4|^8.0" }, - "require-dev": { - "phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0", - "symfony/process": "^3.3" + "conflict": { + "phpstan/phpstan-shim": "*" }, + "bin": [ + "phpstan", + "phpstan.phar" + ], "type": "library", "autoload": { - "classmap": [ - "src/" + "files": [ + "bootstrap.php" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Ondřej Mirtes" }, { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" } ], - "description": "sebastian/diff v3 backport support for PHP 5.6+", - "homepage": "https://github.com/PHP-CS-Fixer", + "description": "PHPStan - PHP Static Analysis Tool", "keywords": [ - "diff" + "dev", + "static analysis" ], "support": { - "issues": "https://github.com/PHP-CS-Fixer/diff/issues", - "source": "https://github.com/PHP-CS-Fixer/diff/tree/v2.0.2" + "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" }, - "abandoned": true, - "time": "2020-10-14T08:32:19+00:00" + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-09-12T21:39:33+00:00" }, { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", + "name": "phpunit/php-code-coverage", + "version": "14.3.3", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "f8640c238e930b914d0098a1edbad6652e61a64a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f8640c238e930b914d0098a1edbad6652e61a64a", + "reference": "f8640c238e930b914d0098a1edbad6652e61a64a", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "ext-dom": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.8.0", + "php": ">=8.4", + "phpunit/php-text-template": "^6.0", + "sebastian/complexity": "^6.0", + "sebastian/environment": "^9.3.2", + "sebastian/git-state": "^1.0", + "sebastian/lines-of-code": "^5.0.2", + "sebastian/version": "^7.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.3" + }, + "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-2.x": "2.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" + "coverage", + "testing", + "xunit" ], "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + "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/14.3.3" }, - "time": "2020-06-27T09:03:43+00:00" + "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": "2026-09-10T13:08:32+00:00" }, { - "name": "phpdocumentor/reflection-docblock", - "version": "5.6.1", + "name": "phpunit/php-file-iterator", + "version": "7.0.2", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8" + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "9bb4e6c58b62c1e043be995c66abec7c97307aae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", - "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/9bb4e6c58b62c1e043be995c66abec7c97307aae", + "reference": "9bb4e6c58b62c1e043be995c66abec7c97307aae", "shasum": "" }, "require": { - "doctrine/deprecations": "^1.1", - "ext-filter": "*", - "php": "^7.4 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.7", - "phpstan/phpdoc-parser": "^1.7|^2.0", - "webmozart/assert": "^1.9.1" + "php": ">=8.4" }, "require-dev": { - "mockery/mockery": "~1.3.5 || ~1.6.0", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-webmozart-assert": "^1.2", - "phpunit/phpunit": "^9.5", - "psalm/phar": "^5.26" + "phpunit/phpunit": "^13.3.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.x-dev" + "dev-main": "7.0-dev" } }, "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "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/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.1" + "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/7.0.2" }, - "time": "2024-12-07T09:39:29+00:00" + "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-08-25T14:47:43+00:00" }, { - "name": "phpdocumentor/type-resolver", - "version": "1.10.0", + "name": "phpunit/php-invoker", + "version": "7.0.0", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", - "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", "shasum": "" }, "require": { - "doctrine/deprecations": "^1.0", - "php": "^7.3 || ^8.0", - "phpdocumentor/reflection-common": "^2.0", - "phpstan/phpdoc-parser": "^1.18|^2.0" + "php": ">=8.4" }, "require-dev": { - "ext-tokenizer": "*", - "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpunit/phpunit": "^9.5", - "rector/rector": "^0.13.9", - "vimeo/psalm": "^4.25" + "ext-pcntl": "*", + "phpunit/phpunit": "^13.0" + }, + "suggest": { + "ext-pcntl": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-1.x": "1.x-dev" + "dev-main": "7.0-dev" } }, "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" + "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/7.0.0" }, - "time": "2024-11-09T15:12:26+00:00" + "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-invoker", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:34:47+00:00" }, { - "name": "phpspec/prophecy", - "version": "v1.20.0", + "name": "phpunit/php-text-template", + "version": "6.0.0", "source": { "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "a0165c648cab6a80311c74ffc708a07bb53ecc93" + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/a0165c648cab6a80311c74ffc708a07bb53ecc93", - "reference": "a0165c648cab6a80311c74ffc708a07bb53ecc93", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/a47af19f93f76aa3368303d752aa5272ca3299f4", + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.2 || ^2.0", - "php": "^7.2 || 8.0.* || 8.1.* || 8.2.* || 8.3.* || 8.4.*", - "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0 || ^5.0 || ^6.0", - "sebastian/recursion-context": "^3.0 || ^4.0 || ^5.0 || ^6.0" + "php": ">=8.4" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.40", - "phpspec/phpspec": "^6.0 || ^7.0", - "phpstan/phpstan": "^1.9", - "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-main": "6.0-dev" } }, "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", "keywords": [ - "Double", - "Dummy", - "dev", - "fake", - "mock", - "spy", - "stub" + "template" ], "support": { - "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/v1.20.0" + "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/6.0.0" }, - "time": "2024-11-19T13:12:41+00:00" + "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-text-template", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:36:37+00:00" }, { - "name": "phpstan/phpdoc-parser", - "version": "2.0.0", + "name": "phpunit/php-timer", + "version": "9.0.0", "source": { "type": "git", - "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "c00d78fb6b29658347f9d37ebe104bffadf36299" + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/c00d78fb6b29658347f9d37ebe104bffadf36299", - "reference": "c00d78fb6b29658347f9d37ebe104bffadf36299", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/a0e12065831f6ab0d83120dc61513eb8d9a966f6", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0" + "php": ">=8.4" }, "require-dev": { - "doctrine/annotations": "^2.0", - "nikic/php-parser": "^5.3.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^9.6", - "symfony/process": "^5.2" + "phpunit/phpunit": "^13.0" }, "type": "library", - "autoload": { - "psr-4": { - "PHPStan\\PhpDocParser\\": [ - "src/" - ] + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "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" ], - "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { - "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.0.0" + "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/9.0.0" }, - "time": "2024-10-13T11:29:49+00:00" + "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-timer", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:37:53+00:00" }, { - "name": "phpstan/phpstan", - "version": "1.12.13", + "name": "phpunit/phpunit", + "version": "13.3.4", "source": { "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "9b469068840cfa031e1deaf2fa1886d00e20680f" + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "d19d90cf860efb7e4d96cee8243e06a248578085" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b469068840cfa031e1deaf2fa1886d00e20680f", - "reference": "9b469068840cfa031e1deaf2fa1886d00e20680f", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d19d90cf860efb7e4d96cee8243e06a248578085", + "reference": "d19d90cf860efb7e4d96cee8243e06a248578085", "shasum": "" }, "require": { - "php": "^7.2|^8.0" - }, - "conflict": { - "phpstan/phpstan-shim": "*" + "ext-dom": "*", + "ext-filter": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.14.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.4.1", + "phpunit/php-code-coverage": "^14.3.3", + "phpunit/php-file-iterator": "^7.0.2", + "phpunit/php-invoker": "^7.0.0", + "phpunit/php-text-template": "^6.0.0", + "phpunit/php-timer": "^9.0.0", + "sebastian/cli-parser": "^5.0.1", + "sebastian/comparator": "^8.4", + "sebastian/diff": "^9.0.1", + "sebastian/environment": "^9.3.2", + "sebastian/exporter": "^8.2.1", + "sebastian/file-filter": "^1.0", + "sebastian/git-state": "^1.0", + "sebastian/global-state": "^9.0.1", + "sebastian/object-enumerator": "^8.1.0", + "sebastian/recursion-context": "^8.0.1", + "sebastian/type": "^7.0.2", + "sebastian/version": "^7.0.0", + "staabm/side-effects-detector": "^1.0.5" }, "bin": [ - "phpstan", - "phpstan.phar" + "phpunit" ], "type": "library", + "extra": { + "branch-alias": { + "dev-main": "13.3-dev" + } + }, "autoload": { "files": [ - "bootstrap.php" + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], - "description": "PHPStan - PHP Static Analysis Tool", + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", "keywords": [ - "dev", - "static analysis" + "phpunit", + "testing", + "xunit" ], "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" + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/13.3.4" }, "funding": [ { - "url": "https://github.com/ondrejmirtes", - "type": "github" - }, - { - "url": "https://github.com/phpstan", - "type": "github" + "url": "https://phpunit.de/sponsoring.html", + "type": "other" } ], - "time": "2024-12-17T17:00:20+00:00" + "time": "2026-09-15T07:37:05+00:00" }, { - "name": "phpstan/phpstan-strict-rules", - "version": "1.6.1", + "name": "react/cache", + "version": "v1.2.0", "source": { "type": "git", - "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "daeec748b53de80a97498462513066834ec28f8b" + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/daeec748b53de80a97498462513066834ec28f8b", - "reference": "daeec748b53de80a97498462513066834ec28f8b", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.12.4" + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" }, "require-dev": { - "nikic/php-parser": "^4.13.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-deprecation-rules": "^1.1", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^9.5" - }, - "type": "phpstan-extension", - "extra": { - "phpstan": { - "includes": [ - "rules.neon" - ] - } + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" }, + "type": "library", "autoload": { "psr-4": { - "PHPStan\\": "src/" + "React\\Cache\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Extra strict and opinionated rules for PHPStan", + "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/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.6.1" + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" }, - "time": "2024-09-20T14:04:44+00:00" + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" }, { - "name": "phpunit/php-code-coverage", - "version": "7.0.17", + "name": "react/child-process", + "version": "v0.6.7", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "40a4ed114a4aea5afd6df8d0f0c9cd3033097f66" + "url": "https://github.com/reactphp/child-process.git", + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/40a4ed114a4aea5afd6df8d0f0c9cd3033097f66", - "reference": "40a4ed114a4aea5afd6df8d0f0c9cd3033097f66", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/970f0e71945556422ee4570ccbabaedc3cf04ad3", + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-xmlwriter": "*", - "php": ">=7.2", - "phpunit/php-file-iterator": "^2.0.2", - "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^3.1.3 || ^4.0", - "sebastian/code-unit-reverse-lookup": "^1.0.1", - "sebastian/environment": "^4.2.2", - "sebastian/version": "^2.0.1", - "theseer/tokenizer": "^1.1.3" + "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": "^8.2.2" - }, - "suggest": { - "ext-xdebug": "^2.7.2" + "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", - "extra": { - "branch-alias": { - "dev-master": "7.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "React\\ChildProcess\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "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": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "description": "Event-driven library for executing child processes with ReactPHP.", "keywords": [ - "coverage", - "testing", - "xunit" + "event-driven", + "process", + "reactphp" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/7.0.17" + "issues": "https://github.com/reactphp/child-process/issues", + "source": "https://github.com/reactphp/child-process/tree/v0.6.7" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2024-03-02T06:09:37+00:00" + "time": "2025-12-23T15:25:20+00:00" }, { - "name": "phpunit/php-file-iterator", - "version": "2.0.6", + "name": "react/dns", + "version": "v1.14.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "69deeb8664f611f156a924154985fbd4911eb36b" + "url": "https://github.com/reactphp/dns.git", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/69deeb8664f611f156a924154985fbd4911eb36b", - "reference": "69deeb8664f611f156a924154985fbd4911eb36b", + "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3", "shasum": "" }, "require": { - "php": ">=7.1" + "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": "^8.5" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" + "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": { - "classmap": [ - "src/" - ] + "psr-4": { + "React\\EventLoop\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "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": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", "keywords": [ - "filesystem", - "iterator" + "asynchronous", + "event-loop" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.6" + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2024-03-01T13:39:50+00:00" + "time": "2025-11-17T20:46:25+00:00" }, { - "name": "phpunit/php-text-template", - "version": "1.2.1", + "name": "react/promise", + "version": "v3.3.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + "url": "https://github.com/reactphp/promise.git", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.12.28 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" }, "type": "library", "autoload": { - "classmap": [ - "src/" - ] + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "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": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "description": "A lightweight implementation of CommonJS Promises/A for PHP", "keywords": [ - "template" + "promise", + "promises" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.3.0" }, - "time": "2015-06-21T13:50:34+00:00" + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-08-19T18:57:03+00:00" }, { - "name": "phpunit/php-timer", - "version": "2.1.4", + "name": "react/socket", + "version": "v1.17.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "a691211e94ff39a34811abd521c31bd5b305b0bb" + "url": "https://github.com/reactphp/socket.git", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/a691211e94ff39a34811abd521c31bd5b305b0bb", - "reference": "a691211e94ff39a34811abd521c31bd5b305b0bb", + "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08", "shasum": "" }, "require": { - "php": ">=7.1" + "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": "^8.5" + "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", - "extra": { - "branch-alias": { - "dev-master": "2.1-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "React\\Socket\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "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": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", "keywords": [ - "timer" + "Connection", + "Socket", + "async", + "reactphp", + "stream" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/2.1.4" + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.17.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2024-03-01T13:42:41+00:00" + "time": "2025-11-19T20:47:34+00:00" }, { - "name": "phpunit/php-token-stream", - "version": "4.0.4", + "name": "react/stream", + "version": "v1.4.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3" + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/a853a0e183b9db7eed023d7933a858fa1c8d25a3", - "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", "shasum": "" }, "require": { - "ext-tokenizer": "*", - "php": "^7.3 || ^8.0" + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" }, "require-dev": { - "phpunit/phpunit": "^9.0" + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "React\\Stream\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "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": "Wrapper around PHP's tokenizer extension.", - "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", "keywords": [ - "tokenizer" + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", - "source": "https://github.com/sebastianbergmann/php-token-stream/tree/master" + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "abandoned": true, - "time": "2020-08-04T08:28:15+00:00" + "time": "2024-06-11T12:45:25+00:00" }, { - "name": "phpunit/phpunit", - "version": "8.5.41", + "name": "sebastian/cli-parser", + "version": "5.0.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "d843cb5bcf0bf9ae3484016444fe0c5b6ec7e4fa" + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "eeb759ad3146b7096fb59c3195d39e071cd409e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d843cb5bcf0bf9ae3484016444fe0c5b6ec7e4fa", - "reference": "d843cb5bcf0bf9ae3484016444fe0c5b6ec7e4fa", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/eeb759ad3146b7096fb59c3195d39e071cd409e3", + "reference": "eeb759ad3146b7096fb59c3195d39e071cd409e3", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.5.0", - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.1", - "phar-io/manifest": "^2.0.4", - "phar-io/version": "^3.2.1", - "php": ">=7.2", - "phpunit/php-code-coverage": "^7.0.17", - "phpunit/php-file-iterator": "^2.0.6", - "phpunit/php-text-template": "^1.2.1", - "phpunit/php-timer": "^2.1.4", - "sebastian/comparator": "^3.0.5", - "sebastian/diff": "^3.0.6", - "sebastian/environment": "^4.2.5", - "sebastian/exporter": "^3.1.6", - "sebastian/global-state": "^3.0.5", - "sebastian/object-enumerator": "^3.0.5", - "sebastian/resource-operations": "^2.0.3", - "sebastian/type": "^1.1.5", - "sebastian/version": "^2.0.1" + "php": ">=8.4" }, - "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage", - "phpunit/php-invoker": "To allow enforcing time limits" + "require-dev": { + "phpunit/phpunit": "^13.2.6" }, - "bin": [ - "phpunit" - ], "type": "library", "extra": { "branch-alias": { - "dev-master": "8.5-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -2409,160 +2620,220 @@ "role": "lead" } ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { - "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.41" + "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/5.0.1" }, "funding": [ - { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, { "url": "https://github.com/sebastianbergmann", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "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/cli-parser", "type": "tidelift" } ], - "time": "2024-12-05T13:44:26+00:00" + "time": "2026-08-01T04:27:14+00:00" }, { - "name": "psr/cache", - "version": "3.0.0", + "name": "sebastian/comparator", + "version": "8.4.0", "source": { "type": "git", - "url": "https://github.com/php-fig/cache.git", - "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "3b070e608146cba00fd6fd1f0ffba89e5a8897fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", - "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/3b070e608146cba00fd6fd1f0ffba89e5a8897fb", + "reference": "3b070e608146cba00fd6fd1f0ffba89e5a8897fb", "shasum": "" }, "require": { - "php": ">=8.0.0" + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.4", + "sebastian/diff": "^9.0", + "sebastian/exporter": "^8.2" + }, + "require-dev": { + "phpunit/phpunit": "^13.3" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-main": "8.4-dev" } }, "autoload": { - "psr-4": { - "Psr\\Cache\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "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": "Common interface for caching libraries", + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", "keywords": [ - "cache", - "psr", - "psr-6" + "comparator", + "compare", + "equality" ], "support": { - "source": "https://github.com/php-fig/cache/tree/3.0.0" + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/8.4.0" }, - "time": "2021-02-03T23:26:27+00:00" + "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-08-07T07:23:13+00:00" }, { - "name": "psr/container", - "version": "2.0.2", + "name": "sebastian/complexity", + "version": "6.0.0", "source": { "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/c5651c795c98093480df79350cb050813fc7a2f3", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3", "shasum": "" }, "require": { - "php": ">=7.4.0" + "nikic/php-parser": "^5.0", + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-main": "6.0-dev" } }, "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/6.0.0" }, - "time": "2021-11-05T16:47:00+00:00" + "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/complexity", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:41:32+00:00" }, { - "name": "sebastian/code-unit-reverse-lookup", - "version": "1.0.3", + "name": "sebastian/diff", + "version": "9.0.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "92a1a52e86d34cde6caa54f1b5ffa9fda18e5d54" + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "a2df6626c1baf31d5a88674882a3072f151b5a26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/92a1a52e86d34cde6caa54f1b5ffa9fda18e5d54", - "reference": "92a1a52e86d34cde6caa54f1b5ffa9fda18e5d54", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/a2df6626c1baf31d5a88674882a3072f151b5a26", + "reference": "a2df6626c1baf31d5a88674882a3072f151b5a26", "shasum": "" }, "require": { - "php": ">=5.6" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^8.5" + "phpunit/phpunit": "^13.3.1", + "symfony/process": "^7.4.17" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-main": "9.0-dev" } }, "autoload": { @@ -2578,48 +2849,72 @@ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" } ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.3" + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/9.0.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/diff", + "type": "tidelift" } ], - "time": "2024-03-01T13:45:45+00:00" + "time": "2026-08-25T15:38:55+00:00" }, { - "name": "sebastian/comparator", - "version": "3.0.5", + "name": "sebastian/environment", + "version": "9.3.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "1dc7ceb4a24aede938c7af2a9ed1de09609ca770" + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "6c9e487c9eb706a8d258102a1c0b0a3e53e86c2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1dc7ceb4a24aede938c7af2a9ed1de09609ca770", - "reference": "1dc7ceb4a24aede938c7af2a9ed1de09609ca770", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6c9e487c9eb706a8d258102a1c0b0a3e53e86c2e", + "reference": "6c9e487c9eb706a8d258102a1c0b0a3e53e86c2e", "shasum": "" }, "require": { - "php": ">=7.1", - "sebastian/diff": "^3.0", - "sebastian/exporter": "^3.1" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^8.5" + "phpunit/phpunit": "^13.1.11" + }, + "suggest": { + "ext-posix": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "9.3-dev" } }, "autoload": { @@ -2635,64 +2930,66 @@ { "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", + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", "keywords": [ - "comparator", - "compare", - "equality" + "Xdebug", + "environment", + "hhvm" ], "support": { - "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.5" + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/9.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/environment", + "type": "tidelift" } ], - "time": "2022-09-14T12:31:48+00:00" + "time": "2026-05-25T13:41:38+00:00" }, { - "name": "sebastian/diff", - "version": "3.0.6", + "name": "sebastian/exporter", + "version": "8.2.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "98ff311ca519c3aa73ccd3de053bdb377171d7b6" + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "24a3b69bba4a12ab615fca9d34680c5598d9ab7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/98ff311ca519c3aa73ccd3de053bdb377171d7b6", - "reference": "98ff311ca519c3aa73ccd3de053bdb377171d7b6", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/24a3b69bba4a12ab615fca9d34680c5598d9ab7a", + "reference": "24a3b69bba4a12ab615fca9d34680c5598d9ab7a", "shasum": "" }, "require": { - "php": ">=7.1" + "ext-mbstring": "*", + "php": ">=8.4", + "sebastian/recursion-context": "^8.0.1" }, "require-dev": { - "phpunit/phpunit": "^7.5 || ^8.0", - "symfony/process": "^2 || ^3.3 || ^4" + "phpunit/phpunit": "^13.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "8.2-dev" } }, "autoload": { @@ -2710,57 +3007,77 @@ "email": "sebastian@phpunit.de" }, { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.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": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" + "export", + "exporter" ], "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/3.0.6" + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/8.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/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T06:16:36+00:00" + "time": "2026-08-07T07:22:06+00:00" }, { - "name": "sebastian/environment", - "version": "4.2.5", + "name": "sebastian/file-filter", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "56932f6049a0482853056ffd617c91ffcc754205" + "url": "https://github.com/sebastianbergmann/file-filter.git", + "reference": "33a26f394330f6faa7684bb9cc73afb7727aae93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/56932f6049a0482853056ffd617c91ffcc754205", - "reference": "56932f6049a0482853056ffd617c91ffcc754205", + "url": "https://api.github.com/repos/sebastianbergmann/file-filter/zipball/33a26f394330f6faa7684bb9cc73afb7727aae93", + "reference": "33a26f394330f6faa7684bb9cc73afb7727aae93", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^7.5" - }, - "suggest": { - "ext-posix": "*" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.2-dev" + "dev-main": "1.0-dev" } }, "autoload": { @@ -2775,54 +3092,61 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], + "description": "Library for filtering files", + "homepage": "https://github.com/sebastianbergmann/file-filter", "support": { - "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/4.2.5" + "issues": "https://github.com/sebastianbergmann/file-filter/issues", + "security": "https://github.com/sebastianbergmann/file-filter/security/policy", + "source": "https://github.com/sebastianbergmann/file-filter/tree/1.0.0" }, "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/file-filter", + "type": "tidelift" } ], - "time": "2024-03-01T13:49:59+00:00" + "time": "2026-04-22T07:20:04+00:00" }, { - "name": "sebastian/exporter", - "version": "3.1.6", + "name": "sebastian/git-state", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "1939bc8fd1d39adcfa88c5b35335910869214c56" + "url": "https://github.com/sebastianbergmann/git-state.git", + "reference": "792a952e0eba55b6960a48aeceb9f371aad1f76b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/1939bc8fd1d39adcfa88c5b35335910869214c56", - "reference": "1939bc8fd1d39adcfa88c5b35335910869214c56", + "url": "https://api.github.com/repos/sebastianbergmann/git-state/zipball/792a952e0eba55b6960a48aeceb9f371aad1f76b", + "reference": "792a952e0eba55b6960a48aeceb9f371aad1f76b", "shasum": "" }, "require": { - "php": ">=7.2", - "sebastian/recursion-context": "^3.0" + "php": ">=8.4" }, "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^8.5" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1.x-dev" + "dev-main": "1.0-dev" } }, "autoload": { @@ -2837,73 +3161,64 @@ "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" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], + "description": "Library for describing the state of a Git checkout", + "homepage": "https://github.com/sebastianbergmann/git-state", "support": { - "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.6" + "issues": "https://github.com/sebastianbergmann/git-state/issues", + "security": "https://github.com/sebastianbergmann/git-state/security/policy", + "source": "https://github.com/sebastianbergmann/git-state/tree/1.0.0" }, "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/git-state", + "type": "tidelift" } ], - "time": "2024-03-02T06:21:38+00:00" + "time": "2026-03-21T12:54:28+00:00" }, { "name": "sebastian/global-state", - "version": "3.0.5", + "version": "9.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "91c7c47047a971f02de57ed6f040087ef110c5d9" + "reference": "ba68ba79da690cf7eddefd3ce5b78b20b9ba9945" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/91c7c47047a971f02de57ed6f040087ef110c5d9", - "reference": "91c7c47047a971f02de57ed6f040087ef110c5d9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ba68ba79da690cf7eddefd3ce5b78b20b9ba9945", + "reference": "ba68ba79da690cf7eddefd3ce5b78b20b9ba9945", "shasum": "" }, "require": { - "php": ">=7.2", - "sebastian/object-reflector": "^1.1.1", - "sebastian/recursion-context": "^3.0" + "php": ">=8.4", + "sebastian/object-reflector": "^6.0", + "sebastian/recursion-context": "^8.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^8.0" - }, - "suggest": { - "ext-uopz": "*" + "phpunit/phpunit": "^13.1.13" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "9.0-dev" } }, "autoload": { @@ -2922,48 +3237,60 @@ } ], "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", "keywords": [ "global state" ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.5" + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/9.0.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/global-state", + "type": "tidelift" } ], - "time": "2024-03-02T06:13:16+00:00" + "time": "2026-06-01T15:11:33+00:00" }, { - "name": "sebastian/object-enumerator", - "version": "3.0.5", + "name": "sebastian/lines-of-code", + "version": "5.0.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "ac5b293dba925751b808e02923399fb44ff0d541" + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d1b6f8fce682505dbd048977f1abedf1b8ad3ff8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/ac5b293dba925751b808e02923399fb44ff0d541", - "reference": "ac5b293dba925751b808e02923399fb44ff0d541", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d1b6f8fce682505dbd048977f1abedf1b8ad3ff8", + "reference": "d1b6f8fce682505dbd048977f1abedf1b8ad3ff8", "shasum": "" }, "require": { - "php": ">=7.0", - "sebastian/object-reflector": "^1.1.1", - "sebastian/recursion-context": "^3.0" + "nikic/php-parser": "^5.8.0", + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^13.2.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -2978,47 +3305,62 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "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/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.5" + "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/5.0.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/lines-of-code", + "type": "tidelift" } ], - "time": "2024-03-01T13:54:02+00:00" + "time": "2026-07-09T08:42:34+00:00" }, { - "name": "sebastian/object-reflector", - "version": "1.1.3", + "name": "sebastian/object-enumerator", + "version": "8.1.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "1d439c229e61f244ff1f211e5c99737f90c67def" + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "511064ecde82bd747e2ba2fab3dda8d977b59576" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/1d439c229e61f244ff1f211e5c99737f90c67def", - "reference": "1d439c229e61f244ff1f211e5c99737f90c67def", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/511064ecde82bd747e2ba2fab3dda8d977b59576", + "reference": "511064ecde82bd747e2ba2fab3dda8d977b59576", "shasum": "" }, "require": { - "php": ">=7.0" + "php": ">=8.4", + "sebastian/recursion-context": "^8.0.1" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^13.3.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-main": "8.1-dev" } }, "autoload": { @@ -3036,44 +3378,57 @@ "email": "sebastian@phpunit.de" } ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "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-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.3" + "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/8.1.0" }, "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/object-enumerator", + "type": "tidelift" } ], - "time": "2024-03-01T13:56:04+00:00" + "time": "2026-08-13T07:05:05+00:00" }, { - "name": "sebastian/recursion-context", - "version": "3.0.2", + "name": "sebastian/object-reflector", + "version": "6.1.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "9bfd3c6f1f08c026f542032dfb42813544f7d64c" + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "f71bbcdc4f95456b4622810bec64eb06372e25b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/9bfd3c6f1f08c026f542032dfb42813544f7d64c", - "reference": "9bfd3c6f1f08c026f542032dfb42813544f7d64c", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/f71bbcdc4f95456b4622810bec64eb06372e25b2", + "reference": "f71bbcdc4f95456b4622810bec64eb06372e25b2", "shasum": "" }, "require": { - "php": ">=7.0" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^13.3.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -3089,51 +3444,59 @@ { "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": "http://www.github.com/sebastianbergmann/recursion-context", + "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/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.2" + "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/6.1.0" }, "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/object-reflector", + "type": "tidelift" } ], - "time": "2024-03-01T14:07:30+00:00" + "time": "2026-08-13T06:34:36+00:00" }, { - "name": "sebastian/resource-operations", - "version": "2.0.3", + "name": "sebastian/recursion-context", + "version": "8.0.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "72a7f7674d053d548003b16ff5a106e7e0e06eee" + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/72a7f7674d053d548003b16ff5a106e7e0e06eee", - "reference": "72a7f7674d053d548003b16ff5a106e7e0e06eee", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0", + "reference": "32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.2.6" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -3149,45 +3512,67 @@ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" } ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { - "source": "https://github.com/sebastianbergmann/resource-operations/tree/2.0.3" + "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/8.0.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/recursion-context", + "type": "tidelift" } ], - "time": "2024-03-01T13:59:09+00:00" + "time": "2026-08-03T05:58:12+00:00" }, { "name": "sebastian/type", - "version": "1.1.5", + "version": "7.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "18f071c3a29892b037d35e6b20ddf3ea39b42874" + "reference": "bd1df467864cb95140414059a535b2d906173fcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/18f071c3a29892b037d35e6b20ddf3ea39b42874", - "reference": "18f071c3a29892b037d35e6b20ddf3ea39b42874", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/bd1df467864cb95140414059a535b2d906173fcf", + "reference": "bd1df467864cb95140414059a535b2d906173fcf", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^8.2" + "phpunit/phpunit": "^13.3.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -3210,37 +3595,50 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/1.1.5" + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/7.0.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/type", + "type": "tidelift" } ], - "time": "2024-03-01T14:04:07+00:00" + "time": "2026-08-10T08:00:57+00:00" }, { "name": "sebastian/version", - "version": "2.0.1", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/ad37a5552c8e2b88572249fdc19b6da7792e021b", + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b", "shasum": "" }, "require": { - "php": ">=5.6" + "php": ">=8.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -3263,43 +3661,62 @@ "homepage": "https://github.com/sebastianbergmann/version", "support": { "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/master" + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/7.0.0" }, - "time": "2016-10-03T07:35:21+00:00" + "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/version", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:52:52+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.11.2", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "1368f4a58c3c52114b86b1abe8f4098869cb0079" + "reference": "bbdc3d0532623e21838b7041a4364383a8126f96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/1368f4a58c3c52114b86b1abe8f4098869cb0079", - "reference": "1368f4a58c3c52114b86b1abe8f4098869cb0079", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/bbdc3d0532623e21838b7041a4364383a8126f96", + "reference": "bbdc3d0532623e21838b7041a4364383a8126f96", "shasum": "" }, "require": { + "ext-libxml": "*", "ext-simplexml": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": ">=5.4.0" + "php": ">=7.2.0" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + "phpunit/phpunit": "^8.4.0 || ^9.3.4 || ^10.5.32 || 11.3.3 - 11.5.28 || ^11.5.31" + }, + "suggest": { + "ext-iconv": "For accurate character length calculation when the checked files contain multi-byte characters.", + "ext-pcntl": "For parallel processing support via the --parallel CLI option." }, "bin": [ "bin/phpcbf", "bin/phpcs" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" @@ -3318,7 +3735,7 @@ "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" } ], - "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "description": "PHP_CodeSniffer tokenizes PHP files and detects violations of a defined set of coding standards.", "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", "keywords": [ "phpcs", @@ -3341,55 +3758,113 @@ "type": "github" }, { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2026-08-06T02:45:27+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-12-11T16:04:26+00:00" + "time": "2024-10-20T05:08:20+00:00" }, { "name": "symfony/console", - "version": "v6.4.15", + "version": "v8.1.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "f1fc6f47283e27336e7cebb9e8946c8de7bff9bd" + "reference": "29afb89f4e941f68a6e90f28e3f52ff1f6793a7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/f1fc6f47283e27336e7cebb9e8946c8de7bff9bd", - "reference": "f1fc6f47283e27336e7cebb9e8946c8de7bff9bd", + "url": "https://api.github.com/repos/symfony/console/zipball/29afb89f4e941f68a6e90f28e3f52ff1f6793a7d", + "reference": "29afb89f4e941f68a6e90f28e3f52ff1f6793a7d", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.4.1", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php85": "^1.32", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^5.4|^6.0|^7.0" + "symfony/string": "^7.4.6|^8.0.6" }, "conflict": { - "symfony/dependency-injection": "<5.4", - "symfony/dotenv": "<5.4", - "symfony/event-dispatcher": "<5.4", - "symfony/lock": "<5.4", - "symfony/process": "<5.4" + "symfony/dependency-injection": "<8.1", + "symfony/event-dispatcher": "<8.1" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^8.1", + "symfony/event-dispatcher": "^8.1", + "symfony/filesystem": "^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/mime": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -3423,7 +3898,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.15" + "source": "https://github.com/symfony/console/tree/v8.1.7" }, "funding": [ { @@ -3434,25 +3909,29 @@ "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-11-06T14:19:14+00:00" + "time": "2026-09-13T10:55:57+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", "shasum": "" }, "require": { @@ -3460,12 +3939,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" } }, "autoload": { @@ -3490,7 +3969,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" }, "funding": [ { @@ -3501,33 +3980,38 @@ "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-25T14:20:29+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.4.13", + "version": "v8.1.5", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e" + "reference": "7458da64220376b2e0dc2d8451bf43382c1ad297" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", - "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/7458da64220376b2e0dc2d8451bf43382c1ad297", + "reference": "7458da64220376b2e0dc2d8451bf43382c1ad297", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<5.4", + "symfony/security-http": "<7.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -3536,13 +4020,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/error-handler": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^5.4|^6.0|^7.0", + "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": "^5.4|^6.0|^7.0" + "symfony/stopwatch": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -3570,7 +4055,7 @@ "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/v6.4.13" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.1.5" }, "funding": [ { @@ -3581,25 +4066,29 @@ "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-25T14:18:03+00:00" + "time": "2026-08-21T17:47:34+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.1", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e", "shasum": "" }, "require": { @@ -3608,12 +4097,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" } }, "autoload": { @@ -3646,7 +4135,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.1" }, "funding": [ { @@ -3657,34 +4146,39 @@ "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-25T14:20:29+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/filesystem", - "version": "v6.4.13", + "version": "v8.1.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3" + "reference": "7599ebb855fede59413ddb7f15198d67bfcae7ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/4856c9cf585d5a0313d8d35afd681a526f038dd3", - "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/7599ebb855fede59413ddb7f15198d67bfcae7ba", + "reference": "7599ebb855fede59413ddb7f15198d67bfcae7ba", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^5.4|^6.4|^7.0" + "symfony/process": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -3712,7 +4206,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.13" + "source": "https://github.com/symfony/filesystem/tree/v8.1.6" }, "funding": [ { @@ -3723,32 +4217,36 @@ "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-10-25T15:07:50+00:00" + "time": "2026-08-23T10:06:25+00:00" }, { "name": "symfony/finder", - "version": "v6.4.13", + "version": "v8.1.7", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "daea9eca0b08d0ed1dc9ab702a46128fd1be4958" + "reference": "4fbe46a3eb64abf8a57f0364075b91f4a233e062" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/daea9eca0b08d0ed1dc9ab702a46128fd1be4958", - "reference": "daea9eca0b08d0ed1dc9ab702a46128fd1be4958", + "url": "https://api.github.com/repos/symfony/finder/zipball/4fbe46a3eb64abf8a57f0364075b91f4a233e062", + "reference": "4fbe46a3eb64abf8a57f0364075b91f4a233e062", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.4.1" }, "require-dev": { - "symfony/filesystem": "^6.0|^7.0" + "symfony/filesystem": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -3776,7 +4274,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.13" + "source": "https://github.com/symfony/finder/tree/v8.1.7" }, "funding": [ { @@ -3787,29 +4285,33 @@ "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-10-01T08:30:56+00:00" + "time": "2026-09-10T19:14:39+00:00" }, { "name": "symfony/options-resolver", - "version": "v6.4.16", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "368128ad168f20e22c32159b9f761e456cec0c78" + "reference": "88f9c561f678a02d54b897014049fa839e33ff82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/368128ad168f20e22c32159b9f761e456cec0c78", - "reference": "368128ad168f20e22c32159b9f761e456cec0c78", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/88f9c561f678a02d54b897014049fa839e33ff82", + "reference": "88f9c561f678a02d54b897014049fa839e33ff82", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.4.1", "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", @@ -3843,7 +4345,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v6.4.16" + "source": "https://github.com/symfony/options-resolver/tree/v8.1.0" }, "funding": [ { @@ -3854,25 +4356,29 @@ "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-11-20T10:57:02+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -3922,7 +4428,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -3933,25 +4439,29 @@ "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" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "version": "v1.41.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", "shasum": "" }, "require": { @@ -4000,7 +4510,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0" }, "funding": [ { @@ -4011,25 +4521,29 @@ "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" + "time": "2026-07-28T08:25:59+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.42.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/aa20edea75bd9c48cfecc8360922e5a6e5c44502", + "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502", "shasum": "" }, "require": { @@ -4081,7 +4595,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.42.0" }, "funding": [ { @@ -4092,28 +4606,33 @@ "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" + "time": "2026-08-07T06:33:24+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -4161,7 +4680,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -4172,25 +4691,29 @@ "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" + "time": "2026-05-27T06:59:30+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.31.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", "shasum": "" }, "require": { @@ -4241,7 +4764,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" }, "funding": [ { @@ -4252,25 +4775,29 @@ "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" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.31.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + "reference": "6bfb9c766cacffbc8e118cb87217d08ed84e5cd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/6bfb9c766cacffbc8e118cb87217d08ed84e5cd7", + "reference": "6bfb9c766cacffbc8e118cb87217d08ed84e5cd7", "shasum": "" }, "require": { @@ -4317,7 +4844,167 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.38.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": "2026-05-26T12:45:58+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "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.38.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": "2026-05-26T12:51:13+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.41.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/255fab485aaa1006ed411040c42aecd7b5302d7a", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a", + "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.41.0" }, "funding": [ { @@ -4328,29 +5015,33 @@ "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" + "time": "2026-07-01T12:47:55+00:00" }, { "name": "symfony/process", - "version": "v6.4.15", + "version": "v8.1.7", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "3cb242f059c14ae08591c5c4087d1fe443564392" + "reference": "10823b09358e690df4ff943e24e8492bb1019fc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/3cb242f059c14ae08591c5c4087d1fe443564392", - "reference": "3cb242f059c14ae08591c5c4087d1fe443564392", + "url": "https://api.github.com/repos/symfony/process/zipball/10823b09358e690df4ff943e24e8492bb1019fc3", + "reference": "10823b09358e690df4ff943e24e8492bb1019fc3", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.4.1" }, "type": "library", "autoload": { @@ -4378,7 +5069,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.15" + "source": "https://github.com/symfony/process/tree/v8.1.7" }, "funding": [ { @@ -4389,25 +5080,29 @@ "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-11-06T14:19:14+00:00" + "time": "2026-09-02T12:40:29+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.7.3", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", "shasum": "" }, "require": { @@ -4420,12 +5115,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" } }, "autoload": { @@ -4461,7 +5156,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.3" }, "funding": [ { @@ -4472,29 +5167,33 @@ "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-25T14:20:29+00:00" + "time": "2026-07-27T15:39:01+00:00" }, { "name": "symfony/stopwatch", - "version": "v6.4.13", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "2cae0a6f8d04937d02f6d19806251e2104d54f92" + "reference": "21c07b026905d596e8379caeb115d87aa479499d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/2cae0a6f8d04937d02f6d19806251e2104d54f92", - "reference": "2cae0a6f8d04937d02f6d19806251e2104d54f92", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/21c07b026905d596e8379caeb115d87aa479499d", + "reference": "21c07b026905d596e8379caeb115d87aa479499d", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.4.1", "symfony/service-contracts": "^2.5|^3" }, "type": "library", @@ -4523,7 +5222,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v6.4.13" + "source": "https://github.com/symfony/stopwatch/tree/v8.1.0" }, "funding": [ { @@ -4534,44 +5233,47 @@ "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-25T14:18:03+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/string", - "version": "v7.2.0", + "version": "v8.1.7", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" + "reference": "d950140b5f56f31901e5b7a0c04ffc3a3deb943c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", + "url": "https://api.github.com/repos/symfony/string/zipball/d950140b5f56f31901e5b7a0c04ffc3a3deb943c", + "reference": "d950140b5f56f31901e5b7a0c04ffc3a3deb943c", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=8.4.1", + "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.1", - "symfony/error-handler": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "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": "^6.4|^7.0" + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -4610,7 +5312,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.2.0" + "source": "https://github.com/symfony/string/tree/v8.1.7" }, "funding": [ { @@ -4621,32 +5323,36 @@ "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-11-13T13:31:26+00:00" + "time": "2026-09-11T14:51:16+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", "shasum": "" }, "require": { "ext-dom": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" + "php": "^8.1" }, "type": "library", "autoload": { @@ -4668,7 +5374,7 @@ "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.2.3" + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" }, "funding": [ { @@ -4676,65 +5382,7 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.11.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "php": "^7.2 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.11.0" - }, - "time": "2022-06-03T18:03:27+00:00" + "time": "2025-12-08T11:19:18+00:00" } ], "aliases": [], @@ -4743,8 +5391,8 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^7.2 || ^8.0" + "php": "^8.3" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/docker-compose.override.yml b/docker-compose.override.yml deleted file mode 100644 index b9f2173..0000000 --- a/docker-compose.override.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: '3.7' -services: - composer: - volumes: - - ./:${APP_DIR:-/app} - - phpunit: - volumes: - - ./:${APP_DIR:-/app} diff --git a/docker-compose.yml b/docker-compose.yml index 0bdcd19..f27f3e9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,11 @@ -version: '3.7' services: - composer: + tests: build: context: . dockerfile: Dockerfile args: - APP_DIR: ${APP_DIR:-/app} - APP_VERSION: ${APP_VERSION:-latest} - image: coisa/error-handler:${APP_VERSION:-latest} - entrypoint: composer - - phpunit: - depends_on: - - composer - image: coisa/error-handler:${APP_VERSION:-latest} - command: composer test + PHP_VERSION: ${PHP_VERSION:-8.3} + network_mode: none + mem_limit: 512m + cpus: 2 + pids_limit: 64 diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..1de5fff --- /dev/null +++ b/docs/api.md @@ -0,0 +1,62 @@ +# API and composition + +All classes are under `CoiSA\ErrorHandler`. Handler interfaces remain small so reporting, application termination, HTTP rendering and event dispatch can be composed explicitly. + +## Lifecycle + +```php +$handler = new ErrorHandler($reporter, $optionalPhpErrorHandler, $optionalShutdownHandler); +$handler->register(); +try { + // Work that must return or propagate to a caller. +} finally { + $handler->unregister(); +} +``` + +Use this scoped form for code whose caller handles propagated exceptions. For process-wide uncaught exceptions, register at application bootstrap and keep the handler active through termination, as in [`examples/cli.php`](../examples/cli.php). PHP executes `finally` before dispatching an uncaught exception, so unregistering there intentionally returns responsibility to the previous exception handler. + +Registration pushes PHP callbacks onto the native stacks. Previous callbacks are restored, including their error-level masks, when unregistered in reverse order. They are **not automatically called** while this handler is active. A second registration of the same instance is a no-op, not a nested reference count. Shutdown registration happens once per instance because PHP provides no shutdown-unregister function; inactive instances do nothing at shutdown. Reuse an instance when practical: PHP retains registered shutdown callbacks until process exit. + +Only the instance owning both current PHP callbacks receives shutdown events. If another integration replaces a callback, restore it before unregistering this instance. Otherwise `unregister()` rejects the operation without altering either stack. + +| API | Behavior | +| --- | --- | +| `ErrorHandlerInterface::register(): void` | Installs error, exception and shutdown handling | +| `ErrorHandlerInterface::unregister(): void` | Restores previous error/exception callbacks; deactivates shutdown | +| `ThrowableHandlerInterface::handleThrowable(Throwable): void` | Reports the original object; a direct call does not exit | +| `PhpErrorHandlerInterface::handlePhpError(int, string, string, int): void` | Processes a PHP error with severity, message, file and line | +| `ShutdownHandlerInterface::handleShutdown(): void` | Handles a shutdown notification | + +`ErrorHandler` defaults to `ThrowErrorExceptionPhpErrorHandler` and `PhpLastErrorShutdownHandler`. The former respects the current `error_reporting()` bitmask (including `@`) and converts reportable errors to the package's `ErrorException`. The outer `ErrorHandler` delivers that exception and then resumes normal execution for recoverable warnings. The shutdown handler inspects `error_get_last()` for `E_ERROR`, `E_PARSE`, `E_CORE_ERROR` or `E_COMPILE_ERROR`; it does not turn fatal termination into recovery. + +A reporter that throws is marked failed and is not retried during the same registration. Recursive calls through the same `ErrorHandler` raise `LogicException`. Direct calls propagate reporter failures; uncaught reporter failures fall back to PHP's native failure path. `unregister()` followed by `register()` begins a fresh lifecycle. The library does not choose an alternative logger or retry remote delivery. + +## Reporter composition and PSR-14 + +`CallableThrowableHandler` adapts `callable(Throwable): void`. `ThrowableHandlerAggregate` accepts variadic reporters and supports `attach()`; it calls them in attachment order with the same throwable object. It stops if a reporter throws. Use a custom composite when independent best-effort reporting is required. + +`DispatchThrowableHandler` dispatches the throwable itself through a PSR-14 dispatcher. `DispatchErrorEventThrowableHandler` dispatches an `ErrorEvent` containing it. `ThrowableListenerProvider` and `ErrorEventListenerProvider` select compatible events; the callable listeners adapt application callbacks. `LogErrorEventListener` forwards an error event to a PSR-3 logger. Event stringification contains exception details and is intended for private diagnostic sinks, not untrusted HTTP output. + +## PSR-15/PSR-17 HTTP + +```php +$streams = new ThrowableStreamFactory($psr17Factory, $reporter); +$responses = new ThrowableResponseFactory($streams, $psr17Factory); +$middleware = new ErrorHandlerMiddleware(new ErrorHandler($reporter), $responses); +$response = $middleware->process($serverRequest, $requestHandler); +``` + +Use the `Http\Message` and `Http\Middleware` namespaces for these classes. The PSR-17 factory must implement both stream and response factories. A complete runnable composition is in [`tests/Http/router.php`](../tests/Http/router.php). + +The middleware returns normal downstream responses unchanged and catches downstream `Throwable` objects to generate an error response. It always unregisters its handler after processing. Use a distinct handler instance for each nested middleware scope; do not share/pre-register one instance across simultaneous requests or fibers. PHP's global callback stacks are process state, not request-local storage. + +`ThrowableStreamFactory` captures reporter output. Empty output maps to `Internal Server Error`; `0` is a valid body. The factory cleans buffers it opens if rendering fails. Renderers must balance their own successful nested buffers and must not close the caller's buffers. `ThrowableResponseFactory` keeps integer codes 400–599, uses 500 otherwise, and lets the PSR-17 factory supply the standard reason phrase. + +Neither the middleware nor the global handler emits PSR-7 responses to a server. The application/framework owns emission, headers, content type and transport. A PHP engine fatal error bypasses normal middleware recovery; shutdown reporting cannot reliably replace an already-started HTTP response. + +## PSR-11 container integration + +`Container\ConfigProvider` exposes `__invoke()`, `getDependencies()`, `getAliases()` and `getFactories()` for compatible containers such as Laminas ServiceManager. `ErrorHandlerContainer` can wrap a parent PSR-11 container, delegates known services to it, and caches services built by the library factories. + +Provide `ThrowableHandlerInterface` for a custom reporter. Otherwise `ThrowableHandlerAggregateFactory` composes any available callable/event-dispatch reporters; an empty aggregate intentionally produces no diagnostics. HTTP factories also require PSR-17 `ResponseFactoryInterface` and `StreamFactoryInterface` services. PSR-14 integration needs `EventDispatcherInterface`. These implementations belong to the application; the package ships interfaces and adapters, not a logger, event bus or HTTP framework. diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..e8ba674 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,81 @@ + + + + + + + CoiSA Error Handler — Make failure observable + + + +
{!}CoiSA / Error HandlerView source ↗
+
+ +
+
+
A small PHP library. An explicit failure contract.
+

Make failure
observable.

+

Connect PHP warnings, uncaught exceptions and shutdown errors to the reporting tools your application already uses.

+
PHP 8.3–8.5PSR-11 · PSR-14 · PSR-15MIT license
+ +
Next major · unreleased. This guide describes the modernization branch. Published version 2.0.0 has different compatibility and behavior. Review the migration guide before adoption.
+
+
+
01 / Reproduce it

Install the development checkout.

+

Use PHP 8.5 and Composer 2 for the checked-in dependency lock. Check out the pull-request branch when reviewing unmerged changes.

+
TERMINAL · DEVELOPMENT CHECKOUT
git clone https://github.com/coisa/error-handler.git
+cd error-handler
+composer install --no-plugins --no-scripts
+composer --no-plugins check
+php examples/cli.php
+

The example reports a warning, then an uncaught exception, and deliberately exits with status 1. On PHP 8.3 or 8.4, use composer update --no-plugins --no-scripts first to resolve compatible development tools.

+

composer require coisa/error-handler installs the published Packagist release; it does not select this unreleased work.

+
+
+
02 / Three useful boundaries

Keep reporting composable.

+
01 — CAPTURE

PHP callbacks

Register for errors, uncaught throwables and shutdown. Respect suppression and restore earlier handlers.

02 — REPORT

Your reporter

Adapt a callable, compose a sequence of reporters, or dispatch a PSR-14 event to your own listeners.

03 — RESPOND

HTTP middleware

Use PSR-15 and PSR-17 to turn a downstream throwable into a response with a controlled public body.

+
PHP · LOAD COMPOSER AUTOLOAD FIRST
use CoiSA\ErrorHandler\ErrorHandler;
+use CoiSA\ErrorHandler\Handler\CallableThrowableHandler;
+
+$reporter = new CallableThrowableHandler(
+    static function (Throwable $error): void {
+        error_log($error->getMessage());
+    }
+);
+
+$errors = new ErrorHandler($reporter);
+$errors->register();
+

Keep diagnostic output in an appropriate private sink. A reporter that emits nothing produces the generic HTTP body Internal Server Error.

+
+
+
03 / Observable behavior

Know what each boundary does.

+
Input or operationResult
Reportable warningOne error exception; execution continues.
@ or an excluded severityThe default reporter does not receive the warning.
Uncaught throwableReport the original object; exit 1 when reporting returns.
Direct handleThrowable()Report and return; the caller controls execution.
Nested registrationsUnwind in reverse order; only the active handler reports shutdown.
Reporter failure or recursionStop retrying that reporter and propagate the failure.
HTTP error responsePreserve integer codes 400–599; otherwise use 500. No exception text in the reason phrase.
+

Read the complete API and composition guide ↗

+
+
+
04 / Exercise the failure path

A test should survive the crash.

+
20ISOLATED SCENARIOS

Real warnings, throwable failures, compile fatals, allocation failure, registration cycles and reporter recursion run in child processes. Each child has a five-second deadline, a 16 MiB memory limit and a 256 KiB output cap.

+

The runner checks delivery counts, ordering, throwable classes, stderr and exact exit status. Separate integration tests exercise PSR composition and real HTTP 500/503 responses through a loopback-only server.

+
RUN THE EVIDENCE YOURSELF
php tests/Fire/run.php
+composer --no-plugins test
+php tests/Http/run.php
+ +
+
+
05 / Move deliberately

Upgrade as a major release.

+
  • Raise the runtime to PHP 8.3 or newer.
  • Expect failure exit status 1 after a reported uncaught exception.
  • Unregister handlers in reverse order; use distinct instances for nested middleware.
  • Review the generic HTTP fallback and standard reason phrases.
  • Replace the old PHP 7.2 development container with the current Compose test service.
+

Read the 2.0 migration guide ↗

+
+
+
06 / What this evidence means

Reporting is not recovery.

+

A shutdown callback cannot rescue startup errors, a killed process or every real-world out-of-memory condition. It does not restart the application or guarantee delivery to a remote logger.

+

Local evidence covers macOS and Linux arm64 CLI runtimes, plus PHP's built-in HTTP server. PHP-FPM, Apache, Windows, concurrent request runtimes and production load need application-specific validation. Use the recorded platform details and the current commit's CI results.

+
+
CoiSA Error Handler · Felipe Sayão Lobato Abreu · MIT
Static documentation. PHP execution belongs in your application, not on GitHub Pages.
+
+
+ + diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 0000000..166f184 --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,14 @@ +# Migrating from 2.0 + +This is the migration contract for the unreleased next major. It does not describe an already published Packagist release. + +1. Upgrade the application to PHP 8.3, 8.4 or 8.5. Resolve Composer dependencies again. The checked-in development lock targets PHP 8.5; developers on 8.3/8.4 use `composer update --no-plugins --no-scripts` to resolve compatible tools. Consumers resolve their own lock as usual. +2. Expect exit status **1** after a globally uncaught `Throwable` is reported successfully. Version 2 could exit with status 0. A reporter that explicitly terminates the process still controls its own exit code. Direct `handleThrowable()` calls do not exit. +3. Pair `register()` and `unregister()` in reverse order. A repeated `register()` on the same instance is idempotent, not reference counted. Out-of-order unregistration now throws `LogicException` instead of silently removing someone else's callback. Use separate instances for nested middleware and do not pre-register the middleware's own instance. +4. A reporter must not call its own `ErrorHandler` recursively. After a reporter throws, that instance stops retrying it until it is unregistered and registered again. The failure propagates to the caller or PHP; it is not silently swallowed. Only the active registration receives shutdown notifications, preventing duplicates across nested handlers. +5. Review custom HTTP rendering. Empty output now produces `Internal Server Error`, instead of a stringified exception and stack trace. The string `0` is preserved. Exception messages no longer become HTTP reason phrases. Codes 400–599 map to their matching status; other integer codes and string SQLSTATE codes map to 500. +6. If you used the old PHP 7.2 Docker setup, replace the two-service invocation with `docker compose run --rm --build tests`. It defaults to PHP 8.3 and resolves dependencies in the image, without using your host `vendor/`. + +Public handler and factory interfaces retain their names and method signatures. The PHP floor, CLI exit semantics, lifecycle rejection and HTTP output defaults warrant a major release even though most integration code can remain unchanged. + +Before upgrading a real application, run the [documented checks](testing.md) and application-level tests for the actual reporter, middleware stack and PHP SAPI. These isolated fixtures do not establish reliability of third-party loggers, remote services or production infrastructure. diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..b728bcc --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,78 @@ +# Reproducing the failure tests + +The suite deliberately causes errors in **child processes**, never in PHPUnit or a host application. Fixtures contain only synthetic data. No external service or credential is used. + +## Commands + +```sh +# Reference development lock: PHP 8.5 + Composer 2. +composer install --no-plugins --no-scripts --no-interaction --prefer-dist +composer --no-plugins check + +# On PHP 8.3 or 8.4, first resolve compatible development dependencies. +composer update --no-plugins --no-scripts --no-interaction --prefer-dist +composer --no-plugins check + +# Core crash tests need PHP only; no Composer dependencies or extensions. +php tests/Fire/run.php + +# PSR integration and real HTTP responses. +composer --no-plugins test +php tests/Http/run.php +``` + +The checks run PSR-12 formatting (with the existing declare-before-license convention), PHP_CodeSniffer, PHPUnit, PHPStan at maximum level over `src/`, crash tests and loopback HTTP smoke tests. Deliberately invalid `.inc` crash fixtures are excluded from lint and autoload classmaps. PHPUnit restores its own error handler between tests; the PHP-warning integration case explicitly disables PHPUnit's error handler so the package receives the real warning. + +## Fire-test contract + +Every child uses `php -n`, a 16 MiB PHP memory limit, a five-second wall-clock deadline and a 256 KiB combined output cap. The parent captures stdout, stderr, exact exit status, event ordering, event counts, throwable class and applicable severity. Exceeding a limit terminates that child and fails the suite. A native fatal diagnostic on stderr is expected only in the scenarios that explicitly exercise it. + +| Cases | Expected observation | +| --- | --- | +| User/native warnings | One `ErrorException`; execution continues, exit 0 | +| `error_reporting()` and `@` | Suppressed warnings are not delivered | +| Uncaught exception, `Error`, `TypeError`, included parse error | One original throwable, exit 1 | +| Compile fatal, nested-handler compile fatal | One active-handler delivery, native diagnostic, exit 255 | +| Controlled allocation failure | One fatal delivery, native diagnostic, exit 255 | +| 25 register/unregister cycles and final registration | One shutdown callback | +| Unregistered instance | No shutdown delivery | +| Nested handlers and previous native callbacks | Restoration preserves the original error mask; previous exception handler exits 42 | +| Out-of-order unregister | Rejected without removing the active handler | +| Reporter throws, shutdown reporter throws, recursive reporter | One reporter invocation and a bounded nonzero failure | +| Warning inside the PHP-error reporter | Native diagnostic; no recursive reporter call | +| Custom shutdown throws | Its throwable is delivered once | +| Aggregate | Reporters run in attachment order | + +The allocation case deliberately requests 32 MiB under a 16 MiB limit. It proves this failure path only. There is no memory reserve and **no guarantee** of reporting arbitrary real-world out-of-memory conditions. + +## Observed baseline and verification + +On 2026-09-15, the original `f7284cd` source passed 10 of the first 19 isolated scenarios on macOS arm64/PHP 8.5.10. Four uncaught throwable cases exited 0; 25 registration cycles caused 26 shutdown deliveries; failing reporters were called three times; recursion produced 9,389 events before the output cap terminated the child. The original Composer lock could not install on PHP 8.5 because several locked integrations excluded it. + +The final 20-case core suite, including nested fatal-handler coverage, passed on macOS arm64/PHP 8.5.10 and official Linux aarch64 images for PHP 8.3.33, 8.4.25 and 8.5.10. PHP 8.2.33 also passed the original 19 core cases during investigation but is outside the new support contract. + +Native PHP 8.5.10 verification additionally runs PHPUnit 13.3.4, PHPStan 2.2.14 and three real HTTP requests through PHP's built-in server: 500, 503 and TypeError→500, all with a redacted body. The CI matrix checks lowest/highest compatible dependencies on PHP 8.3, highest on 8.4, and locked/highest on 8.5. A green result applies only to the exact checked commit; consult the [Actions run](https://github.com/coisa/error-handler/actions/workflows/ci.yml) rather than assuming that a configured matrix has passed. + +The full `composer --no-plugins check` command also passed in the rebuilt PHP 8.3.33 container with PHPUnit 12.5.35: 100 tests and 144 assertions, maximum-level source analysis, formatting, all 20 crash scenarios and all three HTTP responses. The runnable CLI example was checked for exit status 1, empty stdout and both expected diagnostic messages on stderr. + +```sh +docker run --rm --network none --read-only --memory 128m --cpus 1 --pids-limit 32 \ + -v "$PWD:/app:ro" -w /app php:8.3-cli-alpine php tests/Fire/run.php +docker compose run --rm --build tests +``` + +Container builds resolve development packages for the requested PHP version. For a specific matrix run, record the actual PHP version, resolved dependencies and image digest; floating version tags may change. The recorded image digests are: + +- PHP 8.3.33: `sha256:afdf8b1fee58486ccc0dab5f30f634b86873d56dac985f71ba217945647c05ad` +- PHP 8.4.25: `sha256:2f389f933c3cc58cc622bd243bb4ecff7e6553e2de4387a239bca640c988be19` +- PHP 8.5.10: `sha256:aa66ee4275e0472bafcd18e35142499fcbda6ac90e641e751418e052e6a0d812` + +## Boundaries and troubleshooting + +- PHP must have `proc_open()` enabled for crash/HTTP tests. Unit tests require the normal PHPUnit extensions. The HTTP runner binds only to a dynamically selected `127.0.0.1` port, waits at most five seconds to start, gives each request two seconds, and terminates its fixture server in `finally`. +- PHP-FPM, Apache modules, Windows, production load, asynchronous/concurrent runtimes, remote logging failures and process signals are not covered by the local evidence. The Linux CI runner adds its own platform evidence when it succeeds. +- Errors before registration, startup failures and `SIGKILL` cannot be rescued by a PHP shutdown callback. A handler does not make a fatal error recoverable. Native shutdown diagnostics may still be emitted by PHP; applications must configure `display_errors`/logging appropriately. +- A global Composer plugin can interfere with newer tooling. The documented `--no-plugins` commands isolate this project from plugins; they do not modify global configuration. +- If unregistration throws, unwind the middleware/handler stack in reverse order. If a renderer throws, its exception propagates after library-owned buffers are cleaned. Renderers must not close buffers owned by the caller. + +Sources: [PHP supported versions](https://www.php.net/supported-versions.php), [error-handler semantics](https://www.php.net/manual/en/function.set-error-handler.php), [exception handlers](https://www.php.net/manual/en/function.set-exception-handler.php), [shutdown semantics](https://www.php.net/manual/en/function.register-shutdown-function.php). diff --git a/examples/cli.php b/examples/cli.php new file mode 100644 index 0000000..4b752bf --- /dev/null +++ b/examples/cli.php @@ -0,0 +1,17 @@ +getMessage() . PHP_EOL); +}); +$handler = new ErrorHandler($reporter); +$handler->register(); + +trigger_error('Example warning; execution continues.', E_USER_WARNING); +throw new RuntimeException('Example uncaught exception; exit status is 1.'); diff --git a/phpcs.xml b/phpcs.xml index e98ff5f..7b9e967 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,10 +1,18 @@ - + + + + + + tests/Fire/bootstrap.php + src tests vendor/* tests/html/* + + tests/Fire/*.inc diff --git a/phpstan.neon b/phpstan.neon index f5a5301..9192c76 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,7 +1,5 @@ -includes: - - vendor/jangregor/phpstan-prophecy/src/extension.neon - parameters: - ignoreErrors: - - message: '/^Parameter \#1 \$error_handler of function set_error_handler expects/' - path: src/ErrorHandler.php + level: max + paths: + - src + tmpDir: tmp/phpstan diff --git a/phpunit.xml b/phpunit.xml index 13673ab..c7ff5c2 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,12 +1,13 @@ + cacheDirectory="tmp/phpunit" + failOnRisky="true" + failOnWarning="true" + failOnDeprecation="true" + failOnPhpunitDeprecation="true" + colors="true"> tests/Unit @@ -15,15 +16,5 @@ tests/Functional - - - src - - - - - - - - + src diff --git a/src/Container/ConfigProvider.php b/src/Container/ConfigProvider.php index 35b5dac..cbf5f89 100644 --- a/src/Container/ConfigProvider.php +++ b/src/Container/ConfigProvider.php @@ -35,6 +35,8 @@ * required for the proper functioning of the error-handler library. * * @package CoiSA\ErrorHandler\Container + * @phpstan-type Factories array + * @phpstan-type Dependencies array{aliases: array, factories: Factories} */ final class ConfigProvider { @@ -45,7 +47,7 @@ final class ConfigProvider * for dependency injection containers, including dependencies, aliases, * and factories. * - * @return array The configuration array. + * @return array{dependencies: Dependencies} The configuration array. */ public function __invoke(): array { @@ -60,7 +62,7 @@ public function __invoke(): array * This method SHALL define aliases and factories used for resolving * dependencies in the container. * - * @return array An array containing aliases and factories. + * @return Dependencies An array containing aliases and factories. */ public function getDependencies(): array { @@ -76,7 +78,7 @@ public function getDependencies(): array * This method SHALL define class aliases to allow easier resolution * of services in the dependency injection container. * - * @return array An associative array of aliases. + * @return array An associative array of aliases. */ public function getAliases(): array { @@ -93,7 +95,7 @@ public function getAliases(): array * This method SHALL define factories responsible for instantiating * and configuring services in the dependency injection container. * - * @return array An associative array of service factories. + * @return Factories An associative array of service factories. */ public function getFactories(): array { diff --git a/src/Container/ErrorHandlerContainer.php b/src/Container/ErrorHandlerContainer.php index 821fcd4..03a688d 100644 --- a/src/Container/ErrorHandlerContainer.php +++ b/src/Container/ErrorHandlerContainer.php @@ -28,6 +28,7 @@ * to a parent container when configured. * * @package CoiSA\ErrorHandler\Container + * @phpstan-import-type Factories from ConfigProvider */ final class ErrorHandlerContainer implements ContainerInterface { @@ -37,7 +38,7 @@ final class ErrorHandlerContainer implements ContainerInterface private ?ContainerInterface $container; /** - * @var string[] List of factories mapped to their service IDs. + * @var Factories List of factories mapped to their service IDs. */ private array $factories; @@ -47,7 +48,7 @@ final class ErrorHandlerContainer implements ContainerInterface private array $aliases; /** - * @var object[] Cached instances of resolved services. + * @var array Cached services, including values delegated through aliases. */ private array $instances = []; @@ -97,7 +98,7 @@ public function has(string $id): bool * * @return mixed The resolved service instance. */ - public function get(string $id) + public function get(string $id): mixed { if ($this->container && $this->container->has($id)) { return $this->container->get($id); @@ -139,6 +140,11 @@ private function getFactory(string $id): callable return new Factory\AliasFactory($this->aliases[$id]); } - return new $this->factories[$id](); + $factory = new $this->factories[$id](); + if (!is_callable($factory)) { + throw new ContainerException(sprintf('Factory for class %s is not callable', $id)); + } + + return $factory; } } diff --git a/src/ErrorHandler.php b/src/ErrorHandler.php index 40a1474..8eacb24 100644 --- a/src/ErrorHandler.php +++ b/src/ErrorHandler.php @@ -56,6 +56,16 @@ final class ErrorHandler implements */ private bool $isRegistered = false; + private bool $shutdownRegistered = false; + + private bool $isHandlingThrowable = false; + + private bool $handlerFailed = false; + + private \Closure $errorCallback; + + private \Closure $exceptionCallback; + /** * Constructs the ErrorHandler. * @@ -73,6 +83,8 @@ public function __construct( $this->throwableHandler = $throwableHandler; $this->phpErrorHandler = $phpErrorHandler ?? new ThrowErrorExceptionPhpErrorHandler(); $this->shutdownHandler = $shutdownHandler ?? new PhpLastErrorShutdownHandler($this); + $this->errorCallback = $this->onPhpError(...); + $this->exceptionCallback = $this->onUncaughtThrowable(...); } /** @@ -87,10 +99,14 @@ public function register(): void return; } - set_error_handler([$this, 'handlePhpError']); - set_exception_handler([$this, 'handleThrowable']); - register_shutdown_function([$this, 'handleShutdown']); + set_error_handler($this->errorCallback); + set_exception_handler($this->exceptionCallback); + if (!$this->shutdownRegistered) { + register_shutdown_function([$this, 'handleShutdown']); + $this->shutdownRegistered = true; + } + $this->handlerFailed = false; $this->isRegistered = true; } @@ -105,6 +121,10 @@ public function unregister(): void return; } + if (!$this->hasActiveCallbacks()) { + throw new \LogicException('Error handlers must be unregistered in reverse registration order.'); + } + restore_error_handler(); restore_exception_handler(); @@ -118,7 +138,22 @@ public function unregister(): void */ public function handleThrowable(\Throwable $throwable): void { - $this->throwableHandler->handleThrowable($throwable); + if ($this->isHandlingThrowable) { + throw new \LogicException('Recursive throwable handling is not supported.', 0, $throwable); + } + if ($this->handlerFailed) { + throw $throwable; + } + + $this->isHandlingThrowable = true; + try { + $this->throwableHandler->handleThrowable($throwable); + } catch (\Throwable $failure) { + $this->handlerFailed = true; + throw $failure; + } finally { + $this->isHandlingThrowable = false; + } } /** @@ -145,11 +180,11 @@ public function handlePhpError(int $code, string $message, string $filename, int /** * Handles shutdown events. * - * Ensures graceful shutdown and captures any remaining errors. + * Reports observable fatal errors or custom shutdown notifications while active. */ public function handleShutdown(): void { - if (!$this->isRegistered) { + if (!$this->isRegistered || $this->handlerFailed || !$this->hasActiveCallbacks()) { return; } @@ -159,4 +194,30 @@ public function handleShutdown(): void $this->handleThrowable($throwable); } } + + private function onPhpError(int $code, string $message, string $filename, int $line): bool + { + $this->handlePhpError($code, $message, $filename, $line); + + return true; + } + + private function hasActiveCallbacks(): bool + { + // Probe without changing the existing stacks or their error-level masks. + $currentErrorHandler = set_error_handler(static fn (): bool => false); + restore_error_handler(); + $currentExceptionHandler = set_exception_handler(static function (\Throwable $error): void { + }); + restore_exception_handler(); + + return $currentErrorHandler === $this->errorCallback + && $currentExceptionHandler === $this->exceptionCallback; + } + + private function onUncaughtThrowable(\Throwable $throwable): never + { + $this->handleThrowable($throwable); + exit(1); + } } diff --git a/src/ErrorHandlerInterface.php b/src/ErrorHandlerInterface.php index 85384d2..7428b71 100644 --- a/src/ErrorHandlerInterface.php +++ b/src/ErrorHandlerInterface.php @@ -42,7 +42,7 @@ public function register(): void; * Unregisters the error handler. * * Implementing classes MUST ensure that this method restores - * previous error, exception, and shutdown handlers. If the handler + * previous error and exception handlers and deactivate shutdown handling. If the handler * was never registered, this method SHOULD do nothing. * * @return void diff --git a/src/EventDispatcher/ListenerProvider/ErrorEventListenerProvider.php b/src/EventDispatcher/ListenerProvider/ErrorEventListenerProvider.php index c015a1e..16522f3 100644 --- a/src/EventDispatcher/ListenerProvider/ErrorEventListenerProvider.php +++ b/src/EventDispatcher/ListenerProvider/ErrorEventListenerProvider.php @@ -66,7 +66,7 @@ public function attach(callable $listener): void * * @param object $event The event for which listeners are requested. * - * @return iterable The list of applicable listeners. + * @return iterable The list of applicable listeners. */ public function getListenersForEvent(object $event): iterable { diff --git a/src/EventDispatcher/ListenerProvider/ThrowableListenerProvider.php b/src/EventDispatcher/ListenerProvider/ThrowableListenerProvider.php index 1eec4e3..665ba34 100644 --- a/src/EventDispatcher/ListenerProvider/ThrowableListenerProvider.php +++ b/src/EventDispatcher/ListenerProvider/ThrowableListenerProvider.php @@ -65,7 +65,7 @@ public function attach(callable $listener): void * * @param object $event The event for which listeners are requested. * - * @return iterable The list of applicable listeners. + * @return iterable The list of applicable listeners. */ public function getListenersForEvent(object $event): iterable { diff --git a/src/Handler/PhpLastErrorShutdownHandler.php b/src/Handler/PhpLastErrorShutdownHandler.php index fef3898..de82ac4 100644 --- a/src/Handler/PhpLastErrorShutdownHandler.php +++ b/src/Handler/PhpLastErrorShutdownHandler.php @@ -72,7 +72,7 @@ public function handleShutdown(): void * * This method SHALL check if the provided error level corresponds * to one of the fatal error types: E_ERROR, E_PARSE, E_CORE_ERROR, - * E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING. + * E_COMPILE_ERROR. Warnings are not fatal shutdown events. * * @param int $level The PHP error level to check. * @@ -83,9 +83,7 @@ private function isCatchableFatalError(int $level): bool $errors = E_ERROR | E_PARSE | E_CORE_ERROR - | E_CORE_WARNING - | E_COMPILE_ERROR - | E_COMPILE_WARNING; + | E_COMPILE_ERROR; return ($level & $errors) > 0; } diff --git a/src/Http/Message/ThrowableResponseFactory.php b/src/Http/Message/ThrowableResponseFactory.php index 79178e5..324037e 100644 --- a/src/Http/Message/ThrowableResponseFactory.php +++ b/src/Http/Message/ThrowableResponseFactory.php @@ -57,8 +57,8 @@ public function __construct( /** * Creates a PSR-7 response from a throwable instance. * - * This method SHALL create a response using the provided throwable's code and message, - * set the HTTP status code appropriately, and include the throwable details in the body. + * This method SHALL map the throwable's code to an HTTP status, use the factory's + * standard reason phrase, and include the stream factory's public response body. * * @param \Throwable $throwable The throwable to convert into an HTTP response. * @@ -67,11 +67,9 @@ public function __construct( public function createResponseFromThrowable(\Throwable $throwable): ResponseInterface { $code = $throwable->getCode(); - $message = $throwable->getMessage(); - $statusCode = $this->getStatusCode($code); - $response = $this->responseFactory->createResponse($statusCode, $message); + $response = $this->responseFactory->createResponse($statusCode); $stream = $this->streamFactory->createStreamFromThrowable($throwable); return $response->withBody($stream); @@ -80,15 +78,15 @@ public function createResponseFromThrowable(\Throwable $throwable): ResponseInte /** * Determines the HTTP status code based on the throwable code. * - * This method SHALL return the throwable code if it falls within the 400–500 range. + * This method SHALL return an integer throwable code in the 400–599 range. * Otherwise, it SHALL return a default status code of 500. * - * @param int $code The throwable code. + * @param int|string $code The throwable code (PDO can use a SQLSTATE string). * * @return int The HTTP status code to use in the response. */ - private function getStatusCode(int $code): int + private function getStatusCode(int|string $code): int { - return ($code >= 400 && $code <= 500) ? $code : 500; + return (is_int($code) && $code >= 400 && $code <= 599) ? $code : 500; } } diff --git a/src/Http/Message/ThrowableStreamFactory.php b/src/Http/Message/ThrowableStreamFactory.php index 5b8dd98..64a3545 100644 --- a/src/Http/Message/ThrowableStreamFactory.php +++ b/src/Http/Message/ThrowableStreamFactory.php @@ -63,13 +63,24 @@ public function __construct( * * @param \Throwable $throwable The throwable instance to process. * - * @return StreamInterface The resulting PSR-7 stream containing throwable details. + * @return StreamInterface The rendered response body or a generic fallback. */ public function createStreamFromThrowable(\Throwable $throwable): StreamInterface { + $initialLevel = ob_get_level(); ob_start(); - $this->throwableHandler->handleThrowable($throwable); - $buffer = ob_get_clean() ?: (string) $throwable; + try { + $this->throwableHandler->handleThrowable($throwable); + $buffer = ob_get_contents(); + } finally { + while (ob_get_level() > $initialLevel) { + ob_end_clean(); + } + } + + if ($buffer === false || $buffer === '') { + $buffer = 'Internal Server Error'; + } return $this->streamFactory->createStream($buffer); } diff --git a/tests/Fire/bootstrap.php b/tests/Fire/bootstrap.php new file mode 100644 index 0000000..47ef4c8 --- /dev/null +++ b/tests/Fire/bootstrap.php @@ -0,0 +1,16 @@ + $name] + $data, JSON_THROW_ON_ERROR) . "\n"; +} diff --git a/tests/Fire/compile-fatal.inc b/tests/Fire/compile-fatal.inc new file mode 100644 index 0000000..b98bc59 --- /dev/null +++ b/tests/Fire/compile-fatal.inc @@ -0,0 +1,3 @@ + [0, ['error', 'continued'], 'CoiSA\\ErrorHandler\\Exception\\ErrorException', E_USER_WARNING], + 'native-warning' => [0, ['error', 'continued'], 'CoiSA\\ErrorHandler\\Exception\\ErrorException', E_WARNING], + 'suppression' => [0, ['continued']], + 'exception' => [1, ['error'], RuntimeException::class], + 'error' => [1, ['error'], Error::class], + 'type-error' => [1, ['error'], TypeError::class], + 'parse-error' => [1, ['error'], ParseError::class], + 'compile-fatal' => [255, ['error'], 'CoiSA\\ErrorHandler\\Exception\\ErrorException', E_COMPILE_ERROR], + 'nested-fatal' => [255, ['error'], 'CoiSA\\ErrorHandler\\Exception\\ErrorException', E_COMPILE_ERROR], + 'memory-limit' => [255, ['error'], 'CoiSA\\ErrorHandler\\Exception\\ErrorException', E_ERROR], + 'shutdown-once' => [0, ['shutdown']], + 'unregistered-shutdown' => [0, ['continued']], + 'restore' => [42, ['error', 'previous-error', 'previous-exception']], + 'out-of-order' => [0, ['rejected', 'error']], + 'reporter-throws' => [255, ['reporter-called']], + 'shutdown-reporter-throws' => [255, ['reporter-called']], + 'recursive-reporter' => [255, ['reporter-called']], + 'reporter-warning' => [0, ['reporter-called', 'continued']], + 'shutdown-throws' => [0, ['error'], RuntimeException::class], + 'aggregate' => [0, ['first', 'second']], +]; +$failed = 0; +printf("PHP %s | %s %s | %d isolated scenarios\n", PHP_VERSION, PHP_OS_FAMILY, php_uname('m'), count($cases)); +foreach ($cases as $name => $expected) { + $process = proc_open([ + PHP_BINARY, '-n', '-d', 'display_errors=stderr', '-d', 'log_errors=0', + '-d', 'error_reporting=-1', '-d', 'memory_limit=16M', + __DIR__ . '/scenario.php', $name, + ], [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes); + if (!is_resource($process)) { + throw new RuntimeException('Could not start child process'); + } + fclose($pipes[0]); + stream_set_blocking($pipes[1], false); + stream_set_blocking($pipes[2], false); + $stdout = $stderr = ''; + $deadline = microtime(true) + 5; + $bounded = true; + do { + $stdout .= stream_get_contents($pipes[1]); + $stderr .= stream_get_contents($pipes[2]); + $status = proc_get_status($process); + if (microtime(true) > $deadline || strlen($stdout) + strlen($stderr) > 262144) { + proc_terminate($process, 9); + $bounded = false; + break; + } + if ($status['running']) { + usleep(1000); + } + } while ($status['running']); + $stdout .= stream_get_contents($pipes[1]); + $stderr .= stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + $closed = proc_close($process); + $exitCode = $status['exitcode'] >= 0 ? $status['exitcode'] : $closed; + $events = []; + $validJson = true; + foreach (array_filter(explode("\n", trim($stdout))) as $line) { + $event = json_decode($line, true); + if (!is_array($event) || !isset($event['event'])) { + $validJson = false; + break; + } + $events[] = $event; + } + $actualNames = array_column($events, 'event'); + $ok = $bounded && $validJson && $exitCode === $expected[0] && $actualNames === $expected[1]; + if (isset($expected[2])) { + $ok = $ok && ($events[0]['class'] ?? null) === $expected[2]; + } + if (isset($expected[3])) { + $ok = $ok && ($events[0]['severity'] ?? null) === $expected[3]; + } + $nativeDiagnostic = in_array($name, [ + 'compile-fatal', 'nested-fatal', 'memory-limit', 'reporter-throws', 'shutdown-reporter-throws', + 'recursive-reporter', 'reporter-warning', + ], true); + $ok = $ok && ($nativeDiagnostic ? $stderr !== '' : $stderr === ''); + printf("%s %-27s exit=%d events=%d\n", $ok ? 'PASS' : 'FAIL', $name, $exitCode, count($events)); + if (!$ok) { + ++$failed; + printf( + " bounded=%s expected=%s actual=%s stderr=%s\n", + $bounded ? 'yes' : 'no', + json_encode($expected), + json_encode(array_slice($events, 0, 4)), + substr($stderr, 0, 600) + ); + } +} +printf("%d passed; %d failed.\n", count($cases) - $failed, $failed); +exit($failed === 0 ? 0 : 1); diff --git a/tests/Fire/scenario.php b/tests/Fire/scenario.php new file mode 100644 index 0000000..0f2cddf --- /dev/null +++ b/tests/Fire/scenario.php @@ -0,0 +1,171 @@ + get_class($error), + 'message' => $error->getMessage(), + 'severity' => $error instanceof ErrorException ? $error->getSeverity() : null, + ]); +}); +$handler = new ErrorHandler($reporter); +$scenario = $argv[1] ?? ''; + +switch ($scenario) { + case 'warning': + $handler->register(); + trigger_error('visible warning', E_USER_WARNING); + event('continued'); + break; + case 'native-warning': + $handler->register(); + fopen(__DIR__ . '/does-not-exist.fixture', 'r'); + event('continued'); + break; + case 'suppression': + $handler->register(); + error_reporting(E_ALL & ~E_USER_WARNING); + trigger_error('masked warning', E_USER_WARNING); + error_reporting(E_ALL); + @trigger_error('suppressed warning', E_USER_WARNING); + @fopen(__DIR__ . '/does-not-exist.fixture', 'r'); + event('continued'); + break; + case 'exception': + $handler->register(); + throw new RuntimeException('uncaught exception'); + case 'error': + $handler->register(); + error_handler_nonexistent_function(); + break; + case 'type-error': + $handler->register(); + strlen([]); + break; + case 'parse-error': + $handler->register(); + require __DIR__ . '/parse-error.inc'; + break; + case 'compile-fatal': + case 'nested-fatal': + $handler->register(); + if ($scenario === 'nested-fatal') { + (new ErrorHandler($reporter))->register(); + } + require __DIR__ . '/compile-fatal.inc'; + break; + case 'memory-limit': + $handler->register(); + // Allocation fails only inside this child, capped at 16 MiB by the runner. + str_repeat('x', 32 * 1024 * 1024); + break; + case 'shutdown-once': + $handler = new ErrorHandler($reporter, null, new CallableShutdownHandler(static function (): void { + event('shutdown'); + })); + for ($i = 0; $i < 25; ++$i) { + $handler->register(); + $handler->register(); + $handler->unregister(); + } + $handler->register(); + break; + case 'unregistered-shutdown': + $handler = new ErrorHandler($reporter, null, new CallableShutdownHandler(static function (): void { + event('unexpected-shutdown'); + })); + $handler->register(); + $handler->unregister(); + $handler->unregister(); + event('continued'); + break; + case 'restore': + $previousError = static function (): bool { + event('previous-error'); + return true; + }; + $previousException = static function (Throwable $error): void { + event('previous-exception'); + exit(42); + }; + set_error_handler($previousError, E_USER_NOTICE); + set_exception_handler($previousException); + $inner = new ErrorHandler($reporter); + $handler->register(); + $inner->register(); + $inner->unregister(); + trigger_error('outer active', E_USER_WARNING); + $handler->unregister(); + @trigger_error('outside previous mask', E_USER_WARNING); + trigger_error('previous active', E_USER_NOTICE); + throw new RuntimeException('previous exception active'); + case 'out-of-order': + $inner = new ErrorHandler($reporter); + $handler->register(); + $inner->register(); + try { + $handler->unregister(); + } catch (LogicException $error) { + event('rejected'); + } + trigger_error('inner still active', E_USER_WARNING); + $inner->unregister(); + $handler->unregister(); + break; + case 'reporter-throws': + case 'shutdown-reporter-throws': + $handler = new ErrorHandler(new CallableThrowableHandler(static function (): void { + event('reporter-called'); + throw new RuntimeException('reporter failed'); + })); + $handler->register(); + if ($scenario === 'shutdown-reporter-throws') { + require __DIR__ . '/compile-fatal.inc'; + } + throw new RuntimeException('original failure'); + case 'recursive-reporter': + $handler = new ErrorHandler(new CallableThrowableHandler( + static function (Throwable $error) use (&$handler): void { + event('reporter-called'); + $handler->handleThrowable($error); + } + )); + $handler->register(); + throw new RuntimeException('original failure'); + case 'reporter-warning': + $handler = new ErrorHandler(new CallableThrowableHandler(static function (): void { + event('reporter-called'); + trigger_error('reporter warning', E_USER_WARNING); + })); + $handler->register(); + trigger_error('original warning', E_USER_WARNING); + event('continued'); + break; + case 'shutdown-throws': + $handler = new ErrorHandler($reporter, null, new CallableShutdownHandler(static function (): void { + throw new RuntimeException('shutdown failed'); + })); + $handler->register(); + break; + case 'aggregate': + $aggregate = new ThrowableHandlerAggregate( + new CallableThrowableHandler(static function (Throwable $error): void { + event('first'); + }), + new CallableThrowableHandler(static function (Throwable $error): void { + event('second'); + }) + ); + (new ErrorHandler($aggregate))->handleThrowable(new RuntimeException('chain')); + break; + default: + throw new InvalidArgumentException('Unknown scenario: ' . $scenario); +} diff --git a/tests/Functional/Container/ConfigProviderTest.php b/tests/Functional/Container/ConfigProviderTest.php index 0ba7403..536163e 100644 --- a/tests/Functional/Container/ConfigProviderTest.php +++ b/tests/Functional/Container/ConfigProviderTest.php @@ -23,6 +23,7 @@ use Phly\EventDispatcher\EventDispatcher; use Phly\EventDispatcher\ListenerProvider\ListenerProviderAggregate; use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\StreamFactoryInterface; @@ -68,33 +69,33 @@ public function setUpServiceManager(ServiceManager $serviceManager): void $serviceManager->setService(StreamFactoryInterface::class, $factory); } - public function provideContainerDependenciesClassNames(): array + public static function provideContainerDependenciesClassNames(): array { $dependencies = (new ConfigProvider())->getDependencies(); return array_chunk(array_keys(array_merge(...array_values($dependencies))), 1); } - /** @dataProvider provideContainerDependenciesClassNames */ + #[DataProvider('provideContainerDependenciesClassNames')] public function testServiceManagerHasConfigProviderDependencies(string $className): void { static::assertTrue($this->serviceManager->has($className)); } - /** @dataProvider provideContainerDependenciesClassNames */ + #[DataProvider('provideContainerDependenciesClassNames')] public function testContainerWithServiceManagerHasConfigProviderDependencies(string $className): void { static::assertTrue($this->container->has($className)); } - /** @dataProvider provideContainerDependenciesClassNames */ + #[DataProvider('provideContainerDependenciesClassNames')] public function testContainerWithoutServiceManagerHasConfigProviderDependencies(string $className): void { $container = new ErrorHandlerContainer(); static::assertTrue($container->has($className)); } - public function provideContainerDependenciesWithInstanceType(): array + public static function provideContainerDependenciesWithInstanceType(): array { $dependencies = (new ConfigProvider())->getDependencies(); $classNames = array_keys(array_merge(...array_values($dependencies))); @@ -109,33 +110,27 @@ public function provideContainerDependenciesWithInstanceType(): array }, $classNames); } - /** - * @dataProvider provideContainerDependenciesWithInstanceType - */ + #[DataProvider('provideContainerDependenciesWithInstanceType')] public function testServiceManagerCanCreateDependecyThatImplements(string $className, string $instanceOf): void { $object = $this->serviceManager->get($className); static::assertInstanceOf($instanceOf, $object); } - /** - * @dataProvider provideContainerDependenciesWithInstanceType - */ + #[DataProvider('provideContainerDependenciesWithInstanceType')] public function testContainerCanCreateDependecyThatImplements(string $className, string $instanceOf): void { $object = $this->container->get($className); static::assertInstanceOf($instanceOf, $object); } - /** - * @dataProvider provideContainerDependenciesWithInstanceType - */ + #[DataProvider('provideContainerDependenciesWithInstanceType')] public function testWithCleanServiceManagerCanCreateDependecyThatImplements( string $className, string $instanceOf ): void { $serviceManager = new ServiceManager(); - $container = new ErrorHandlerContainer($this->serviceManager); + $container = new ErrorHandlerContainer($serviceManager); $this->setUpServiceManager($serviceManager); diff --git a/tests/Functional/Container/ErrorHandlerContainerTest.php b/tests/Functional/Container/ErrorHandlerContainerTest.php index 884632f..535f4a6 100644 --- a/tests/Functional/Container/ErrorHandlerContainerTest.php +++ b/tests/Functional/Container/ErrorHandlerContainerTest.php @@ -37,6 +37,8 @@ use Phly\EventDispatcher\EventDispatcher; use Phly\EventDispatcher\ListenerProvider\ListenerProviderAggregate; use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\WithoutErrorHandler; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; @@ -80,20 +82,25 @@ protected function setUp(): void $this->serviceManager->setService(StreamFactoryInterface::class, $factory); } - public function provideServiceProviderClassNames() + protected function tearDown(): void + { + $this->container->get(ErrorHandler::class)->unregister(); + } + + public static function provideServiceProviderClassNames(): array { $factories = (new ConfigProvider())->getFactories(); return array_chunk(array_keys($factories), 1); } - /** @dataProvider provideServiceProviderClassNames */ + #[DataProvider('provideServiceProviderClassNames')] public function testContainerHasConfigProviderFactories(string $service): void { static::assertTrue($this->container->has($service)); } - /** @dataProvider provideServiceProviderClassNames */ + #[DataProvider('provideServiceProviderClassNames')] public function testContainerProvideConfigProviderFactories(string $service): void { $this->serviceManager->setService(EventDispatcherInterface::class, $this->eventDispatcher); @@ -197,6 +204,7 @@ public function testErrorHandlerRespectErrorReporting(): void static::assertTrue(true); } + #[WithoutErrorHandler] public function testThrowErrorExceptionPhpErrorHandlerThrowErrorException(): void { $callableThrowableHandler = new CallableThrowableHandler(function ($throwable): void { @@ -207,7 +215,7 @@ public function testThrowErrorExceptionPhpErrorHandlerThrowErrorException(): voi $errorHandler = $this->container->get(ErrorHandler::class); $errorHandler->register(); - trigger_error('Test user error', E_USER_ERROR); + trigger_error('Test user warning', E_USER_WARNING); } public function testErrorHandlerMiddlewareHandleRequestException(): void @@ -255,11 +263,9 @@ public function handle(ServerRequestInterface $serverRequest): ResponseInterface public function testUnregisteredErrorHandlerHandleShutdownWillDoNothing(): void { + $this->expectOutputString(''); $errorHandler = $this->container->get(ErrorHandler::class); $errorHandler->handleShutdown(); - - $output = $this->getActualOutput(); - static::assertEmpty($output); } public function testErrorHandlerHandleShutdownWithErrorWillHandledByThrowableHandler(): void diff --git a/tests/Functional/Http/Middleware/ErrorHandlerCleanupTest.php b/tests/Functional/Http/Middleware/ErrorHandlerCleanupTest.php new file mode 100644 index 0000000..55ed6d8 --- /dev/null +++ b/tests/Functional/Http/Middleware/ErrorHandlerCleanupTest.php @@ -0,0 +1,62 @@ + true; + $previousException = static function (\Throwable $error): void { + }; + set_error_handler($previousError, E_USER_NOTICE); + set_exception_handler($previousException); + $failure = new \RuntimeException('response factory failed'); + $responses = new class ($failure) implements ThrowableResponseFactoryInterface { + public function __construct(private \Throwable $failure) + { + } + public function createResponseFromThrowable(\Throwable $throwable): ResponseInterface + { + throw $this->failure; + } + }; + $next = new class () implements RequestHandlerInterface { + public function handle(ServerRequestInterface $request): ResponseInterface + { + throw new \TypeError('request failed'); + } + }; + $reporter = new CallableThrowableHandler(static function (): void { + }); + $middleware = new ErrorHandlerMiddleware(new ErrorHandler($reporter), $responses); + try { + $middleware->process((new Psr17Factory())->createServerRequest('GET', '/'), $next); + self::fail('Response factory failure must propagate.'); + } catch (\RuntimeException $actual) { + self::assertSame($failure, $actual); + $activeError = set_error_handler(static fn (): bool => true); + restore_error_handler(); + $activeException = set_exception_handler(static function (\Throwable $error): void { + }); + restore_exception_handler(); + self::assertSame($previousError, $activeError); + self::assertSame($previousException, $activeException); + } finally { + restore_error_handler(); + restore_exception_handler(); + } + } +} diff --git a/tests/Functional/Http/Middleware/ErrorHandlerMiddlewareTest.php b/tests/Functional/Http/Middleware/ErrorHandlerMiddlewareTest.php index 0f68885..0be46ca 100644 --- a/tests/Functional/Http/Middleware/ErrorHandlerMiddlewareTest.php +++ b/tests/Functional/Http/Middleware/ErrorHandlerMiddlewareTest.php @@ -14,7 +14,7 @@ * @license https://opensource.org/licenses/MIT MIT License */ -namespace CoiSA\ErrorHandler\Test\Functional\Container; +namespace CoiSA\ErrorHandler\Test\Functional\Http\Middleware; use CoiSA\ErrorHandler\ErrorHandler; use CoiSA\ErrorHandler\Handler\CallableThrowableHandler; diff --git a/tests/Http/router.php b/tests/Http/router.php new file mode 100644 index 0000000..f6f4b23 --- /dev/null +++ b/tests/Http/router.php @@ -0,0 +1,40 @@ +getUri()->getPath() === '/unavailable') { + throw new RuntimeException('private upstream address', 503); + } + if ($request->getUri()->getPath() === '/type-error') { + strlen([]); + } + throw new RuntimeException('private connection string'); + } +}; +$response = $middleware->process($factory->createServerRequest('GET', $_SERVER['REQUEST_URI']), $next); +http_response_code($response->getStatusCode()); +header('Content-Type: text/plain; charset=utf-8'); +echo $response->getBody(); diff --git a/tests/Http/run.php b/tests/Http/run.php new file mode 100644 index 0000000..1a9368d --- /dev/null +++ b/tests/Http/run.php @@ -0,0 +1,67 @@ + ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes); +if (!is_resource($process)) { + throw new RuntimeException('Could not start fixture server'); +} +fclose($pipes[0]); +stream_set_blocking($pipes[1], false); +stream_set_blocking($pipes[2], false); +try { + $ready = false; + $deadline = microtime(true) + 5; + do { + $connection = @stream_socket_client('tcp://' . $address, $errno, $error, 0.1); + if (is_resource($connection)) { + fclose($connection); + $ready = true; + break; + } + if (!proc_get_status($process)['running']) { + break; + } + usleep(10000); + } while (microtime(true) < $deadline); + if (!$ready) { + throw new RuntimeException('Fixture server did not start: ' . stream_get_contents($pipes[2])); + } + foreach (['/error' => 500, '/unavailable' => 503, '/type-error' => 500] as $route => $expected) { + $context = stream_context_create(['http' => ['ignore_errors' => true, 'timeout' => 2]]); + $stream = fopen('http://' . $address . $route, 'r', false, $context); + if ($stream === false) { + throw new RuntimeException('Request failed: ' . $route); + } + $body = stream_get_contents($stream); + $metadata = stream_get_meta_data($stream); + fclose($stream); + $status = $metadata['wrapper_data'][0]; + if (!str_contains($status, ' ' . $expected . ' ') || $body !== 'Internal Server Error') { + throw new RuntimeException('Unexpected response: ' . $status . ' ' . $body); + } + printf("PASS HTTP %-14s status=%d body=redacted\n", $route, $expected); + } +} finally { + proc_terminate($process); + $deadline = microtime(true) + 1; + while (proc_get_status($process)['running'] && microtime(true) < $deadline) { + usleep(10000); + } + if (proc_get_status($process)['running']) { + proc_terminate($process, 9); + } + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); +} diff --git a/tests/Unit/Container/ErrorHandlerContainerTest.php b/tests/Unit/Container/ErrorHandlerContainerTest.php index 75aea3c..969f679 100644 --- a/tests/Unit/Container/ErrorHandlerContainerTest.php +++ b/tests/Unit/Container/ErrorHandlerContainerTest.php @@ -22,7 +22,7 @@ use CoiSA\ErrorHandler\ErrorHandler; use CoiSA\ErrorHandler\Handler\ThrowableHandlerInterface; use PHPUnit\Framework\TestCase; -use Prophecy\Prophecy\ObjectProphecy; +use PHPUnit\Framework\MockObject\Stub; use Psr\Container\ContainerInterface; /** @@ -35,7 +35,7 @@ */ final class ErrorHandlerContainerTest extends TestCase { - /** @var ContainerInterface|ObjectProphecy */ + /** @var ContainerInterface&Stub */ private $innerContainer; /** @var ErrorHandlerContainer */ @@ -43,25 +43,27 @@ final class ErrorHandlerContainerTest extends TestCase protected function setUp(): void { - $this->innerContainer = $this->prophesize(ContainerInterface::class); - $this->container = new ErrorHandlerContainer($this->innerContainer->reveal()); + $this->innerContainer = $this->createStub(ContainerInterface::class); + $this->container = new ErrorHandlerContainer($this->innerContainer); } public function testGetWithInvalidServiceWillThrowNotFoundException(): void { $this->expectException(NotFoundException::class); $index = uniqid('test', true); - $this->innerContainer->has($index)->willReturn(false); + $this->innerContainer->method('has')->willReturn(false); $this->container->get($index); } public function testGetWithServiceThatThrowsExceptionWillThrowContainerException(): void { - $this->innerContainer->has(ErrorHandler::class)->willReturn(false); - $this->innerContainer->has(ThrowableHandlerInterface::class)->willReturn(true); - $this->innerContainer->get(ThrowableHandlerInterface::class)->willThrow(\UnexpectedValueException::class); + $this->innerContainer->method('has')->willReturnMap([ + [ErrorHandler::class, false], + [ThrowableHandlerInterface::class, true], + ]); + $this->innerContainer->method('get')->willThrowException(new \UnexpectedValueException()); - $container = new ErrorHandlerContainer($this->innerContainer->reveal()); + $container = new ErrorHandlerContainer($this->innerContainer); $this->expectException(ContainerException::class); $container->get(ErrorHandler::class); diff --git a/tests/Unit/Http/Message/ThrowableResponseFactoryTest.php b/tests/Unit/Http/Message/ThrowableResponseFactoryTest.php new file mode 100644 index 0000000..39e6ff5 --- /dev/null +++ b/tests/Unit/Http/Message/ThrowableResponseFactoryTest.php @@ -0,0 +1,88 @@ +createResponseFromThrowable($error); + + self::assertSame($expected, $response->getStatusCode()); + self::assertStringNotContainsString('private detail', $response->getReasonPhrase()); + self::assertSame('Internal Server Error', (string) $response->getBody()); + self::assertSame([$error], $reported); + } + + public function testStringSqlStateFallsBackTo500(): void + { + $error = new class () extends \PDOException { + public function __construct() + { + $this->code = 'HY000'; + } + }; + $factory = new Psr17Factory(); + $reporter = new CallableThrowableHandler(static function (): void { + }); + $responses = new ThrowableResponseFactory(new ThrowableStreamFactory($factory, $reporter), $factory); + self::assertSame(500, $responses->createResponseFromThrowable($error)->getStatusCode()); + } + + public function testZeroIsAValidRenderedBody(): void + { + $reporter = new CallableThrowableHandler(static function (): void { + echo '0'; + }); + $streams = new ThrowableStreamFactory(new Psr17Factory(), $reporter); + self::assertSame('0', (string) $streams->createStreamFromThrowable(new \RuntimeException('private'))); + } + + public function testRendererFailureCleansItsBuffersAndPreservesCallerBuffer(): void + { + $failure = new \RuntimeException('renderer failed'); + $reporter = new CallableThrowableHandler(static function () use ($failure): void { + echo 'partial output'; + ob_start(); + echo 'nested output'; + throw $failure; + }); + $streams = new ThrowableStreamFactory(new Psr17Factory(), $reporter); + ob_start(); + echo 'caller output'; + $level = ob_get_level(); + try { + $streams->createStreamFromThrowable(new \RuntimeException('original')); + self::fail('Renderer failure must propagate.'); + } catch (\RuntimeException $actual) { + self::assertSame($failure, $actual); + self::assertSame($level, ob_get_level()); + self::assertSame('caller output', ob_get_contents()); + } finally { + ob_end_clean(); + } + } +} From dfb0433db0effd298080ae5f8e63f3da051e6cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Tue, 15 Sep 2026 20:39:20 -0300 Subject: [PATCH 2/4] fix: preserve shutdown delivery and bound renderer cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- .github/dependabot.yml | 12 ++++ CHANGELOG.md | 4 +- README.md | 2 + composer.json | 2 +- docs/api.md | 4 +- docs/index.html | 2 +- docs/testing.md | 25 ++++++-- phpstan.neon | 2 + src/ErrorHandler.php | 15 +++-- src/Http/Message/ThrowableStreamFactory.php | 22 ++++++- tests/Fire/run.php | 18 ++++++ tests/Fire/scenario.php | 65 +++++++++++++++++++++ tests/StaticAnalysis/output-buffer.stub | 9 +++ 13 files changed, 167 insertions(+), 15 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 tests/StaticAnalysis/output-buffer.stub diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..bf60b15 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: composer + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 diff --git a/CHANGELOG.md b/CHANGELOG.md index d7e486e..ee1723f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,12 +14,14 @@ - Re-registering an instance no longer adds duplicate shutdown callbacks. - Failing or recursive reporters are not invoked repeatedly during exception/shutdown handling. -- Throwable rendering cleans its output buffers even when the renderer throws, preserves the body `0`, and accepts SQLSTATE exception codes without raising a secondary type error. +- Custom shutdown callbacks still run after a reported uncaught throwable; replacing the active callback continues to suppress the old registration. +- Throwable rendering cleans reachable removable buffers when the renderer throws, stops at non-removable buffers, preserves the original failure if cleanup also throws, preserves the body `0`, and accepts SQLSTATE exception codes without raising a secondary type error. - Shutdown reporting excludes nonfatal core and compile warnings. ### Added - Bounded subprocess fire tests, HTTP loopback tests, lifecycle and response regression tests. - English usage, API, migration and testing documentation, plus a static documentation site with manual default-branch Pages deployment. +- Weekly Composer and GitHub Actions update proposals through Dependabot. The historical 2.0.0 and 1.0.0 releases are available in [GitHub Releases](https://github.com/coisa/error-handler/releases). diff --git a/README.md b/README.md index 8bc1ff7..d44b8ea 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,8 @@ The middleware restores handlers in `finally`, including when response rendering GitHub Pages is prepared as a **manual** workflow restricted to `master`. After review and merge, an owner can enable Pages with GitHub Actions as the source and run the Documentation workflow from `master`. Nothing in the pull-request CI publishes documentation or executes PHP on Pages. +Dependabot is configured to propose Composer and GitHub Actions updates weekly after this configuration reaches the default branch. Updates remain reviewable pull requests. + ## Confidence and boundaries The test suite includes isolated real PHP failures rather than only calls to mocked handlers. Child processes have memory, time and output bounds; tests check termination, delivery counts and restoration. This is evidence for the exercised cases, not a reliability guarantee for every fatal error, PHP SAPI or downstream reporter. See the [failure matrix](docs/testing.md). diff --git a/composer.json b/composer.json index bb24d70..44649dd 100644 --- a/composer.json +++ b/composer.json @@ -64,7 +64,7 @@ ], "sniff": "phpcs", "test": "phpunit", - "fire-test": "@php tests/Fire/run.php", + "fire-test": "@php tests/Fire/run.php --with-psr", "http-test": "@php tests/Http/run.php" } } diff --git a/docs/api.md b/docs/api.md index 1de5fff..f34bad9 100644 --- a/docs/api.md +++ b/docs/api.md @@ -18,7 +18,7 @@ Use this scoped form for code whose caller handles propagated exceptions. For pr Registration pushes PHP callbacks onto the native stacks. Previous callbacks are restored, including their error-level masks, when unregistered in reverse order. They are **not automatically called** while this handler is active. A second registration of the same instance is a no-op, not a nested reference count. Shutdown registration happens once per instance because PHP provides no shutdown-unregister function; inactive instances do nothing at shutdown. Reuse an instance when practical: PHP retains registered shutdown callbacks until process exit. -Only the instance owning both current PHP callbacks receives shutdown events. If another integration replaces a callback, restore it before unregistering this instance. Otherwise `unregister()` rejects the operation without altering either stack. +Only the active instance receives shutdown events. This includes a custom shutdown callback after an uncaught throwable has been reported: PHP temporarily clears the exception callback during its invocation, and the library tracks that terminating path. Replacing either callback with another integration still transfers ownership away. Restore that callback before unregistering this instance; otherwise `unregister()` rejects the operation without altering either stack. | API | Behavior | | --- | --- | @@ -51,7 +51,7 @@ Use the `Http\Message` and `Http\Middleware` namespaces for these classes. The P The middleware returns normal downstream responses unchanged and catches downstream `Throwable` objects to generate an error response. It always unregisters its handler after processing. Use a distinct handler instance for each nested middleware scope; do not share/pre-register one instance across simultaneous requests or fibers. PHP's global callback stacks are process state, not request-local storage. -`ThrowableStreamFactory` captures reporter output. Empty output maps to `Internal Server Error`; `0` is a valid body. The factory cleans buffers it opens if rendering fails. Renderers must balance their own successful nested buffers and must not close the caller's buffers. `ThrowableResponseFactory` keeps integer codes 400–599, uses 500 otherwise, and lets the PSR-17 factory supply the standard reason phrase. +`ThrowableStreamFactory` captures reporter output. Empty output maps to `Internal Server Error`; `0` is a valid body. On renderer failure, cleanup removes reachable removable buffers and preserves the renderer exception. It stops if the top buffer is non-removable or cleanup makes no progress. PHP cannot remove a non-removable buffer or reach the buffers beneath it; those remain active, so renderers must never create non-removable buffers. Renderers must balance their own successful nested buffers and must not close the caller's buffers. `ThrowableResponseFactory` keeps integer codes 400–599, uses 500 otherwise, and lets the PSR-17 factory supply the standard reason phrase. Neither the middleware nor the global handler emits PSR-7 responses to a server. The application/framework owns emission, headers, content type and transport. A PHP engine fatal error bypasses normal middleware recovery; shutdown reporting cannot reliably replace an already-started HTTP response. diff --git a/docs/index.html b/docs/index.html index e8ba674..a23a43b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -57,7 +57,7 @@

Make failure
observable.

04 / Exercise the failure path

A test should survive the crash.

-
20ISOLATED SCENARIOS

Real warnings, throwable failures, compile fatals, allocation failure, registration cycles and reporter recursion run in child processes. Each child has a five-second deadline, a 16 MiB memory limit and a 256 KiB output cap.

+
27ISOLATED SCENARIOS

Real warnings, throwable failures, compile fatals, allocation failure, custom shutdown, reporter recursion and renderer failures run in child processes. Each child has a five-second deadline, a 16 MiB memory limit and a 256 KiB output cap.

The runner checks delivery counts, ordering, throwable classes, stderr and exact exit status. Separate integration tests exercise PSR composition and real HTTP 500/503 responses through a loopback-only server.

RUN THE EVIDENCE YOURSELF
php tests/Fire/run.php
 composer --no-plugins test
diff --git a/docs/testing.md b/docs/testing.md
index b728bcc..ac7bf86 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -16,12 +16,15 @@ composer --no-plugins check
 # Core crash tests need PHP only; no Composer dependencies or extensions.
 php tests/Fire/run.php
 
+# Include the three isolated PSR renderer regressions after Composer installation.
+php tests/Fire/run.php --with-psr
+
 # PSR integration and real HTTP responses.
 composer --no-plugins test
 php tests/Http/run.php
 ```
 
-The checks run PSR-12 formatting (with the existing declare-before-license convention), PHP_CodeSniffer, PHPUnit, PHPStan at maximum level over `src/`, crash tests and loopback HTTP smoke tests. Deliberately invalid `.inc` crash fixtures are excluded from lint and autoload classmaps. PHPUnit restores its own error handler between tests; the PHP-warning integration case explicitly disables PHPUnit's error handler so the package receives the real warning.
+The checks run PSR-12 formatting (with the existing declare-before-license convention), PHP_CodeSniffer, PHPUnit, PHPStan at maximum level over `src/`, crash tests and loopback HTTP smoke tests. A narrow PHPStan stub records that `ob_end_clean()` can throw through a user-defined output callback, as demonstrated by a child regression; no analysis errors are suppressed. Deliberately invalid `.inc` crash fixtures are excluded from lint and autoload classmaps. PHPUnit restores its own error handler between tests; the PHP-warning integration case explicitly disables PHPUnit's error handler so the package receives the real warning.
 
 ## Fire-test contract
 
@@ -32,6 +35,9 @@ Every child uses `php -n`, a 16 MiB PHP memory limit, a five-second wall-clock d
 | User/native warnings | One `ErrorException`; execution continues, exit 0 |
 | `error_reporting()` and `@` | Suppressed warnings are not delivered |
 | Uncaught exception, `Error`, `TypeError`, included parse error | One original throwable, exit 1 |
+| Uncaught exception with custom/nested shutdown | Only the active custom callback runs; exit 1 |
+| Uncaught exception followed by a throwing shutdown callback | Original and shutdown throwables each delivered once; exit 1 |
+| Exception callback replaced by the reporter | Replaced instance does not receive shutdown |
 | Compile fatal, nested-handler compile fatal | One active-handler delivery, native diagnostic, exit 255 |
 | Controlled allocation failure | One fatal delivery, native diagnostic, exit 255 |
 | 25 register/unregister cycles and final registration | One shutdown callback |
@@ -42,6 +48,8 @@ Every child uses `php -n`, a 16 MiB PHP memory limit, a five-second wall-clock d
 | Warning inside the PHP-error reporter | Native diagnostic; no recursive reporter call |
 | Custom shutdown throws | Its throwable is delivered once |
 | Aggregate | Reporters run in attachment order |
+| Renderer failure with removable/non-removable nested buffer (`--with-psr`) | Original renderer failure propagates without a loop; non-removable buffers remain active |
+| Renderer and output-cleanup callback both throw (`--with-psr`) | Original renderer failure wins; reachable buffers are cleaned |
 
 The allocation case deliberately requests 32 MiB under a 16 MiB limit. It proves this failure path only. There is no memory reserve and **no guarantee** of reporting arbitrary real-world out-of-memory conditions.
 
@@ -49,16 +57,23 @@ The allocation case deliberately requests 32 MiB under a 16 MiB limit. It proves
 
 On 2026-09-15, the original `f7284cd` source passed 10 of the first 19 isolated scenarios on macOS arm64/PHP 8.5.10. Four uncaught throwable cases exited 0; 25 registration cycles caused 26 shutdown deliveries; failing reporters were called three times; recursion produced 9,389 events before the output cap terminated the child. The original Composer lock could not install on PHP 8.5 because several locked integrations excluded it.
 
-The final 20-case core suite, including nested fatal-handler coverage, passed on macOS arm64/PHP 8.5.10 and official Linux aarch64 images for PHP 8.3.33, 8.4.25 and 8.5.10. PHP 8.2.33 also passed the original 19 core cases during investigation but is outside the new support contract.
+The final 24-case core suite, including nested fatal-handler and custom shutdown coverage, passed on macOS arm64/PHP 8.5.10 and official Linux aarch64 images for PHP 8.3.33, 8.4.25 and 8.5.10. PHP 8.2.33 also passed the original 19 core cases during investigation but is outside the new support contract.
+
+Independent review exposed two regressions in the first modernization draft: custom shutdown was skipped after an uncaught exception, and cleanup looped on a non-removable output buffer. The new child tests failed before the fixes; the non-removable case was terminated by the runner's output cap. An additional regression demonstrated that a throwing output callback could replace the original renderer failure and leave a buffer open. With the fixes, the suite includes 24 core cases and three PSR renderer cases. The latter require Composer dependencies and run automatically through `composer --no-plugins check`.
 
 Native PHP 8.5.10 verification additionally runs PHPUnit 13.3.4, PHPStan 2.2.14 and three real HTTP requests through PHP's built-in server: 500, 503 and TypeError→500, all with a redacted body. The CI matrix checks lowest/highest compatible dependencies on PHP 8.3, highest on 8.4, and locked/highest on 8.5. A green result applies only to the exact checked commit; consult the [Actions run](https://github.com/coisa/error-handler/actions/workflows/ci.yml) rather than assuming that a configured matrix has passed.
 
-The full `composer --no-plugins check` command also passed in the rebuilt PHP 8.3.33 container with PHPUnit 12.5.35: 100 tests and 144 assertions, maximum-level source analysis, formatting, all 20 crash scenarios and all three HTTP responses. The runnable CLI example was checked for exit status 1, empty stdout and both expected diagnostic messages on stderr.
+The full `composer --no-plugins check` command passed on native PHP 8.5.10/PHPUnit 13.3.4 and isolated PHP 8.3.33 with both highest dependencies (PHPUnit 12.5.35) and lowest allowed dependencies (PHPUnit 12.5.8, PHPStan 2.1.0, PSR Container 1.1.0): 100 tests and 144 assertions, maximum-level source analysis, formatting, all 27 isolated scenarios and all three HTTP responses. The runnable CLI example was checked for exit status 1, empty stdout and both expected diagnostic messages on stderr.
 
 ```sh
 docker run --rm --network none --read-only --memory 128m --cpus 1 --pids-limit 32 \
   -v "$PWD:/app:ro" -w /app php:8.3-cli-alpine php tests/Fire/run.php
 docker compose run --rm --build tests
+
+# Lowest allowed dependencies, isolated from the host's vendor/ and lock.
+docker run --rm --memory 512m --cpus 2 --pids-limit 64 \
+  error-handler-modernization-tests sh -c \
+  'composer update --prefer-lowest --prefer-stable --no-plugins --no-scripts --no-interaction --prefer-dist --no-progress && composer --no-plugins check'
 ```
 
 Container builds resolve development packages for the requested PHP version. For a specific matrix run, record the actual PHP version, resolved dependencies and image digest; floating version tags may change. The recorded image digests are:
@@ -73,6 +88,6 @@ Container builds resolve development packages for the requested PHP version. For
 - PHP-FPM, Apache modules, Windows, production load, asynchronous/concurrent runtimes, remote logging failures and process signals are not covered by the local evidence. The Linux CI runner adds its own platform evidence when it succeeds.
 - Errors before registration, startup failures and `SIGKILL` cannot be rescued by a PHP shutdown callback. A handler does not make a fatal error recoverable. Native shutdown diagnostics may still be emitted by PHP; applications must configure `display_errors`/logging appropriately.
 - A global Composer plugin can interfere with newer tooling. The documented `--no-plugins` commands isolate this project from plugins; they do not modify global configuration.
-- If unregistration throws, unwind the middleware/handler stack in reverse order. If a renderer throws, its exception propagates after library-owned buffers are cleaned. Renderers must not close buffers owned by the caller.
+- If unregistration throws, unwind the middleware/handler stack in reverse order. If a renderer throws, its exception propagates after reachable removable buffers are cleaned. Cleanup stops at a non-removable buffer because PHP cannot remove it or reach lower buffers. Renderers must use removable buffers and must not close buffers owned by the caller.
 
-Sources: [PHP supported versions](https://www.php.net/supported-versions.php), [error-handler semantics](https://www.php.net/manual/en/function.set-error-handler.php), [exception handlers](https://www.php.net/manual/en/function.set-exception-handler.php), [shutdown semantics](https://www.php.net/manual/en/function.register-shutdown-function.php).
+Sources: [PHP supported versions](https://www.php.net/supported-versions.php), [error-handler semantics](https://www.php.net/manual/en/function.set-error-handler.php), [exception handlers](https://www.php.net/manual/en/function.set-exception-handler.php), [shutdown semantics](https://www.php.net/manual/en/function.register-shutdown-function.php), [output buffer status](https://www.php.net/manual/en/function.ob-get-status.php).
diff --git a/phpstan.neon b/phpstan.neon
index 9192c76..6d4afaf 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -2,4 +2,6 @@ parameters:
   level: max
   paths:
     - src
+  stubFiles:
+    - tests/StaticAnalysis/output-buffer.stub
   tmpDir: tmp/phpstan
diff --git a/src/ErrorHandler.php b/src/ErrorHandler.php
index 8eacb24..9e624a7 100644
--- a/src/ErrorHandler.php
+++ b/src/ErrorHandler.php
@@ -60,6 +60,8 @@ final class ErrorHandler implements
 
     private bool $isHandlingThrowable = false;
 
+    private bool $isHandlingUncaughtThrowable = false;
+
     private bool $handlerFailed = false;
 
     private \Closure $errorCallback;
@@ -184,7 +186,7 @@ public function handlePhpError(int $code, string $message, string $filename, int
      */
     public function handleShutdown(): void
     {
-        if (!$this->isRegistered || $this->handlerFailed || !$this->hasActiveCallbacks()) {
+        if (!$this->isRegistered || $this->handlerFailed || !$this->hasActiveCallbacks(true)) {
             return;
         }
 
@@ -202,7 +204,7 @@ private function onPhpError(int $code, string $message, string $filename, int $l
         return true;
     }
 
-    private function hasActiveCallbacks(): bool
+    private function hasActiveCallbacks(bool $duringShutdown = false): bool
     {
         // Probe without changing the existing stacks or their error-level masks.
         $currentErrorHandler = set_error_handler(static fn (): bool => false);
@@ -211,12 +213,17 @@ private function hasActiveCallbacks(): bool
         });
         restore_exception_handler();
 
-        return $currentErrorHandler === $this->errorCallback
-            && $currentExceptionHandler === $this->exceptionCallback;
+        // PHP clears the exception callback while invoking it. exit() keeps that
+        // temporary state through shutdown, but does not transfer ownership.
+        $ownsExceptionCallback = $currentExceptionHandler === $this->exceptionCallback
+            || ($duringShutdown && $this->isHandlingUncaughtThrowable && $currentExceptionHandler === null);
+
+        return $currentErrorHandler === $this->errorCallback && $ownsExceptionCallback;
     }
 
     private function onUncaughtThrowable(\Throwable $throwable): never
     {
+        $this->isHandlingUncaughtThrowable = true;
         $this->handleThrowable($throwable);
         exit(1);
     }
diff --git a/src/Http/Message/ThrowableStreamFactory.php b/src/Http/Message/ThrowableStreamFactory.php
index 64a3545..37de5f0 100644
--- a/src/Http/Message/ThrowableStreamFactory.php
+++ b/src/Http/Message/ThrowableStreamFactory.php
@@ -68,16 +68,36 @@ public function __construct(
     public function createStreamFromThrowable(\Throwable $throwable): StreamInterface
     {
         $initialLevel = ob_get_level();
+        $failure = null;
+        $buffer = false;
         ob_start();
         try {
             $this->throwableHandler->handleThrowable($throwable);
             $buffer = ob_get_contents();
+        } catch (\Throwable $rendererFailure) {
+            $failure = $rendererFailure;
         } finally {
             while (ob_get_level() > $initialLevel) {
-                ob_end_clean();
+                $flags = ob_get_status()['flags'] ?? null;
+                if (!is_int($flags) || ($flags & PHP_OUTPUT_HANDLER_REMOVABLE) === 0) {
+                    // PHP cannot remove this buffer or reach the buffers below it.
+                    break;
+                }
+                $level = ob_get_level();
+                try {
+                    ob_end_clean();
+                } catch (\Throwable $cleanupFailure) {
+                    $failure ??= $cleanupFailure;
+                }
+                if (ob_get_level() >= $level) {
+                    break;
+                }
             }
         }
 
+        if ($failure !== null) {
+            throw $failure;
+        }
         if ($buffer === false || $buffer === '') {
             $buffer = 'Internal Server Error';
         }
diff --git a/tests/Fire/run.php b/tests/Fire/run.php
index d5f0775..b2f3fde 100644
--- a/tests/Fire/run.php
+++ b/tests/Fire/run.php
@@ -9,6 +9,11 @@
     'native-warning' => [0, ['error', 'continued'], 'CoiSA\\ErrorHandler\\Exception\\ErrorException', E_WARNING],
     'suppression' => [0, ['continued']],
     'exception' => [1, ['error'], RuntimeException::class],
+    'exception-shutdown' => [1, ['error', 'shutdown'], RuntimeException::class],
+    'nested-exception-shutdown' => [1, ['error', 'shutdown'], RuntimeException::class],
+    'exception-shutdown-throws' => [1, ['error', 'error'], RuntimeException::class, null,
+        [0 => ['message' => 'uncaught exception'], 1 => ['message' => 'shutdown failed']]],
+    'exception-replaced-handler' => [1, ['error'], RuntimeException::class],
     'error' => [1, ['error'], Error::class],
     'type-error' => [1, ['error'], TypeError::class],
     'parse-error' => [1, ['error'], ParseError::class],
@@ -26,6 +31,14 @@
     'shutdown-throws' => [0, ['error'], RuntimeException::class],
     'aggregate' => [0, ['first', 'second']],
 ];
+if (in_array('--with-psr', $argv, true)) {
+    $cases['render-removable'] = [0, ['renderer-failure'], RuntimeException::class, null,
+        [0 => ['message' => 'renderer failed', 'remainingBuffers' => 0]]];
+    $cases['render-non-removable'] = [0, ['renderer-failure'], RuntimeException::class, null,
+        [0 => ['message' => 'renderer failed', 'remainingBuffers' => 2]]];
+    $cases['render-cleanup-throws'] = [0, ['renderer-failure'], RuntimeException::class, null,
+        [0 => ['message' => 'renderer failed', 'remainingBuffers' => 0]]];
+}
 $failed = 0;
 printf("PHP %s | %s %s | %d isolated scenarios\n", PHP_VERSION, PHP_OS_FAMILY, php_uname('m'), count($cases));
 foreach ($cases as $name => $expected) {
@@ -80,6 +93,11 @@
     if (isset($expected[3])) {
         $ok = $ok && ($events[0]['severity'] ?? null) === $expected[3];
     }
+    foreach ($expected[4] ?? [] as $index => $fields) {
+        foreach ($fields as $field => $value) {
+            $ok = $ok && ($events[$index][$field] ?? null) === $value;
+        }
+    }
     $nativeDiagnostic = in_array($name, [
         'compile-fatal', 'nested-fatal', 'memory-limit', 'reporter-throws', 'shutdown-reporter-throws',
         'recursive-reporter', 'reporter-warning',
diff --git a/tests/Fire/scenario.php b/tests/Fire/scenario.php
index 0f2cddf..8734e31 100644
--- a/tests/Fire/scenario.php
+++ b/tests/Fire/scenario.php
@@ -42,6 +42,39 @@
     case 'exception':
         $handler->register();
         throw new RuntimeException('uncaught exception');
+    case 'exception-replaced-handler':
+        $handler = new ErrorHandler(
+            new CallableThrowableHandler(static function (Throwable $failure) use ($reporter): void {
+                $reporter->handleThrowable($failure);
+                set_exception_handler(static function (): void {
+                    event('unexpected-exception');
+                });
+            }),
+            null,
+            new CallableShutdownHandler(static function (): void {
+                event('unexpected-shutdown');
+            })
+        );
+        $handler->register();
+        throw new RuntimeException('uncaught exception');
+    case 'exception-shutdown':
+    case 'nested-exception-shutdown':
+    case 'exception-shutdown-throws':
+        if ($scenario === 'nested-exception-shutdown') {
+            (new ErrorHandler($reporter, null, new CallableShutdownHandler(static function (): void {
+                event('unexpected-outer-shutdown');
+            })))->register();
+        }
+        $handler = new ErrorHandler($reporter, null, new CallableShutdownHandler(
+            static function () use ($scenario): void {
+                if ($scenario === 'exception-shutdown-throws') {
+                    throw new RuntimeException('shutdown failed');
+                }
+                event('shutdown');
+            }
+        ));
+        $handler->register();
+        throw new RuntimeException('uncaught exception');
     case 'error':
         $handler->register();
         error_handler_nonexistent_function();
@@ -166,6 +199,38 @@ static function (Throwable $error) use (&$handler): void {
         );
         (new ErrorHandler($aggregate))->handleThrowable(new RuntimeException('chain'));
         break;
+    case 'render-removable':
+    case 'render-non-removable':
+    case 'render-cleanup-throws':
+        require dirname(__DIR__, 2) . '/vendor/autoload.php';
+        $flags = PHP_OUTPUT_HANDLER_CLEANABLE | PHP_OUTPUT_HANDLER_FLUSHABLE;
+        if ($scenario !== 'render-non-removable') {
+            $flags |= PHP_OUTPUT_HANDLER_REMOVABLE;
+        }
+        $renderer = new CallableThrowableHandler(static function () use ($flags, $scenario): void {
+            $callback = $scenario === 'render-cleanup-throws' ? static function (): never {
+                throw new LogicException('cleanup failed');
+            } : null;
+            ob_start($callback, 0, $flags);
+            throw new RuntimeException('renderer failed');
+        });
+        $factory = new CoiSA\ErrorHandler\Http\Message\ThrowableStreamFactory(
+            new Nyholm\Psr7\Factory\Psr17Factory(),
+            $renderer
+        );
+        $initialLevel = ob_get_level();
+        try {
+            $factory->createStreamFromThrowable(new RuntimeException('original'));
+        } catch (Throwable $failure) {
+            // Bypass the deliberately retained non-removable buffer.
+            fwrite(STDOUT, json_encode([
+                'event' => 'renderer-failure',
+                'class' => get_class($failure),
+                'message' => $failure->getMessage(),
+                'remainingBuffers' => ob_get_level() - $initialLevel,
+            ], JSON_THROW_ON_ERROR) . "\n");
+        }
+        break;
     default:
         throw new InvalidArgumentException('Unknown scenario: ' . $scenario);
 }
diff --git a/tests/StaticAnalysis/output-buffer.stub b/tests/StaticAnalysis/output-buffer.stub
new file mode 100644
index 0000000..b325e7d
--- /dev/null
+++ b/tests/StaticAnalysis/output-buffer.stub
@@ -0,0 +1,9 @@
+
Date: Tue, 15 Sep 2026 21:56:21 -0300
Subject: [PATCH 3/4] feat: integrate FastForward providers and add agent
 guidance
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Felipe Sayão Lobato Abreu 
---
 .../skills/error-handler-integration/SKILL.md |  94 +++
 .github/workflows/ci.yml                      |   5 +-
 AGENTS.md                                     |  71 +++
 CHANGELOG.md                                  |   3 +
 README.md                                     |  10 +
 composer.json                                 |   5 +
 composer.lock                                 | 596 +++++++++++++++++-
 docs/api.md                                   |   5 +
 docs/fast-forward.md                          | 136 ++++
 docs/index.html                               |   9 +-
 docs/migration.md                             |   7 +
 docs/testing.md                               |  36 +-
 examples/fast-forward.php                     |  41 ++
 .../ThrowableHandlerAggregateFactory.php      |  11 +-
 .../ErrorHandlerServiceProvider.php           |  57 ++
 .../Factory/ErrorHandlerMiddlewareFactory.php |  24 +
 .../ErrorHandlerServiceProviderTest.php       | 381 +++++++++++
 .../ThrowableHandlerAggregateFactoryTest.php  |  57 ++
 18 files changed, 1541 insertions(+), 7 deletions(-)
 create mode 100644 .agents/skills/error-handler-integration/SKILL.md
 create mode 100644 AGENTS.md
 create mode 100644 docs/fast-forward.md
 create mode 100644 examples/fast-forward.php
 create mode 100644 src/ServiceProvider/ErrorHandlerServiceProvider.php
 create mode 100644 src/ServiceProvider/Factory/ErrorHandlerMiddlewareFactory.php
 create mode 100644 tests/Functional/ServiceProvider/ErrorHandlerServiceProviderTest.php
 create mode 100644 tests/Unit/Container/ThrowableHandlerAggregateFactoryTest.php

diff --git a/.agents/skills/error-handler-integration/SKILL.md b/.agents/skills/error-handler-integration/SKILL.md
new file mode 100644
index 0000000..0697139
--- /dev/null
+++ b/.agents/skills/error-handler-integration/SKILL.md
@@ -0,0 +1,94 @@
+---
+name: error-handler-integration
+description: Integrate coisa/error-handler into PHP applications using explicit process handlers, FastForward service providers or PSR-15 middleware, and verify reporting and callback restoration.
+license: MIT
+---
+
+# Integrate Error Handler
+
+Use the installed package or selected checkout as the API source. This skill
+describes the PHP 8.3+ development line after 2.0; verify the version before
+using its provider or changed exit-status contract. A future php-fast-forward
+move is not an existing package name or namespace alias.
+
+## Choose the scope
+
+For a CLI or process bootstrap, create one handler with an application-owned
+reporter and keep it registered until process exit when it must see uncaught
+exceptions. For a callable operation whose caller catches failures, unregister
+in `finally`. PHP runs that `finally` before dispatching an uncaught exception.
+
+```php
+use CoiSA\ErrorHandler\ErrorHandler;
+use CoiSA\ErrorHandler\Handler\CallableThrowableHandler;
+
+$errors = new ErrorHandler(new CallableThrowableHandler(
+    static function (Throwable $error): void {
+        error_log($error->getMessage()); // Replace with an appropriate private sink.
+    }
+));
+$errors->register();
+```
+
+Load Composer's autoloader first. Direct `handleThrowable()` calls return;
+the registered uncaught-exception callback exits 1 after successful reporting.
+Recoverable warnings are reported and execution continues. The default error
+conversion honors suppression and `error_reporting()`.
+
+## Wire FastForward definitions
+
+Use `CoiSA\ErrorHandler\ServiceProvider\ErrorHandlerServiceProvider` with
+`FastForward\Container\container()`. It implements the Interop provider contract
+(`getFactories()` and `getExtensions()`), and resolves
+`CoiSA\ErrorHandler\ErrorHandlerInterface`. To override the reporter binding
+`CoiSA\ErrorHandler\Handler\ThrowableHandlerInterface`, merge definitions with
+`FastForward\Container\ServiceProvider\AggregateServiceProvider`: library first,
+application **last**, then pass that one provider into `container(...)`.
+
+Separate variadic providers allow FastForward to fall back to the library's
+silent default if the application's reporter fails with a PSR container error.
+Merged definitions replace that default and make required-reporter construction
+fail. FastForward may translate the PSR error to not-found; do not promise that
+its original exception cause survives. Use the executable example in the
+matching checkout for the complete composition.
+
+The factory maps do not install PHP callbacks: explicitly register the resolved
+process handler at bootstrap. An empty default reporter aggregate is silent;
+configure a reporter or an event dispatcher before expecting diagnostics.
+
+FastForward caches resolved instances, including middleware. To create a fresh
+handler, resolve `CoiSA\ErrorHandler\Container\Factory\ErrorHandlerFactory` and
+invoke it with the container. For fresh middleware, resolve and invoke
+`CoiSA\ErrorHandler\ServiceProvider\Factory\ErrorHandlerMiddlewareFactory`.
+Do not mistake repeated `get()` calls for a transient lifetime.
+
+## Wire HTTP deliberately
+
+Supply PSR-17 response and stream factories. The provider creates middleware
+with a handler distinct from the process service; it restores native callbacks
+after processing and after renderer failure. Create separate middleware for
+nested scopes. This library does not make callbacks local to concurrent fibers
+or requests, and it does not emit HTTP responses.
+
+Reporters and renderers have different output responsibilities. Empty captured
+reporter output becomes `Internal Server Error`; emitted text becomes the public
+body. Keep private diagnostics out of renderer output. Renderers must use
+removable, balanced buffers and must not close caller-owned buffers.
+
+## Verify the integration
+
+Exercise a reported warning, an uncaught throwable in a bounded child process,
+and callback restoration after success and failure. If using middleware, also
+exercise nesting beneath a registered process handler and renderer failure.
+Check delivery counts, response redaction and exit status, not just that a
+service can be resolved. Never run a fatal-error experiment in a user process.
+
+In the library checkout, use `composer --no-plugins check` and
+`php examples/fast-forward.php`. In a consuming application, add focused tests
+against its configured reporters and middleware; the library's passing suite
+does not prove remote logger delivery or application-specific transport behavior.
+
+For full examples, consult the matching checkout's `docs/fast-forward.md`,
+`docs/api.md` and `docs/testing.md`, or the
+[source repository](https://github.com/coisa/error-handler). Keep the observed
+version, integration changes, executed checks and remaining limits explicit.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d66e826..6d9ab23 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -55,8 +55,11 @@ jobs:
           result = subprocess.run(['php', 'examples/cli.php'], capture_output=True, text=True, timeout=5)
           assert result.returncode == 1 and result.stdout == ''
           assert 'Example warning' in result.stderr and 'Example uncaught exception' in result.stderr
+          provider = subprocess.run(['php', 'examples/fast-forward.php'], capture_output=True, text=True, timeout=5)
+          assert provider.returncode == 0 and provider.stderr == ''
+          assert provider.stdout == 'Reported: FastForward provider warning\n'
           PY
       - name: Verify production-only autoload
         run: |
           composer install --no-dev --no-plugins --no-scripts --no-interaction --no-progress
-          php -r 'require "vendor/autoload.php"; new CoiSA\ErrorHandler\Container\ErrorHandlerContainer();'
+          php -r 'require "vendor/autoload.php"; new CoiSA\ErrorHandler\Container\ErrorHandlerContainer(); (new CoiSA\ErrorHandler\ServiceProvider\ErrorHandlerServiceProvider())->getFactories(); if (class_exists(FastForward\Container\AggregateContainer::class)) { throw new RuntimeException("FastForward must remain optional"); }'
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..836e34a
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,71 @@
+# Error Handler maintenance
+
+## Purpose
+
+Maintain a small PHP error-handling library with explicit reporting, process
+lifecycle and PSR-15 response boundaries. The current Composer package is
+`coisa/error-handler`, under the `CoiSA\ErrorHandler` namespace.
+
+## Ownership
+
+The repository owns its core handlers, PSR adapters, interoperability provider,
+tests, documentation and consumer skill. Applications own reporters, PSR-14 and
+PSR-17 implementations, HTTP emission and the moment PHP callbacks are installed.
+
+## Local Contracts
+
+- Write code, public documentation, examples and agent guidance in English.
+- Target PHP 8.3–8.5; the current branch describes an unreleased major after 2.0.
+  Do not present branch behavior as an already published Composer release.
+- Keep core handlers independent of FastForward. The service provider uses
+  `Interop\Container\ServiceProviderInterface`; FastForward is an optional
+  consumer and a development integration test dependency.
+- Constructing or discovering a service must not call `register()`. Native PHP
+  callbacks are process-wide. Unregister in reverse order and use distinct
+  handlers for nested scopes. Follow [the lifecycle contract](docs/api.md).
+- FastForward caches services. Keep the middleware handler separate from the
+  process handler and use the provider's factory services for fresh scopes.
+  Merge application overrides after library definitions in one
+  `AggregateServiceProvider`, avoiding silent reporter fallback on PSR errors.
+- Keep the Laminas ConfigProvider and PSR-11 integration working. A possible
+  move to php-fast-forward does not itself change package identity or namespace.
+- Reporters own diagnostic output; HTTP fallback bodies must not leak exception
+  details. A shutdown callback reports a fatal error and does not recover it.
+
+## Work Guidance
+
+Inspect the current branch and relevant interfaces before changes. Update
+source, meaningful regression tests, examples, affected guides and Unreleased
+changelog together. Keep the skill aligned with tested public APIs, without
+copying the full manual or adding host-specific dependencies.
+
+Run fatal-error and process-termination experiments only through bounded child
+fixtures. Reuse the existing fire/HTTP runners and their temporary data. Never
+use application credentials, production logs or a real web server as fixtures.
+
+Publishing Pages, merging, releasing or transferring the repository requires
+the owner's corresponding instruction. Preparing a PR does not perform them.
+
+## Verification
+
+Use Composer 2 with `--no-plugins --no-scripts` for dependency installation;
+then run `composer --no-plugins check`. This runs formatting, PHPUnit, maximum
+PHPStan, bounded fire tests and loopback HTTP checks. See [testing](docs/testing.md)
+for PHP-version resolution, optional dependencies and actual platform evidence.
+Run `php examples/fast-forward.php` after provider changes. Verify a production
+installation without development dependencies still loads the core and provider.
+Check documentation links and inspect the actual CI result for the tested SHA.
+
+## Child DOX Index
+
+- [README.md](README.md): installation and entrypoints for consumers.
+- [docs/api.md](docs/api.md): core lifecycle, reporters and PSR composition.
+- [docs/fast-forward.md](docs/fast-forward.md): provider definitions, overrides,
+  cached services and fresh middleware scopes.
+- [docs/testing.md](docs/testing.md): failure runners, commands and evidence limits.
+- [docs/migration.md](docs/migration.md): breaking changes from published 2.0.
+- [.agents/skills/error-handler-integration/SKILL.md](.agents/skills/error-handler-integration/SKILL.md):
+  portable consumer integration procedure; no global installation required.
+
+No subtree needs a separate maintenance contract; the skill's procedure stays
+in its SKILL.md entrypoint.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee1723f..008bef5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@
 
 ### Fixed
 
+- Skip an optional callable reporter when an autowiring container advertises it but retrieval reports PSR-11 not-found; continue propagating other construction exceptions.
 - Re-registering an instance no longer adds duplicate shutdown callbacks.
 - Failing or recursive reporters are not invoked repeatedly during exception/shutdown handling.
 - Custom shutdown callbacks still run after a reported uncaught throwable; replacing the active callback continues to suppress the old registration.
@@ -20,6 +21,8 @@
 
 ### Added
 
+- An Interop service provider matching FastForward's callable definitions, explicit process-handler binding and factory services for independent middleware scopes. FastForward remains optional at runtime.
+- Integration tests using the released FastForward container, an executable provider example, English wiring/lifetime documentation, AGENTS.md and a portable consumer integration skill.
 - Bounded subprocess fire tests, HTTP loopback tests, lifecycle and response regression tests.
 - English usage, API, migration and testing documentation, plus a static documentation site with manual default-branch Pages deployment.
 - Weekly Composer and GitHub Actions update proposals through Dependabot.
diff --git a/README.md b/README.md
index d44b8ea..16b1760 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,7 @@ The middleware restores handlers in `finally`, including when response rendering
 ## Documentation
 
 - [API and composition](docs/api.md): lifecycle, factories, handlers, containers and events.
+- [FastForward service provider](docs/fast-forward.md): callable definitions, application overrides and independent process/middleware handlers, with a [runnable example](examples/fast-forward.php).
 - [Testing and limitations](docs/testing.md): reproducible fire tests, exact observed platforms, isolation and failure boundaries.
 - [Migration from 2.0](docs/migration.md): breaking behavior and compatibility changes.
 - [Static documentation site](docs/index.html): install, composition and evidence at a glance. Open the file locally, or preview the `docs/` directory with a loopback static server.
@@ -62,6 +63,15 @@ GitHub Pages is prepared as a **manual** workflow restricted to `master`. After
 
 Dependabot is configured to propose Composer and GitHub Actions updates weekly after this configuration reaches the default branch. Updates remain reviewable pull requests.
 
+## Working with coding agents
+
+[AGENTS.md](AGENTS.md) is the repository maintenance entrypoint. The portable
+[error-handler-integration skill](.agents/skills/error-handler-integration/SKILL.md)
+helps an assistant choose a process or HTTP scope, wire the real provider API
+and validate reporting and callback restoration. It can be read directly or
+used as a skill folder by a compatible assistant; no global installation is
+required. Keep it with the matching package version when copying it elsewhere.
+
 ## Confidence and boundaries
 
 The test suite includes isolated real PHP failures rather than only calls to mocked handlers. Child processes have memory, time and output bounds; tests check termination, delivery counts and restoration. This is evidence for the exercised cases, not a reliability guarantee for every fatal error, PHP SAPI or downstream reporter. See the [failure matrix](docs/testing.md).
diff --git a/composer.json b/composer.json
index 44649dd..116fbfb 100644
--- a/composer.json
+++ b/composer.json
@@ -21,6 +21,7 @@
     },
     "require": {
         "php": "^8.3",
+        "container-interop/service-provider": "^0.4.1",
         "psr/container": "^1.1 || ^2.0",
         "psr/http-server-middleware": "^1.0",
         "psr/http-factory": "^1.1",
@@ -39,6 +40,7 @@
         }
     },
     "require-dev": {
+        "fast-forward/container": "^1.6",
         "friendsofphp/php-cs-fixer": "^3.95",
         "laminas/laminas-servicemanager": "^4.3",
         "nyholm/psr7": "^1.8",
@@ -47,6 +49,9 @@
         "phpunit/phpunit": "^12.5 || ^13.0",
         "squizlabs/php_codesniffer": "^4.0"
     },
+    "suggest": {
+        "fast-forward/container": "Use the service provider with FastForward (tested with ^1.6)."
+    },
     "scripts": {
         "check": [
             "@cs-check",
diff --git a/composer.lock b/composer.lock
index 8da8111..4119042 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,8 +4,43 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "3fb4b5cf36d726d5fac54be222e04d90",
+    "content-hash": "e4a6b75d2104c8f62f570efb15551fc2",
     "packages": [
+        {
+            "name": "container-interop/service-provider",
+            "version": "v0.4.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/container-interop/service-provider.git",
+                "reference": "e04441ca21ef03e10dce70b0af29269281eec6dc"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/container-interop/service-provider/zipball/e04441ca21ef03e10dce70b0af29269281eec6dc",
+                "reference": "e04441ca21ef03e10dce70b0af29269281eec6dc",
+                "shasum": ""
+            },
+            "require": {
+                "psr/container": "^1.0 || ^2.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Interop\\Container\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "Promoting container interoperability through standard service providers",
+            "homepage": "https://github.com/container-interop/service-provider",
+            "support": {
+                "issues": "https://github.com/container-interop/service-provider/issues",
+                "source": "https://github.com/container-interop/service-provider/tree/v0.4.1"
+            },
+            "time": "2023-12-14T14:50:12+00:00"
+        },
         {
             "name": "psr/container",
             "version": "2.0.2",
@@ -714,6 +749,81 @@
             ],
             "time": "2024-05-06T16:37:16+00:00"
         },
+        {
+            "name": "dflydev/dot-access-data",
+            "version": "v3.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/dflydev/dflydev-dot-access-data.git",
+                "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f",
+                "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1 || ^8.0"
+            },
+            "require-dev": {
+                "phpstan/phpstan": "^0.12.42",
+                "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3",
+                "scrutinizer/ocular": "1.6.0",
+                "squizlabs/php_codesniffer": "^3.5",
+                "vimeo/psalm": "^4.0.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "3.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Dflydev\\DotAccessData\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Dragonfly Development Inc.",
+                    "email": "info@dflydev.com",
+                    "homepage": "http://dflydev.com"
+                },
+                {
+                    "name": "Beau Simensen",
+                    "email": "beau@dflydev.com",
+                    "homepage": "http://beausimensen.com"
+                },
+                {
+                    "name": "Carlos Frutos",
+                    "email": "carlos@kiwing.it",
+                    "homepage": "https://github.com/cfrutos"
+                },
+                {
+                    "name": "Colin O'Dell",
+                    "email": "colinodell@gmail.com",
+                    "homepage": "https://www.colinodell.com"
+                }
+            ],
+            "description": "Given a deep data structure, access data by dot notation.",
+            "homepage": "https://github.com/dflydev/dflydev-dot-access-data",
+            "keywords": [
+                "access",
+                "data",
+                "dot",
+                "notation"
+            ],
+            "support": {
+                "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues",
+                "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3"
+            },
+            "time": "2024-07-08T12:26:09+00:00"
+        },
         {
             "name": "ergebnis/agent-detector",
             "version": "1.2.0",
@@ -830,6 +940,123 @@
             },
             "time": "2023-08-08T05:53:35+00:00"
         },
+        {
+            "name": "fast-forward/config",
+            "version": "v1.4.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fast-forward/config.git",
+                "reference": "930434f7f7aaf02d551eb53ab86ed25e6511522c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fast-forward/config/zipball/930434f7f7aaf02d551eb53ab86ed25e6511522c",
+                "reference": "930434f7f7aaf02d551eb53ab86ed25e6511522c",
+                "shasum": ""
+            },
+            "require": {
+                "dflydev/dot-access-data": "^3.0",
+                "laminas/laminas-config-aggregator": "^1.13",
+                "php": "^8.1",
+                "psr/container": "^1.0||^2.0",
+                "psr/simple-cache": "^3.0"
+            },
+            "require-dev": {
+                "coisa/php-cs-fixer": "^2.1",
+                "phpspec/prophecy-phpunit": "^2.3",
+                "phpunit/phpunit": "^9.6 || ^10.5 || ^11.5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/functions.php"
+                ],
+                "psr-4": {
+                    "FastForward\\Config\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Felipe Sayão Lobato Abreu",
+                    "email": "github@mentordosnerds.com"
+                }
+            ],
+            "description": "Fast Forward Config utility classes",
+            "homepage": "https://github.com/php-fast-forward",
+            "support": {
+                "issues": "https://github.com/php-fast-forward/config/issues",
+                "source": "https://github.com/php-fast-forward/config"
+            },
+            "time": "2025-06-11T14:36:02+00:00"
+        },
+        {
+            "name": "fast-forward/container",
+            "version": "v1.6.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fast-forward/container.git",
+                "reference": "d9dd6d964189db80db0490cbe19dcd29841deba4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fast-forward/container/zipball/d9dd6d964189db80db0490cbe19dcd29841deba4",
+                "reference": "d9dd6d964189db80db0490cbe19dcd29841deba4",
+                "shasum": ""
+            },
+            "require": {
+                "container-interop/service-provider": "^0.4.1",
+                "fast-forward/config": "^1.1",
+                "php": "^8.3",
+                "php-di/php-di": "^7.0",
+                "psr/container": "^1.0 || ^2.0"
+            },
+            "require-dev": {
+                "fast-forward/dev-tools": "dev-main"
+            },
+            "type": "library",
+            "extra": {
+                "grumphp": {
+                    "config-default-path": "vendor/fast-forward/dev-tools/grumphp.yml"
+                },
+                "branch-alias": {
+                    "dev-main": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/functions.php"
+                ],
+                "psr-4": {
+                    "FastForward\\Container\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Felipe Sayão Lobato Abreu",
+                    "email": "github@mentordosnerds.com"
+                }
+            ],
+            "description": "Fast Forward PSR-11 Container utility classes",
+            "homepage": "https://github.com/php-fast-forward",
+            "support": {
+                "issues": "https://github.com/php-fast-forward/container/issues",
+                "source": "https://github.com/php-fast-forward/container"
+            },
+            "time": "2026-03-27T15:33:59+00:00"
+        },
         {
             "name": "fidry/cpu-core-counter",
             "version": "1.3.0",
@@ -995,6 +1222,74 @@
             ],
             "time": "2026-09-08T11:11:37+00:00"
         },
+        {
+            "name": "laminas/laminas-config-aggregator",
+            "version": "1.20.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/laminas/laminas-config-aggregator.git",
+                "reference": "75331b17a9ba72c5fa8679988ef08a588ddd86af"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/laminas/laminas-config-aggregator/zipball/75331b17a9ba72c5fa8679988ef08a588ddd86af",
+                "reference": "75331b17a9ba72c5fa8679988ef08a588ddd86af",
+                "shasum": ""
+            },
+            "require": {
+                "brick/varexporter": "^0.5.0 || ^0.4.0 || ^0.6.0 || ^0.7.0",
+                "laminas/laminas-stdlib": "^3.18.0",
+                "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
+                "webimpress/safe-writer": "^2.2.0"
+            },
+            "conflict": {
+                "nikic/php-parser": "<4.12",
+                "zendframework/zend-config-aggregator": "*"
+            },
+            "require-dev": {
+                "laminas/laminas-coding-standard": "~3.1.0",
+                "laminas/laminas-config": "^3.10.1",
+                "phpunit/phpunit": "^11.5.42",
+                "psalm/plugin-phpunit": "^0.19.5",
+                "vimeo/psalm": "^6.13.1"
+            },
+            "suggest": {
+                "laminas/laminas-config": "Allows loading configuration from XML, INI, YAML, and JSON files",
+                "laminas/laminas-config-aggregator-modulemanager": "Allows loading configuration from laminas-mvc Module classes",
+                "laminas/laminas-config-aggregator-parameters": "Allows usage of templated parameters within your configuration"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Laminas\\ConfigAggregator\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "description": "Lightweight library for collecting and merging configuration from different sources",
+            "homepage": "https://laminas.dev",
+            "keywords": [
+                "config-aggregator",
+                "laminas"
+            ],
+            "support": {
+                "chat": "https://laminas.dev/chat",
+                "docs": "https://docs.laminas.dev/laminas-config-aggregator/",
+                "forum": "https://discourse.laminas.dev",
+                "issues": "https://github.com/laminas/laminas-config-aggregator/issues",
+                "rss": "https://github.com/laminas/laminas-config-aggregator/releases.atom",
+                "source": "https://github.com/laminas/laminas-config-aggregator"
+            },
+            "funding": [
+                {
+                    "url": "https://funding.communitybridge.org/projects/laminas-project",
+                    "type": "community_bridge"
+                }
+            ],
+            "time": "2026-06-01T15:37:55+00:00"
+        },
         {
             "name": "laminas/laminas-servicemanager",
             "version": "4.5.1",
@@ -1139,6 +1434,67 @@
             ],
             "time": "2025-10-11T18:13:12+00:00"
         },
+        {
+            "name": "laravel/serializable-closure",
+            "version": "v2.0.16",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/laravel/serializable-closure.git",
+                "reference": "7cfc24e4fa2cca045fb8dd2a797a2b2b13b655ed"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/7cfc24e4fa2cca045fb8dd2a797a2b2b13b655ed",
+                "reference": "7cfc24e4fa2cca045fb8dd2a797a2b2b13b655ed",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^8.1"
+            },
+            "require-dev": {
+                "illuminate/support": "^10.0|^11.0|^12.0|^13.0",
+                "nesbot/carbon": "^2.67|^3.0",
+                "pestphp/pest": "^2.36|^3.0|^4.0",
+                "phpstan/phpstan": "^2.0",
+                "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Laravel\\SerializableClosure\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Taylor Otwell",
+                    "email": "taylor@laravel.com"
+                },
+                {
+                    "name": "Nuno Maduro",
+                    "email": "nuno@laravel.com"
+                }
+            ],
+            "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.",
+            "keywords": [
+                "closure",
+                "laravel",
+                "serializable"
+            ],
+            "support": {
+                "issues": "https://github.com/laravel/serializable-closure/issues",
+                "source": "https://github.com/laravel/serializable-closure"
+            },
+            "time": "2026-08-18T20:28:54+00:00"
+        },
         {
             "name": "myclabs/deep-copy",
             "version": "1.14.0",
@@ -1513,6 +1869,134 @@
             },
             "time": "2026-09-09T16:22:59+00:00"
         },
+        {
+            "name": "php-di/invoker",
+            "version": "2.3.7",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/PHP-DI/Invoker.git",
+                "reference": "3c1ddfdef181431fbc4be83378f6d036d59e81e1"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/3c1ddfdef181431fbc4be83378f6d036d59e81e1",
+                "reference": "3c1ddfdef181431fbc4be83378f6d036d59e81e1",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.3",
+                "psr/container": "^1.0|^2.0"
+            },
+            "require-dev": {
+                "athletic/athletic": "~0.1.8",
+                "mnapoli/hard-mode": "~0.3.0",
+                "phpunit/phpunit": "^9.0 || ^10 || ^11 || ^12"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Invoker\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "Generic and extensible callable invoker",
+            "homepage": "https://github.com/PHP-DI/Invoker",
+            "keywords": [
+                "callable",
+                "dependency",
+                "dependency-injection",
+                "injection",
+                "invoke",
+                "invoker"
+            ],
+            "support": {
+                "issues": "https://github.com/PHP-DI/Invoker/issues",
+                "source": "https://github.com/PHP-DI/Invoker/tree/2.3.7"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/mnapoli",
+                    "type": "github"
+                }
+            ],
+            "time": "2025-08-30T10:22:22+00:00"
+        },
+        {
+            "name": "php-di/php-di",
+            "version": "7.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/PHP-DI/PHP-DI.git",
+                "reference": "f88054cc052e40dbe7b383c8817c19442d480352"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/f88054cc052e40dbe7b383c8817c19442d480352",
+                "reference": "f88054cc052e40dbe7b383c8817c19442d480352",
+                "shasum": ""
+            },
+            "require": {
+                "laravel/serializable-closure": "^1.0 || ^2.0",
+                "php": ">=8.0",
+                "php-di/invoker": "^2.0",
+                "psr/container": "^1.1 || ^2.0"
+            },
+            "provide": {
+                "psr/container-implementation": "^1.0"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "^3",
+                "friendsofphp/proxy-manager-lts": "^1",
+                "mnapoli/phpunit-easymock": "^1.3",
+                "phpunit/phpunit": "^9.6 || ^10 || ^11",
+                "vimeo/psalm": "^5|^6"
+            },
+            "suggest": {
+                "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "src/functions.php"
+                ],
+                "psr-4": {
+                    "DI\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "The dependency injection container for humans",
+            "homepage": "https://php-di.org/",
+            "keywords": [
+                "PSR-11",
+                "container",
+                "container-interop",
+                "dependency injection",
+                "di",
+                "ioc",
+                "psr11"
+            ],
+            "support": {
+                "issues": "https://github.com/PHP-DI/PHP-DI/issues",
+                "source": "https://github.com/PHP-DI/PHP-DI/tree/7.1.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/mnapoli",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/php-di/php-di",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2025-08-16T11:10:48+00:00"
+        },
         {
             "name": "phpstan/phpstan",
             "version": "2.2.14",
@@ -2052,6 +2536,57 @@
             ],
             "time": "2026-09-15T07:37:05+00:00"
         },
+        {
+            "name": "psr/simple-cache",
+            "version": "3.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/simple-cache.git",
+                "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865",
+                "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.0.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\SimpleCache\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interfaces for simple caching",
+            "keywords": [
+                "cache",
+                "caching",
+                "psr",
+                "psr-16",
+                "simple-cache"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/simple-cache/tree/3.0.0"
+            },
+            "time": "2021-10-29T13:26:27+00:00"
+        },
         {
             "name": "react/cache",
             "version": "v1.2.0",
@@ -5383,6 +5918,65 @@
                 }
             ],
             "time": "2025-12-08T11:19:18+00:00"
+        },
+        {
+            "name": "webimpress/safe-writer",
+            "version": "2.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/webimpress/safe-writer.git",
+                "reference": "9d37cc8bee20f7cb2f58f6e23e05097eab5072e6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/webimpress/safe-writer/zipball/9d37cc8bee20f7cb2f58f6e23e05097eab5072e6",
+                "reference": "9d37cc8bee20f7cb2f58f6e23e05097eab5072e6",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.3 || ^8.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.5.4",
+                "vimeo/psalm": "^4.7",
+                "webimpress/coding-standard": "^1.2.2"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.2.x-dev",
+                    "dev-develop": "2.3.x-dev",
+                    "dev-release-1.0": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Webimpress\\SafeWriter\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-2-Clause"
+            ],
+            "description": "Tool to write files safely, to avoid race conditions",
+            "keywords": [
+                "concurrent write",
+                "file writer",
+                "race condition",
+                "safe writer",
+                "webimpress"
+            ],
+            "support": {
+                "issues": "https://github.com/webimpress/safe-writer/issues",
+                "source": "https://github.com/webimpress/safe-writer/tree/2.2.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/michalbundyra",
+                    "type": "github"
+                }
+            ],
+            "time": "2021-04-19T16:34:45+00:00"
         }
     ],
     "aliases": [],
diff --git a/docs/api.md b/docs/api.md
index f34bad9..47d8be7 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -57,6 +57,11 @@ Neither the middleware nor the global handler emits PSR-7 responses to a server.
 
 ## PSR-11 container integration
 
+`ServiceProvider\ErrorHandlerServiceProvider` implements the Interop contract
+used by FastForward, with callable factories and no automatic callback
+registration. See [FastForward integration](fast-forward.md) for process-service
+aliases, application overrides and fresh middleware scopes.
+
 `Container\ConfigProvider` exposes `__invoke()`, `getDependencies()`, `getAliases()` and `getFactories()` for compatible containers such as Laminas ServiceManager. `ErrorHandlerContainer` can wrap a parent PSR-11 container, delegates known services to it, and caches services built by the library factories.
 
 Provide `ThrowableHandlerInterface` for a custom reporter. Otherwise `ThrowableHandlerAggregateFactory` composes any available callable/event-dispatch reporters; an empty aggregate intentionally produces no diagnostics. HTTP factories also require PSR-17 `ResponseFactoryInterface` and `StreamFactoryInterface` services. PSR-14 integration needs `EventDispatcherInterface`. These implementations belong to the application; the package ships interfaces and adapters, not a logger, event bus or HTTP framework.
diff --git a/docs/fast-forward.md b/docs/fast-forward.md
new file mode 100644
index 0000000..835cd70
--- /dev/null
+++ b/docs/fast-forward.md
@@ -0,0 +1,136 @@
+# FastForward service provider
+
+This integration belongs to the unreleased next major of `coisa/error-handler`.
+It follows the provider contract used by FastForward Container 1.6.0 and
+Framework 1.4.0. The existing `CoiSA\ErrorHandler` namespace remains the current
+API; a possible future move to php-fast-forward has not happened.
+
+## Installation and bootstrap
+
+The library requires the small `container-interop/service-provider:^0.4.1`
+interface package. Its core and provider do not require FastForward or a full
+framework. Applications choosing FastForward install its container explicitly:
+
+```sh
+composer require fast-forward/container:^1.6
+```
+
+The development checkout already includes it in `require-dev`. After installing
+the checkout dependencies, run the complete [example](../examples/fast-forward.php):
+
+```sh
+php examples/fast-forward.php
+```
+
+It reports one warning and restores the prior PHP callbacks. Merge the library
+and application definitions into one provider, with the application last:
+
+```php
+use CoiSA\ErrorHandler\ErrorHandlerInterface;
+use CoiSA\ErrorHandler\ServiceProvider\ErrorHandlerServiceProvider;
+use FastForward\Container\ServiceProvider\AggregateServiceProvider;
+use function FastForward\Container\container;
+
+// $application is the ServiceProviderInterface implementation in the example.
+$services = container(new AggregateServiceProvider(new ErrorHandlerServiceProvider(), $application));
+$errors = $services->get(ErrorHandlerInterface::class);
+$errors->register();
+```
+
+For a process-wide handler, keep it registered through termination. For a scoped
+operation, use `try/finally` and unregister; the caller then owns any propagated
+exception. See [lifecycle](api.md#lifecycle) before combining these patterns.
+
+`getFactories()` declares lazy services; `getExtensions()` returns an empty
+array. Neither method, service construction nor `has()` installs PHP callbacks.
+There is no provider boot hook. The application decides when to register.
+
+## Definitions and overrides
+
+| Service | Provider behavior |
+| --- | --- |
+| `ErrorHandlerInterface` / `ErrorHandler` | The same cached process handler; initially unregistered |
+| `ThrowableHandlerInterface` | Default aggregate of available callable and PSR-14 reporters |
+| `DispatchThrowableHandler` / `DispatchErrorEventThrowableHandler` | Existing PSR-14 adapters; require an application dispatcher |
+| `ThrowableStreamFactoryInterface` / `ThrowableResponseFactoryInterface` | Existing rendering adapters; require PSR-17 stream/response factories |
+| `ErrorHandlerMiddleware` | Cached middleware with a handler separate from the process handler |
+| `Container\Factory\ErrorHandlerFactory` | Reusable callable that creates a fresh handler on each invocation |
+| `ServiceProvider\Factory\ErrorHandlerMiddlewareFactory` | Reusable callable that creates fresh middleware and a fresh handler |
+
+Names in the table are relative to `CoiSA\ErrorHandler`; reporter classes are
+under `Handler`, HTTP adapters under `Http\Message`, and middleware under
+`Http\Middleware`.
+
+Bind `ThrowableHandlerInterface` in the application provider for an explicit
+reporter. Alternatively provide `CallableThrowableHandler` or
+`Psr\EventDispatcher\EventDispatcherInterface` to populate the aggregate. The
+PSR-14 path dispatches both the original throwable and an `ErrorEvent` wrapper;
+subscribe intentionally to avoid duplicate application reporting. An aggregate
+with no configured reporters is silent. No logger is silently installed.
+
+Some autowiring containers advertise `CallableThrowableHandler` through `has()`
+even when its required callable cannot be constructed. Optional discovery skips
+only a PSR-11 not-found result from `get()`; other construction exceptions are
+propagated. FastForward 1.6.0 can itself translate a failed concrete dependency
+into not-found, so the library cannot distinguish that from an absent optional
+reporter. Use an explicit `ThrowableHandlerInterface` binding for a required
+reporter and the merged-provider composition above; the next paragraph explains
+why both are needed.
+
+`AggregateServiceProvider` uses the last definition for each service ID, so the
+application's required reporter replaces the default. Passing separate providers
+to `container(...)` instead creates a fallback chain: FastForward may suppress
+the application's PSR container failure and use the library's silent aggregate.
+The merged form prevents that fallback and makes required-reporter failures
+fail construction of both process handler and middleware. FastForward still
+translates PSR failures to not-found; it does not preserve their original cause.
+An ordinary application exception continues to propagate.
+
+Optional `PhpErrorHandlerInterface` and `ShutdownHandlerInterface` bindings are
+passed to fresh handlers too. The provider reuses the package's PSR-11 factories
+and introduces no separate configuration format or automatic event subscriptions.
+The Laminas `ConfigProvider` and `ErrorHandlerContainer` remain available.
+
+## HTTP scopes and fresh instances
+
+Supply `Psr\Http\Message\ResponseFactoryInterface` and
+`Psr\Http\Message\StreamFactoryInterface`, for example using an application's
+Nyholm PSR-7 factory. The normal middleware service can process sequential
+requests and restores an already registered process handler after each one.
+
+FastForward caches `get()` results. For nested scopes, resolve a factory object
+and invoke it for each middleware instance:
+
+```php
+use CoiSA\ErrorHandler\ServiceProvider\Factory\ErrorHandlerMiddlewareFactory;
+
+$makeMiddleware = $services->get(ErrorHandlerMiddlewareFactory::class);
+$outer = $makeMiddleware($services);
+$inner = $makeMiddleware($services);
+```
+
+Likewise invoke the `Container\Factory\ErrorHandlerFactory` service to create
+fresh direct handlers. Avoid sharing the process handler with middleware and
+do not pre-register the middleware's own handler. The reusable factory pattern
+does not provide concurrent-request isolation: PHP callback stacks remain
+process-wide. Reuse instances for sequential work when practical because PHP
+retains registered shutdown callbacks until process exit.
+
+## Verification and migration readiness
+
+The integration tests use the real released FastForward container, including
+provider-class bootstrap, explicit and optional reporters, custom PHP/shutdown
+handlers, event dispatch, fresh factories, nested HTTP scopes and restoration
+after renderer failure. The full [test command](testing.md) retains the existing
+fatal-error and HTTP checks. CI also loads the provider without development
+dependencies, proving that installing the whole FastForward framework is not
+necessary to use the core library.
+
+This separates the public Interop/PSR contracts from the optional framework
+consumer. A later move still needs an explicit package/namespace decision,
+release and migration guidance; this provider is not a claim that those changes
+have already been made.
+
+Sources: [FastForward Container 1.6.0](https://github.com/php-fast-forward/container/tree/v1.6.0),
+[provider contract](https://github.com/container-interop/service-provider/tree/v0.4.1),
+[factory lifetime pattern](https://github.com/php-fast-forward/container/blob/v1.6.0/docs/advanced/special-patterns.rst).
diff --git a/docs/index.html b/docs/index.html
index a23a43b..9baa8f9 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -13,7 +13,7 @@
 
 
{!}CoiSA / Error HandlerView source ↗
- +
A small PHP library. An explicit failure contract.
@@ -50,6 +50,13 @@

Make failure
observable.

$errors->register();

Keep diagnostic output in an appropriate private sink. A reporter that emits nothing produces the generic HTTP body Internal Server Error.

+
+
Container integration

Declare services. Choose when to register.

+

The Interop service provider follows FastForward's callable factory definitions. Resolve the process handler through ErrorHandlerInterface, configure your reporter in an application provider, and explicitly register at bootstrap.

+

FastForward caches services. The middleware receives a separate handler; callable factory services create fresh instances for nested scopes. Core use and the provider remain independent of the full framework.

+
RUN THE COMPLETE PROVIDER EXAMPLE
php examples/fast-forward.php
+ +
03 / Observable behavior

Know what each boundary does.

Input or operationResult
Reportable warningOne error exception; execution continues.
@ or an excluded severityThe default reporter does not receive the warning.
Uncaught throwableReport the original object; exit 1 when reporting returns.
Direct handleThrowable()Report and return; the caller controls execution.
Nested registrationsUnwind in reverse order; only the active handler reports shutdown.
Reporter failure or recursionStop retrying that reporter and propagate the failure.
HTTP error responsePreserve integer codes 400–599; otherwise use 500. No exception text in the reason phrase.
diff --git a/docs/migration.md b/docs/migration.md index 166f184..15945ad 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -2,6 +2,13 @@ This is the migration contract for the unreleased next major. It does not describe an already published Packagist release. +FastForward applications can now adopt the [Interop service provider](fast-forward.md) +without changing the existing package or namespace. Merge definitions with +`AggregateServiceProvider`, library first and application last, so a required +reporter's failure cannot fall back to the silent default. Registration remains explicit; +use factory services for fresh/nested scopes because container lookups are cached. +Existing Laminas and standalone PSR-11 integration remain supported. + 1. Upgrade the application to PHP 8.3, 8.4 or 8.5. Resolve Composer dependencies again. The checked-in development lock targets PHP 8.5; developers on 8.3/8.4 use `composer update --no-plugins --no-scripts` to resolve compatible tools. Consumers resolve their own lock as usual. 2. Expect exit status **1** after a globally uncaught `Throwable` is reported successfully. Version 2 could exit with status 0. A reporter that explicitly terminates the process still controls its own exit code. Direct `handleThrowable()` calls do not exit. 3. Pair `register()` and `unregister()` in reverse order. A repeated `register()` on the same instance is idempotent, not reference counted. Out-of-order unregistration now throws `LogicException` instead of silently removing someone else's callback. Use separate instances for nested middleware and do not pre-register the middleware's own instance. diff --git a/docs/testing.md b/docs/testing.md index ac7bf86..e0fb5c3 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -63,7 +63,7 @@ Independent review exposed two regressions in the first modernization draft: cus Native PHP 8.5.10 verification additionally runs PHPUnit 13.3.4, PHPStan 2.2.14 and three real HTTP requests through PHP's built-in server: 500, 503 and TypeError→500, all with a redacted body. The CI matrix checks lowest/highest compatible dependencies on PHP 8.3, highest on 8.4, and locked/highest on 8.5. A green result applies only to the exact checked commit; consult the [Actions run](https://github.com/coisa/error-handler/actions/workflows/ci.yml) rather than assuming that a configured matrix has passed. -The full `composer --no-plugins check` command passed on native PHP 8.5.10/PHPUnit 13.3.4 and isolated PHP 8.3.33 with both highest dependencies (PHPUnit 12.5.35) and lowest allowed dependencies (PHPUnit 12.5.8, PHPStan 2.1.0, PSR Container 1.1.0): 100 tests and 144 assertions, maximum-level source analysis, formatting, all 27 isolated scenarios and all three HTTP responses. The runnable CLI example was checked for exit status 1, empty stdout and both expected diagnostic messages on stderr. +Before the service-provider addition, the full `composer --no-plugins check` command passed on native PHP 8.5.10/PHPUnit 13.3.4 and isolated PHP 8.3.33 with both highest dependencies (PHPUnit 12.5.35) and lowest allowed dependencies (PHPUnit 12.5.8, PHPStan 2.1.0, PSR Container 1.1.0): 100 tests and 144 assertions, maximum-level source analysis, formatting, all 27 isolated scenarios and all three HTTP responses. The runnable CLI example was checked for exit status 1, empty stdout and both expected diagnostic messages on stderr. ```sh docker run --rm --network none --read-only --memory 128m --cpus 1 --pids-limit 32 \ @@ -82,6 +82,40 @@ Container builds resolve development packages for the requested PHP version. For - PHP 8.4.25: `sha256:2f389f933c3cc58cc622bd243bb4ecff7e6553e2de4387a239bca640c988be19` - PHP 8.5.10: `sha256:aa66ee4275e0472bafcd18e35142499fcbda6ac90e641e751418e052e6a0d812` +## Service-provider verification + +The provider follow-up passes the full check on macOS arm64/PHP 8.5.10 with +FastForward Container 1.6.0: **118 tests and 196 assertions**, maximum PHPStan, +formatting, the same 27 isolated scenarios and three HTTP responses. Eighteen new +cases cover real-container resolution, reporter overrides, optional discovery, +PSR-14 dispatch, custom PHP/shutdown callbacks, independent factories, nested +middleware and restoration after renderer failure. Missing optional reporters +are skipped only for PSR not-found; separate regressions require other +construction exceptions to propagate. + +Independent review reproduced silent reporter fallback when application and +library providers were passed separately to FastForward. Six real-container +regressions now cover required-reporter PSR construction/not-found failures and +ordinary application exceptions for both process handler and fully configured +middleware. Four cases failed with the old composition; all six pass with one +`AggregateServiceProvider`, library first and application last. The skill and +example use this same composition. FastForward may normalize a PSR failure to +not-found, so the contract promises failed construction rather than preservation +of the original exception cause. + +`php examples/fast-forward.php` exits 0, emits exactly +`Reported: FastForward provider warning` followed by a newline, and leaves stderr +empty. CI verifies that output. A separate production-only Composer installation +also loads the core and all 13 provider definitions without FastForward installed; +the required integration dependency is the small Interop interface package. +The five-job PHP/dependency CI matrix exercises the provider tests too; consult +the current run for commit-specific Linux evidence. + +The consumer skill's frontmatter and source references were validated, and its +integration procedure is checked against these same public APIs. Repository +maintenance guidance is in [AGENTS.md](../AGENTS.md); the detailed wiring contract +is in [FastForward integration](fast-forward.md). + ## Boundaries and troubleshooting - PHP must have `proc_open()` enabled for crash/HTTP tests. Unit tests require the normal PHPUnit extensions. The HTTP runner binds only to a dynamically selected `127.0.0.1` port, waits at most five seconds to start, gives each request two seconds, and terminates its fixture server in `finally`. diff --git a/examples/fast-forward.php b/examples/fast-forward.php new file mode 100644 index 0000000..2de5988 --- /dev/null +++ b/examples/fast-forward.php @@ -0,0 +1,41 @@ + static fn (): CallableThrowableHandler => + new CallableThrowableHandler(static function (Throwable $error): void { + printf("Reported: %s\n", $error->getMessage()); + }), + ]; + } + + public function getExtensions(): array + { + return []; + } +}; + +$services = container(new AggregateServiceProvider(new ErrorHandlerServiceProvider(), $application)); +/** @var ErrorHandlerInterface $errors */ +$errors = $services->get(ErrorHandlerInterface::class); +$errors->register(); +try { + trigger_error('FastForward provider warning', E_USER_WARNING); +} finally { + $errors->unregister(); +} diff --git a/src/Container/Factory/ThrowableHandlerAggregateFactory.php b/src/Container/Factory/ThrowableHandlerAggregateFactory.php index 23f2879..28a1067 100644 --- a/src/Container/Factory/ThrowableHandlerAggregateFactory.php +++ b/src/Container/Factory/ThrowableHandlerAggregateFactory.php @@ -21,6 +21,7 @@ use CoiSA\ErrorHandler\Handler\DispatchThrowableHandler; use CoiSA\ErrorHandler\Handler\ThrowableHandlerAggregate; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; use Psr\EventDispatcher\EventDispatcherInterface; /** @@ -50,9 +51,13 @@ public function __invoke(ContainerInterface $container): ThrowableHandlerAggrega $handler = new ThrowableHandlerAggregate(); if ($container->has(CallableThrowableHandler::class)) { - /** @var CallableThrowableHandler $callableHandler */ - $callableHandler = $container->get(CallableThrowableHandler::class); - $handler->attach($callableHandler); + try { + /** @var CallableThrowableHandler $callableHandler */ + $callableHandler = $container->get(CallableThrowableHandler::class); + $handler->attach($callableHandler); + } catch (NotFoundExceptionInterface) { + // Autowiring may advertise a class whose required callable is not configured. + } } if ($container->has(EventDispatcherInterface::class)) { diff --git a/src/ServiceProvider/ErrorHandlerServiceProvider.php b/src/ServiceProvider/ErrorHandlerServiceProvider.php new file mode 100644 index 0000000..1aaf64c --- /dev/null +++ b/src/ServiceProvider/ErrorHandlerServiceProvider.php @@ -0,0 +1,57 @@ + */ + public function getFactories(): array + { + return [ + ErrorHandlerInterface::class => new AliasFactory(ErrorHandler::class), + ErrorHandler::class => new ErrorHandlerFactory(), + ErrorHandlerFactory::class => static fn (): ErrorHandlerFactory => new ErrorHandlerFactory(), + ThrowableHandlerInterface::class => new AliasFactory(ThrowableHandlerAggregate::class), + ThrowableHandlerAggregate::class => new ThrowableHandlerAggregateFactory(), + DispatchThrowableHandler::class => new DispatchThrowableHandlerFactory(), + DispatchErrorEventThrowableHandler::class => new DispatchErrorEventThrowableHandlerFactory(), + ThrowableResponseFactoryInterface::class => new AliasFactory(ThrowableResponseFactory::class), + ThrowableResponseFactory::class => new ThrowableResponseFactoryFactory(), + ThrowableStreamFactoryInterface::class => new AliasFactory(ThrowableStreamFactory::class), + ThrowableStreamFactory::class => new ThrowableStreamFactoryFactory(), + ErrorHandlerMiddleware::class => new ErrorHandlerMiddlewareFactory(), + ErrorHandlerMiddlewareFactory::class => static fn (): ErrorHandlerMiddlewareFactory => + new ErrorHandlerMiddlewareFactory(), + ]; + } + + /** @return array */ + public function getExtensions(): array + { + return []; + } +} diff --git a/src/ServiceProvider/Factory/ErrorHandlerMiddlewareFactory.php b/src/ServiceProvider/Factory/ErrorHandlerMiddlewareFactory.php new file mode 100644 index 0000000..c7ed5d3 --- /dev/null +++ b/src/ServiceProvider/Factory/ErrorHandlerMiddlewareFactory.php @@ -0,0 +1,24 @@ +get(ErrorHandlerFactory::class); + /** @var ThrowableResponseFactoryInterface $responses */ + $responses = $container->get(ThrowableResponseFactoryInterface::class); + + return new ErrorHandlerMiddleware($handlerFactory($container), $responses); + } +} diff --git a/tests/Functional/ServiceProvider/ErrorHandlerServiceProviderTest.php b/tests/Functional/ServiceProvider/ErrorHandlerServiceProviderTest.php new file mode 100644 index 0000000..12ba547 --- /dev/null +++ b/tests/Functional/ServiceProvider/ErrorHandlerServiceProviderTest.php @@ -0,0 +1,381 @@ +has(ErrorHandlerInterface::class)); + $handler = $container->get(ErrorHandlerInterface::class); + self::assertInstanceOf(ErrorHandler::class, $handler); + self::assertSame($handler, $container->get(ErrorHandler::class)); + self::assertSame($handler, $container->get(ErrorHandlerInterface::class)); + self::assertSame($before, self::callbacks()); + $handler->handleThrowable(new \RuntimeException('No reporter is configured.')); + } + + public function testApplicationReporterOverridesTheDefaultAggregate(): void + { + $received = []; + $reporter = new CallableThrowableHandler(static function (\Throwable $error) use (&$received): void { + $received[] = $error; + }); + $container = self::services([ThrowableHandlerInterface::class => $reporter]); + $error = new \RuntimeException('Custom reporter'); + + self::assertSame($reporter, $container->get(ThrowableHandlerInterface::class)); + $container->get(ErrorHandlerInterface::class)->handleThrowable($error); + + self::assertSame([$error], $received); + } + + #[DataProvider('requiredReporterFailures')] + public function testRequiredReporterFailureCannotFallBackToASilentDefault( + string $service, + \Throwable $failure, + string $expectedException + ): void { + $application = new class ($failure) implements ServiceProviderInterface { + public function __construct(private \Throwable $failure) + { + } + + public function getFactories(): array + { + return [ + ThrowableHandlerInterface::class => function (): never { + throw $this->failure; + }, + ResponseFactoryInterface::class => static fn (): Psr17Factory => new Psr17Factory(), + StreamFactoryInterface::class => static fn (): Psr17Factory => new Psr17Factory(), + ]; + } + + public function getExtensions(): array + { + return []; + } + }; + $container = self::withApplication($application); + $this->expectException($expectedException); + + $container->get($service); + } + + public static function requiredReporterFailures(): iterable + { + $failures = [ + 'PSR construction failure' => [ + new class ('Reporter failed') extends \RuntimeException implements ContainerExceptionInterface { + }, + ContainerExceptionInterface::class, + ], + 'PSR missing dependency' => [ + new class ('Missing dependency') extends \RuntimeException implements NotFoundExceptionInterface { + }, + ContainerExceptionInterface::class, + ], + 'application failure' => [new \LogicException('Reporter failed'), \LogicException::class], + ]; + foreach ([ErrorHandlerInterface::class, ErrorHandlerMiddleware::class] as $service) { + foreach ($failures as $label => [$failure, $expectedException]) { + yield $service . ' / ' . $label => [$service, $failure, $expectedException]; + } + } + } + + public function testAnOptionalCallableReporterIsComposedWithoutAnEventDispatcher(): void + { + $received = []; + $container = self::services([ + CallableThrowableHandler::class => new CallableThrowableHandler( + static function (\Throwable $error) use (&$received): void { + $received[] = $error; + } + ), + ]); + $error = new \RuntimeException('Callable reporter'); + + $container->get(ErrorHandlerInterface::class)->handleThrowable($error); + + self::assertSame([$error], $received); + } + + public function testOptionalPsr14DispatcherReceivesTheThrowableAndErrorEvent(): void + { + $dispatcher = new class () implements EventDispatcherInterface { + public array $events = []; + + public function dispatch(object $event): object + { + $this->events[] = $event; + return $event; + } + }; + $container = self::services([EventDispatcherInterface::class => $dispatcher]); + $error = new \RuntimeException('Dispatched'); + + $container->get(ErrorHandlerInterface::class)->handleThrowable($error); + + self::assertCount(2, $dispatcher->events); + self::assertSame($error, $dispatcher->events[0]); + self::assertInstanceOf(ErrorEvent::class, $dispatcher->events[1]); + self::assertSame($error, $dispatcher->events[1]->getThrowable()); + } + + #[WithoutErrorHandler] + public function testCustomPhpAndShutdownHandlersAreInjected(): void + { + $phpHandler = new class () implements PhpErrorHandlerInterface { + public array $messages = []; + + public function handlePhpError(int $errno, string $message, string $file, int $line): void + { + $this->messages[] = $message; + } + }; + $shutdowns = 0; + $container = self::services([ + PhpErrorHandlerInterface::class => $phpHandler, + ShutdownHandlerInterface::class => new CallableShutdownHandler( + static function () use (&$shutdowns): void { + ++$shutdowns; + } + ), + ]); + $handler = $container->get(ErrorHandler::class); + $before = self::callbacks(); + $handler->register(); + try { + trigger_error('Custom PHP handler', E_USER_WARNING); + $handler->handleShutdown(); + } finally { + $handler->unregister(); + } + + self::assertSame(['Custom PHP handler'], $phpHandler->messages); + self::assertSame(1, $shutdowns); + self::assertSame($before, self::callbacks()); + } + + public function testReusableFactoryCreatesFreshHandlersWithoutReplacingTheProcessService(): void + { + $container = self::services(); + $process = $container->get(ErrorHandlerInterface::class); + $factory = $container->get(ErrorHandlerFactory::class); + $first = $factory($container); + $second = $factory($container); + + self::assertInstanceOf(ErrorHandler::class, $first); + self::assertNotSame($first, $second); + self::assertNotSame($first, $process); + self::assertSame($process, $container->get(ErrorHandlerInterface::class)); + } + + #[WithoutErrorHandler] + public function testMiddlewareRestoresTheAlreadyRegisteredProcessHandler(): void + { + $received = []; + $container = self::httpServices($received); + $process = $container->get(ErrorHandlerInterface::class); + $process->register(); + try { + $before = self::callbacks(); + $middleware = $container->get(ErrorHandlerMiddleware::class); + self::assertSame($before, self::callbacks()); + $failure = new \RuntimeException('Private diagnostics', 503); + $response = $middleware->process(self::request(), self::failsWith($failure)); + + self::assertSame(503, $response->getStatusCode()); + self::assertSame('Internal Server Error', (string) $response->getBody()); + self::assertSame($before, self::callbacks()); + trigger_error('After middleware', E_USER_WARNING); + } finally { + $process->unregister(); + } + + self::assertCount(2, $received); + self::assertSame($failure, $received[0]); + self::assertInstanceOf(ErrorException::class, $received[1]); + self::assertSame('After middleware', $received[1]->getMessage()); + } + + #[WithoutErrorHandler] + public function testFactoryCreatesIndependentMiddlewareForNestedScopes(): void + { + $received = []; + $container = self::httpServices($received); + $factory = $container->get(ErrorHandlerMiddlewareFactory::class); + $outer = $factory($container); + $inner = $factory($container); + $failure = new \RuntimeException('Nested'); + $downstream = new class ($inner, self::failsWith($failure)) implements RequestHandlerInterface { + public function __construct( + private ErrorHandlerMiddleware $inner, + private RequestHandlerInterface $downstream + ) { + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + return $this->inner->process($request, $this->downstream); + } + }; + $process = $container->get(ErrorHandlerInterface::class); + $process->register(); + try { + $before = self::callbacks(); + $response = $outer->process(self::request(), $downstream); + self::assertSame(500, $response->getStatusCode()); + self::assertSame($before, self::callbacks()); + } finally { + $process->unregister(); + } + + self::assertNotSame($outer, $inner); + self::assertSame([$failure], $received); + } + + #[WithoutErrorHandler] + public function testRendererFailureRestoresTheProcessHandler(): void + { + $failure = new \RuntimeException('Renderer failed'); + $renderer = new class ($failure) implements ThrowableResponseFactoryInterface { + public function __construct(private \Throwable $failure) + { + } + + public function createResponseFromThrowable(\Throwable $throwable): ResponseInterface + { + throw $this->failure; + } + }; + $container = self::services([ThrowableResponseFactoryInterface::class => $renderer]); + $process = $container->get(ErrorHandlerInterface::class); + $process->register(); + try { + $before = self::callbacks(); + $middleware = $container->get(ErrorHandlerMiddleware::class); + try { + $middleware->process(self::request(), self::failsWith(new \RuntimeException('Original'))); + self::fail('The renderer failure must propagate.'); + } catch (\RuntimeException $caught) { + self::assertSame($failure, $caught); + } + self::assertSame($before, self::callbacks()); + } finally { + $process->unregister(); + } + } + + /** @param array $services */ + private static function services(array $services = []): ContainerInterface + { + $application = new class ($services) implements ServiceProviderInterface { + public function __construct(private array $services) + { + } + + public function getFactories(): array + { + return array_map(static fn (object $service): \Closure => + static fn (): object => $service, $this->services); + } + + public function getExtensions(): array + { + return []; + } + }; + + return self::withApplication($application); + } + + private static function withApplication(ServiceProviderInterface $application): ContainerInterface + { + return container(new AggregateServiceProvider(new ErrorHandlerServiceProvider(), $application)); + } + + private static function httpServices(array &$received): ContainerInterface + { + $psr17 = new Psr17Factory(); + + return self::services([ + ThrowableHandlerInterface::class => new CallableThrowableHandler( + static function (\Throwable $error) use (&$received): void { + $received[] = $error; + } + ), + ResponseFactoryInterface::class => $psr17, + StreamFactoryInterface::class => $psr17, + ]); + } + + private static function request(): ServerRequestInterface + { + return (new Psr17Factory())->createServerRequest('GET', '/provider-test'); + } + + private static function failsWith(\Throwable $failure): RequestHandlerInterface + { + return new class ($failure) implements RequestHandlerInterface { + public function __construct(private \Throwable $failure) + { + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + throw $this->failure; + } + }; + } + + private static function callbacks(): array + { + $error = set_error_handler(static fn (): bool => false); + restore_error_handler(); + $exception = set_exception_handler(static function (\Throwable $error): void { + }); + restore_exception_handler(); + + return [$error, $exception]; + } +} diff --git a/tests/Unit/Container/ThrowableHandlerAggregateFactoryTest.php b/tests/Unit/Container/ThrowableHandlerAggregateFactoryTest.php new file mode 100644 index 0000000..9e84712 --- /dev/null +++ b/tests/Unit/Container/ThrowableHandlerAggregateFactoryTest.php @@ -0,0 +1,57 @@ +containerFailingWith($missing); + $aggregate = (new ThrowableHandlerAggregateFactory())($container); + + $this->expectOutputString(''); + $aggregate->handleThrowable(new \RuntimeException('No reporter')); + } + + #[DataProvider('constructionFailures')] + public function testConstructionErrorsAreNotHidden(\Throwable $failure): void + { + $container = $this->containerFailingWith($failure); + $this->expectExceptionObject($failure); + + (new ThrowableHandlerAggregateFactory())($container); + } + + public static function constructionFailures(): iterable + { + yield 'PSR construction failure' => [ + new class ('Invalid reporter') extends \RuntimeException implements ContainerExceptionInterface { + }, + ]; + yield 'application failure' => [new \LogicException('Reporter initialization failed')]; + } + + private function containerFailingWith(\Throwable $failure): ContainerInterface + { + $container = $this->createMock(ContainerInterface::class); + $container->method('has')->willReturnCallback( + static fn (string $id): bool => $id === CallableThrowableHandler::class + ); + $container->expects(self::once())->method('get') + ->with(CallableThrowableHandler::class)->willThrowException($failure); + + return $container; + } +} From 75137ca20aa825c6816b4c0b686a4a46fbd04003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Tue, 15 Sep 2026 22:18:56 -0300 Subject: [PATCH 4/4] docs: identify the Error Handler skill by its package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- .../SKILL.md | 11 ++++++++--- AGENTS.md | 4 +++- CHANGELOG.md | 2 +- README.md | 2 +- docs/index.html | 2 +- 5 files changed, 14 insertions(+), 7 deletions(-) rename .agents/skills/{error-handler-integration => coisa-error-handler}/SKILL.md (89%) diff --git a/.agents/skills/error-handler-integration/SKILL.md b/.agents/skills/coisa-error-handler/SKILL.md similarity index 89% rename from .agents/skills/error-handler-integration/SKILL.md rename to .agents/skills/coisa-error-handler/SKILL.md index 0697139..d0cbe9a 100644 --- a/.agents/skills/error-handler-integration/SKILL.md +++ b/.agents/skills/coisa-error-handler/SKILL.md @@ -1,10 +1,15 @@ --- -name: error-handler-integration -description: Integrate coisa/error-handler into PHP applications using explicit process handlers, FastForward service providers or PSR-15 middleware, and verify reporting and callback restoration. +name: coisa-error-handler +description: Integrate CoiSA Error Handler (coisa/error-handler) into PHP applications using explicit process handlers, FastForward service providers or PSR-15 middleware, and verify reporting and callback restoration. license: MIT --- -# Integrate Error Handler +# CoiSA Error Handler integration + +Package: `coisa/error-handler`. Source and maintenance: +[coisa/error-handler on GitHub](https://github.com/coisa/error-handler). +This skill belongs to that library and retains its package identity when copied +into a consuming project. Use the installed package or selected checkout as the API source. This skill describes the PHP 8.3+ development line after 2.0; verify the version before diff --git a/AGENTS.md b/AGENTS.md index 836e34a..a597c51 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,8 @@ PSR-17 implementations, HTTP emission and the moment PHP callbacks are installed ## Local Contracts - Write code, public documentation, examples and agent guidance in English. +- Identify the public consumer skill as `coisa-error-handler`, matching its + Composer package, and retain the source repository link for copied installs. - Target PHP 8.3–8.5; the current branch describes an unreleased major after 2.0. Do not present branch behavior as an already published Composer release. - Keep core handlers independent of FastForward. The service provider uses @@ -64,7 +66,7 @@ Check documentation links and inspect the actual CI result for the tested SHA. cached services and fresh middleware scopes. - [docs/testing.md](docs/testing.md): failure runners, commands and evidence limits. - [docs/migration.md](docs/migration.md): breaking changes from published 2.0. -- [.agents/skills/error-handler-integration/SKILL.md](.agents/skills/error-handler-integration/SKILL.md): +- [.agents/skills/coisa-error-handler/SKILL.md](.agents/skills/coisa-error-handler/SKILL.md): portable consumer integration procedure; no global installation required. No subtree needs a separate maintenance contract; the skill's procedure stays diff --git a/CHANGELOG.md b/CHANGELOG.md index 008bef5..b6f7ead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ ### Added - An Interop service provider matching FastForward's callable definitions, explicit process-handler binding and factory services for independent middleware scopes. FastForward remains optional at runtime. -- Integration tests using the released FastForward container, an executable provider example, English wiring/lifetime documentation, AGENTS.md and a portable consumer integration skill. +- Integration tests using the released FastForward container, an executable provider example, English wiring/lifetime documentation, AGENTS.md and the portable `coisa-error-handler` consumer skill with explicit package provenance. - Bounded subprocess fire tests, HTTP loopback tests, lifecycle and response regression tests. - English usage, API, migration and testing documentation, plus a static documentation site with manual default-branch Pages deployment. - Weekly Composer and GitHub Actions update proposals through Dependabot. diff --git a/README.md b/README.md index 16b1760..8cd5f26 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Dependabot is configured to propose Composer and GitHub Actions updates weekly a ## Working with coding agents [AGENTS.md](AGENTS.md) is the repository maintenance entrypoint. The portable -[error-handler-integration skill](.agents/skills/error-handler-integration/SKILL.md) +[coisa-error-handler skill](.agents/skills/coisa-error-handler/SKILL.md) helps an assistant choose a process or HTTP scope, wire the real provider API and validate reporting and callback restoration. It can be read directly or used as a skill folder by a compatible assistant; no global installation is diff --git a/docs/index.html b/docs/index.html index 9baa8f9..79eaace 100644 --- a/docs/index.html +++ b/docs/index.html @@ -55,7 +55,7 @@

Make failure
observable.

The Interop service provider follows FastForward's callable factory definitions. Resolve the process handler through ErrorHandlerInterface, configure your reporter in an application provider, and explicitly register at bootstrap.

FastForward caches services. The middleware receives a separate handler; callable factory services create fresh instances for nested scopes. Core use and the provider remain independent of the full framework.

RUN THE COMPLETE PROVIDER EXAMPLE
php examples/fast-forward.php
- +
03 / Observable behavior

Know what each boundary does.