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.

A Minimal Set of PHP Practices for Readable Code

⚡ 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 falsy

After:

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 false

After:

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.

By submitting, you agree that we’ll process your data to respond to your enquiry and, if applicable, to take pre-contract steps at your request (GDPR Art. 6(1)(b)) or for our legitimate interests (Art. 6(1)(f)). Please avoid sharing special-category data. See our Privacy Policy.
We reply within 1 business day.