Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 28, 2025

Pull Request

Description

What does this PR do?

Adds a comprehensive event-driven system with hooks for request/response lifecycle events, enabling observability, metrics collection, debugging, and custom logic injection.

Fixes #(issue_number)

// Global event listeners
fetch_client()
    ->onRequest(fn($e) => $logger->info('Request: '.$e->getRequest()->getUri()))
    ->onResponse(fn($e) => $metrics->record('http.duration', $e->getDuration()))
    ->onError(fn($e) => $alerting->notify($e->getException()->getMessage()))
    ->get('/api/users');

// Per-request hooks with shorthand names
fetch()->hooks([
    'before_send' => fn($e) => $e->getRequest()->withHeader('X-Trace', $traceId),
    'on_retry' => fn($e) => $logger->warning("Retry {$e->getAttempt()}"),
]);

// Priority-based listeners
fetch_client()->when('response.received', fn($e) => /* ... */, priority: 10);

Type of Change

  • ✨ New feature (non-breaking change which adds functionality)

Changes Made

  • Event Classes (src/Fetch/Events/): FetchEvent (base), RequestEvent, ResponseEvent, ErrorEvent, RetryEvent, TimeoutEvent, RedirectEvent
  • EventDispatcher: Priority-based dispatch, listener caching, error isolation (logs but doesn't stop propagation)
  • SupportsHooks trait: Fluent API (onRequest(), onResponse(), onError(), onRetry(), onTimeout(), onRedirect(), when(), hooks())
  • Lifecycle integration: Events dispatched in PerformsHttpRequests and ManagesRetries
  • Interface updates: ClientHandler interface extended with hook methods
  • No new dependencies

Testing

How has this been tested?

  • Unit tests added/updated (45 new tests, 112 assertions)
  • All existing tests pass (370 total)

Test Environment:

  • PHP Version: 8.3
  • Operating System: Ubuntu

Documentation

  • Code is self-documenting with clear variable names and logic
  • PHPDoc comments added for public methods

Checklist

  • My code follows the project's coding standards
  • I have performed a self-review of my code
  • My changes generate no new warnings or errors
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Additional Notes

Event names follow category.action pattern: request.sending, response.received, error.occurred, request.retrying, request.timeout, request.redirecting.

Hook shorthand mappings: before_sendrequest.sending, after_responseresponse.received, on_errorerror.occurred, etc.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/ad732c2e9299c9743f9c55ae53cc0e7642ab1155
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/Thavarshan/matrix/zipball/17077b50c6ab2689847561752fae9940e49c10e8
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/F0ZQOl /usr/bin/composer install --prefer-source --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/reactphp/child-process/zipball/1721e2b93d89b745664353b9cfc8f155ba8a6159
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c346885c95423eda3f65d85a194aaa24873cda82
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/symfony/console/zipball/0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/symfony/event-dispatcher/zipball/9dddcddff1ef974ad87b3708e4b442dc38b2261d
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/symfony/finder/zipball/340b9ed7320570f319028a2cbec46d40535e94bd
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/symfony/options-resolver/zipball/b38026df55197f9e39a44f3215788edf83187b80
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/symfony/process/zipball/7ca8dc2d0dcf4882658313aba8be5d9fd01026c8
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/pYhtcH /usr/bin/composer install (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/stopwatch/zipball/8a24af0a2e8a872fb745047180649b8418303084
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/symfony/string/zipball/d50e862cb0a0e0886f73ca1f31b865efbb795003
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/symfony/var-dumper/zipball/41fd6c4ae28c38b294b42af6db61446594a0dece
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)
  • https://api.github.com/repos/tighten/duster/zipball/0260abaaecabd9655a0836e4038238e6585a8b45
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/SyEDBv /usr/bin/composer install --no-interaction --prefer-dist (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/vT5ZWk /usr/bin/composer install --no-interaction (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/YLs2p1 /usr/bin/composer remove --dev phpstan/phpstan --no-interaction (http block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

This section details on the original issue you should resolve

<issue_title>Request/Response Hooks & Events System</issue_title>
<issue_description>## Summary
Add a comprehensive event-driven system with hooks for request/response lifecycle events, enabling fine-grained observability, monitoring, and custom logic injection.

Motivation

Modern HTTP clients need extensible event systems for:

  • Observability: Detailed request/response lifecycle monitoring
  • Metrics Collection: Performance monitoring and analytics
  • Debugging: Request/response inspection and logging
  • Business Logic: Custom workflows triggered by HTTP events
  • Error Handling: Centralized error processing and alerting
  • Compliance: Audit trails and security monitoring
  • Performance Optimization: Request/response analysis and optimization

Proposed API

// Global event listeners
fetch_client()
    ->onRequest(function (RequestEvent $event) {
        $this->logger->info('Request started', [
            'method' => $event->getRequest()->getMethod(),
            'uri' => (string) $event->getRequest()->getUri(),
            'correlation_id' => $event->getCorrelationId(),
        ]);
    })
    ->onResponse(function (ResponseEvent $event) {
        $this->metrics->record('http.duration', $event->getDuration(), [
            'status' => $event->getResponse()->getStatusCode(),
            'method' => $event->getRequest()->getMethod(),
        ]);
    })
    ->onError(function (ErrorEvent $event) {
        $this->alerting->notify('HTTP Request Failed', [
            'error' => $event->getException()->getMessage(),
            'uri' => (string) $event->getRequest()->getUri(),
            'attempt' => $event->getAttempt(),
        ]);
    })
    ->get('/api/users');

// Per-request hooks
fetch('/api/data')
    ->hooks([
        'before_send' => fn($request) => $request->withHeader('X-Trace-ID', $this->generateTraceId()),
        'after_response' => fn($response) => $this->validateApiContract($response),
        'on_retry' => fn($attempt, $error) => $this->backoffStrategy->delay($attempt),
        'on_timeout' => fn($request) => $this->fallbackService->getData(),
    ]);

// Event-driven patterns
fetch_client()
    ->when('request.sending', function ($event) {
        // Add security headers
        $event->getRequest()->withHeader('X-API-Version', '2.1');
    })
    ->when('response.received', function ($event) {
        // Cache successful responses
        if ($event->getResponse()->successful()) {
            $this->cache->store($event);
        }
    })
    ->when('error.occurred', function ($event) {
        // Circuit breaker logic
        $this->circuitBreaker->recordFailure($event->getRequest()->getUri());
    });

Implementation Details

Event Classes

abstract class FetchEvent
{
    public function __construct(
        protected RequestInterface $request,
        protected string $correlationId,
        protected float $timestamp,
        protected array $context = []
    ) {}
    
    public function getRequest(): RequestInterface { return $this->request; }
    public function getCorrelationId(): string { return $this->correlationId; }
    public function getTimestamp(): float { return $this->timestamp; }
    public function getContext(): array { return $this->context; }
    
    abstract public function getName(): string;
}

class RequestEvent extends FetchEvent
{
    public function __construct(
        RequestInterface $request,
        string $correlationId,
        float $timestamp,
        array $context = [],
        protected array $options = []
    ) {
        parent::__construct($request, $correlationId, $timestamp, $context);
    }
    
    public function getName(): string { return 'request.sending'; }
    public function getOptions(): array { return $this->options; }
}

class ResponseEvent extends FetchEvent
{
    public function __construct(
        RequestInterface $request,
        protected ResponseInterface $response,
        string $correlationId,
        float $timestamp,
        protected float $duration,
        array $context = []
    ) {
        parent::__construct($request, $correlationId, $timestamp, $context);
    }
    
    public function getName(): string { return 'response.received'; }
    public function getResponse(): ResponseInterface { return $this->response; }
    public function getDuration(): float { return $this->duration; }
    public function getLatency(): int { return (int) ($this->duration * 1000); }
}

class ErrorEvent extends FetchEvent
{
    public function __construct(
        RequestInterface $request,
        protected \Throwable $exception,
        string $correlationId,
        float $timestamp,
        protected int $attempt = 1,
        protected ?ResponseInterface $response = null,
        array $context = []
    ) {
        parent::__construct($request, $correlationId, $timestamp, $context);
    }
    
    public function ...

</details>

- Fixes Thavarshan/fetch-php#41

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

@netlify
Copy link

netlify bot commented Nov 28, 2025

Deploy Preview for fetch-php canceled.

Name Link
🔨 Latest commit aa7fe26
🔍 Latest deploy log https://app.netlify.com/projects/fetch-php/deploys/6929de8006dbc10008828313

@github-actions
Copy link
Contributor

github-actions bot commented Nov 28, 2025

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

Copilot AI and others added 3 commits November 28, 2025 17:36
- Create FetchEvent base class and event subclasses (RequestEvent, ResponseEvent, ErrorEvent, RetryEvent, TimeoutEvent, RedirectEvent)
- Create EventDispatcher and EventDispatcherInterface
- Create SupportsHooks trait for hook registration methods
- Integrate event dispatching into ClientHandler request lifecycle
- Add comprehensive unit tests for the event system

Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
…tant

Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
Copilot AI changed the title [WIP] Add request/response hooks and events system Add Request/Response Hooks & Events System Nov 28, 2025
Copilot AI requested a review from Thavarshan November 28, 2025 17:43
@Thavarshan Thavarshan requested review from Copilot and removed request for Thavarshan November 28, 2025 17:46
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a comprehensive event-driven system for HTTP request/response lifecycle management, enabling observability, metrics collection, and custom logic injection through hooks and events.

Key changes:

  • Introduces 7 event classes (RequestEvent, ResponseEvent, ErrorEvent, RetryEvent, TimeoutEvent, RedirectEvent, plus base FetchEvent)
  • Implements EventDispatcher with priority-based listener management and error isolation
  • Adds SupportsHooks trait providing fluent API methods (onRequest(), onResponse(), etc.) with shorthand hook name support
  • Integrates event dispatching into existing request/response and retry flows

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
src/Fetch/Events/FetchEvent.php Base abstract event class with common properties (request, correlationId, timestamp, context)
src/Fetch/Events/RequestEvent.php Event dispatched before sending HTTP requests with options
src/Fetch/Events/ResponseEvent.php Event dispatched after receiving responses with duration and latency
src/Fetch/Events/ErrorEvent.php Event for error handling with retryability detection
src/Fetch/Events/RetryEvent.php Event dispatched when retrying requests with attempt tracking
src/Fetch/Events/TimeoutEvent.php Event for request timeouts with elapsed time tracking
src/Fetch/Events/RedirectEvent.php Event for HTTP redirects with location and count tracking
src/Fetch/Events/EventDispatcherInterface.php Interface defining event dispatcher contract
src/Fetch/Events/EventDispatcher.php Priority-based dispatcher implementation with error isolation and listener caching
src/Fetch/Concerns/SupportsHooks.php Trait providing fluent hook registration API with shorthand mappings
src/Fetch/Interfaces/ClientHandler.php Extended interface with hook-related method signatures
src/Fetch/Http/ClientHandler.php Added SupportsHooks trait usage
src/Fetch/Concerns/PerformsHttpRequests.php Integrated RequestEvent and ResponseEvent dispatching into request flow
src/Fetch/Concerns/ManagesRetries.php Added retryRequestWithEvents method with ErrorEvent and RetryEvent dispatching
tests/Unit/Events/FetchEventsTest.php Comprehensive tests for all event classes (208 lines)
tests/Unit/Events/EventDispatcherTest.php Tests for dispatcher functionality including priority ordering and error handling
tests/Unit/SupportsHooksTest.php Tests for hooks trait including fluent API and shorthand name normalization
tests/Unit/ManagesDebugAndProfilingTest.php Removed unused import

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +188 to +190
$correlationId = method_exists($handler, 'generateCorrelationId')
? $handler->generateCorrelationId()
: bin2hex(random_bytes(16));
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The correlation ID generation on lines 188-190 uses method_exists($handler, 'generateCorrelationId') check before calling, but generateCorrelationId() is defined in the SupportsHooks trait which is used by ClientHandler. This dynamic check is unnecessary since the trait is already in use and will always be available. Consider removing the method_exists check and directly calling $handler->generateCorrelationId(), or if this code needs to work with handlers that don't use the trait, document why the check is necessary.

Suggested change
$correlationId = method_exists($handler, 'generateCorrelationId')
? $handler->generateCorrelationId()
: bin2hex(random_bytes(16));
$correlationId = $handler->generateCorrelationId();

Copilot uses AI. Check for mistakes.
Comment on lines +199 to +207
$psrRequest = new GuzzleRequest($methodStr, $fullUri, $guzzleOptions['headers'] ?? []);
$handler->dispatchEvent(new RequestEvent(
$psrRequest,
$correlationId,
$startTime,
[],
$guzzleOptions
));
}
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The PSR-7 Request object is created twice in the same method:

  1. Line 199 for dispatching the RequestEvent
  2. Line 381 (in the original code) and line 426 (in the retry logic) for retry handling

Creating the PSR request once at line 381 and reusing it at line 199 would be more efficient and ensure both the event and the retry logic use the exact same request object. Currently, the request object used in events might have different headers than the one used in retry logic if headers are modified between these points.

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +33
protected static array $hookNameMappings = [
'before_send' => 'request.sending',
'after_response' => 'response.received',
'on_error' => 'error.occurred',
'on_retry' => 'request.retrying',
'on_timeout' => 'request.timeout',
'on_redirect' => 'request.redirecting',
];
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The $hookNameMappings array is declared as static (line 26), which means it's shared across all instances using this trait. While this is likely intentional for memory efficiency since the mappings are constant, consider documenting this design decision or making it a const array instead if PHP version allows (PHP 8.2+). If it needs to remain mutable for extensibility, document that subclasses can modify the static mappings, but be aware this would affect all instances globally.

Copilot uses AI. Check for mistakes.
*/
public function isLastAttempt(): bool
{
return $this->attempt >= $this->maxAttempts;
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isLastAttempt() method uses >= comparison (line 85), which may produce incorrect results depending on how $attempt and $maxAttempts are counted. According to the PHPDoc (line 20-21), $attempt is "1-based" and $maxAttempts is "the maximum number of attempts allowed". If:

  • $maxAttempts = 3 (meaning 3 total attempts allowed)
  • $attempt = 3 (the 3rd attempt)
    Then isLastAttempt() returns true

However, if the retry event is only dispatched BEFORE retrying (not on the last attempt), then $attempt would never equal $maxAttempts because the last attempt isn't a "retry". The naming and logic need clarification:

  • Is $maxAttempts the number of retries or total attempts?
  • Is a retry event dispatched before the final attempt or only for actual retries?

This needs to be consistent with the usage in ManagesRetries::retryRequestWithEvents where it's called with $attempt + 1 and $attempts on line 346-350.

Suggested change
return $this->attempt >= $this->maxAttempts;
return $this->attempt === $this->maxAttempts;

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +29
* @param ResponseInterface $response The HTTP response received
* @param string $correlationId Unique identifier for correlating related events
* @param float $timestamp Unix timestamp with microseconds when the event occurred
* @param float $duration Time in seconds the request took to complete
* @param array<string, mixed> $context Additional contextual data
*/
public function __construct(
RequestInterface $request,
protected ResponseInterface $response,
string $correlationId,
float $timestamp,
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The parameter order in the ResponseEvent constructor is inconsistent with other event classes. The $response parameter comes before $correlationId, while in ErrorEvent and RedirectEvent, the response-like parameters come after the basic parameters but before correlation ID. Consider reordering parameters to follow the pattern: $request, $correlationId, $timestamp, then additional specific parameters like $response and $duration, for better consistency across event classes.

Suggested change
* @param ResponseInterface $response The HTTP response received
* @param string $correlationId Unique identifier for correlating related events
* @param float $timestamp Unix timestamp with microseconds when the event occurred
* @param float $duration Time in seconds the request took to complete
* @param array<string, mixed> $context Additional contextual data
*/
public function __construct(
RequestInterface $request,
protected ResponseInterface $response,
string $correlationId,
float $timestamp,
* @param string $correlationId Unique identifier for correlating related events
* @param float $timestamp Unix timestamp with microseconds when the event occurred
* @param ResponseInterface $response The HTTP response received
* @param float $duration Time in seconds the request took to complete
* @param array<string, mixed> $context Additional contextual data
*/
public function __construct(
RequestInterface $request,
string $correlationId,
float $timestamp,
protected ResponseInterface $response,

Copilot uses AI. Check for mistakes.
/**
* Register multiple hooks at once.
*
* @param array<string, callable> $hooks Array of hook name => callback pairs
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PHPDoc for the hooks method states "Array of hook name => callback pairs" but doesn't document that shorthand hook names (like before_send, after_response) are supported alongside full event names (like request.sending, response.received). Consider documenting the supported shorthand names in the PHPDoc or referencing the $hookNameMappings in SupportsHooks trait.

Suggested change
* @param array<string, callable> $hooks Array of hook name => callback pairs
* @param array<string, callable> $hooks Array of hook name => callback pairs. Hook names may be either full event names (e.g., "request.sending", "response.received") or supported shorthand names (e.g., "before_send", "after_response"). See the implementation or the SupportsHooks trait for the list of supported shorthand names.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +90
public function test_multiple_listeners_are_called()
{
$order = [];

$this->dispatcher->addListener('response.received', function () use (&$order) {
$order[] = 'first';
});

$this->dispatcher->addListener('response.received', function () use (&$order) {
$order[] = 'second';
});

$request = new Request('GET', 'https://example.com/api');
$event = new RequestEvent($request, 'corr-123', microtime(true));
// Set the event name by using a real RequestEvent (which has 'request.sending')
// We need to test with an event that matches the listener
$this->dispatcher->dispatch($event);

// Nothing was called because we registered for 'response.received' but dispatched 'request.sending'
$this->assertEmpty($order);

// Now let's test properly
$this->dispatcher->clearListeners();

$this->dispatcher->addListener('request.sending', function () use (&$order) {
$order[] = 'first';
});

$this->dispatcher->addListener('request.sending', function () use (&$order) {
$order[] = 'second';
});

$this->dispatcher->dispatch($event);

$this->assertEquals(['first', 'second'], $order);
}
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The test test_multiple_listeners_are_called has an unusual structure with a deliberate mismatch test (lines 59-74) followed by clearing and retesting (lines 77-89). While this approach works, it's confusing because:

  1. It tests a negative case (event type mismatch) that's not clear from the test name
  2. It then clears and tests the actual intended behavior

Consider either:

  • Splitting this into two separate tests: test_listeners_not_called_for_different_event and test_multiple_listeners_called_in_order
  • Or removing the mismatch test and only testing the positive case with the correct event type from the start

This would make the test clearer and more focused.

Copilot uses AI. Check for mistakes.
Comment on lines +186 to +204
if ($eventName === null) {
// Check if any listeners are registered
$events = [
'request.sending',
'response.received',
'error.occurred',
'request.retrying',
'request.timeout',
'request.redirecting',
];

foreach ($events as $event) {
if ($this->eventDispatcher->hasListeners($event)) {
return true;
}
}

return false;
}
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hasHooks method with $eventName = null (line 186) iterates through a hardcoded list of event names to check if any listeners exist. However, this doesn't account for custom events registered via the when() method. If a user registers a custom event like custom.event, hasHooks() will return false even though hooks are registered. Consider either:

  1. Checking if the eventDispatcher has ANY listeners at all (e.g., checking if the $listeners array is non-empty)
  2. Documenting that hasHooks() only checks for standard events, not custom ones
  3. Making the event list dynamic or extensible

Copilot uses AI. Check for mistakes.
Comment on lines +288 to +355
for ($attempt = 0; $attempt <= $attempts; $attempt++) {
try {
// Execute the request
return $request();
} catch (Throwable $e) {
// Collect exception for later
$exceptions[] = $e;

// Get response from exception if available
$response = null;
if ($e instanceof FetchRequestException && $e->getResponse()) {
$response = $e->getResponse();
} elseif (method_exists($e, 'getResponse')) {
$response = $e->getResponse();
}

// If this was the last attempt, dispatch error event and break
if ($attempt === $attempts) {
// Dispatch error event
if (method_exists($this, 'dispatchEvent')) {
$this->dispatchEvent(new ErrorEvent(
$psrRequest,
$e,
$correlationId,
microtime(true),
$attempt + 1,
$response
));
}
break;
}

// Only retry on retryable errors
if (! $this->isRetryableError($e)) {
// Dispatch error event for non-retryable errors
if (method_exists($this, 'dispatchEvent')) {
$this->dispatchEvent(new ErrorEvent(
$psrRequest,
$e,
$correlationId,
microtime(true),
$attempt + 1,
$response
));
}
throw $e;
}

// Log the retry for debugging purposes
if (method_exists($this, 'logRetry')) {
$this->logRetry($attempt + 1, $attempts, $e);
}

// Calculate delay with exponential backoff and jitter
$currentDelay = $this->calculateBackoffDelay($delay, $attempt);

// Dispatch retry event
if (method_exists($this, 'dispatchEvent')) {
$this->dispatchEvent(new RetryEvent(
$psrRequest,
$e,
$attempt + 1,
$attempts,
$currentDelay,
$correlationId,
microtime(true)
));
}
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the retryRequestWithEvents method, the attempt number passed to events uses $attempt + 1 to make it 1-based (lines 313, 329), but this is inconsistent because the loop variable $attempt already represents the actual attempt index (0 for first attempt). This means:

  • First attempt: $attempt = 0, events report attempt 1
  • Second attempt: $attempt = 1, events report attempt 2
  • Last attempt: $attempt = $attempts, events report attempt $attempts + 1

When $attempts = 3 (meaning max 3 retries, 4 total attempts), the final error event would report attempt 4 instead of 3. The loop condition $attempt <= $attempts combined with $attempt + 1 causes an off-by-one error in the reported attempt numbers.

Copilot uses AI. Check for mistakes.
Comment on lines +366 to +378
if ($lastException instanceof FetchRequestException && $lastException->getResponse()) {
$statusCode = $lastException->getResponse()->getStatusCode();
throw new RuntimeException(
sprintf(
'Request failed after %d attempts with status code %d: %s',
$attempts + 1,
$statusCode,
$lastException->getMessage()
),
$statusCode,
$lastException
);
}
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message on line 368 uses $attempts + 1 in the message "Request failed after %d attempts", but $attempts represents the number of retries, not total attempts. This should be either:

  • "Request failed after %d retries" using $attempts
  • "Request failed after %d attempts" using $attempts + 1

However, given that the loop already runs from 0 to $attempts (inclusive), this gives $attempts + 1 total executions. The message construction is misleading about whether it's counting retries or total attempts.

Copilot uses AI. Check for mistakes.
@Thavarshan Thavarshan closed this Nov 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants