A Minimal Set of PHP Practices for Readable Code
Clean code isn’t a religion—it’s how you spare your future self (and teammates) from confusion. Below is a friendly set of practices that make PHP code more predictable and easy to maintain.

⚡ Note: examples use Laravel for clarity, but the ideas apply to any framework, CMS, or plain PHP.
1) PSR-1, PSR-4, PSR-12: shared language and predictability
Why it matters: consistent structure and formatting remove noise. Reviews focus on logic instead of spaces and braces.
How to adopt: enable phpcs and an auto-fixer, configure PSR-4 autoloading in composer.json, align on PSR-12.
Before:
class usercontroller {function STORE($Req){return User::Create($Req->all());}}After:
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
class UserController
{
public function store(Request $request): User
{
return User::create($request->all());
}
}2) SOLID without zealotry: remember SRP first
Why it matters: a class should have one reason to change. Lower coupling, easier testing, safer refactors.
Typical pitfall: a “God object” doing DB, email, logging, and validation.
Before:
class UserManager {
public function register(array $data) {
$user = User::create($data);
Mail::to($user->email)->send(new WelcomeMail($user));
Log::info('User created', ['id' => $user->id]);
return $user;
}
}After:
class UserService
{
public function __construct(
protected UserRepository $users,
protected Mailer $mailer,
protected Logger $logger,
) {}
public function register(array $data): User
{
$user = $this->users->create($data);
$this->mailer->sendWelcome($user);
$this->logger->userCreated($user);
return $user;
}
}3) Encapsulation by default: be stingy with public
Why it matters: a smaller public surface protects your API and keeps internals flexible.
Before:
class ReportGenerator {
public function connectDb() {/*...*/}
public function fetchData() {/*...*/}
public function renderReport() {/*...*/}
}After:
class ReportGenerator {
protected function connectDb() {/*...*/}
protected function fetchData() {/*...*/}
public function renderReport() {/*...*/}
}4) Strict typing: type parameters and returns
Why it matters: fewer surprises, better IDE hints, safer refactors. Prefer DTOs/value objects over loose arrays.
Before:
public function calculate($a, $b) {
return $a + $b;
}After:
public function calculate(int $a, int $b): int {
return $a + $b;
}5) ≤ 20 lines per method: check abstraction levels
Why it matters: long methods often mix validation, domain rules, side effects, and I/O.
Before:
public function processOrder(array $data) {
// validate...
// create order...
// charge...
// email...
// log...
}After:
public function processOrder(array $data) {
$this->validate($data);
$order = $this->createOrder($data);
$this->charge($order);
$this->notify($order);
$this->log($order);
}6) Duplicate in 2+ places? Extract
Why it matters: DRY reduces drift. Fixing one copy but not the other is a classic source of bugs.
Before:
$user = User::create($data);
Mail::to($user->email)->send(new WelcomeMail($user));
$admin = User::create($adminData);
Mail::to($admin->email)->send(new WelcomeMail($admin));After:
class UserService {
public function register(array $data): User {
$user = User::create($data);
Mail::to($user->email)->send(new WelcomeMail($user));
return $user;
}
}7) Repository + Service: thin controllers, clear domain
Why it matters: keeping DB access (repositories) apart from business use-cases (services) makes code explainable and testable.
Before:
public function index()
{
return User::where('active', true)->get();
}After:
class UserRepository {
public function active(): Collection {
return User::where('active', true)->get();
}
}
class UserService {
public function __construct(private UserRepository $users){}
public function getActive(): Collection { return $this->users->active(); }
}8) Strict comparison and explicit conditions
Why it matters:=== avoids PHP’s quirky coercions. Only use if ($var) for guaranteed booleans.
Before:
if ($user->age == '18') { /* ... */ }
if ($discount) { /* ... */ } // '0' is falsyAfter:
if ($user->age === 18) { /* ... */ }
if ($discount !== null && $discount > 0) { /* ... */ }9) Naming: optimize for reader attention
Why it matters: good names are built-in documentation. Include units/format where relevant.
Before:
$amount = 100;After:
$orderTotalCents = 100;10) PHP 8+ Constructor Promotion
Why it matters: less boilerplate, clearer intent.
final class CreateInvoice
{
public function __construct(
private CustomerRepository $customers,
private TaxCalculator $taxes,
) {}
public function handle(int $customerId, int $amountCents): Invoice { /*...*/ }
}11) Laravel Query Builder: use conditional clauses
Why it matters: eliminate the if-forest around queries; build them compositionally.
Before:
$query = User::query();
if ($active) { $query->where('active', true); }
if ($role) { $query->where('role', $role); }
if ($fromDate) { $query->whereDate('created_at', '>=', $fromDate); }
$users = $query->get();After:
$users = User::query()
->when($active, fn($q) => $q->where('active', true))
->when($role, fn($q) => $q->where('role', $role))
->when($fromDate, fn($q) => $q->whereDate('created_at', '>=', $fromDate))
->get();12) Multiple DB state mutations? Wrap in a transaction
Why it matters: keep consistency even when a step fails mid-flight.
Before:
$order = Order::create($data);
$payment->charge($order->total);
$order->markPaid();After:
DB::transaction(function () use ($data, $payment) {
$order = Order::create($data);
$payment->charge($order->total);
$order->markPaid();
});13) Return early with guard clauses
Why it matters: less nesting, easier scanning, fewer missed else branches.
Before:
public function handle(?User $user) {
if ($user) {
if ($user->isActive()) {
// ...
}
}
}After:
public function handle(?User $user) {
if (!$user || !$user->isActive()) {
return;
}
// main logic
}14) Avoid truthiness where intent is unclear
Why it matters:if ($variable) hides type ambiguity; be explicit for strings/numbers/arrays.
Before:
if ($limit) { /* ... */ } // '0' becomes falseAfter:
if ($limit !== null) { /* ... */ }15) Automate so habits don’t decay
Why it matters: humans are inconsistent; tools aren’t. Let automation keep style and safety in check.
- Pre-commit: fixer + PHPCS + PHPStan/Psalm
- CI: same checks + tests
- IDE templates/snippets for common classes
- Laravel: run Pint (
./vendor/bin/pint)
16) Guidelines, not commandments
Real life has deadlines, legacy, and constraints. Break rules consciously, leave a note (why) and a backlog task (what to improve). The goal is readability and predictability, not ritual purity.
Get in touch
Need an external audit of Your project?
Tell us your context and the outcome you want, and we’ll suggest the simplest next step.