pekral/arch-app-services
A structured application-services layer for Laravel that keeps controllers thin by moving use-case logic into dedicated, testable service classes with a consistent architecture.
pekral/arch-app-services is an architecture package: it gives you base classes for Actions (single-purpose application service classes), Data Validators (validated input/transfer objects), transaction helpers, a Result pattern, and a set of PHPStan rules that enforce the boundary between your HTTP layer and your application core.
Minimum setup:
composer require pekral/arch-app-servicesphpstan.neon so violations fail CI, not review:services:
-
class: Pekral\Arch\PHPStan\Rules\NoHttpExceptionsInActionsRule
arguments:
actionNamespaces:
- App\Actions\
tags:
- phpstan.rules.rule
-
class: Pekral\Arch\PHPStan\Rules\NoHttpExceptionsInDataValidatorsRule
arguments:
dataValidatorNamespaces:
- App\
tags:
- phpstan.rules.rule
Where to look first: the README sections on transactions, the Result pattern, and the PHPStan rules (all substantially expanded in 0.5.x), then the package's own Action and Data Validator base classes — they define the contracts your code inherits.
First use case: take one existing controller method doing validation → business logic → persistence, and move the middle part into an Action that accepts a Data Validator and returns a Result. That single refactor exercises every feature of the package at once: validation boundary, exception boundary, transaction handling, and error propagation.
Controller → Action → Result. Controllers should validate HTTP input, build a Data Validator, invoke the Action, and translate the Result into a response. The Action never sees a Request and never throws an HTTP exception — that's the contract the PHPStan rules enforce.
Actions as invokable single-purpose units. Keep one public __invoke() per Action; the README explicitly allows arguments on __invoke(), so pass the Data Validator (plus any context like an authenticated user ID) as parameters rather than resolving collaborators from the container inside the method.
Transactions live in the Action, not the controller. Wrap the write path with the package's transaction helper so the unit of work coincides with the Action boundary. If an Action calls another Action, decide up front which one owns the transaction — nesting is the usual source of surprise rollbacks.
Result pattern at the boundary. Actions return success/failure results carrying an error payload; the HTTP layer decides status codes. This is what lets the same Action be reused from a queued job or console command without leaking framework concerns.
PHPStan namespace scoping per module. The two rules are configured independently and take namespace lists, so you can scope them narrowly: App\Actions\ for the Action rule, and a tighter list for the Data Validator rule. If your data validators live in App\Data\, configure App\Data\ rather than the broad App\ shown in the release notes — a wide namespace will start flagging unrelated classes.
Gradual adoption. Because the rule identifiers did not change in 0.5.1, you can keep your existing ignoreErrors entries while you migrate. Add the new rule classes first, keep the old baseline, then shrink the ignore list as you clean each namespace.
⚠️ Breaking change in 0.5.1 — the combined rule no longer exists. NoHttpExceptionsInActionsAndDataValidatorsRule has been removed and split into NoHttpExceptionsInActionsRule and NoHttpExceptionsInDataValidatorsRule. Any phpstan.neon still registering the old class will fail — PHPStan errors on a missing service class, and depending on your config that surfaces as a hard bootstrap failure rather than a skipped rule. Update both service blocks in the same commit as the composer bump; this is the single most likely thing to break a build on upgrade.
Identifiers are stable, class names are not. ignoreErrors entries reference rule identifiers, which did not change, so your baselines survive. But if you were suppressing via class name anywhere (custom formatters, CI annotations, @phpstan-ignore on specific lines), audit those separately — they may now reference a class that no longer exists.
Namespace arguments are required, not inferred. Both rules take a mandatory namespace argument. An empty or overly broad list silently changes what gets enforced: too narrow and real violations slip through; too broad (e.g. App\ for the Action rule) and you'll get noise on classes that legitimately throw framework exceptions. Treat these lists as configuration worth reviewing at each refactor.
Don't let the PHPStan rule become the only validation boundary. The rules catch HTTP exceptions in application code, but they don't stop a Data Validator from quietly assuming a Request exists. Keep the Data Validator constructible from a plain array so it stays usable from jobs and tests.
Verify the failure path, not just the happy path. The Result pattern and transaction handling are easy to wire up and easy to get subtly wrong — test what happens when the Action returns a failure result mid-transaction, and confirm the controller maps it to the intended status code.
Pin and test before rolling out. 0.5.1 is a patch version carrying a breaking change to configuration. Bump it in a branch, run PHPStan and your test suite, and confirm the two new rule services are actually being applied (temporarily introduce a violation to prove the rule is live).
Extension points to watch: the Action and Data Validator base classes and the transaction helper are your primary override surfaces. Prefer composing around an Action (a decorator that adds logging or retries) over subclassing the base class, so package upgrades don't collide with your customisations.
How can I help you explore Laravel packages today?