suhock/disposable

A minimal disposable library for deterministic resource cleanup

Maintainers

Package info

github.com/suhock/php-disposable

pkg:composer/suhock/disposable

Transparency log

Statistics

Installs: 5

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0 2026-07-18 12:47 UTC

This package is auto-updated.

Last update: 2026-07-18 12:57:47 UTC


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
    }
}