Architectural Principles
These principles apply across all projects and have been established through deliberation. They guide every architectural decision we make.
1. Explicit Over Implicit
Actions are the authority for business logic. No magic traits, no database-level cascading, no hidden behavior. If something happens, you should be able to trace it to an explicit call in the code.
In practice: When a model is deleted, the Action handles all child cleanup explicitly — not through ON DELETE CASCADE or model observers.
2. Action Pattern
Both projects use final readonly action classes with a single execute() method. Constructor dependency injection only. No facades in actions.
final readonly class CreateIssueAction
{
public function __construct(
private ConnectionInterface $db,
) {}
public function execute(User $user, CreateIssueDTO $dto): Issue
{
return $this->db->transaction(function () use ($user, $dto): Issue {
// Business logic here
});
}
}Why: Actions are small, testable, and focused. Each one does exactly one thing. When you need to understand what happens when an issue is created, you read one file.
3. Form Request → DTO → Action Pipeline
Type safety at every boundary. HTTP input is validated in a Form Request, converted to a typed DTO, and passed to an Action. No raw arrays flowing through business logic.
Controller → FormRequest (validate) → DTO (type) → Action (execute)Why: Each layer has a clear responsibility. The Form Request handles HTTP validation rules. The DTO provides type-safe data transfer. The Action contains business logic. No layer knows about the others' internals.
4. No ON DELETE CASCADE
Foreign keys serve as guards — they throw errors on constraint violations. They do not silently delete child records. All deletion must be explicit in the Action.
Why: We want to know when something is being deleted. Database-level cascading is invisible to the application, invisible to audit logging, and invisible to tests. See Cascade Deletion & Soft Deletes for the full rationale.
5. Selective Soft Deletes
Soft deletes are applied only to high-value models as determined by data classification. Not every model gets SoftDeletes — only those where the data must be preserved for compliance or business reasons.
Why: Blanket soft deletes create confusion (is this record really deleted?), complicate queries (every query needs a scope), and bloat the database. We apply them surgically where they matter.
6. Transaction Safety
Every mutation is wrapped in a database transaction. If any step fails, the entire operation rolls back. No partial writes.
Why: Partial state is worse than failure. If creating an issue and logging the audit entry are in the same transaction, either both succeed or neither does.
7. Architecture Tests Enforce Patterns
Pest PHP architecture tests prevent structural regression at CI time. These tests verify that actions follow the pattern, that cascade relations are handled, that audit logging is called, and more.
Why: Code review catches most issues, but humans miss things. Architecture tests are automated pattern enforcement — they run on every PR and catch drift before it reaches production.