suhock / disposable
A minimal disposable library for deterministic resource cleanup
Requires
- php: ^8.3
Requires (Dev)
- ergebnis/phpstan-rules: ^2.0
- friendsofphp/php-cs-fixer: ^3.0
- phpstan/phpstan: ^2.2
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^12.0 || ^13.0
README
A minimal primitive for deterministic resource cleanup: the DisposableInterface
contract, and a using() helper that runs a callback and disposes afterward.
Installation
composer require suhock/disposable
Usage
Implement DisposableInterface on anything that must release something when its
lifetime ends, and wrap its use in using():
use Suhock\Disposable\DisposableInterface; use function Suhock\Disposable\using; final class UnitOfWork implements DisposableInterface { private bool $disposed = false; public function __construct( private readonly Connection $connection, ) {} public function dispose(): void { if ($this->disposed) { return; } $this->disposed = true; $this->connection->rollBackIfActive(); } } // using() runs the callback, then disposes $work, whether run() returns or throws $result = using( new UnitOfWork($connection), fn(UnitOfWork $work) => $work->run(), );
If both the callback and dispose() throw, the callback exception takes
precedence and propagates, while the disposal exception is discarded.
The disposal pattern
Disposal exists to release resources deterministically without having to rely on
the timing of the garbage collector to call __destruct(). This can be
particularly useful in background daemons, event loops, or other long-running
processes where resource contention is a risk.
Three rules make an implementation robust.
1. Make dispose() idempotent.
Disposing an already disposed object must do nothing; guard it with a flag
(as above) so the release happens exactly once.
2. Separate the object's own release from cascading.
If an object owns other disposables, dispose them from the object's dispose(),
as well as releasing whatever the object holds directly.
Implement __destruct() as a backstop for objects dropped without a dispose(),
but limit it to the object's own release and never cascade from it. During cycle
collection and shutdown PHP runs destructors in an undefined order, so a disposable
you own may already have been destroyed by the time your __destruct() runs.
Anything that must be released at a definite point belongs in dispose(), not
__destruct(). When an object owns several disposables, cascade them in reverse
construction order: release dependents before the things they depend on.
3. Guard against use after disposal.
Throw a DisposedException (or similar) to prevent accidental use of a disposed
object.
Example
use Suhock\Disposable\DisposableInterface; use Suhock\Disposable\DisposedException; // One iteration of a queue worker's loop. Both resources it holds are contended. // Leaving them to the garbage collector would stall the other workers waiting on // the pool and the lock. final class Job implements DisposableInterface { private bool $disposed = false; private readonly Connection $connection; private readonly Lock $lock; public function __construct( private readonly ConnectionPool $pool, LockManager $locks, ) { $this->connection = $pool->lease(); // must return to pool when done $this->lock = $locks->acquire('reindex'); // must dispose when done } public function run(): void { DisposedException::throwIf($this->disposed, $this); // ... perform some work ... } public function dispose(): void { $this->disposeSelf(); // return our connection to the pool $this->lock->dispose(); // then cascade to the lock we own } public function __destruct() { $this->disposeSelf(); // backstop: return the connection, no cascade } private function disposeSelf(): void { if ($this->disposed) { return; } $this->disposed = true; $this->pool->release($this->connection); // returned to the pool } }