DEV Community

Cover image for Mocking HTTP Services in PHP with Phiremock
BRUNO SOUZA
BRUNO SOUZA

Posted on

Mocking HTTP Services in PHP with Phiremock

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
Enter fullscreen mode Exit fullscreen mode

If you also need Guzzle as the HTTP client (default):

composer require --dev mcustiel/phiremock-server mcustiel/phiremock-client guzzlehttp/guzzle:^6.0
Enter fullscreen mode Exit fullscreen mode

Starting the Server

Start Phiremock from the command line after installing:

vendor/bin/phiremock
Enter fullscreen mode Exit fullscreen mode

By default, it listens on 0.0.0.0:8086. To customise:

vendor/bin/phiremock --port 8089 --ip 127.0.0.1 --debug
Enter fullscreen mode Exit fullscreen mode

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,
  ];
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
);
Enter fullscreen mode Exit fullscreen mode

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')
    )
);
Enter fullscreen mode Exit fullscreen mode

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')
    )
);
Enter fullscreen mode Exit fullscreen mode

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')
    )
);
Enter fullscreen mode Exit fullscreen mode

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)
        );
    }
Enter fullscreen mode Exit fullscreen mode

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');
    }
Enter fullscreen mode Exit fullscreen mode

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']);
    }
Enter fullscreen mode Exit fullscreen mode

[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);
    }
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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)