eliashaeussler/deep-closure-comparator
PHPUnit comparator/assertion that deeply compares closures inside objects/arrays by serializing them via opis/closure. Use DeepClosureAssert::assertEquals() as a drop-in replacement for PHPUnit’s assertEquals when closures should be treated as equal.
composer require --dev eliashaeussler/deep-closure-comparator
DeepClosureAssert class in your test file:
use EliasHaeussler\DeepClosureComparator\DeepClosureAssert;
use PHPUnit\Framework\TestCase;
self::assertEquals() with DeepClosureAssert::assertEquals() for closure comparisons:
DeepClosureAssert::assertEquals($expectedClosure, $actualClosure);
use EliasHaeussler\DeepClosureComparator\DeepClosureAssert;
public function testMiddlewareClosure()
{
$expected = fn ($request) => $request->merge(['middleware' => 'processed']);
$actual = fn ($request) => $request->merge(['middleware' => 'processed']);
DeepClosureAssert::assertEquals($expected, $actual);
}
DeepClosureAssert.php: Core assertion logic and usage examples.src/Comparator/ClosureComparator.php: Underlying comparison mechanism (uses opis/closure).tests/: Example test cases for edge scenarios (nested closures, bound variables).DeepClosureAssert::assertEquals() instead of PHPUnit’s default:
// Before (fails for closures)
self::assertEquals($expected, $actual);
// After (works for closures)
DeepClosureAssert::assertEquals($expected, $actual);
$nestedExpected = fn () => fn () => 'deep';
$nestedActual = fn () => fn () => 'deep';
DeepClosureAssert::assertEquals($nestedExpected, $nestedActual);
public function testEventListenerClosure()
{
$expected = fn (OrderPlaced $event) => Log::info('Order placed: '.$event->id);
$actual = app()->make(OrderPlacedListener::class)->handle(...);
DeepClosureAssert::assertEquals($expected, $actual);
}
public function testServiceBindingClosure()
{
$expected = fn () => new UserRepository();
$actual = app()->make(UserRepository::class);
// Compare the binding closure (if accessible)
DeepClosureAssert::assertEquals($expected, app()->bound('App\Repositories\UserRepository') ?? null);
}
public function testMiddlewareChain()
{
$middleware = app()->make(Middleware::class);
$expected = fn ($request) => $middleware->handle($request, fn () => 'next');
DeepClosureAssert::assertEquals($expected, $middleware->getClosure());
}
function assertClosuresEqual($expected, $actual)
{
DeepClosureAssert::assertEquals($expected, $actual);
}
Usage:
assertClosuresEqual(fn () => 1, fn () => 1);
opis/closure) can be resource-intensive.$serialized = \Opis\Closure\serialize($closure);
PHPUnit Version Mismatch
^10.0 in composer.json or use a test project to validate compatibility."require-dev": {
"phpunit/phpunit": "^10.0",
"eliashaeussler/deep-closure-comparator": "^1.2"
}
Closure Variable Capture
use ($user)) may fail if the variables differ in type/value.opis/closure directly:
$serialized1 = \Opis\Closure\serialize(fn () => $user->id);
$serialized2 = \Opis\Closure\serialize(fn () => $user->id);
self::assertEquals($serialized1, $serialized2);
Dynamic Closures
create_function or eval) may not serialize predictably.opis/closure::serialize() explicitly.Laravel-Specific Edge Cases
app()->bind(), ensure they’re not singleton instances with side effects.// ❌ Fails if $user->id differs
DeepClosureAssert::assertEquals(
fn () => $user->name,
fn () => $user->name
);
False Positives with Anonymous Functions
opis/closure::serialize() to inspect differences:
dd(\Opis\Closure\serialize($closure1), \Opis\Closure\serialize($closure2));
$serialized = \Opis\Closure\serialize($closure);
file_put_contents('closure_serialized.txt', $serialized);
$outer1 = fn () => fn () => 'test';
$outer2 = fn () => fn () => 'test';
DeepClosureAssert::assertEquals($outer1(), $outer2()); // Compare inner closures
getComparator() for Custom Logic
Access the underlying comparator for advanced use:
$comparator = DeepClosureAssert::getComparator();
$result = $comparator->compare($expected, $actual);
Custom Comparator
Extend EliasHaeussler\DeepClosureComparator\Comparator\ClosureComparator to handle Laravel-specific cases:
class LaravelClosureComparator extends ClosureComparator
{
protected function compareVariables($expected, $actual)
{
if ($expected instanceof Closure && $actual instanceof Closure) {
// Add Laravel-specific logic (e.g., ignore container bindings)
return true;
}
return parent::compareVariables($expected, $actual);
}
}
Register it in DeepClosureAssert:
DeepClosureAssert::setComparator(new LaravelClosureComparator());
Integration with Laravel’s TestCase
Override createApplication() to include the package:
use EliasHaeussler\DeepClosureComparator\DeepClosureAssert;
class TestCase extends \Illuminate\Foundation\Testing\TestCase
{
protected function setUp(): void
{
parent::setUp();
DeepClosureAssert::setComparatorFactory(fn () => new ClosureComparator());
}
}
Performance Optimization Cache serialized closures in a static map:
static $closureCache = [];
function serializeClosure(Closure $closure)
{
$key = spl_object_hash($closure);
return $closureCache[$key] ?? $closureCache[$key] = \Opis\Closure\serialize($closure);
}
config/ or service provider: The package is assertion-only; no Laravel-specific setup is required.opis/closure is compatible with your PHP version (v4.4+ for PHP 8.5).How can I help you explore Laravel packages today?