ibecsystems/laravel-porto
A Laravel package for building applications with Porto architecture. It helps organize code into Containers, Ship, and Components, with Artisan scaffolding for modular, domain-focused development that stays clean and maintainable as projects grow.
Porto (the "Porto" / Containers-Ship pattern by Mahmoud Zalt) splits a Laravel app into two top-level namespaces:
Ship/ — shared infrastructure: base classes, abstract repositories, cross-cutting helpers.Containers/ — one directory per business capability, each self-contained (Actions/, Tasks/, Models/, Data/, UI/ with routes/controllers, plus its own Providers/, Migrations/, Tests/).ibecsystems/laravel-porto is a scaffolding package: it registers a service provider that boots a set of porto:* generator commands so you don't hand-create that tree every time.
Minimal steps:
composer require ibecsystems/laravel-porto:^3.0 --dev
Treat it as a dev dependency — it exists to generate code, not to serve runtime logic.php artisan list | grep porto
The list output is your source of truth. This is a low-traffic package (maintained by the IBEC-BOX org, now on the v3 line with v3.0.2 as the latest patch), so do not assume the README's command names are complete or current — verify against src/Commands/ in vendor/ibecsystems/laravel-porto.composer.json:
"autoload": {
"psr-4": {
"App\\": "app/",
"Containers\\": "src/Containers/",
"Ship\\": "src/Ship/"
}
}
Then composer dump-autoload.Action, then the controller/route inside that container's UI/ folder. Wire the route in that container's own service provider and register that provider in bootstrap/providers.php.Where to look first, in order: the package's composer.json and src/PortoServiceProvider.php (what's registered, config key names), then src/Commands/ (signature + available stubs), then a generated container (to see the exact namespace/class layout the tool assumes).
The day-to-day value is narrow but real: it standardizes where a "feature" lives so you never debate folder structure again, and it makes containers detachable — you can delete a container folder and the app still boots if nothing references it.
Action-per-endpoint, Task-per-step. The core discipline is that an Action is a single public entry point (usually run() or handle()) that orchestrates Tasks. Controllers should be thin: validate via a Request, call one Action, return a Transformer-formatted response. If an Action starts containing if branches over persistence details, extract a Task.
Container ownership of routes. Keep route files inside each container (Containers/<Name>/UI/API/Routes/*.php) and register them from that container's own provider with Route::middleware(...)->group(...). This is the pattern's main ergonomic win — routes, controllers, and requests for a capability travel together. Downside: debugging route lists needs php artisan route:list plus knowing that each provider loads its own file, so caching behavior deserves attention (see Gotchas).
Repository interface bound in the container provider. Put the contract in Containers/<Name>/Data/Repositories/<X>RepositoryInterface.php, the Eloquent implementation beside it, and bind in Containers/<Name>/Providers/. Resolve the interface in Actions via constructor injection — never app(XRepository::class) inline, or you lose the swap point the pattern is selling you.
Admin UI stubs are part of the offer. The v3.0.x line ships Filament-oriented generators alongside the plain controller/route ones — the recent v3.0.2 patch is literally a stub fix for Filament 2 use imports. If you use Filament, generate the resource into the owning container's UI/ tree rather than under app/Filament, so admin concerns stay inside the capability that owns them. Verify the generated imports resolve before you build on them (see Gotchas).
Generate-then-edit, not generate-then-trust. The generators produce skeletons. Expect to immediately: add use statements, extend the correct base Ship class, and adjust namespaces. Budget for this — a generator that gets you 70% there is still faster than 0%, but do not paste generated code into a PR without reading it.
Per-container migrations and tests. Keep migrations under Containers/<Name>/Database/Migrations and load them from the container provider with $this->loadMigrationsFrom(...). Same instinct for tests: a Pest/PHPUnit suite that lives next to the container is the only reliable way to prove the container is self-contained enough to extract.
Shared abstractions go to Ship, not to a container. When a second container needs the same base repository or value object, promote it to Ship/. The failure mode of Porto in practice is Containers/Foo importing from Containers/Bar — one such import and you've rebuilt a monolith with extra directories. Add a CI grep or an architecture test to ban cross-container imports:
# fail the build on cross-container coupling
grep -rn "use Containers\\\\" src/Containers --include=*.php \
| grep -vE "use Containers\\\\($(...))" # allow only own container
Integration with the rest of Laravel. Porto does not replace Eloquent, service container, or queue. Jobs, Events, Listeners, and Policies all fit inside the container that owns them. Register event listeners in the container provider. For queued Actions, add ShouldQueue to the Action itself rather than wrapping in a Job — it keeps the call-site identical between sync and async.
Testing shape that actually validates the pattern. One feature test per Action (Action::run(dto) → assert repository calls/response), one unit test per Task with the repository mocked. Because the Action is the seam, these tests survive controller/route refactors — that's the payoff you're buying.
v3.0.2 release (fix: filament2 use classes) confirms active patch releases on the 3.x line. Pin "ibecsystems/laravel-porto": "^3.0" (or a commit hash if you need to reproduce a specific stub output) and pin your Laravel version too. The generators are small; if the package breaks on a future Laravel, forking the porto:* commands into your own app/Console/Commands is a half-day job, not a week.v3.0.2 patched wrong/absent use imports in the Filament 2 templates — meaning Filament-generated code was previously emitting classes that didn't resolve. Two consequences: (a) if you generated Filament resources before upgrading, regenerate or manually correct those imports, don't assume the fix retroactively applies to files already on disk; (b) validate generated output by actually booting the resource (php artisan route:list, open the panel), not just by reading the file.Containers\ / Ship\ PSR-4 mappings produces classes that exist but cannot be autoloaded. Symptom: Class "Containers\Blog\Actions\..." not found immediately after generation. Fix: composer dump-autoload (-o in CI).--dev vs. production. If you install it in require rather than require-dev, you ship generator commands to production where php artisan list exposes them. Move it to require-dev or wrap command registration in $this->app->isLocal().Containers/<Name>/Providers/*ServiceProvider.php must be added to bootstrap/providers.php (Laravel 11+) or config/app.php by hand. A container whose provider isn't registered looks functional until routes are missing — check route:list and event:list first when something "silently doesn't exist."Route::group inside boot() are fine, but anything that does runtime discovery or dynamic middleware in that callback will break under php artisan route:cache. Test route:cache && route:clear in CI on every container-provider change.loadMigrationsFrom inside container providers registers files but ordering is by filename, not by provider boot order. If Foo's migration alters Bar's table, name files with a numeric prefix (2024_01_01_000100_...) so they sort deterministically.src/Commands for a --stubs publish target, or read the command's getStub() path). That's the intended customization hook and it survives regeneration. If no publish hook exists, copy the command class into your app and point it at your own stubs/ directory.Ship\Parents\* that may not exist yet. Error like Class "Ship\Parents\Action" not found means either the PSR-4 root is wrong or the package's Ship scaffolding wasn't published/copied. Read src/ for what Ship files the package ships, and copy them into src/Ship/ once, up front.src/Commands/ and the stub directory between tags (git diff v3.0.1..v3.0.2 -- src/), then regenerate one throwaway container to eyeball the output. Patch versions have already touched generator output, which is the thing most likely to silently drift.Data/ become an anemic DTO graveyard. Porto encourages a Data/ folder for DTOs and value objects. In practice, half of them end up unused. Prune aggressively — a container with 12 models and 3 actions is a sign you under-used Actions, not that you over-used Data.Blog vs blog) produce mismatched directory/namespace pairs that work on macOS and fail on Linux CI. Enforce PascalCase at the generator call site or in an architecture test.How can I help you explore Laravel packages today?