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

Mod Laravel Package

tey/mod

A lightweight Laravel/PHP package for building modular applications: organize code into self-contained modules, register providers, and keep large projects maintainable with a simple, configurable structure. Ideal for teams splitting features into reusable components.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the baseline. composer require tey/mod and publish config/mod.php. With no config file the package defaults to the laravel layout, which places files exactly where make:* would — so installing it is a no-op until you opt into something. Verify that assumption before you change anything:

php artisan list mod          # generators this layout actually exposes
php artisan mod:list --json    # active layout, file types, templates, scaffolds, discovery
php artisan help mod:model     # per-command placement options

Where to look first. Three sources, in this order:

  1. php artisan mod:list --json — the machine inventory. It is the ground truth for this app; do not infer placement from the README.
  2. vendor/tey/mod/docs/layouts.md — every built-in layout's folder map. Read it at the pinned version, since it ships inside the package and shifts on upgrade.
  3. app/Providers/* — grep for Mod::layout(. A call with a built-in name customises it; any other name defines a new one. This is where the app's real structure lives.

First use case. Take an existing app that already has a directory convention (say app/Modules/Billing) but where developers keep hand-moving generated files out of app/Models. Declare the layout once:

use Tey\Mod\Facades\Mod;

Mod::layout('modules')
    ->path('app/Modules/{module}')
    ->allowsNesting();

Then swap php artisan make:model Billing/Invoice -mf for php artisan mod:model Billing:Invoice --all. Same Laravel generator underneath, same options, but the model, factory, migration, seeder, policy and controller land in the module instead of scattering. That single swap — replacing make:* with mod:* in the team's muscle memory — is the whole value proposition for most apps. Everything else is refinement.

If you are on the ddd preset or any custom layout with roots outside app/, run php artisan mod:autoload immediately. Generation warns and still writes files when a root is not autoloaded, so a silent broken autoloader is the most common first-day failure.

Implementation Patterns

Declare layouts in a dedicated provider. Keeping Mod::layout(...) out of AppServiceProvider makes the structure auditable and gives agents one file to read. Order matters inside a chain — extends() first, then everything else:

// Areas inherit the modules folders, but use their own token and root.
Mod::layout('areas')
    ->extends('modules')          // must be first; copies parent at this moment
    ->path('src/Areas/{area}')
    ->generates('validator', in: 'Modules/{area}/Validators');

Each {placeholder} in a folder becomes a command option, so {area} yields --area=. Add ->allowsNesting() if you want Sales/Quotes style sub-groups, and name the dimension explicitly on layouts with more than one group.

Dry-run everything, twice. The authoring loop that pays for itself is --dry-run --json → inspect → run for real:

php artisan mod:model Billing:Invoice --all --dry-run --json
php artisan mod:model Billing:Invoice --all

The plan carries files (alias, file type, path, class, group, existing/exists), inserts (destination, anchor, rendered stub), warnings, and would_write. Since v0.7.2, scaffold plans also carry an optional state per whole-file member — unchanged or edited, decided by byte-comparing the existing file against what the recipe would write now, so a rerun tells you whether it would drift a kept file. Human output labels kept members with the same state. Part and tree scaffolds now reuse the root migration's resolved identity — including the written timestamp — so the predicted migration filename in the plan matches the one a real run produces. In code review, a pasted plan is a far better artefact than "I ran the generator". Read warnings and would_write before committing — see the gotcha below about exit codes.

Write agent instructions around the inventory, not around commands. The package is explicitly built for coding agents, and the pattern that works is: tell the agent to run mod:list --json first, then the matching mod:* generator, then --dry-run --json before writing. The missing_placement warning carries the dimension, accepted options and available choices, which is enough for an agent to self-correct without a human round-trip. Scaffold plans no longer repeat placeholder warnings per class member (each member is rendered by its own generator now), so a scaffold warning list is shorter and easier to route on. If the app uses Laravel Boost, the shipped resources/boost/ guidance arrives automatically; otherwise copy the mod-development skill into your AGENTS.md and trim it.

Codify repeated clusters as recipes. One-off generators handle single files; recipes handle the thing your team actually does weekly. mod:crud Knowledge:Document --dry-run --json describes model, migration, factory, store/update requests, resource, policy and controller in one shot. Re-running a recipe against a half-generated cluster is safer than it used to be: use --skip-existing, then read each member's state in the dry-run to see which kept files are unchanged and which are edited (i.e. would diverge if regenerated). If your domain has a recurring cluster (a resource plus tabs, a command plus handler plus validator), register the recipe and confirm it appears in mod:list --json before advertising it — these are application-registered examples, not built-ins.

Standardise stubs with templates, not with review comments. When a generated class needs a house header, a base class, or a specific constructor shape, snapshot a curated class and let the generator reuse it:

php artisan mod:template --from=App\\Support\\Concerns\\HasAudit --into=stubs/mod/Probe --dry-run --json

Pair this with mod:bases, which emits the missing base classes a layout expects (existing ones are kept). Together they mean the layout carries the conventions, rather than the code review doing it.

Treat the frontend config as explicit, not implied. ->frontend(pages: ..., components: ..., css: ..., views: ..., pageName: ...) customises paths, and omitted arguments keep defaults. Do not assume an Inertia stack from the presence of pages; inspect frontend paths and page_name in mod:list --json before writing UI generators into a Blade app. Built-in page folders follow the app's existing pages/Pages casing, while explicit overrides keep their spelling — so mixed casing is a real outcome.

Wire it into CI cheaply. You do not need to run generators in CI. A useful, low-cost guard is a test that asserts the layout inventory matches the folders on disk, plus a lint pass over the provider that declares the layout. For anything more sophisticated, composer test-style gates are the package's own concern, not yours.

Gotchas and Tips

--dry-run --json exits 0 even when the plan is invalid. A missing answer sets would_write: false but the command still succeeds. Scripts and agents that check only the exit code will happily proceed on a plan that writes nothing. Always parse would_write, and treat any non-empty warnings as requiring resolution. Warning codes are stable (missing_placement) but tolerate additional codes and fields — the schema is additive, and state (added in v0.7.2) is a good example: it is optional and only present for scaffold whole-file members.

extends() must be first, and it copies eagerly. It snapshots the parent at the call site, not by reference. Chain it after path() and you will silently get the parent's folder layout instead of your own. Similarly, a token declared with path() on the parent is inherited and cannot be renamed by the child; only name-derived tokens (child name areas → area) follow the child.

Roots outside app/ need mod:autoload. Choosing ddd or any custom root means src/Domain/ needs a PSR-4 entry. Generation does not add it — it warns and writes anyway, producing classes Composer cannot load. Run php artisan mod:autoload --dry-run first to preview the composer.json diff, then for real. Resolve conflicting mappings by hand; the command will not guess. In scripted environments use --no-dump when your build already runs Composer.

mod:migration refuses --path and --realpath. Scripts copied from make:migration will break, and that is deliberate — the whole point is that placement comes from the layout, not from an ad-hoc path flag. Rewrite those invocations to use --in or the group dimension. --create and --table still work.

Discovery is cached. After changing the layout, adding a file type, or moving folders, stale discovery produces confusing results. php artisan mod:cache rebuilds, php artisan mod:clear clears, and increasing verbosity shows rejected files and why. When a generator "can't find" a class that clearly exists, suspect the cache before the layout.

Case-only folder collisions are checked, not prevented. mod inspects frontend paths for case-only collisions at compile time, but a custom API Resources/ folder next to resources/ is the classic clash on macOS and Windows. The documented escape hatch is moving frontend paths under ui/. This matters more than usual here because CI runs a Windows matrix — a layout that works on your Mac may not work on the maintainer's.

Nested groups need a named dimension. Calling ->allowsNesting() on a layout with multiple group placeholders without naming the dimension produces ambiguous options. Name it once, in the layout.

Pin the version and read the vendored docs at that pin. The package is at zero stars and zero dependents, which means you are an early adopter and the docs you read on GitHub may not match what is installed. vendor/tey/mod/docs/layouts.md is authoritative for the version you run. The contributor conventions are a genuinely good signal — ~2,000 Pest tests, PHPStan over src and tests, Pint, a Linux/Windows matrix across PHP 8.3–8.5 and Laravel 12–13, plus a published-API snapshot and audits — so the risk is adoption breadth and API churn, not neglect. v0.7.2 is a purely additive/fix release within that policy: new optional state in dry-run JSON, and bug fixes to scaffold --force/--skip-existing behaviour and migration identity — no breaking surface.

Stay on @api, treat everything else as private. The public surface is marked @api; the rest is @internal. Extend through the layout DSL (extends, path, generates, allowsNesting, frontend) and through recipes and templates, not by subclassing internals or reaching into the plan objects. The snapshot in tests/Fixtures/published-api.json and the audits under tests/Unit/Audit/ exist to make API changes deliberate — which is exactly why depending on non-@api symbols is a bet against the maintainers' own policy. The new state field lands inside the plan JSON, which is the documented schema; don't start keying off internal renderer classes to derive it yourself.

Version floor is real. PHP 8.3+ and Laravel 12/13 only. This is not a package you can introduce into an L10/L11 app while you are mid-upgrade; sequence it after the framework bump, when make:* habits are already being renegotiated.

--force and --skip-existing are still not symmetric, but scaffolds now tell you more. As of v0.7.2, scaffold tree roots and parts honour both flags — previously a part of a tree could ignore them and clobber (or fail to preserve) kept members. --skip-existing keeps existing scaffold members, which is the right default when re-running a recipe against a partially generated cluster. --force only permits overwriting files the command supports, and generated bases are always kept regardless. Preview with --dry-run --json and read the per-file exists and the per-member state (unchanged vs edited) rather than guessing which files a re-run will clobber — the plan's predicted outcome now matches what a real run does for timestamped migration paths and kept members alike.

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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
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
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core