php-mock/php-mock-prophecy
Prophecy integration for php-mock lets you mock PHP built-in functions like time, rand, and sleep inside Prophecy test doubles. Define expected behavior for global functions alongside class mocks, making time-dependent code testable without hard dependencies.
Install (dev-only; it hooks into the test suite, never runtime):
composer require --dev php-mock/php-mock-prophecy
It pulls in php-mock/php-mock (the engine that actually defines the function stubs) and Prophecy (already a transitive dependency of phpspec, so this is a natural fit there). PHPUnit also works if you're already comfortable with Prophecy.
Where to look first: the README example — it's essentially the whole package. Then read phpmock\prophecy\Prophet and phpmock\prophecy\FunctionMock, which are thin wrappers around php-mock's FunctionMock. You should understand php-mock's core trick before anything else: it cannot replace a built-in function. Instead it defines a same-named function in a chosen namespace, so PHP's unqualified-call fallback resolves your SUT's time() to the generated stub.
First use case — a deterministic clock:
namespace App\Service;
class OrderNumberGenerator
{
public function generate(): string
{
return 'ORD-' . time(); // unqualified: resolves to App\Service\time() if defined
}
}
namespace App\Service;
class OrderNumberGeneratorTest extends \PHPUnit\Framework\TestCase
{
public function testGenerateUsesCurrentTimestamp(): void
{
$prophet = new \phpmock\prophecy\Prophet(__NAMESPACE__);
$prophecy = $prophet->prophesize('time');
$prophecy->willReturn(1234567890);
$prophecy->reveal(); // defines App\Service\time()
$this->assertSame('ORD-1234567890', (new OrderNumberGenerator())->generate());
}
}
The namespace argument is the crux. It must match the namespace your code-under-test calls the function from. If the constructor signature differs in your installed version, check the README — but the concept is always the same: you are mocking the function in a namespace, not the global one. Once that clicks, the package is trivial to use. Until it clicks, every test "fails silently" by calling the real time().
Deterministic time. Mock time(), microtime(), and date() (or, more reliably, inject a clock and only mock time() at the edge). Use with() to answer differently per argument:
$prophecy = $prophet->prophesize('date');
$prophecy->with('Y-m-d', 1234567890)->willReturn('2009-02-13');
Seeded randomness. rand(), mt_rand(), random_int(), uniqid() — return a fixed value or a sequence:
$prophecy->rand()->willReturn(42);
// or a queue
$prophecy->rand()->willReturn(1, 2, 3);
Filesystem and network boundaries. file_get_contents(), fopen(), file_exists(), curl_exec(), fsockopen(), gethostbyname(). This is where the package earns its keep for legacy code you can't refactor:
$prophecy = $prophet->prophesize('file_exists');
$prophecy->with('/etc/config.yml')->willReturn(false);
Failure paths without touching the real world. willThrow() beats @group integration for exercising error branches:
$prophecy->file_get_contents()->willReturn(false);
$prophecy->curl_exec()->willThrow(new \RuntimeException('timeout'));
Call verification. Treat it like any Prophecy double — shouldBeCalled(), shouldBeCalledTimes(), shouldNotBeCalled() work, but remember the Prophecy predictions are only checked when something runs checkPredictions(). Under phpspec that's automatic; under PHPUnit call it in tearDown() or use ->shouldBeCalled() alongside a $prophet->checkPredictions().
phpspec integration. In let()/it_*() you already have a Prophet, so keep the namespace argument tied to your spec's subject and you get one consistent mocking style across the whole spec. This is the package's real audience.
Process-global mocks. Because the stub is a real function, one reveal() covers every call site in that namespace for the rest of the process. Prefer a fresh Prophet per test, and avoid static caches in the SUT (a cached time() will never be re-mocked).
Keep it at the edges. Wrap built-ins (Clock, Filesystem, Random) and mock those collaborators directly; reserve php-mock-prophecy for code you can't restructure.
\time()), use function time; imports, and calls from a different namespace all bypass the mock. The most common "it doesn't work" bug is a mismatch between the mocked namespace and the SUT's actual namespace. Verify with function_exists('App\Service\time') after reveal().disable()/enable() on the underlying FunctionMock for that reason; repeated reveal() calls in the same process can conflict. If tests bleed into each other, consider @runInSeparateProcess or a dedicated test process.isset, echo, new, instanceof) and constants are out of scope. For date_default_timezone_get()-style configuration, prefer putenv()/date_default_timezone_set() in setUp()/tearDown().reveal() at the top of the test (or in setUpAfterClass for shared setups). A reveal() that runs after the assertion reads as a no-op.private static ?int $cachedNow in the SUT defeats the mock.ReflectionFunction('App\Service\time')->getFileName() tells you whether the stub or the real builtin was resolved (real builtins report as internal). error_log inside the stub is not possible here, but willReturn of a sentinel value makes a hit obvious.php-mock core, Prophecy/PHPUnit). Pin versions in composer.json and add a smoke test that asserts the mock actually takes effect, so a dependency bump can't silently turn your deterministic tests back into flaky ones.php-mock/php-mock-phpunit if you use PHPUnit more than phpspec — its PHPMock trait is the more actively exercised code path in the same family. And for new code, an injected clock beats mocking built-ins every time; this package is best reserved for untangling legacy code.How can I help you explore Laravel packages today?