antecedent/patchwork
Patchwork is a PHP library for monkey patching: redefine functions, methods, and classes at runtime to ease testing and legacy refactors. Works with Composer and popular test tools, enabling stubs/mocks without changing existing code.
Installation:
composer require antecedent/patchwork
No additional configuration is required—Patchwork works out-of-the-box.
First Use Case:
Override a global function (e.g., str_replace) for testing:
use Patchwork\Patchwork;
$patchwork = new Patchwork();
// Patch str_replace to return a hardcoded value
$patchwork->getFactory()->patch(
function ($subject) {
return 'PATCHED';
},
'str_replace'
);
// Now str_replace behaves differently
echo str_replace('foo', 'bar', 'hello'); // Outputs: "PATCHED"
Where to Look First:
README or docs folder).Patchwork class and its Factory methods (patch, patchMethod, patchStaticMethod).Patchwork\Patchwork namespace for core functionality.Testing Legacy Code:
patch() to stub or mock global functions (e.g., file_get_contents, date).file_get_contents to return a fake file for unit tests:
$patchwork->getFactory()->patch(
function () { return 'fake content'; },
'file_get_contents'
);
Intercepting Static Methods:
SomeClass::staticMethod()) to inject behavior:
$patchwork->getFactory()->patchStaticMethod(
function ($args) { return 'intercepted'; },
'SomeClass',
'staticMethod'
);
Method Interception:
$patchwork->getFactory()->patchMethod(
function ($obj, $args) { return 'patched'; },
'SomeClass',
'publicMethod'
);
Conditional Patching:
if (app()->environment('testing')) {
$patchwork->getFactory()->patch(
function () { return 'test value'; },
'some_function'
);
}
Laravel Service Provider: Bind Patchwork to the container for easy access:
public function register()
{
$this->app->singleton(Patchwork::class, function () {
return new Patchwork();
});
}
Then inject Patchwork into tests or services:
use Illuminate\Support\ServiceProvider;
class TestServiceProvider extends ServiceProvider
{
public function boot(Patchwork $patchwork)
{
$patchwork->getFactory()->patch(...);
}
}
Test Setup/Teardown:
Apply patches in setUp() and restore original functions in tearDown():
public function setUp(): void
{
$this->patchwork->getFactory()->patch(...);
parent::setUp();
}
public function tearDown(): void
{
$this->patchwork->getFactory()->unpatchAll();
parent::tearDown();
}
Patchwork with Laravel Facades:
Patch facade methods (e.g., Hash::make()) by targeting the underlying class:
$patchwork->getFactory()->patchMethod(
function ($obj, $args) { return 'hashed'; },
'Illuminate\Support\Facades\Hash',
'make'
);
Scope Leaks:
unpatchAll() or unpatch() can cause tests to interfere with each other.tearDown() or use context managers (e.g., Patchwork\Context).Static Method Ambiguity:
\Some\Namespace\Class::method).Performance Overhead:
Global State Pollution:
date, time) can affect unrelated parts of the application.Closure Captures:
$this) may not behave as expected in static contexts.$patchwork->getFactory()->patch(
fn() => 'value',
'some_function'
);
Verify Patches:
Use Patchwork\Patchwork::getPatches() to inspect active patches:
print_r($patchwork->getPatches());
Patch Order Matters:
Later patches override earlier ones. Use unpatch() to revert selectively:
$patchwork->getFactory()->unpatch('some_function');
Logging Patches:
Wrap patch logic in try-catch blocks to log failures:
try {
$patchwork->getFactory()->patch(...);
} catch (\Exception $e) {
\Log::error("Patch failed: " . $e->getMessage());
}
Custom Patchwork Factory:
Extend Patchwork\Factory to add domain-specific patching logic:
class CustomFactory extends \Patchwork\Factory
{
public function patchLaravelFacade($callback, $facade, $method)
{
$this->patchMethod($callback, $facade, $method);
}
}
Patchwork Middleware: Create middleware to apply patches per request (e.g., for debugging):
class PatchMiddleware
{
public function handle($request, Closure $next)
{
$patchwork = app(Patchwork::class);
$patchwork->getFactory()->patch(...);
return $next($request);
}
}
Patchwork with PHPUnit:
Use Patchwork\Context to scope patches to specific test methods:
use Patchwork\Context;
public function testSomething()
{
Context::patch($patchwork, function () {
// Patches active only here
$this->assertTrue(true);
});
}
Patchwork for Debugging:
Temporarily patch dd() or dump() to log variables:
$patchwork->getFactory()->patch(
function (...$args) {
\Log::info('Dumped: ' . print_r($args, true));
return $args[0];
},
'dd'
);
How can I help you explore Laravel packages today?