PHP Best Practices
Habits that separate maintainable, professional PHP from code that merely runs.
3 min read
Plenty of PHP "works" while still being fragile, insecure, or painful to maintain. A handful of habits, applied consistently, make the real difference.
Always enable strict types
<?php
declare(strict_types=1);Put this as the very first line of every new PHP file. As covered earlier, it turns PHP's usually-silent type coercion into an immediate TypeError when a function receives the wrong type — catching a whole category of bugs at the moment they happen instead of somewhere downstream.
Follow PSR standards
<?php
namespace App\Services;
class UserService
{
public function __construct(private UserRepository $repository)
{
}
public function findActiveUsers(): array
{
return $this->repository->findBy(['active' => true]);
}
}The PHP-FIG group publishes PSR (PHP Standards Recommendations) — PSR-4 for autoloading (covered earlier), PSR-12 for code style (brace placement, naming conventions, spacing). Following them means any PHP developer can read your code without adjusting to a one-off personal style, and tools like PHP-CS-Fixer can enforce them automatically rather than relying on manual review.
Never trust input, ever
Every value from $_GET, $_POST, $_COOKIE, an uploaded file, or even an API response from a third party should be treated as untrusted until validated — the security lesson covered why, but it's worth repeating as a standing habit rather than a one-time checklist item.
Use dependency injection over global state
<?php
// Fragile: hidden dependency on a global
function sendWelcomeEmail($user) {
global $mailer;
$mailer->send($user->email, "Welcome!");
}
// Better: the dependency is explicit and testable
class WelcomeEmailSender {
public function __construct(private Mailer $mailer) {}
public function send(User $user): void {
$this->mailer->send($user->email, "Welcome!");
}
}Passing dependencies explicitly (through a constructor, as shown) makes a class's requirements visible in its own signature and lets tests substitute a fake Mailer without touching global state. Every modern framework is built around a dependency injection container that wires this up automatically.
Keep business logic out of controllers/templates
A controller should coordinate — receive a request, call the relevant service or model, return a response — not contain the actual business rules (calculating a discount, validating an order) directly. Pushing that logic into templates or controllers makes it untestable in isolation and impossible to reuse from, say, a scheduled command or an API endpoint that needs the same calculation.
Write tests, especially around anything handling money or auth
<?php
public function test_insufficient_funds_throws_exception(): void
{
$account = new BankAccount(balance: 10);
$this->expectException(InsufficientFundsException::class);
$account->withdraw(50);
}PHPUnit (bundled or easily added to every major framework) makes this kind of test straightforward to write. Code paths involving money, authentication, or permissions are exactly where a silent regression does the most damage — they deserve tests even in a codebase that otherwise has light coverage.
Keep dependencies up to date
composer outdated
composer updateAn outdated dependency isn't just missing features — it can carry known, publicly disclosed security vulnerabilities. Running composer outdated periodically (and actually reading changelogs before a major version bump) is a small habit that avoids running known-vulnerable code in production for months without noticing.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.