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
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 ?>