PHP Development
Modern PHP 8.x development patterns and best practices.
PHP 8.x Features
Constructor Property Promotion
// Before PHP 8 class User { private string $name; private int $age;
public function __construct(string $name, int $age) {
$this->name = $name;
$this->age = $age;
}
}
// PHP 8+ class User { public function __construct( private string $name, private int $age, private bool $active = true ) {} }
Named Arguments
function createUser(string $name, string $email, bool $admin = false): User { // ... }
// Named arguments (order doesn't matter) createUser(email: 'john@example.com', name: 'John', admin: true);
Match Expression
// BAD: Switch with many breaks switch ($status) { case 'pending': $color = 'yellow'; break; case 'approved': $color = 'green'; break; default: $color = 'gray'; }
// GOOD: Match expression $color = match($status) { 'pending' => 'yellow', 'approved', 'published' => 'green', 'rejected' => 'red', default => 'gray', };
Null Safe Operator
// Before $country = null; if ($user !== null && $user->getAddress() !== null) { $country = $user->getAddress()->getCountry(); }
// PHP 8+ $country = $user?->getAddress()?->getCountry();
Union Types & Intersection Types
// Union types function process(int|float|string $value): int|float { return is_string($value) ? strlen($value) : $value * 2; }
// Intersection types (PHP 8.1+) function save(Countable&Iterator $items): void { foreach ($items as $item) { // ... } }
Enums (PHP 8.1+)
enum Status: string { case Draft = 'draft'; case Published = 'published'; case Archived = 'archived';
public function label(): string {
return match($this) {
self::Draft => 'Draft',
self::Published => 'Published',
self::Archived => 'Archived',
};
}
public function color(): string {
return match($this) {
self::Draft => 'gray',
self::Published => 'green',
self::Archived => 'red',
};
}
}
// Usage $post->status = Status::Published; echo $post->status->label(); // "Published"
Readonly Properties (PHP 8.1+)
class User { public function __construct( public readonly int $id, public readonly string $email, private string $password ) {} }
$user = new User(1, 'john@example.com', 'hashed'); $user->id = 2; // Error: Cannot modify readonly property
Type Safety
Strict Types
<?php declare(strict_types=1);
function add(int $a, int $b): int { return $a + $b; }
add(1, 2); // OK add('1', '2'); // TypeError
Return Types
class UserRepository { public function find(int $id): ?User { // Returns User or null }
public function findOrFail(int $id): User {
return $this->find($id) ?? throw new NotFoundException();
}
public function all(): array {
// Returns array of Users
}
public function save(User $user): void {
// Returns nothing
}
public function delete(User $user): never {
// Never returns (throws or exits)
throw new NotImplementedException();
}
}
OOP Patterns
Dependency Injection
// BAD: Hard dependency class OrderService { private $mailer;
public function __construct() {
$this->mailer = new SmtpMailer(); // Hard to test
}
}
// GOOD: Dependency injection interface MailerInterface { public function send(string $to, string $subject, string $body): void; }
class OrderService { public function __construct( private MailerInterface $mailer, private LoggerInterface $logger ) {}
public function complete(Order $order): void {
$this->mailer->send($order->email, 'Order Complete', '...');
$this->logger->info('Order completed', ['id' => $order->id]);
}
}
Value Objects
final class Email { private function __construct( private readonly string $value ) {}
public static function fromString(string $email): self {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email');
}
return new self(strtolower($email));
}
public function toString(): string {
return $this->value;
}
public function equals(self $other): bool {
return $this->value === $other->value;
}
}
// Usage $email = Email::fromString('John@Example.com'); echo $email->toString(); // "john@example.com"
Repository Pattern
interface UserRepositoryInterface { public function find(int $id): ?User; public function findByEmail(string $email): ?User; public function save(User $user): void; public function delete(User $user): void; }
class DatabaseUserRepository implements UserRepositoryInterface { public function __construct( private PDO $pdo ) {}
public function find(int $id): ?User {
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ? $this->hydrate($row) : null;
}
private function hydrate(array $data): User {
return new User(
id: $data['id'],
email: $data['email'],
name: $data['name']
);
}
}
Error Handling
Custom Exceptions
class DomainException extends Exception {} class ValidationException extends DomainException {} class NotFoundException extends DomainException {}
class InsufficientFundsException extends DomainException { public function __construct( public readonly float $balance, public readonly float $required ) { parent::__construct( "Insufficient funds: have {$balance}, need {$required}" ); } }
// Usage throw new InsufficientFundsException(balance: 50.00, required: 100.00);
Try-Catch Best Practices
// BAD: Catching generic Exception try { $result = $service->process($data); } catch (Exception $e) { log($e->getMessage()); }
// GOOD: Specific exception handling try { $result = $service->process($data); } catch (ValidationException $e) { return response()->json(['errors' => $e->getErrors()], 422); } catch (NotFoundException $e) { return response()->json(['error' => 'Not found'], 404); } catch (Throwable $e) { $this->logger->error('Unexpected error', ['exception' => $e]); return response()->json(['error' => 'Server error'], 500); }
Security
Input Validation
// Always validate and sanitize input $email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL); $age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, [ 'options' => ['min_range' => 0, 'max_range' => 150] ]);
if ($email === false || $email === null) { throw new ValidationException('Invalid email'); }
Password Hashing
// Always use password_hash/password_verify $hash = password_hash($password, PASSWORD_DEFAULT);
if (password_verify($inputPassword, $storedHash)) { // Password correct
// Rehash if needed (algorithm upgrade)
if (password_needs_rehash($storedHash, PASSWORD_DEFAULT)) {
$newHash = password_hash($inputPassword, PASSWORD_DEFAULT);
$user->updatePassword($newHash);
}
}
SQL Injection Prevention
// BAD: Direct concatenation $sql = "SELECT * FROM users WHERE email = '$email'"; // VULNERABLE
// GOOD: Prepared statements $stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?'); $stmt->execute([$email]);
// GOOD: Named parameters $stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND status = :status'); $stmt->execute(['email' => $email, 'status' => 'active']);
Testing
PHPUnit
class UserServiceTest extends TestCase { private UserService $service; private MockObject $repository;
protected function setUp(): void {
$this->repository = $this->createMock(UserRepositoryInterface::class);
$this->service = new UserService($this->repository);
}
public function testFindUserReturnsUser(): void {
$expected = new User(1, 'john@example.com');
$this->repository
->expects($this->once())
->method('find')
->with(1)
->willReturn($expected);
$result = $this->service->find(1);
$this->assertEquals($expected, $result);
}
public function testFindUserThrowsWhenNotFound(): void {
$this->repository
->method('find')
->willReturn(null);
$this->expectException(NotFoundException::class);
$this->service->findOrFail(999);
}
}
Data Providers
#[DataProvider('validEmailProvider')] public function testValidEmails(string $email): void { $result = Email::fromString($email); $this->assertInstanceOf(Email::class, $result); }
public static function validEmailProvider(): array { return [ 'simple' => ['test@example.com'], 'with subdomain' => ['test@mail.example.com'], 'with plus' => ['test+label@example.com'], ]; }
Performance
Opcache
; php.ini opcache.enable=1 opcache.memory_consumption=256 opcache.max_accelerated_files=20000 opcache.validate_timestamps=0 ; Production only
Array Functions
// Use array functions instead of loops when possible $names = array_map(fn($user) => $user->name, $users); $adults = array_filter($users, fn($user) => $user->age >= 18); $total = array_reduce($orders, fn($sum, $order) => $sum + $order->total, 0);
// Generators for large datasets function readLargeFile(string $path): Generator { $handle = fopen($path, 'r'); while (($line = fgets($handle)) !== false) { yield trim($line); } fclose($handle); }
foreach (readLargeFile('huge.csv') as $line) { // Process one line at a time, low memory }
Composer Best Practices
{ "require": { "php": "^8.2", "monolog/monolog": "^3.0" }, "require-dev": { "phpunit/phpunit": "^10.0", "phpstan/phpstan": "^1.0" }, "autoload": { "psr-4": { "App\": "src/" } }, "config": { "sort-packages": true, "optimize-autoloader": true } }
Production deployment
composer install --no-dev --optimize-autoloader --classmap-authoritative