NightFox

NightFox

Application Architecture for PHP Applications

A small script can live happily in one file. I've written plenty of those. But once an application starts growing, it needs some boundaries, or routing, database access, business rules, and HTML all melt together into one block nobody wants to touch. This is the structure I keep coming back to for plain PHP apps.

The boundaries I use

HTTP layer

Routes requests, reads input, performs request-level validation, and returns HTML or JSON.

Application layer

Coordinates a use case such as creating a booking, closing a ticket, or recording a payment.

Domain rules

Contains the rules that define what is allowed, independent of HTTP and database details.

Infrastructure

Talks to MySQL, mail servers, filesystems, queues, and external APIs.

What a request looks like

POST /customers/42/contacts
        │
        ▼
CustomerContactController
        │ validates HTTP input
        ▼
AddCustomerContact service
        │ applies permissions and business rules
        ▼
CustomerRepository + ContactRepository
        │ executes SQL inside a transaction
        ▼
Redirect to /customers/42

A plain-PHP layout that ages well

app/
├── public/
│   └── index.php
├── src/
│   ├── Application/
│   ├── Domain/
│   ├── Http/
│   └── Infrastructure/
├── templates/
├── config/
├── migrations/
├── tests/
├── storage/
│   └── logs/
├── composer.json
└── vendor/

The important part: only public/ is the web root. Config, source, logs, and vendor/ sit outside it, so nobody can download your database password because a server config got changed.

Services and repositories

A service is one thing the app does ("suspend a customer"). A repository hides how data gets saved and loaded. That doesn't mean every table needs a class or every function needs an interface. I only add structure where it cuts down on duplication or makes a rule testable. Otherwise it's just ceremony.

<?php
declare(strict_types=1);

final class SuspendCustomer
{
    public function __construct(
        private CustomerRepository $customers,
        private AuditLog $auditLog
    ) {
    }

    public function execute(int $customerId, int $actorId, string $reason): void
    {
        $customer = $this->customers->getById($customerId);

        if ($customer->isSuspended()) {
            return;
        }

        $customer->suspend($reason);
        $this->customers->save($customer);
        $this->auditLog->record($actorId, 'customer.suspended', $customerId);
    }
}

Wrap transactions around business operations

A transaction should usually cover one complete business operation, not the whole web request. Creating an order and its line items? One transaction. Sending the confirmation email? Not inside it. Queue the email or send it after the commit, so a hiccup with the mail server doesn't leave your database half-updated.

Start with a modular monolith

Most small-business apps don't need microservices, and I'll happily die on that hill. A well-organized monolith is easier to deploy, monitor, back up, and debug. Keep the modules separated inside it, use clear interfaces where outside systems are involved, and only split things out when there's a real operational reason.

A quick gut check: could you swap out how the data is stored, add a second interface (say, an API next to the HTML), or test a business rule without rewriting the whole feature? If not, things are probably more tangled than they need to be.

Last Modified: October 12, 2026, 11:46 am