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
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 (Authentication section might hold User, 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.