Application Architecture for PHP Applications
A small script can keep everything in one file. A growing application needs boundaries so routing, database access, business rules, and presentation do not become one untestable block.
On This Page
A Practical Set of Boundaries
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.
Example Request Flow
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 Maintainable Plain-PHP Structure
app/ ├── public/ │ └── index.php ├── src/ │ ├── Application/ │ ├── Domain/ │ ├── Http/ │ └── Infrastructure/ ├── templates/ ├── config/ ├── migrations/ ├── tests/ ├── storage/ │ └── logs/ ├── composer.json └── vendor/
Only public/ should be exposed by the web server. Configuration, source files, logs, and dependencies should not be directly downloadable.
Services and Repositories
A service expresses a use case. A repository hides persistence details. This does not mean every table needs a class or every function needs an interface. Add structure where it reduces duplication or makes a rule testable.
<?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);
}
}
Put Transactions Around Business Operations
A transaction should normally cover one complete business operation, not an entire web request. Creating an order and its line items belongs in one transaction. Sending email usually does not; queue it or send it after commit so a temporary mail failure does not corrupt database state.
Start With a Modular Monolith
Most small-business applications do not need microservices. A well-structured monolith is easier to deploy, monitor, back up, and debug. Keep modules internally separated, use explicit interfaces where external systems are involved, and split services only when there is a clear operational reason.