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

Roster Laravel Package

laravel/roster

Laravel Roster is a dev-only detection package that reads lockfiles, config markers, and optional source code to report installed packages, app stack and frontend, browser test tools, AI agents and editors, JS package managers, and codebase conventions via the Project facade or roster:scan command.

View on GitHub
Deep Wiki
Context7

Getting Started

Install as a dev dependency — this package is for tooling, generators, CI jobs, and internal developer experience, not for production request paths:

composer require laravel/roster --dev

The Project facade is the first thing to reach for inside a Laravel app. The first call triggers a scan of base_path(); every later call in the same process reuses that result:

use Laravel\Roster\Enums\Stack;
use Laravel\Roster\Facades\Project;

if (Project::php()->uses('inertiajs/inertia-laravel', '^2.0')) {
    // ...
}

Project::stacks()->uses(Stack::InertiaReact);

The fastest way to see what Roster actually knows about a codebase is the Artisan command, which dumps the whole surface as JSON:

php artisan roster:scan
php artisan roster:scan /path/to/other/project

Run it once on your own app before writing any code against the facade — it tells you exactly which packages, stacks, editors, and package managers are detected, which saves you a round of guessing at enum names or lockfile expectations.

The canonical first use case is conditional scaffolding: a starter-kit installer, a make:*-style generator, or a package's install command that needs to decide between Pest and PHPUnit, Livewire and Inertia, npm and pnpm. Scan once, branch on the answer, skip the interactive question entirely.

When you are outside a Laravel app — a CLI tool, a standalone script, a test fixture — skip the facade and use the manager with an explicit path:

use Laravel\Roster\ProjectManager;

$project = (new ProjectManager)->scan('/path/to/project');

Implementation Patterns

Gate on packages, then on versions. Presence alone is a weak signal; constraints make intent explicit. Note that a bare version is an exact match, so prefer caret/range constraints:

$project->php()->uses('laravel/framework', '^12.0');
$project->php()->uses('laravel/framework', '>=11 <14');

Pick OR vs AND deliberately. uses() is OR across an array; usesAll() is AND. The array shape must be all-indexed or all-associative:

$project->php()->uses(['pestphp/pest', 'phpunit/phpunit']);           // any
$project->php()->usesAll(['pestphp/pest' => '^3.0', 'laravel/framework' => '^11.0']); // all

Distinguish direct from transitive. This is the difference between "the app chose this" and "something dragged it in" — critical for generators, where you only want to react to deliberate choices:

if ($project->php()->usesDirect('livewire/livewire')) {
    // the developer opted into Livewire
}

$project->php()->packages()->direct();
$project->php()->packages()->dev();

Read the underlying Package when you need numbers. version(), major(), and friends let you branch without re-parsing lockfiles yourself:

$major = $project->js()->package('vue')?->major();

if ($project->minimumPhpVersion() === '8.3') {
    // emit readonly-class style scaffolding
}

Wrap detection behind your own small service. A generator that calls Project:: in twenty places is untestable and couples you to the facade's caching. Put the questions behind one class, inject a Project instance, and swap in a fake in tests:

final class ProjectProfile
{
    public function __construct(private Project $project) {}

    public function usesPest(): bool
    {
        return $this->project->php()->usesDirect('pestphp/pest');
    }

    public function packageManager(): JsPackageManager
    {
        return $this->project->js()->packageManager() ?? JsPackageManager::Npm;
    }
}

Use approaches() for style-conforming codegen, not for linting. It is the only part of the package that reads your actual source, and it is exactly what you want when generating a new model, command, or form request into an existing codebase that has already picked a convention:

use Laravel\Roster\Enums\Approach;

$fillable = $project->approaches()->uses(Approach::MassAssignmentFillable);
$uuidKeys = $project->approaches()->uses(Approach::ModelKeyUuid);

$result = $project->approaches()->result(Approach::ValidationPipeSyntax);
// $result->confidence, ->matched, ->total, ->paths

Use the enum sets as a single membership question. stacks(), frontends(), browserTestFrameworks(), agents(), and editors() all behave identically — uses() (any), usesAll() (every), all() (everything detected). Agents and editors are filesystem markers, which makes them useful for deciding which instruction files your tooling should write:

if ($project->agents()->uses(Agent::ClaudeCode)) {
    // emit CLAUDE.md / .claude content
}

if ($project->js()->usesPackageManager('pnpm')) {
    // generate pnpm-flavoured commands and CI steps
}

In CI, prefer the command over PHP. roster:scan emits JSON, so you can assert on it with jq without bootstrapping the framework inside a script:

php artisan roster:scan --approaches | jq '.php.packages'

Gotchas and Tips

It is a dev dependency — do not call it from production code. In a deploy that runs composer install --no-dev, the facade class will not exist. Keep all usage inside console commands, generators, and dev-only service providers, and guard registration with $this->app->runningInConsole() or an environment check.

Approaches are not cached, and they are expensive. They are computed lazily, once per scan instance, only when you ask. toArray() and json() omit them, and roster:scan requires --approaches. Scanning a large repo means reading every PHP file under your PSR-4 roots — do not call approaches() in a tight loop or from a request-time helper.

Approaches abstain quietly. A result needs at least 3 votes and 80% of votes cast. A 2/3 majority is rejected; an evenly split codebase produces nothing. Small or mixed codebases will return null from result() more often than you expect — always handle the null case rather than assuming a default style. Detection is lightweight pattern matching, not a parser, so a comment or string literal mentioning $fillable can cast a vote.

The cache key is content-hash based. Editing composer.lock, package.json, or even adding a .claude directory invalidates the persisted entry. That is usually what you want, but it means a scan right after a dependency change is always cold. Use Project::fresh() to force a bypass of both the in-process memoization and the cache — necessary in Octane and long-running queue workers, where the memoized instance survives until the worker restarts.

ProjectManager::scan() falls back to getcwd(). If base_path() is not meaningful in your context (a queued job, a package test, a monorepo subdirectory), pass the path explicitly rather than relying on the fallback.

Lockfile ambiguity is resolved silently. Only one JS lockfile should be committed; if several are present, Roster picks npm, then pnpm, then Yarn, then Bun — in that order. A stale package-lock.json left behind after migrating to pnpm will quietly make packageManager() report npm. Worth asserting on in CI.

Transitive dev/production classification is incomplete for some managers. It works for Composer and npm lockfiles; Yarn, pnpm, and Bun report transitive packages as production. If you are filtering on packages()->dev(), be aware the result degrades on those projects. usesDirect() sidesteps this entirely and is the safer primitive for generator logic.

Mixing array shapes throws. Both uses() and usesAll() require an entirely indexed or an entirely associative array. ['pestphp/pest', 'phpunit/phpunit' => '^10.0'] raises InvalidArgumentException at runtime — a trivial mistake to make when refactoring.

Enum set uses() accepts arrays too. stacks()->uses([Stack::A, Stack::B]) is OR, not AND, mirroring the package API. It reads similarly to usesAll() at a glance; prefer the single-case form when you mean one thing.

Extension points are effectively closed. Detection is driven by internal enums and hardcoded manifest/lockfile readers; there is no documented plugin or detector registry. If you need a signal Roster does not provide, compose around it rather than trying to hook in — combine packages() with your own file checks, and keep your detection logic in your own class so a future Roster change does not ripple through your codebase.

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.
bernskioldmedia/laravel-matomo
bernskioldmedia/laravel-harvest
bernskiold/laravel-data-scrubber
bernskiold/laravel-currency-converter
bernskiold/laravel-campaign-monitor
bernskioldmedia/laravel-autotranslate
siberfx/telegram-bot
roy-sandip/laravel-view-extended
gregsphotography/site-builder-core
roy-sandip/laravel-view
anodyne/blade-fluentui-icons
webx-ui/module-catalog-collections
webx-ui/module-catalog-feeds
makidizajnerica/laravel-dto
matviib/scheduler
webx-ui/templates
anodyne/blade-tabler-icons
bref/laravel-websockets
anodyne/blade-hugeicons
1994/flatpack-statamic