This article is part of a series on mock servers for backend developers. Part 1 covers language-agnostic tools (Postman, WireMock, and Pact Stub Server). This article focuses on Phiremock, the first of three PHP-specific tools covered in the series.
When your PHP application integrates with third-party APIs — payment gateways, logistics providers, marketplaces — testing those integrations reliably is a real challenge. Hitting real sandboxes is slow, fragile, and sometimes billable. Phiremock solves this by running a real HTTP server that your application calls instead, returning whatever response you define.
What is Phiremock?
Phiremock is a standalone HTTP mock server written in PHP, heavily inspired by WireMock. You run it as a separate process (or Docker container) and configure it via JSON expectation files or programmatically through its PHP client.
Current version: v1.5.1 (released December 2024)
PHP compatibility: ^7.2 | ^8.0
GitHub: https://github.com/mcustiel/phiremock
The key advantage over in-process mocking libraries is that Phiremock runs as a real HTTP server. Your application doesn't know it's talking to a mock; it makes real HTTP calls, making the tests more realistic.
Installation
Since version 2, Phiremock is split into two packages: the server and the client. Install both as dev dependencies:
composer require --dev mcustiel/phiremock-server mcustiel/phiremock-client
If you also need Guzzle as the HTTP client (default):
composer require --dev mcustiel/phiremock-server mcustiel/phiremock-client guzzlehttp/guzzle:^6.0
Starting the Server
Start Phiremock from the command line after installing:
vendor/bin/phiremock
By default, it listens on 0.0.0.0:8086. To customise:
vendor/bin/phiremock --port 8089 --ip 127.0.0.1 --debug
Configuration File
For a consistent setup, create a .phiremock file at your project root:
<?php
return [
'port' => 8089,
'ip' => '0.0.0.0',
'expectations-dir' => './tests/expectations',
'debug' => false,
];
Phiremock searches for this file automatically at startup. Command-line arguments take priority over the config file.
Docker Compose
For a shared dev/CI environment, run Phiremock alongside your application:
services:
app:
build: .
environment:
PAYMENT_API_URL: http://phiremock:8086
depends_on:
- phiremock
phiremock:
image: mcustiel/phiremock
ports:
- "8086:8086"
volumes:
- ./tests/expectations:/var/www/phiremock/expectations
Once running, update your application config to point to http://localhost:8086 (or the Docker service name) instead of the real service URL.
Defining Expectations
An expectation tells Phiremock: "When you receive this request, return this response". You can define expectations in two ways: as JSON files loaded on startup, or programmatically via the PHP client at runtime.
JSON Expectations (Declarative)
Place JSON files in your expectations-dir directory. They are loaded automatically when Phiremock starts.
Match a GET request and return a product:
{
"version": "2",
"on": {
"method": { "isSameString": "GET" },
"url": { "isEqualTo": "/api/products/1" }
},
"then": {
"response": {
"statusCode": 200,
"body": "{\"id\": 1, \"name\": \"Gift Box\", \"price\": 49.99}",
"headers": { "Content-Type": "application/json" }
}
}
}
Match a POST with a URL regex and simulate a 100ms delay:
{
"version": "2",
"on": {
"method": { "isSameString": "POST" },
"url": { "matches": "~/api/orders/?~" }
},
"then": {
"delayMillis": 100,
"response": {
"statusCode": 201,
"body": "{\"orderId\": \"abc-123\", \"status\": \"processing\"}",
"headers": { "Content-Type": "application/json" }
}
}
}
Simulate a 503 service unavailable
{
"version": "2",
"on": {
"method": { "isSameString": "GET" },
"url": { "isEqualTo": "/api/inventory/check" }
},
"then": {
"response": {
"statusCode": 503,
"body": "{\"error\": \"Service temporarily unavailable\"}",
"headers": { "Content-Type": "application/json" }
}
}
}
Programmatic Expectations (PHP Client)
For dynamic test scenarios, define expectations in code using the Phiremock client. This is particularly useful in PHPUnit tests where each test needs a different response.
Setting up the client:
<?php
use Mcustiel\Phiremock\Client\Connection\Host;
use Mcustiel\Phiremock\Client\Connection\Port;
use Mcustiel\Phiremock\Client\Factory;
use Mcustiel\Phiremock\Client\Phiremock;
use Mcustiel\Phiremock\Client\Utils\A;
use Mcustiel\Phiremock\Client\Utils\Is;
use Mcustiel\Phiremock\Client\Utils\Respond;
$client = Factory::createDefault()->createPhiremockClient(
new Host('localhost'),
new Port(8086)
);
Creating the expectation
$client->createExpectation(
Phiremock::on(
A::getRequest()->andUrl(Is::equalTo('/api/products/1'))
)->then(
Respond::withStatusCode(200)
->andBody('{"id": 1, "name": "Gift Box", "price": 49.99}')
->andHeader('Content-Type', 'application/json')
)
);
Matching by body content:
$client->createExpectation(
Phiremock::on(
A::postRequest()
->andUrl(Is::equalTo('/api/orders'))
->andBody(Is::containing('"productId":1'))
)->then(
Respond::withStatusCode(201)
->andBody('{"orderId": "abc-123"}')
->andHeader('Content-Type', 'application/json')
)
);
Matching by header:
$client->createExpectation(
Phiremock::on(
A::getRequest()
->andUrl(Is::equalTo('/api/products'))
->andHeader('Authorization', Is::matching('/^Bearer .+/'))
)->then(
Respond::withStatusCode(200)
->andBody('[{"id": 1, "name": "Gift Box"}]')
->andHeader('Content-Type', 'application/json')
)
);
Using with PHPUnit
Here is a complete PHPUnit test class that resets Phiremock state between tests and uses programmatic expectations:
Defining a config to run before the tests:
<?php
namespace App\Tests;
use Mcustiel\Phiremock\Client\Connection\Host;
use Mcustiel\Phiremock\Client\Connection\Port;
use Mcustiel\Phiremock\Client\Factory;
use Mcustiel\Phiremock\Client\Phiremock;
use Mcustiel\Phiremock\Client\Utils\A;
use Mcustiel\Phiremock\Client\Utils\Is;
use Mcustiel\Phiremock\Client\Utils\Respond;
use PHPUnit\Framework\TestCase;
class ProductServiceTest extends TestCase
{
private static Phiremock $phiremock;
private ProductService $productService;
public static function setUpBeforeClass(): void
{
self::$phiremock = Factory::createDefault()->createPhiremockClient(
new Host('localhost'),
new Port(8086)
);
}
Defining the set up for the tests:
protected function setUp(): void
{
// Clear all expectations and request history before each test
self::$phiremock->clearExpectations();
self::$phiremock->resetRequestsLog();
// Point your service at the mock server
$this->productService = new ProductService('http://localhost:8086');
}
Writing the functions to test:
[Positive Scenario]
public function testFetchesProductSuccessfully(): void
{
self::$phiremock->createExpectation(
Phiremock::on(
A::getRequest()->andUrl(Is::equalTo('/api/products/1'))
)->then(
Respond::withStatusCode(200)
->andBody('{"id": 1, "name": "Gift Box", "price": 49.99}')
->andHeader('Content-Type', 'application/json')
)
);
$product = $this->productService->find(1);
$this->assertEquals('Gift Box', $product['name']);
$this->assertEquals(49.99, $product['price']);
}
[Negative Scenario]
public function testHandles404Gracefully(): void
{
self::$phiremock->createExpectation(
Phiremock::on(
A::getRequest()->andUrl(Is::equalTo('/api/products/999'))
)->then(
Respond::withStatusCode(404)
->andBody('{"error": "Not found"}')
->andHeader('Content-Type', 'application/json')
)
);
$this->expectException(ProductNotFoundException::class);
$this->productService->find(999);
}
public function testHandlesServiceTimeout(): void
{
self::$phiremock->createExpectation(
Phiremock::on(
A::getRequest()->andUrl(Is::equalTo('/api/products/1'))
)->then(
Respond::withStatusCode(200)
->andBody('{"id": 1, "name": "Gift Box"}')
->andDelayInMillis(3000) // simulate a slow response
)
);
$this->expectException(ServiceTimeoutException::class);
$this->productService->find(1);
}
Verifying Calls
One of Phiremock's most useful features for strict test assertions is verifying that your application actually made the expected calls — not just that it returned the right result:
public function testCreatesOrderAndCallsPaymentGateway(): void
{
self::$phiremock->createExpectation(
Phiremock::on(
A::postRequest()->andUrl(Is::equalTo('/api/payments/charge'))
)->then(
Respond::withStatusCode(200)
->andBody('{"chargeId": "ch_abc123", "status": "succeeded"}')
)
);
$this->orderService->create(['productId' => 1, 'quantity' => 2]);
// Assert the payment endpoint was called exactly once
$executions = self::$phiremock->countExecutions(
A::postRequest()->andUrl(Is::equalTo('/api/payments/charge'))
);
$this->assertEquals(1, $executions);
}
Stateful Scenarios
Phiremock supports scenarios (sequences of responses that transition state depending on how many times an endpoint has been called). This is useful for simulating async workflows, such as order processing or job polling.
Define two JSON expectations for the same endpoint with different scenario states:
First call — returns "processing":
{
"version": "2",
"scenarioName": "order-lifecycle",
"on": {
"scenarioStateIs": "Scenario.START",
"method": { "isSameString": "GET" },
"url": { "isEqualTo": "/api/orders/abc-123" }
},
"then": {
"newScenarioState": "order-completed",
"response": {
"statusCode": 200,
"body": "{\"orderId\": \"abc-123\", \"status\": \"processing\"}",
"headers": { "Content-Type": "application/json" }
}
}
}
Second call — returns "completed":
{
"version": "2",
"scenarioName": "order-lifecycle",
"on": {
"scenarioStateIs": "order-completed",
"method": { "isSameString": "GET" },
"url": { "isEqualTo": "/api/orders/abc-123" }
},
"then": {
"response": {
"statusCode": 200,
"body": "{\"orderId\": \"abc-123\", \"status\": \"completed\"}",
"headers": { "Content-Type": "application/json" }
}
}
}
The first time your application calls GET /api/orders/abc-123, it receives a processing status. The second call returns completed — no code changes needed between calls.
You can reset all scenarios to their initial state between tests via the client:
self::$phiremock->resetScenarios();
Pros & Cons
Pros:
- Supports PHP 8, actively maintained (v1.5.1, December 2024)
- Runs as a real HTTP server — tests are realistic end-to-end
- Rich matching API: method, URL, body, headers, regex, exact match
- Network latency simulation with delayMillis
- Stateful scenarios for async workflows
- Verify call counts for strict assertion
- Docker image available for shared environments
- Works with any PHP HTTP client
Cons:
- Requires a running server process — adds infrastructure overhead
- No built-in GUI (unlike Postman or WireMock)
- The client requires Guzzle v6 by default (an older version)
- More setup than in-process libraries for simple unit tests
When to Use Phiremock
Phiremock is the right choice when you need a mock server that multiple test suites, developers, or services can share, especially in a Docker Compose environment. If you need stateful scenarios, call verification, or realistic end-to-end HTTP testing where your app makes actual network connections to a real server, Phiremock is the strongest PHP-native option available.
For simpler, inline mocks that live entirely inside a single PHPUnit test class, take a look at the next tools in this series.
This article is part of a series on mock servers. Read Part 1: Language-Agnostic Tools (Postman, WireMock, Pact Stub Server), or continue with the next PHP tool in the series.
If you have questions or want to share how your team uses Phiremock, drop a comment below!
— Bruno Souza | Senior Software Engineer | LinkedIn
Top comments (0)