Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Composer Stager Laravel Package

php-tuf/composer-stager

A PHP library for staging Composer operations in a separate directory before applying them to your project, enabling safer dependency updates, rollback, and testable deployments without touching your active codebase.

View on GitHub
Deep Wiki
Context7

Getting Started

composer-stager wraps a Composer operation in a four-phase lifecycle so you never mutate the live codebase until the change has been fully resolved in a scratch directory.

composer require php-tuf/composer-stager

Requirements: PHP 8.1+, a working composer binary reachable from the PHP process (not just your shell), and proc_open enabled.

Where to look first. Start in ComposerStager\API\Core — the four interfaces you will actually call are:

Interface Responsibility
BeginnerInterface::begin() create/seed the staging dir from the active dir
StagerInterface::stage() run the Composer command inside staging
CommitterInterface::commit() sync staging back over active
CleanerInterface::clean() delete the staging dir

Everything under ComposerStager\Infrastructure\ is the default implementation; ComposerStager\API\Enum\Command holds the supported command names (REQUIRE, UPDATE, REMOVE).

The package is DI-first — it is not meant to be hand-wired with new. The repo's examples/ directory contains a standalone container wiring you can copy, and the ComposerFactoryInterface is your entry point for producing a ComposerInterface.

First use case. A deploy-adjacent "apply a security update" job:

use ComposerStager\API\Core\Beginner\BeginnerInterface;
use ComposerStager\API\Core\Cleaner\CleanerInterface;
use ComposerStager\API\Core\Committer\CommitterInterface;
use ComposerStager\API\Core\Composer\ComposerInterface;
use ComposerStager\API\Core\Stager\StagerInterface;
use ComposerStager\API\Enum\Command;

function stagePackageUpdate(
    BeginnerInterface $beginner,
    StagerInterface $stager,
    CommitterInterface $committer,
    CleanerInterface $cleaner,
    ComposerInterface $composer,
    string $activeDir,
    string $stagingDir,
    string $package,
): void {
    $exclusions = ['vendor', 'storage', '.git', 'node_modules'];

    $beginner->begin($activeDir, $stagingDir, $exclusions);

    try {
        $stager->stage(
            $composer,
            $activeDir,
            $stagingDir,
            [Command::REQUIRE, $package, '--with-all-dependencies'],
        );

        $committer->commit($activeDir, $stagingDir, $exclusions);
    } finally {
        $cleaner->clean($activeDir, $stagingDir, $exclusions);
    }
}

Implementation Patterns

One begin() per logical change set. You can call stage() several times between a single begin()/commit() pair — e.g. require the new package, then update its dependencies. Each call shells out to Composer with cwd set to the staging directory; the composer.json/composer.lock state accumulates across calls.

Always wrap in try/finally. clean() is idempotent and safe to call after a failed begin(), so the finally block is your only protection against a stale staging directory blocking the next run (see Gotchas).

Exclusions are how you protect user data. The third argument on begin()/commit() is an array of paths relative to the active dir. Anything excluded is neither copied to staging at begin nor synced back at commit. For a Laravel app the usual set is vendor, node_modules, .git, storage, and any public/uploads-style directory. Note the trade-off: excluding vendor means a composer require will update composer.lock in the active dir but leave vendor/ untouched — you must run composer install afterwards.

Stream output to the user. Implement ComposerStager\API\Core\IO\IOInterface (a single write() method plus a severity constant) and hand it to stage(). In a queued job, write to a log channel; in an artisan command, write to $this->output. Use ComposerStager\Infrastructure\Service\IO\NullIO when you don't care.

Laravel integration shape. Bind the four core interfaces plus ComposerInterface in a service provider, wrap the lifecycle in a ComposerStager service, and invoke it from either a queued job or an artisan command:

Cache::lock('composer-stager', 600)->block(5, function () use ($service) {
    $service->require(base_path(), storage_path('app/composer-staging'), 'vendor/package');
});

There is no internal locking — a database/cache lock is mandatory if two workers or two admins can trigger a staging run.

Fake the Composer layer in tests. Implement ComposerInterface with a spy that writes a marker file into the staging dir; you then exercise the full begin/stage/commit/clean cycle against a temporary directory tree without running real Composer.

Pre-flight before begin(). Assert: staging parent is writable, staging dir is absent or empty, free disk ≥ the active dir size, and the active dir is a real Composer project. Doing this up front gives users a clear error instead of a Filesystem exception halfway through.

Gotchas and Tips

The staging directory must be empty or non-existent. begin() will not seed into a directory containing leftovers. If a previous run crashed, either call clean() first or make your wrapper do it defensively at the start of every run.

Keep staging on the same filesystem as active. The committer prefers rsync/hard links; a staging dir on a different mount (e.g. storage/app vs /var/www) degrades to a full PHP copy, which is dramatically slower on large trees.

commit() is not atomic. It walks staging and writes file by file. An interrupted commit can leave the active tree half-updated. Take a snapshot/backup (or a git tag) before committing, and treat a commit failure as a serious incident rather than a retryable error.

Exclusion paths have no leading slash and no globs. 'vendor/*' is not a thing; use 'vendor'. Getting this wrong silently excludes nothing, and you'll find out when user uploads get rewritten.

The Composer binary may not be on PATH for the process that runs this. PHP-FPM, systemd units, and queue workers routinely have a different PATH than your login shell. Configure the executable explicitly through ComposerFactoryInterface rather than relying on discovery — a failure here surfaces as a generic "composer not found" process exception.

Composer scripts and plugins run against staging, with cwd = staging. Plugins that resolve paths from realpath(__DIR__) are fine; plugins that hardcode absolute production paths or touch the database will behave unpredictably. Audit post-install-cmd / post-update-cmd before enabling staging on a production clone, and consider --no-scripts for the initial rollout.

Timeouts and memory. This is a subprocess, so PHP's max_execution_time still applies to the waiting parent. Set set_time_limit(0) (or raise it well past your Composer budget) in artisan/job context, and make sure proc_open, exec, and escapeshellarg are not in disable_functions — Symfony Process fails with a confusing message when they are.

Catch ComposerStager\API\Exception\ExceptionInterface, not \Throwable alone. Its getCode() constants distinguish precondition failures (bad dir, wrong state) from Composer failures (non-zero exit), which lets you show an actionable message instead of raw output. Composer's own stderr is available via your IOInterface implementation.

Watch the major version. Most blog posts and Stack Overflow answers describe the 1.x layout (Domain\ / Value\ classes). 2.x moved everything into API\ and replaced several value objects with enums. Pin the major version in composer.json and read the installed interface signatures — stage()'s argument shape in particular has changed.

Windows and containers. Without rsync, commit falls back to PHP filesystem operations: correct but slow, and symlink handling differs. If you're staging inside a container, mount the staging parent as a volume or all work is lost on restart mid-commit.

Extension points worth knowing. Decorate ComposerInterface to inject global flags (mirrors, auth tokens, --prefer-dist), to log every command, or to point at a non-default Composer executable. Decorate CommitterInterface if you need a post-sync hook (cache clear, opcache reset, release marker) — that is the last moment before you declare success.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect