internal/destroy
Explicit, deterministic resource cleanup for PHP via a Destroyable interface. Breaks circular reference chains and prevents memory leaks in long-running apps without relying on __destruct() or expensive gc_collect_cycles(). Ideal for daemons and event loops.
Install the package via Composer:
composer require internal/destroy
The core is the Destroyable interface. Implement it on classes managing non-trivial resources (e.g., listeners, connections, async tasks) to expose a deterministic destroy(): void method. This replaces reliance on PHP’s unpredictable __destruct()—especially vital in long-running processes where circular references cause memory leaks.
First use case: Long-running queue workers. Example:
use Internal\Destroy\Destroyable;
class MessageConsumer implements Destroyable
{
private \Iterator $consumer;
private array $cache = [];
public function __construct(\Iterator $consumer)
{
$this->consumer = $consumer;
}
public function consume(): ?array
{
return $this->consumer->current();
}
public function destroy(): void
{
// Explicitly release resources
$this->cache = [];
$this->consumer = null;
// Close socket/connection if applicable
}
}
Invoke destroy() at worker shutdown (e.g., in finally block or shutdown handler).
EventBus, Server, EventLoop) to cascade destruction. Children must also implement Destroyable.class WebSocketServer implements Destroyable
{
private array $clients = [];
public function addClient(Destroyable $client): void
{
$this->clients[] = $client;
}
public function destroy(): void
{
foreach ($this->clients as $client) {
$client->destroy();
}
$this->clients = [];
// Close server socket, detach event loop, etc.
}
}
Service container shutdown: In Laravel, register a singleton listener that destroys all Destroyable services on App::beforeResponseShutdown() or custom lifecycle hooks.
Test cleanup: In PHPUnit, add destroy() calls to tearDown() to prevent cross-test pollution from lingering state.
Caching layers: Wrap cache adapters that hold internal pools (e.g., Redis cluster clients) in Destroyable to flush buffers and release connections.
destroy() is never auto-called. You must inject shutdown hooks or hooks in worker loops— forgetting this defeats the purpose. Use register_shutdown_function() or Laravel’s Queue::after() for workers.destroy(): Don’t duplicate cleanup logic between __destruct() and destroy(). Prefer destroy() for long-running contexts; let __destruct() be a fallback only if absolutely necessary (but know it may not run).Destroyable dependencies: Laravel’s DB or Cache instances won’t implement Destroyable out-of-the-box. Manually release connections (e.g., DB::purge(), Cache::flush()) inside your destroy() implementation—even if the underlying service doesn’t.phpstan-phpunit to catch un-called destroy() on Destroyable types. Architectural tests (e.g., phpunit-architecture-test) can enforce that all queue job handlers implement Destroyable.destroy(), verify impact with Xdebug Profiler or memory_get_usage(true) before/after shutdown—don’t assume cleanup happened.destroy(). PHP auto-cleans at request end. This package targets only persistent processes.How can I help you explore Laravel packages today?