Porto SAP
- title
- Porto SAP
- type
- concept
- summary
- Layered PHP/Laravel-oriented architectural pattern β Containers (per-domain business logic) + Ship (shared infrastructure), Actions orchestrating single-
run()Tasks, every artifact in its own class and predictable folder - tags
- software-architecture, php, laravel, ddd, clean-architecture
- sources
- porto-readme
- created
- 2026-05-13
- updated
- 2026-09-14
Porto is an architectural pattern published by Mahmoud Zalt (MIT, ~1.6kβ on github.com/Mahmoudz/Porto) for organising server-side application code β primarily aimed at PHP/Laravel but framework-agnostic in principle. The framework that implements Porto for Laravel is Apiato (apiato.io); the Porto repo itself is the pattern spec and documentation, not a runtime.
The current README is a thin landing page β the real content lives in the docs site. Two ideas carry the pattern.
Two layers: Containers and Ship
Application code splits in half:
app/
βββ Containers/
β βββ {Section}/
β βββ {Container}/
β βββ Actions/
β βββ Tasks/
β βββ Models/
β βββ Controllers/
β βββ Requests/
β βββ Routes/
β βββ Transformers/
β βββ Events/
β βββ Listeners/
β βββ Jobs/
β βββ Commands/
β βββ Migrations/
β βββ Tests/
β βββ Configs/
β βββ Exceptions/
βββ Ship/
βββ Parents/ # base classes (Controller, Model, Request, ...)
βββ Providers/
βββ Middleware/
βββ Exceptions/
βββ Configs/
- Containers hold business logic for one bounded area (
User,Billing,Order, ...). Sections group related Containers (Authenticationsection might holdUser,Login,Roles). - Ship holds everything cross-cutting: framework base classes Containers extend, global middleware, shared service providers, app-wide configs.
The stated payoff is a monolith-to-microservices migration path: a Container is meant to be self-contained enough that splitting it into its own service is mostly a build/deploy change, not a code rewrite. Whether real-world Porto codebases actually keep Containers that clean is another question β same gap that bites bounded-context-as-microservice in DDD literature. what-even-are-microservices argues against reading the code split as the hard part at all: extracting a Container is a build and deploy change, but the reason organisations split services is organisational, and a clean boundary does not remove the team-to-team negotiation cost.
Actions and Tasks
Inside a Container, business logic is split into two component types:
- Action β orchestrates one use case (e.g.
RegisterUserAction). It composes Tasks in a sequence and is what Controllers, Jobs, or Commands call into. - Task β does one thing, has one public method
run(), is reusable across Actions (e.g.CreateUserTask,SendWelcomeEmailTask,AssignRoleTask). - Sub-Action β a named group of Tasks shared by multiple Actions, to avoid repeating Task sequences.
This is the same idea as use-case classes in Clean Architecture or interactors/services in DDD-flavoured codebases, with the rule tightened to one-class-one-method-one-job. Controllers stay thin: parse the request, call an Action, return the result.
Everything-has-its-own-class
The pattern's most distinctive trait isn't the layers β it's the insistence that each conceptually distinct artifact lives in its own class in a predictable folder. Routes (web / api / cli) live in dedicated files; request validation in Form Request classes; response shaping in Transformers; cross-cutting concerns in Middleware; domain events in Events + Listeners; queue work in Jobs; CLI in Commands; data access in Repositories; persistent state in Models; DTOs for inter-Container data transfer; Exceptions per error condition.
The result is a wide-and-shallow tree: many small files, each named after what it does. Authors of conventional Laravel codebases sometimes object that this is over-engineering; Porto's counter is that the cost of one more file is small and the cost of finding "where does X happen" in a monolithic Controller is high. The pattern bets on tooling β IDE jump-to-symbol, search β being cheap and exhaustive class-splitting therefore being a structural win.
The AI-friendliness pitch
The current README leads with this:
Porto's strict adherence to the single responsibility principle enhances its compatibility with AI tools like GitHub Copilot, which thrive on clear, well-defined classes.
That argument lines up with clean-code-coding-agents (and its parent reasoning from Uncle Bob's Clean Architecture): LLM context windows are finite, and a codebase where each file does one named thing is cheaper to navigate per token than a codebase where you have to read a 600-line Controller to figure out what happens during POST /users. Porto's small-classes-in-predictable-folders shape is what an agent's grep-and-read loop wants: a file named RegisterUserAction.php in app/Containers/Authentication/User/Actions/ tells the agent what it does and where to look for the next hop without opening the file.
This is the pattern's most defensible 2026-era selling point. The monolith-to-microservices claim is harder to verify (it's the kind of thing that's true on day one and degrades silently as Container boundaries leak), but the agent-friendliness claim is testable per-file: open a file, ask whether its name tells you what it does. Porto says yes by construction; conventional MVC says "maybe, depending on the developer."
Lineage
Porto blends:
- MVC β keeps Models/Views/Controllers as Laravel uses them, but demotes Controllers to thin adapters.
- DDD-lite β Containers are bounded contexts in everything but name. Sections are a coarser grouping.
- Clean Architecture β Actions are use cases; Tasks are the granular operations under them.
- Hexagonal / ports-and-adapters β Ship/Container split echoes the "your domain core doesn't depend on infrastructure" rule, though Porto enforces it by folder convention rather than by dependency-rule lint.
What it doesn't take from DDD: aggregates, value objects, domain events as first-class invariants, ubiquitous language drilling. Porto is a structural pattern, not a modelling philosophy.
When it's a fit
- Laravel apps that have grown past the default
app/Http/Controllers/flat layout and need a place for cross-cutting features. - Teams that want to enforce structure by convention instead of by review.
- Codebases worked on by coding agents alongside humans (see clean-code-coding-agents).
When it isn't
- Small apps where the file overhead is real cost, not paid-back-later cost.
- Teams that haven't bought into one-class-one-job β Porto without that discipline degrades into Laravel-with-more-folders.
- Languages without Laravel-shaped lifecycle hooks (Providers, Middleware, Form Requests, Events) β the pattern transplants, but the implementation specifics don't.
Where it sits in the wiki
- clean-code-coding-agents β the broader argument that structure matters more, not less, with agents in the loop.
- programming-as-theory-building β Naur's frame for why folder-convention enforcement is a weak proxy for the team's shared model, but the best one a pattern can offer.
- no-silver-bullet β Brooks on accidental vs essential complexity; Porto attacks accidental complexity (where does X go?) and leaves essential complexity (what should X do?) to the domain.
- metapatterns β Denys Poltorak's compendium that sorts hundreds of architecture patterns, Layers and Hexagonal Architecture among them, into fewer than 20 metapatterns; Porto is the kind of individual pattern it classifies.
- scotty β the deploy side of the same Laravel world: an SSH task runner in the Envoy lineage, for once the app is structured and has to reach a server.