uru / testhub-bundle
TestHub Bundle for Symfony: routes HttpClient traffic to a TestHub sandbox from the web profiler (dev only)
Requires
- php: >=8.4
- symfony/config: ^7.3 || ^8.0
- symfony/dependency-injection: ^7.3 || ^8.0
- symfony/framework-bundle: ^7.3 || ^8.0
- symfony/http-client: ^7.3 || ^8.0
- symfony/http-client-contracts: ^3.5
- symfony/http-foundation: ^7.3 || ^8.0
- symfony/http-kernel: ^7.3 || ^8.0
- symfony/routing: ^7.3 || ^8.0
- symfony/service-contracts: ^3.5
- symfony/var-dumper: ^7.3 || ^8.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.95
- phpunit/phpunit: ^10.0
- symfony/browser-kit: ^7.3 || ^8.0
- symfony/twig-bundle: ^7.3 || ^8.0
- symfony/web-profiler-bundle: ^7.3 || ^8.0
Suggests
- symfony/web-profiler-bundle: Required to see the Test Hub panel and the "Use sandbox" switch
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-25 07:01:39 UTC
README
Routes your application's outgoing HttpClient traffic to a TestHub sandbox, switchable per browser from the Symfony web profiler. Dev only.
Installation
composer require --dev uru/testhub-bundle
Register the bundle for dev only in config/bundles.php:
return [ // ... TestHub\Bundle\TestHubBundle::class => ['dev' => true], ];
Set the sandbox in .env.local:
SANDBOX_URL=https://sandbox.example.com SANDBOX_API_KEY=your-key # Default state when the profiler switch is untouched (optional, default: false) USE_SANDBOX=false
The bundle also checks the kernel environment: in any environment not listed in test_hub.environments (default ['dev']) it registers nothing, even if it is enabled in bundles.php.
Usage
-
Open any page and click Test Hub in the debug toolbar (or use its Turn on link).
-
In the panel, set Use sandbox to On. The choice is saved in the
testhub_sandboxcookie for this browser. -
Reload your page. Every request made through
http_client, including scoped clients, now goes to the sandbox:- requests with a type (see Request type) and an agent go to
{SANDBOX_URL}/api/{agent}/{type}/{event} - any other request goes to
{SANDBOX_URL}/api_wrap
The original URL is sent in the
sandbox-urlheader, the event in thesandbox-eventheader andSANDBOX_API_KEYin thesandbox-api-keyheader. The API key is sent only to the sandbox, never to real APIs, and is left out when it is empty. - requests with a type (see Request type) and an agent go to
-
The panel lists each outgoing request: original URL, sandbox URL, type/event, status, timing, request options, response headers and the sandbox response body.
Deposit event and Withdrawal event select whether the sandbox answers success or fail.
Request type
The type in the sandbox URL is resolved in this order:
extra.sandboxRequestTypeon the request, for non-standard types:use TestHub\Bundle\Service\SandBoxService; $httpClient->request('GET', 'https://psp.example/balance', [ 'extra' => [SandBoxService::SANDBOX_TYPE => 'balance'], ]);
- The request context's
getDirection(), thengetOperation(), when the value isdeposit,withdraworwithdrawal. Deposits and withdrawals need no tagging. - Otherwise the request goes to
/api_wrap.
The Deposit event and Withdrawal event selectors apply to deposit and to withdraw/withdrawal. Every other type gets success.
The key goes directly under extra, which HttpClient ignores, so this code also runs in prod, where the bundle is not loaded. Use the string 'sandboxRequestType' if the class is not autoloaded in prod, for example because you installed the bundle with --dev.
The legacy location
extra.curl.sandboxRequestTypestill works indev, but in any environment without the bundle it makes cURL throw aTypeError. Move it toextra.
Console commands and workers have no browser cookie, so they use use_sandbox.
Agent context
The {agent} path segment comes from a context provider service. Its get() returns the contexts collected so far, one per outgoing request. The bundle uses the last one, which belongs to the request being sent, and calls its getAgent(). If the list is empty or the agent is null or empty, the request goes to /api_wrap.
Implement the interface in your application. With autoconfiguration on (the Symfony default), the bundle finds your implementation and uses it instead of its empty DefaultContextProvider. You don't need any configuration:
namespace App\Sandbox; use TestHub\Bundle\HttpClient\State\ContextProviderInterface; final class CurrentAgentProvider implements ContextProviderInterface { public function __construct(private AgentRepository $agents) {} public function get(): array { return [$this->agents->current()]; } }
The implementation is chosen in this order:
test_hub.context_provider, if set- your own alias for
ContextProviderInterfaceinservices.yaml - your only autoconfigured implementation. If there are several, the container fails to build and asks you to pick one with option 1.
DefaultContextProvider, which returns no agent
A provider with its own interface
If the bundle is installed with --dev, an application class that must also load in prod cannot implement TestHub\Bundle\...\ContextProviderInterface. Keep your own interface and point the bundle at the service. Any service with a public get(): array method works:
# config/packages/dev/test_hub.yaml test_hub: context_provider: App\HttpClient\State\ContextCollector
The context must be collected before the sandbox decorator runs. The decorator sits on http_client.transport with priority 100, so a collecting decorator with a lower decoration_priority (for example -20) runs first.
The Test Hub panel shows which provider is in use under Configuration. The provider is only built when a request is actually sent to the sandbox, so it can depend on services that use http_client themselves.
Actions
Actions are buttons in the Test Hub panel that run application code, for example processing a test payout that is waiting for the payout cron. Each run is a new request from your browser, so the Use sandbox and event controls apply, and the panel links to the run's profile with its HTTP calls.
Import the bundle routes for dev:
# config/routes/dev/test_hub.yaml test_hub: resource: '@TestHubBundle/config/routes.php' prefix: /_test_hub
Implement ActionInterface. With autoconfiguration on, the action appears in the panel:
namespace App\Sandbox; use TestHub\Bundle\Action\ActionInterface; final class PayoutAction implements ActionInterface { public function getName(): string { return 'payout'; } public function getLabel(): string { return 'Run payout'; } public function getDescription(): string { return 'Processes approved withdrawals of a gateway.'; } /** Form fields, as name => label. */ public function getParameters(): array { return ['provider' => 'Provider', 'gateway' => 'Gateway']; } public function run(array $parameters): string { // ... return 'Payout finished.'; } }
run() gets the submitted fields as trimmed strings. Its return value is shown in the panel; an exception is shown as a failure. Anything the action prints is captured and shown too. The panel remembers the last values in this browser.
Configuration
# config/packages/dev/test_hub.yaml test_hub: environments: ['dev'] sandbox_url: '%env(string:default::SANDBOX_URL)%' use_sandbox: '%env(bool:default::USE_SANDBOX)%' api_key: '%env(string:default::SANDBOX_API_KEY)%' # context_provider: App\Sandbox\CurrentAgentProvider # only needed to override detection
Until sandbox_url is set, the sandbox stays off, whatever the switch says.
Testing
composer test