ciareis / bypass
Bypass for PHP provides a quick way to create a custom instead of an actual HTTP server to return prebaked responses to client requests. This is most useful in tests, when you want to create a mock HTTP server and test how your HTTP client handles different types of responses from the server.
Installs: 12 870
Dependents: 9
Suggesters: 0
Security: 0
Stars: 119
Watchers: 5
Forks: 11
Open Issues: 0
pkg:composer/ciareis/bypass
Requires
- php: ^8.2
Requires (Dev)
- guzzlehttp/guzzle: ^7.5
- laravel/framework: ^9.0|^10.0|^11.0|^12.0
- orchestra/testbench: ^7.0|^8.0|^9.0|^10.0
- pestphp/pest: ^3.7
- phpunit/phpunit: ^11|^12
This package is auto-updated.
Last update: 2026-01-15 23:29:33 UTC
README
Bypass for PHP
About | Requirements | Installation | Writing Tests | API Reference | Examples | Troubleshooting | Contributing | Credits | Inspired
About
|
Bypass for PHP provides a quick way to create a custom HTTP Server to return predefined responses to client requests. This is useful in tests when your application makes requests to external services, and you need to simulate different situations like returning specific data or unexpected server errors. Just open a Bypass server and configure your application/service to reach Bypass instead of the real-world API end point. |
Requirements
- PHP: 8.2 or higher (tested up to PHP 8.4)
- Composer: For dependency management
Known Issues
- PHP 8.4: You may see deprecation warnings from testing dependencies (Pest/PHPUnit) related to
ReflectionMethod::setAccessible(). These warnings are harmless and come from the testing frameworks themselves, not from Bypass. They will be resolved when the dependencies are updated to support PHP 8.4 fully.
Installation
To install via composer, run the following command:
composer require --dev ciareis/bypass
Video demo
Writing Tests
Content
🔥 Check out full code examples here section.
1. Open a Bypass Server
To write a test, first open a Bypass server:
//Open a new Bypass server $bypass = Bypass::open();
Bypass will always run at http://localhost listening to a random port number.
To specify a custom port, just pass it in the argument (int) $port.
//Open a new Bypass using port 8081 $bypass = Bypass::open(8081);
Alternative method: You can also use Bypass::up() which is an alias for Bypass::open():
//Same as Bypass::open() $bypass = Bypass::up();
2. Bypass URL and Port
You can retrieve the Bypass server URL using getBaseUrl().
$bypassUrl = $bypass->getBaseUrl(); //http://localhost:16819
If you need to retrieve only the port number, use the getPort() method:
$bypassPort = $bypass->getPort(); //16819
3. Routes
Bypass provides two types of routes: The Standard Route to return a text body content and the File Route, which returns a binary file.
When running your test suit, you should pass the URL created with Bypass to your service. In this way, you will make the service you are testing reach Bypass instead of reaching the real-world API end point.
3.1 Standard Route
use Ciareis\Bypass\Bypass; //Json body $body = '{"username": "john", "name": "John Smith", "total": 1250}'; //Route retuning the JSON body with HTTP Status 200 $bypass->addRoute(method: 'GET', uri: '/v1/demo/john', status: 200, body: $body); //Instantiates a DemoService class $service = new DemoService(); //Configure your service to access Bypass URL $response = $service->setBaseUrl($bypass->getBaseUrl()) ->getTotalByUser('john'); //Your test assertions here...
The method addRoute() accepts the following parameters:
| Parameter | Type | Description |
|---|---|---|
| HTTP Method | string $method |
HTTP Request Method (GET/POST/PUT/PATCH/DELETE) |
| URI | string $uri |
URI to be served by Bypass |
| Status | int $status |
HTTP Status Code to be returned by Bypass (default: 200) |
| Body | string|array $body |
Body to be served by Bypass (optional) |
| Times | int $times |
How many times the route should be called (default: 1) |
| Headers | array $headers |
Headers to be served by Bypass (optional) |
3.2 File Route
use Ciareis\Bypass\Bypass; //Reads a PDF file $demoFile = \file_get_contents('storage/pdfs/demo.pdf'); //File Route returning a binary file with HTTP Status 200 $bypass->addFileRoute(method: 'GET', uri: '/v1/myfile', status: 200, file: $demoFile); //Instantiates a DemoService class $service = new DemoService(); //Configure your service to access Bypass URL $response = $service->setBaseUrl($bypass->getBaseUrl()) ->getPdf(); //Your test assertions here...
The method addFileRoute() accepts the following parameters:
| Parameter | Type | Description |
|---|---|---|
| HTTP Method | string $method |
HTTP Request Method (GET/POST/PUT/PATCH/DELETE) |
| URI | string $uri |
URI to be served by Bypass |
| Status | int $status |
HTTP Status Code to be returned by Bypass (default: 200) |
| File | binary $file |
Binary file to be served by Bypass |
| Times | int $times |
How many times the route should be called (default: 1) |
| Headers | array $headers |
Headers to be served by Bypass (optional) |
3.3 Bypass Serve and Route Helpers
Bypass provides you with convenient shortcuts to the most-common-used HTTP requests.
These shortcuts are called "Route Helpers" and are served automatically at a random port using Bypass::serve() without the need to call Bypass::open().
In the next example, Bypasss serves two routes: A URL accessible by method GET returning a JSON body with status 200, and a second route URL accessible by method GET and returning status 404.
use Ciareis\Bypass\Bypass; use Ciareis\Bypass\Route; //Create and serve routes $bypass = Bypass::serve( Route::ok(uri: '/v1/demo/john', body: ['username' => 'john', 'name' => 'John Smith', 'total' => 1250]), //method GET, status 200 Route::notFound(uri: '/v1/demo/wally') //method GET, status 404 ); //Instantiates a DemoService class $service = new DemoService(); $service->setBaseUrl($bypass->getBaseUrl()); //Consumes the "OK (200)" route $responseOk = $service->getTotalByUser('john'); //200 - OK with total => 1250 //Consumes the "Not Found (404)" route $responseNotFound = $service->getTotalByUser('wally'); //404 - Not found //Your test assertions here...
Route Helpers
You may find below the list of Route Helpers.
| Route Helper | Default Method | HTTP Status | Body | Common usage |
|---|---|---|---|---|
| Route::ok() | GET | 200 | optional (string|array) | Request was successful |
| Route::created() | POST | 201 | optional (string|array) | Response to a POST request which resulted in a creation |
| Route::badRequest() | POST | 400 | optional (string|array) | Something can't be parsed (ex: wrong parameter) |
| Route::unauthorized() | GET | 401 | optional (string|array) | Not logged in |
| Route::forbidden() | GET | 403 | optional (string|array) | Logged in but trying to request a restricted resource (without permission) |
| Route::notFound() | GET | 404 | optional (string|array) | URL or resource does not exist |
| Route::notAllowed() | GET | 405 | optional (string|array) | Method not allowed |
| Route::validationFailed() | POST | 422 | optional (string|array) | Data sent does not satisfy validation rules |
| Route::tooMany() | GET | 429 | optional (string|array) | Request rejected due to server limitation |
| Route::serverError() | GET | 500 | optional (string|array) | General indication that something is wrong on the server side |
You may also adjust the helpers to your needs by passing arguments:
| Parameter | Type | Description |
|---|---|---|
| URI | string $uri |
URI to be served by Bypass |
| Body | string|array $body |
Body to be served by Bypass (optional) |
| HTTP Method | string $method |
HTTP Request Method (GET/POST/PUT/PATCH/DELETE) |
| Times | int $times |
How many times the route should be called (default: 1) |
| Headers | array $headers |
Headers to be served by Bypass (optional) |
In the example below, you can see the Helper Route::badRequest using method GET instead of its default method POST.
use Ciareis\Bypass\Bypass; use Ciareis\Bypass\Route; Bypass::serve( Route::badRequest(uri: '/v1/users?filter=foo', body: ['error' => 'Filter parameter foo is not allowed.'], method: 'GET') );
📝 Note: Custom routes can be created using a Standard Route in case something you need is not covered by the Helpers.
4. Asserting Route Calling
Sometimes you may need to assert that a route was called at least one or multiple times.
The method assertRoutes() will return a RouteNotCalledException if a route was NOT called as many times as defined in the $times parameter.
If you need to assert that a route is NOT being called by your service, set the parameter $times = 0
//Json body $body = '{"username": "john", "name": "John Smith", "total": 1250}'; //Defines a route which must be called two times $bypass->addRoute(method: 'GET', uri: '/v1/demo/john', status: 200, body: $body, times: 2); //Instantiates a DemoService class $service = new DemoService(); //Consumes the service using the Bypass URL $response = $service->setBaseUrl($bypass->getBaseUrl()) ->getTotalByUser('john'); $bypass->assertRoutes(); //Your test assertions here...
5. Stop or shut down
Bypass will automatically stop its server once your test is done running.
The Bypass server can be stopped or shut down at any point with the following methods:
To stop:
$bypass->stop();
To shut down:
$bypass->down();
API Reference
Methods
Static Methods
-
Bypass::open(?int $port = null): self
Opens a new Bypass server instance. If no port is specified, a random port will be used. -
Bypass::up(?int $port = null): self
Alias foropen(). Opens a new Bypass server instance. -
Bypass::serve(...$routes): self
Creates and serves multiple routes at once. AcceptsRouteobjects,RouteFileobjects, or arrays.
Instance Methods
-
addRoute(string $method, string $uri, int $status = 200, string|array|null $body = null, int $times = 1, array $headers = []): self
Adds a standard route that returns text/JSON content. -
expect(string $method, string $uri, int $status = 200, string|array|null $body = null, int $times = 1, array $headers = []): self
Alias foraddRoute(). Adds a standard route that returns text/JSON content. -
addFileRoute(string $method, string $uri, int $status = 200, ?string $file = null, int $times = 1, array $headers = []): self
Adds a file route that returns binary file content. -
getRoutes(): array
Returns all registered routes as an array of route configurations. -
assertRoutes(): void
Asserts that all registered routes were called the expected number of times. ThrowsRouteNotCalledExceptionif any route was not called as expected. -
getBaseUrl(?string $path = null): string
Returns the base URL of the Bypass server. Optionally appends a path. -
getPort(): int
Returns the port number the Bypass server is listening on. -
stop(): self
Stops the Bypass server by clearing all routes. The server process remains running. -
down(): self
Shuts down the Bypass server process completely.
Exceptions
RouteNotCalledException
Thrown when assertRoutes() is called and a route was not called the expected number of times.
use Ciareis\Bypass\RouteNotCalledException; try { $bypass->assertRoutes(); } catch (RouteNotCalledException $e) { // Handle the exception // Message format: "Bypass expected route '/path' with method 'GET' to be called X times(s). Found Y calls(s) instead." }
Examples
Use case
To better illustrate Bypass usage, imagine you have to write a test for a service that calculates the total game score of a given username.
The score is obtained by making an external request to a fictitious API at emtudo-games.com/v1/score/::USERNAME::. The API returns HTTP Status 200 and a JSON body with a list of games:
{
"games": [
{
"id": 1,
"points": 25
},
{
"id": 2,
"points": 10
}
],
"is_active": true
}
use Ciareis\Bypass\Bypass; //Opens a new Bypass server $bypass = Bypass::open(); //Retrieves the Bypass URL $bypassUrl = $bypass->getBaseUrl(); //Json body $body = '{"games":[{"id":1, "name":"game 1","points":25},{"id":2, "name":"game 2","points":10}],"is_active":true}'; //Defines a route $bypass->addRoute(method: 'GET', uri: '/v1/score/johndoe', status: 200, body: $body); //Instantiates a TotalScoreService $service = new TotalScoreService(); //Configure your service to access Bypass URL $response = $service ->setBaseUrl($bypassUrl) // set the URL to the Bypass URL ->getTotalScoreByUsername('johndoe'); //returns 35 //Pest PHP verifies that response is 35 expect($response)->toBe(35); //PHPUnit verifies that response is 35 $this->assertSame($response, 35);
Quick Test Examples
Click below to see code snippets for Pest PHP and PHPUnit.
Pest PHP
use Ciareis\Bypass\Bypass; it('properly returns the total score by username', function () { //Opens a new Bypass server $bypass = Bypass::open(); //Json body $body = '{"games":[{"id":1, "name":"game 1","points":25},{"id":2, "name":"game 2","points":10}],"is_active":true}'; //Defines a route $bypass->addRoute(method: 'GET', uri: '/v1/score/johndoe', status: 200, body: $body); //Configure your service to access Bypass URL $service = new TotalScoreService(); $response = $service ->setBaseUrl($bypass->getBaseUrl()) ->getTotalScoreByUsername('johndoe'); //Verifies that response is 35 expect($response)->toBe(35); }); it('properly gets the logo', function () { //Opens a new Bypass server $bypass = Bypass::open(); //Reads the file $filePath = 'docs/img/logo.png'; $file = \file_get_contents($filePath); //Defines a route $bypass->addFileRoute(method: 'GET', uri: $filePath, status: 200, file: $file); //Configure your service to access Bypass URL $service = new LogoService(); $response = $service->setBaseUrl($bypass->getBaseUrl()) ->getLogo(); // asserts expect($response)->toEqual($file); });
PHPUnit
use Ciareis\Bypass\Bypass; class BypassTest extends TestCase { public function test_total_score_by_username(): void { //Opens a new Bypass server $bypass = Bypass::open(); //Json body $body = '{"games":[{"id":1,"name":"game 1","points":25},{"id":2,"name":"game 2","points":10}],"is_active":true}'; //Defines a route $bypass->addRoute(method: 'GET', uri: '/v1/score/johndoe', status: 200, body: $body); //Configure your service to access Bypass URL $service = new TotalScoreService(); $response = $service ->setBaseUrl($bypass->getBaseUrl()) ->getTotalScoreByUsername('johndoe'); //Verifies that response is 35 $this->assertSame(35, $response); } public function test_gets_logo(): void { //Opens a new Bypass server $bypass = Bypass::open(); //Reads the file $filePath = 'docs/img/logo.png'; $file = \file_get_contents($filePath); //Defines a route $bypass->addFileRoute(method: 'GET', uri: $filePath, status: 200, file: $file); //Configure your service to access Bypass URL $service = new LogoService(); $response = $service->setBaseUrl($bypass->getBaseUrl()) ->getLogo(); $this->assertSame($response, $file); } }
Test Examples
📚 See Bypass being used in complete tests with Pest PHP and PHPUnit for the GithubRepoService demo service.
Advanced Examples
Using Custom Headers
use Ciareis\Bypass\Bypass; $bypass = Bypass::open(); $bypass->addRoute( method: 'GET', uri: '/v1/api/data', status: 200, body: ['data' => 'example'], headers: [ 'X-Custom-Header' => 'value', 'X-Another-Header' => ['value1', 'value2'], // Multiple values ] );
Multiple Route Calls
use Ciareis\Bypass\Bypass; $bypass = Bypass::open(); // Route must be called exactly 3 times $bypass->addRoute( method: 'GET', uri: '/v1/api/data', status: 200, body: ['data' => 'example'], times: 3 ); $service = new ApiService(); $service->setBaseUrl($bypass->getBaseUrl()); // Call the route 3 times $service->fetchData(); $service->fetchData(); $service->fetchData(); // This will pass $bypass->assertRoutes();
Handling Exceptions
use Ciareis\Bypass\Bypass; use Ciareis\Bypass\RouteNotCalledException; $bypass = Bypass::open(); $bypass->addRoute(method: 'GET', uri: '/v1/api/data', status: 200); $service = new ApiService(); $service->setBaseUrl($bypass->getBaseUrl()); // Don't call the route try { $bypass->assertRoutes(); $this->fail('Expected RouteNotCalledException'); } catch (RouteNotCalledException $e) { $this->assertStringContainsString("expected route '/v1/api/data'", $e->getMessage()); }
Troubleshooting
Common Issues
Port Already in Use
If you specify a port that's already in use, Bypass will fail to start. Use a random port (default) or ensure the port is available:
// Use random port (recommended) $bypass = Bypass::open(); // Or specify a port and handle errors try { $bypass = Bypass::open(8080); } catch (RuntimeException $e) { // Port might be in use, try another $bypass = Bypass::open(8081); }
Server Timeout
Bypass has a default timeout of 5 seconds for server startup. If your system is slow, the server might not start in time. This is rare but can happen in CI environments.
Route Not Found Errors
If you're getting "route not found" errors, ensure:
- The URI matches exactly (including query parameters)
- The HTTP method matches (GET, POST, etc.)
- The route was added before making the request
- The service is using the correct Bypass URL
Getting All Registered Routes
You can inspect all registered routes for debugging:
$bypass = Bypass::open(); $bypass->addRoute(method: 'GET', uri: '/v1/api/data', status: 200); $routes = $bypass->getRoutes(); // Returns array with route configurations
Contributing
We welcome contributions! Here's how you can help:
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Make your changes following the existing code style
- Add tests for new functionality
- Ensure all tests pass:
vendor/bin/pest - Commit your changes:
git commit -m 'Add amazing feature' - Push to the branch:
git push origin feature/amazing-feature - Open a Pull Request
Code Style
- Follow PSR-12 coding standards
- Use type hints where possible
- Add PHPDoc comments for public methods
- Write tests for new features
Testing
- Run tests with:
vendor/bin/pest - Ensure all tests pass before submitting a PR
- Add tests for any new functionality
Credits
And a special thanks to @DanSysAnalyst
Inspired
Code inspired by Bypass
