PHP Best Practices

#php #bestpractices #security #performance #coding-standards #laravel

Last Updated: May 18, 2025 Related: Laravel Development Setup, .NET vs Laravel Complete Developer Guide


Quick Navigation


Modern PHP Practices

๐Ÿš€ PHP 8+ Features Usage

Type Declarations & Strict Types

<?php
declare(strict_types=1);

// Scalar type declarations
function calculateTotal(float $price, int $quantity): float
{
    return $price * $quantity;
}

// Union types (PHP 8.0+)
function processInput(string|int $input): string
{
    return (string) $input;
}

// Intersection types (PHP 8.1+)
function process(Countable&Iterator $items): void
{
    // Process items that are both countable and iterable
}

// Nullable types
function findUser(?int $id): ?User
{
    return $id ? User::find($id) : null;
}

Attributes (PHP 8.0+)

// Custom attributes
#[Attribute]
class Route
{
    public function __construct(
        public string $path,
        public array $methods = ['GET']
    ) {}
}

// Usage
class UserController
{
    #[Route('/users', ['GET', 'POST'])]
    public function index(): JsonResponse
    {
        return responseall();
    }
    
    #[Route('/users/{id}', ['GET'])]
    #[Cache(ttl: 3600)]
    public function show(int $id): JsonResponse
    {
        return responsefindOrFail($id);
    }
}

// Reading attributes
$reflector = new ReflectionMethodclass, 'index';
$attributes = $reflector->getAttributesclass;

foreach ($attributes as $attribute) {
    $route = $attribute->newInstance();
    // Register route: $route->path, $route->methods
}

Constructor Property Promotion

// PHP 8.0+ constructor property promotion
class User
{
    public function __construct(
        private string $name,
        private string $email,
        private readonly DateTime $createdAt = new DateTime(),
        public ?string $avatar = null
    ) {}
    
    // Getters/setters as needed
    public function getName(): string
    {
        return $this->name;
    }
    
    public function getEmail(): string
    {
        return $this->email;
    }
}

// Readonly properties (PHP 8.1+)
class ImmutableUser
{
    public function __construct(
        public readonly string $id,
        public readonly string $name,
        public readonly string $email
    ) {}
}

Named Arguments & Match Expression

// Named arguments (PHP 8.0+)
function createUser(
    string $name,
    string $email,
    bool $isActive = true,
    array $roles = []
): User {
    // Implementation
}

// Usage with named arguments
$user = createUser(
    email: 'john@example.com',
    name: 'John Doe',
    roles: ['user', 'moderator']
);

// Match expression (PHP 8.0+)
function getStatusMessage(string $status): string
{
    return match($status) {
        'pending' => 'Your request is being processed',
        'approved' => 'Welcome! Your account is ready',
        'rejected' => 'Sorry, your application was not accepted',
        'suspended' => 'Your account has been temporarily suspended',
        default => 'Unknown status'
    };
}

// Match with conditions
function calculateDiscount(int $quantity): float
{
    return match(true) {
        $quantity >= 100 => 0.20,
        $quantity >= 50 => 0.15,
        $quantity >= 20 => 0.10,
        $quantity >= 10 => 0.05,
        default => 0.0
    };
}

Enums (PHP 8.1+)

// Basic enum
enum Status
{
    case PENDING;
    case APPROVED;
    case REJECTED;
}

// Backed enum
enum StatusCode: string
{
    case PENDING = 'pending';
    case APPROVED = 'approved';
    case REJECTED = 'rejected';
    
    public function getMessage(): string
    {
        return match($this) {
            self::PENDING => 'Your request is being processed',
            self::APPROVED => 'Welcome! Your account is ready',
            self::REJECTED => 'Sorry, your application was not accepted',
        };
    }
    
    public function isTerminal(): bool
    {
        return match($this) {
            self::PENDING => false,
            self::APPROVED, self::REJECTED => true,
        };
    }
}

// Usage
class User
{
    public function __construct(
        private string $name,
        private StatusCode $status = StatusCode::PENDING
    ) {}
    
    public function approve(): void
    {
        $this->status = StatusCode::APPROVED;
    }
    
    public function getStatusMessage(): string
    {
        return $this->status->getMessage();
    }
}

Security Best Practices

๐Ÿ”’ Input Validation & Sanitization

Comprehensive Input Validation

class InputValidator
{
    public static function validateEmail(string $email): bool
    {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }
    
    public static function validateUrl(string $url): bool
    {
        return filter_var($url, FILTER_VALIDATE_URL) !== false;
    }
    
    public static function sanitizeString(string $input): string
    {
        return trim(strip_tags($input));
    }
    
    public static function validateCsrf(string $token, string $sessionToken): bool
    {
        return hash_equals($sessionToken, $token);
    }
    
    public static function validatePassword(string $password): array
    {
        $errors = [];
        
        if (strlen($password) < 8) {
            $errors[] = 'Password must be at least 8 characters';
        }
        
        if (!preg_match('/[A-Z]/', $password)) {
            $errors[] = 'Password must contain at least one uppercase letter';
        }
        
        if (!preg_match('/[a-z]/', $password)) {
            $errors[] = 'Password must contain at least one lowercase letter';
        }
        
        if (!preg_match('/\d/', $password)) {
            $errors[] = 'Password must contain at least one number';
        }
        
        if (!preg_match('/[^A-Za-z0-9]/', $password)) {
            $errors[] = 'Password must contain at least one special character';
        }
        
        return $errors;
    }
}

// Usage in Laravel Request
class CreateUserRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255|regex:/^[a-zA-Z\s]+$/',
            'email' => 'required|email|unique:users,email|max:255',
            'password' => [
                'required',
                'string',
                'min:8',
                'confirmed',
                function ($attribute, $value, $fail) {
                    $errors = InputValidator::validatePassword($value);
                    if (!empty($errors)) {
                        $fail(implode(', ', $errors));
                    }
                },
            ],
            'phone' => 'nullable|regex:/^\+?[1-9]\d{1,14}$/',
            'website' => 'nullable|url|max:255',
        ];
    }
    
    public function prepareForValidation(): void
    {
        $this->merge([
            'name' => InputValidator::sanitizeString($this->name ?? ''),
            'email' => strtolower(trim($this->email ?? '')),
        ]);
    }
}

SQL Injection Prevention

// โœ… Good: Using Eloquent ORM
class UserRepository
{
    public function findByEmail(string $email): ?User
    {
        return User::where('email', $email)->first();
    }
    
    public function findActiveUsers(array $ids): Collection
    {
        return User::whereIn('id', $ids)
                   ->where('is_active', true)
                   ->get();
    }
}

// โœ… Good: Using Query Builder with parameter binding
class CustomUserRepository
{
    public function findUsersWithCustomQuery(string $status, int $limit): array
    {
        return DB::select(
            'SELECT * FROM users WHERE status = ? ORDER BY created_at DESC LIMIT ?',
            [$status, $limit]
        );
    }
    
    public function updateUserStatus(int $userId, string $status): bool
    {
        return DB::update(
            'UPDATE users SET status = ?, updated_at = NOW() WHERE id = ?',
            [$status, $userId]
        ) > 0;
    }
}

// โŒ Bad: Direct string concatenation (vulnerable to SQL injection)
class VulnerableRepository
{
    public function findUserBadExample(string $email): array
    {
        // NEVER DO THIS
        $query = "SELECT * FROM users WHERE email = '" . $email . "'";
        return DB::select($query);
    }
}

๐Ÿ›ก๏ธ XSS Prevention

// Output escaping helper
class SecurityHelper
{
    public static function escape(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    }
    
    public static function escapeJs(string $text): string
    {
        return json_encode($text, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
    }
    
    public static function stripTags(string $text, array $allowedTags = []): string
    {
        if (empty($allowedTags)) {
            return strip_tags($text);
        }
        
        return strip_tags($text, '<' . implode('><', $allowedTags) . '>');
    }
    
    public static function sanitizeHtml(string $html): string
    {
        // Using HTMLPurifier library
        $config = HTMLPurifier_Config::createDefault();
        $config->set('HTML.Allowed', 'p,b,strong,i,em,u,a[href],ul,ol,li,br');
        $purifier = new HTMLPurifier($config);
        
        return $purifier->purify($html);
    }
}

// Blade template usage
<!-- โœ… Good: Automatic escaping -->
<h1>{{ $user->name }}</h1>
<p>{{ $user->bio }}</p>

<!-- โœ… Good: Explicit escaping for raw output -->
<div>{!! SecurityHelper::sanitizeHtml($user->description) !!}</div>

<!-- โŒ Bad: Raw output without sanitization -->
<div>{!! $user->description !!}</div>

๐Ÿ” Authentication & Authorization

// Secure password hashing
class PasswordManager
{
    public static function hash(string $password): string
    {
        return password_hash($password, PASSWORD_ARGON2ID, [
            'memory_cost' => 65536, // 64 MB
            'time_cost' => 4,       // 4 iterations
            'threads' => 3,         // 3 threads
        ]);
    }
    
    public static function verify(string $password, string $hash): bool
    {
        return password_verify($password, $hash);
    }
    
    public static function needsRehash(string $hash): bool
    {
        return password_needs_rehash($hash, PASSWORD_ARGON2ID, [
            'memory_cost' => 65536,
            'time_cost' => 4,
            'threads' => 3,
        ]);
    }
}

// Two-factor authentication
class TwoFactorAuth
{
    private const TOTP_WINDOW = 1;
    
    public function generateSecret(): string
    {
        return random_bytes(20);
    }
    
    public function generateQrCodeUrl(string $secret, string $user, string $app): string
    {
        $secretEncoded = base32_encode($secret);
        return sprintf(
            'otpauth://totp/%s:%s?secret=%s&issuer=%s',
            urlencode($app),
            urlencode($user),
            $secretEncoded,
            urlencode($app)
        );
    }
    
    public function verifyToken(string $secret, string $token): bool
    {
        $timeSlice = floor(time() / 30);
        
        for ($i = -self::TOTP_WINDOW; $i <= self::TOTP_WINDOW; $i++) {
            $calculatedToken = $this->calculateToken($secret, $timeSlice + $i);
            if (hash_equals($calculatedToken, $token)) {
                return true;
            }
        }
        
        return false;
    }
    
    private function calculateToken(string $secret, int $timeSlice): string
    {
        $time = pack('N*', 0) . pack('N*', $timeSlice);
        $hash = hash_hmac('sha1', $time, $secret, true);
        $offset = ord($hash[19]) & 0xf;
        $code = (
            ((ord($hash[$offset + 0]) & 0x7f) << 24) |
            ((ord($hash[$offset + 1]) & 0xff) << 16) |
            ((ord($hash[$offset + 2]) & 0xff) << 8) |
            (ord($hash[$offset + 3]) & 0xff)
        ) % 1000000;
        
        return str_pad((string) $code, 6, '0', STR_PAD_LEFT);
    }
}

// Rate limiting
class RateLimiter
{
    private Redis $redis;
    
    public function __construct(Redis $redis)
    {
        $this->redis = $redis;
    }
    
    public function attempt(string $key, int $maxAttempts = 5, int $decayInSeconds = 300): bool
    {
        $attempts = $this->redis->incr($key);
        
        if ($attempts === 1) {
            $this->redis->expire($key, $decayInSeconds);
        }
        
        return $attempts <= $maxAttempts;
    }
    
    public function tooManyAttempts(string $key, int $maxAttempts = 5): bool
    {
        return $this->redis->get($key) >= $maxAttempts;
    }
    
    public function clear(string $key): void
    {
        $this->redis->del($key);
    }
    
    public function retriesLeft(string $key, int $maxAttempts = 5): int
    {
        $attempts = $this->redis->get($key) ?? 0;
        return max(0, $maxAttempts - $attempts);
    }
}

Performance Optimization

โšก Efficient Database Operations

Query Optimization

// โœ… Good: Eager loading to prevent N+1 queries
class PostController
{
    public function index(): JsonResponse
    {
        $posts = Post::with(['author', 'comments.user'])
                    ->where('published', true)
                    ->orderBy('created_at', 'desc')
                    ->paginate(20);
        
        return response()->json($posts);
    }
    
    // โœ… Good: Using select to limit columns
    public function summary(): JsonResponse
    {
        $posts = Post::select(['id', 'title', 'created_at', 'author_id'])
                    ->with('author:id,name')
                    ->where('published', true)
                    ->get();
        
        return response()->json($posts);
    }
    
    // โœ… Good: Chunk processing for large datasets
    public function processLargeBatch(): void
    {
        User::where('email_verified_at', null)
            ->chunk(1000, function ($users) {
                foreach ($users as $user) {
                    // Process each user
                    $this->sendReminderEmail($user);
                }
            });
    }
}

// Database indexing helper
class DatabaseOptimizer
{
    public static function suggestIndexes(string $table): array
    {
        $suggestions = [];
        
        // Analyze slow query log
        $slowQueries = DB::select("
            SELECT sql_text, exec_count, avg_timer_wait/1000000000 as avg_seconds
            FROM performance_schema.events_statements_summary_by_digest
            WHERE schema_name = DATABASE()
            AND table_names LIKE ?
            ORDER BY avg_timer_wait DESC
            LIMIT 10
        ", ["%{$table}%"]);
        
        foreach ($slowQueries as $query) {
            if ($query->avg_seconds > 1.0) {
                $suggestions[] = "Consider indexing for: " . substr($query->sql_text, 0, 100);
            }
        }
        
        return $suggestions;
    }
}

Caching Strategies

// Multi-level caching strategy
class CacheManager
{
    private array $stores;
    
    public function __construct(
        private Repository $cache,
        private Redis $redis
    ) {}
    
    public function remember(string $key, int $ttl, callable $callback): mixed
    {
        // Try memory cache first (APCu)
        $memoryKey = "memory:{$key}";
        if (apcu_exists($memoryKey)) {
            return apcu_fetch($memoryKey);
        }
        
        // Try Redis cache
        $redisValue = $this->redis->get($key);
        if ($redisValue !== null) {
            $value = unserialize($redisValue);
            apcu_store($memoryKey, $value, 300); // Cache in memory for 5 min
            return $value;
        }
        
        // Generate value and cache at all levels
        $value = $callback();
        $this->redis->setex($key, $ttl, serialize($value));
        apcu_store($memoryKey, $value, min($ttl, 300));
        
        return $value;
    }
    
    public function tags(array $tags): self
    {
        // Implementation for tagged caching
        return new TaggedCacheManager($this->cache, $tags);
    }
}

// Cache-aside pattern implementation
class UserRepository
{
    public function __construct(
        private CacheManager $cache
    ) {}
    
    public function findById(int $id): ?User
    {
        return $this->cache->remember(
            "user:{$id}",
            3600,
            fn() => User::find($id)
        );
    }
    
    public function findActiveUsers(): Collection
    {
        return $this->cache->tags(['users'])
            ->remember(
                'users:active',
                1800,
                fn() => User::where('is_active', true)->get()
            );
    }
    
    public function invalidateUserCache(int $id): void
    {
        $this->cache->forget("user:{$id}");
        $this->cache->tags(['users'])->flush();
    }
}

๐Ÿ”„ Asynchronous Processing

// Job queue optimization
class OptimizedEmailJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    public int $tries = 3;
    public int $maxExceptions = 2;
    public int $timeout = 120;
    public int $backoff = 60;
    
    public function __construct(
        private int $userId,
        private string $template,
        private array $data = []
    ) {}
    
    public function handle(MailManager $mail): void
    {
        $user = User::find($this->userId);
        
        if (!$user) {
            Log::warning("User not found for email job", ['user_id' => $this->userId]);
            return;
        }
        
        $mail->send($user->email, $this->template, $this->data);
    }
    
    public function failed(Throwable $exception): void
    {
        Log::error('Email job failed', [
            'user_id' => $this->userId,
            'template' => $this->template,
            'error' => $exception->getMessage()
        ]);
        
        // Notify admin or queue alternative notification
    }
    
    public function retryUntil(): DateTime
    {
        return now()->addMinutes(30);
    }
}

// Batch processing
class BatchProcessor
{
    public function processBatch(array $items, string $jobClass): void
    {
        $batches = array_chunk($items, 100);
        
        foreach ($batches as $index => $batch) {
            dispatch(new $jobClass($batch))
                ->onQueue('batch-processing')
                ->delay(now()->addSeconds($index * 10)); // Stagger processing
        }
    }
    
    public function processWithProgress(array $items, callable $processor): void
    {
        $total = count($items);
        $progress = new ProgressBar($total);
        
        foreach ($items as $item) {
            $processor($item);
            $progress->advance();
        }
        
        $progress->finish();
    }
}

๐Ÿ“Š Memory Optimization

// Memory-efficient data processing
class DataProcessor
{
    public function processLargeFile(string $filename): void
    {
        $handle = fopen($filename, 'r');
        
        if (!$handle) {
            throw new RuntimeException("Cannot open file: {$filename}");
        }
        
        try {
            while (($line = fgets($handle)) !== false) {
                $this->processLine($line);
                
                // Clear memory periodically
                if (memory_get_usage() > 256 * 1024 * 1024) { // 256MB
                    gc_collect_cycles();
                }
            }
        } finally {
            fclose($handle);
        }
    }
    
    public function streamCsvData(string $filename): Generator
    {
        $handle = fopen($filename, 'r');
        
        if (!$handle) {
            throw new RuntimeException("Cannot open CSV file: {$filename}");
        }
        
        try {
            // Skip header row
            fgetcsv($handle);
            
            while (($data = fgetcsv($handle)) !== false) {
                yield $data;
            }
        } finally {
            fclose($handle);
        }
    }
    
    public function processInBatches(iterable $items, int $batchSize = 1000): void
    {
        $batch = [];
        $count = 0;
        
        foreach ($items as $item) {
            $batch[] = $item;
            $count++;
            
            if ($count >= $batchSize) {
                $this->processBatch($batch);
                $batch = [];
                $count = 0;
                
                // Force garbage collection
                gc_collect_cycles();
            }
        }
        
        // Process remaining items
        if (!empty($batch)) {
            $this->processBatch($batch);
        }
    }
}

Code Quality Standards

๐Ÿ“ PSR Standards Compliance

PSR-4 Autoloading

// composer.json
{
    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "App\\Models\\": "app/Models/",
            "App\\Services\\": "app/Services/",
            "App\\Repositories\\": "app/Repositories/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Tests\\": "tests/"
        }
    }
}

PSR-12 Code Style

<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\User;
use App\Repositories\UserRepositoryInterface;
use Illuminate\Support\Collection;
use InvalidArgumentException;

/**
 * Service for managing user operations
 */
class UserService
{
    /**
     * @param UserRepositoryInterface $userRepository
     */
    public function __construct(
        private readonly UserRepositoryInterface $userRepository
    ) {}

    /**
     * Create a new user with validation
     *
     * @param array $userData
     * @return User
     * @throws InvalidArgumentException
     */
    public function createUser(array $userData): User
    {
        $this->validateUserData($userData);

        $user = new User([
            'name' => $userData['name'],
            'email' => $userData['email'],
            'password' => bcrypt($userData['password']),
        ]);

        return $this->userRepository->save($user);
    }

    /**
     * Get paginated active users
     *
     * @param int $perPage
     * @return Collection
     */
    public function getActiveUsers(int $perPage = 20): Collection
    {
        return $this->userRepository->getActiveUsers($perPage);
    }

    /**
     * Validate user data
     *
     * @param array $userData
     * @throws InvalidArgumentException
     */
    private function validateUserData(array $userData): void
    {
        $required = ['name', 'email', 'password'];

        foreach ($required as $field) {
            if (!isset($userData[$field]) || empty($userData[$field])) {
                throw new InvalidArgumentException("Required field '{$field}' is missing");
            }
        }

        if (!filter_var($userData['email'], FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('Invalid email format');
        }
    }
}

๐Ÿ—๏ธ SOLID Principles Implementation

// Single Responsibility Principle
class EmailValidator
{
    public function validate(string $email): bool
    {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }
}

class PasswordValidator
{
    public function validate(string $password): array
    {
        $errors = [];
        
        if (strlen($password) < 8) {
            $errors[] = 'Password must be at least 8 characters';
        }
        
        return $errors;
    }
}

// Open/Closed Principle
abstract class NotificationSender
{
    abstract public function send(string $recipient, string $message): bool;
}

class EmailNotificationSender extends NotificationSender
{
    public function send(string $recipient, string $message): bool
    {
        // Send email implementation
        return true;
    }
}

class SmsNotificationSender extends NotificationSender
{
    public function send(string $recipient, string $message): bool
    {
        // Send SMS implementation
        return true;
    }
}

// Liskov Substitution Principle
interface PaymentProcessorInterface
{
    public function process(float $amount): PaymentResult;
}

class CreditCardProcessor implements PaymentProcessorInterface
{
    public function process(float $amount): PaymentResult
    {
        // Credit card processing logic
        return new PaymentResult(true, 'Payment processed successfully');
    }
}

class PayPalProcessor implements PaymentProcessorInterface
{
    public function process(float $amount): PaymentResult
    {
        // PayPal processing logic
        return new PaymentResult(true, 'Payment processed via PayPal');
    }
}

// Interface Segregation Principle
interface UserFinderInterface
{
    public function findById(int $id): ?User;
    public function findByEmail(string $email): ?User;
}

interface UserCreatorInterface
{
    public function create(array $userData): User;
}

interface UserUpdaterInterface
{
    public function update(int $id, array $userData): bool;
}

// Dependency Inversion Principle
class UserController
{
    public function __construct(
        private UserFinderInterface $userFinder,
        private UserCreatorInterface $userCreator,
        private UserUpdaterInterface $userUpdater
    ) {}
    
    public function show(int $id): JsonResponse
    {
        $user = $this->userFinder->findById($id);
        
        if (!$user) {
            return response()->json(['error' => 'User not found'], 404);
        }
        
        return response()->json($user);
    }
}

๐Ÿงช Design Patterns

// Repository Pattern
interface UserRepositoryInterface
{
    public function find(int $id): ?User;
    public function findByEmail(string $email): ?User;
    public function save(User $user): User;
    public function delete(User $user): bool;
}

class EloquentUserRepository implements UserRepositoryInterface
{
    public function find(int $id): ?User
    {
        return User::find($id);
    }
    
    public function findByEmail(string $email): ?User
    {
        return User::where('email', $email)->first();
    }
    
    public function save(User $user): User
    {
        $user->save();
        return $user->fresh();
    }
    
    public function delete(User $user): bool
    {
        return $user->delete();
    }
}

// Factory Pattern
abstract class PaymentFactory
{
    abstract public function createProcessor(string $type): PaymentProcessorInterface;
}

class ConcretePaymentFactory extends PaymentFactory
{
    public function createProcessor(string $type): PaymentProcessorInterface
    {
        return match($type) {
            'credit_card' => new CreditCardProcessor(),
            'paypal' => new PayPalProcessor(),
            'stripe' => new StripeProcessor(),
            default => throw new InvalidArgumentException("Unknown payment type: {$type}")
        };
    }
}

// Observer Pattern
interface OrderObserverInterface
{
    public function handle(OrderEvent $event): void;
}

class EmailNotificationObserver implements OrderObserverInterface
{
    public function handle(OrderEvent $event): void
    {
        if ($event->type === 'created') {
            // Send order confirmation email
        }
    }
}

class InventoryObserver implements OrderObserverInterface
{
    public function handle(OrderEvent $event): void
    {
        if ($event->type === 'created') {
            // Update inventory levels
        }
    }
}

class OrderEventDispatcher
{
    private array $observers = [];
    
    public function attach(OrderObserverInterface $observer): void
    {
        $this->observers[] = $observer;
    }
    
    public function notify(OrderEvent $event): void
    {
        foreach ($this->observers as $observer) {
            $observer->handle($event);
        }
    }
}

// Command Pattern
interface CommandInterface
{
    public function execute(): mixed;
    public function undo(): void;
}

class CreateUserCommand implements CommandInterface
{
    private ?User $createdUser = null;
    
    public function __construct(
        private array $userData,
        private UserRepositoryInterface $repository
    ) {}
    
    public function execute(): User
    {
        $this->createdUser = $this->repository->save(new User($this->userData));
        return $this->createdUser;
    }
    
    public function undo(): void
    {
        if ($this->createdUser) {
            $this->repository->delete($this->createdUser);
        }
    }
}

class CommandInvoker
{
    private array $history = [];
    
    public function execute(CommandInterface $command): mixed
    {
        $result = $command->execute();
        $this->history[] = $command;
        return $result;
    }
    
    public function undo(): void
    {
        if (!empty($this->history)) {
            $command = array_pop($this->history);
            $command->undo();
        }
    }
}

Error Handling & Logging

๐Ÿšจ Exception Handling

// Custom exception hierarchy
abstract class AppException extends Exception
{
    protected string $userMessage;
    protected array $context = [];
    
    public function __construct(
        string $message = '',
        string $userMessage = '',
        array $context = [],
        int $code = 0,
        ?Throwable $previous = null
    ) {
        parent::__construct($message, $code, $previous);
        $this->userMessage = $userMessage ?: $message;
        $this->context = $context;
    }
    
    public function getUserMessage(): string
    {
        return $this->userMessage;
    }
    
    public function getContext(): array
    {
        return $this->context;
    }
}

class ValidationException extends AppException
{
    private array $errors;
    
    public function __construct(array $errors, string $message = 'Validation failed')
    {
        $this->errors = $errors;
        parent::__construct($message, 'Please check your input and try again', ['errors' => $errors]);
    }
    
    public function getErrors(): array
    {
        return $this->errors;
    }
}

class BusinessLogicException extends AppException
{
    // For business rule violations
}

class ExternalServiceException extends AppException
{
    // For third-party API failures
}

// Exception handler
class AppExceptionHandler
{
    public function __construct(
        private LoggerInterface $logger
    ) {}
    
    public function handle(Throwable $exception): JsonResponse
    {
        match(true) {
            $exception instanceof ValidationException => $this->handleValidation($exception),
            $exception instanceof BusinessLogicException => $this->handleBusinessLogic($exception),
            $exception instanceof ExternalServiceException => $this->handleExternalService($exception),
            default => $this->handleGeneric($exception)
        };
    }
    
    private function handleValidation(ValidationException $exception): JsonResponse
    {
        $this->logger->info('Validation error', [
            'errors' => $exception->getErrors(),
            'context' => $exception->getContext()
        ]);
        
        return response()->json([
            'error' => 'Validation failed',
            'message' => $exception->getUserMessage(),
            'errors' => $exception->getErrors()
        ], 422);
    }
    
    private function handleBusinessLogic(BusinessLogicException $exception): JsonResponse
    {
        $this->logger->warning('Business logic violation', [
            'message' => $exception->getMessage(),
            'context' => $exception->getContext()
        ]);
        
        return response()->json([
            'error' => 'Business rule violation',
            'message' => $exception->getUserMessage()
        ], 400);
    }
    
    private function handleExternalService(ExternalServiceException $exception): JsonResponse
    {
        $this->logger->error('External service error', [
            'message' => $exception->getMessage(),
            'context' => $exception->getContext(),
            'trace' => $exception->getTraceAsString()
        ]);
        
        return response()->json([
            'error' => 'Service temporarily unavailable',
            'message' => 'Please try again later'
        ], 503);
    }
    
    private function handleGeneric(Throwable $exception): JsonResponse
    {
        $this->logger->error('Unexpected error', [
            'message' => $exception->getMessage(),
            'file' => $exception->getFile(),
            'line' => $exception->getLine(),
            'trace' => $exception->getTraceAsString()
        ]);
        
        return response()->json([
            'error' => 'Internal server error',
            'message' => 'An unexpected error occurred'
        ], 500);
    }
}

๐Ÿ“ Structured Logging

// Contextual logger
class ContextualLogger
{
    private array $context = [];
    
    public function __construct(
        private LoggerInterface $logger
    ) {}
    
    public function withContext(array $context): self
    {
        $new = clone $this;
        $new->context = array_merge($this->context, $context);
        return $new;
    }
    
    public function info(string $message, array $context = []): void
    {
        $this->logger->info($message, array_merge($this->context, $context));
    }
    
    public function error(string $message, array $context = []): void
    {
        $this->logger->error($message, array_merge($this->context, $context));
    }
    
    public function warning(string $message, array $context = []): void
    {
        $this->logger->warning($message, array_merge($this->context, $context));
    }
    
    public function debug(string $message, array $context = []): void
    {
        $this->logger->debug($message, array_merge($this->context, $context));
    }
}

// Performance logging
class PerformanceLogger
{
    private array $timers = [];
    
    public function __construct(
        private LoggerInterface $logger
    ) {}
    
    public function startTimer(string $operation): void
    {
        $this->timers[$operation] = microtime(true);
    }
    
    public function endTimer(string $operation, array $context = []): void
    {
        if (!isset($this->timers[$operation])) {
            return;
        }
        
        $duration = microtime(true) - $this->timers[$operation];
        unset($this->timers[$operation]);
        
        $this->logger->info('Performance measurement', array_merge($context, [
            'operation' => $operation,
            'duration_ms' => round($duration * 1000, 2),
            'duration_seconds' => round($duration, 4)
        ]));
    }
    
    public function logMemoryUsage(string $checkpoint, array $context = []): void
    {
        $this->logger->info('Memory usage', array_merge($context, [
            'checkpoint' => $checkpoint,
            'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
            'memory_peak_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2)
        ]));
    }
}

// Usage example
class UserService
{
    public function __construct(
        private UserRepositoryInterface $userRepository,
        private ContextualLogger $logger,
        private PerformanceLogger $perfLogger
    ) {}
    
    public function createUser(array $userData): User
    {
        $contextLogger = $this->logger->withContext([
            'operation' => 'create_user',
            'email' => $userData['email'] ?? 'unknown'
        ]);
        
        $this->perfLogger->startTimer('create_user');
        
        try {
            $contextLogger->info('Starting user creation');
            
            $user = $this->userRepository->save(new User($userData));
            
            $contextLogger->info('User created successfully', ['user_id' => $user->id]);
            
            return $user;
        } catch (Exception $e) {
            $contextLogger->error('User creation failed', ['error' => $e->getMessage()]);
            throw $e;
        } finally {
            $this->perfLogger->endTimer('create_user', ['email' => $userData['email'] ?? 'unknown']);
        }
    }
}

Testing Practices

๐Ÿงช Unit Testing with PHPUnit

use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;

class UserServiceTest extends TestCase
{
    private UserService $userService;
    private MockObject $userRepository;
    private MockObject $logger;
    
    protected function setUp(): void
    {
        parent::setUp();
        
        $this->userRepository = $this->createMockclass;
        $this->logger = $this->createMockclass;
        
        $this->userService = new UserService($this->userRepository, $this->logger);
    }
    
    public function testCreateUserSuccessfully(): void
    {
        // Arrange
        $userData = [
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => 'password123'
        ];
        
        $expectedUser = new User($userData);
        $expectedUser->id = 1;
        
        $this->userRepository
            ->expects($this->once())
            ->method('save')
            ->with($this->callback(function (User $user) use ($userData) {
                return $user->name === $userData['name'] &&
                       $user->email === $userData['email'];
            }))
            ->willReturn($expectedUser);
        
        // Act
        $result = $this->userService->createUser($userData);
        
        // Assert
        $this->assertInstanceOfclass, $result;
        $this->assertEquals(1, $result->id);
        $this->assertEquals('John Doe', $result->name);
    }
    
    public function testCreateUserWithInvalidEmail(): void
    {
        // Arrange
        $userData = [
            'name' => 'John Doe',
            'email' => 'invalid-email',
            'password' => 'password123'
        ];
        
        // Act & Assert
        $this->expectExceptionclass;
        $this->expectExceptionMessage('Invalid email format');
        
        $this->userService->createUser($userData);
    }
    
    /**
     * @dataProvider invalidUserDataProvider
     */
    public function testCreateUserWithInvalidData(array $userData, string $expectedError): void
    {
        $this->expectExceptionclass;
        $this->expectExceptionMessage($expectedError);
        
        $this->userService->createUser($userData);
    }
    
    public function invalidUserDataProvider(): array
    {
        return [
            'missing name' => [
                ['email' => 'test@example.com', 'password' => 'password'],
                'Required field \'name\' is missing'
            ],
            'missing email' => [
                ['name' => 'John Doe', 'password' => 'password'],
                'Required field \'email\' is missing'
            ],
            'missing password' => [
                ['name' => 'John Doe', 'email' => 'test@example.com'],
                'Required field \'password\' is missing'
            ],
        ];
    }
}

๐Ÿƒ Feature Testing

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class UserRegistrationTest extends TestCase
{
    use RefreshDatabase, WithFaker;
    
    public function testUserCanRegisterWithValidData(): void
    {
        // Arrange
        $userData = [
            'name' => $this->faker->name,
            'email' => $this->faker->unique()->safeEmail,
            'password' => 'Password123!',
            'password_confirmation' => 'Password123!'
        ];
        
        // Act
        $response = $this->postJson('/api/register', $userData);
        
        // Assert
        $response->assertStatus(201)
                ->assertJsonStructure([
                    'data' => [
                        'id',
                        'name',
                        'email',
                        'created_at'
                    ],
                    'token'
                ]);
        
        $this->assertDatabaseHas('users', [
            'name' => $userData['name'],
            'email' => $userData['email']
        ]);
        
        // Verify password was hashed
        $user = User::where('email', $userData['email'])->first();
        $this->assertNotEquals($userData['password'], $user->password);
        $this->assertTruecheck($userData['password'], $user->password);
    }
    
    public function testUserCannotRegisterWithExistingEmail(): void
    {
        // Arrange
        $existingUser = User::factory()->create();
        
        $userData = [
            'name' => $this->faker->name,
            'email' => $existingUser->email,
            'password' => 'Password123!',
            'password_confirmation' => 'Password123!'
        ];
        
        // Act
        $response = $this->postJson('/api/register', $userData);
        
        // Assert
        $response->assertStatus(422)
                ->assertJsonValidationErrors(['email']);
    }
    
    public function testRegistrationRequiresValidPassword(): void
    {
        // Arrange
        $userData = [
            'name' => $this->faker->name,
            'email' => $this->faker->unique()->safeEmail,
            'password' => 'weak',
            'password_confirmation' => 'weak'
        ];
        
        // Act
        $response = $this->postJson('/api/register', $userData);
        
        // Assert
        $response->assertStatus(422)
                ->assertJsonValidationErrors(['password']);
    }
}

๐Ÿงน Test Helpers and Factories

// Test trait for common assertions
trait AssertsDatabase
{
    protected function assertModelExists(Model $model, array $attributes = []): void
    {
        $this->assertDatabaseHas($model->getTable(), array_merge([
            $model->getKeyName() => $model->getKey()
        ], $attributes));
    }
    
    protected function assertModelMissing(Model $model): void
    {
        $this->assertDatabaseMissing($model->getTable(), [
            $model->getKeyName() => $model->getKey()
        ]);
    }
    
    protected function assertSoftDeleted(Model $model): void
    {
        $this->assertDatabaseHas($model->getTable(), [
            $model->getKeyName() => $model->getKey(),
            'deleted_at' => $model->deleted_at
        ]);
    }
}

// Custom factory with states
class UserFactory extends Factory
{
    protected $model = User::class;
    
    public function definition(): array
    {
        return [
            'name' => $this->faker->name(),
            'email' => $this->faker->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => bcrypt('password'),
            'remember_token' => Str::random(10),
        ];
    }
    
    public function unverified(): static
    {
        return $this->state(fn () => [
            'email_verified_at' => null,
        ]);
    }
    
    public function admin(): static
    {
        return $this->state(fn () => [
            'role' => 'admin',
        ]);
    }
    
    public function withPosts(int $count = 3): static
    {
        return $this->hasfactory()->count($count);
    }
}

// Test data builder
class UserTestDataBuilder
{
    private array $data;
    
    public function __construct()
    {
        $this->data = [
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => 'Password123!',
            'password_confirmation' => 'Password123!'
        ];
    }
    
    public function withName(string $name): self
    {
        $this->data['name'] = $name;
        return $this;
    }
    
    public function withEmail(string $email): self
    {
        $this->data['email'] = $email;
        return $this;
    }
    
    public function withInvalidEmail(): self
    {
        $this->data['email'] = 'invalid-email';
        return $this;
    }
    
    public function withWeakPassword(): self
    {
        $this->data['password'] = 'weak';
        $this->data['password_confirmation'] = 'weak';
        return $this;
    }
    
    public function withoutField(string $field): self
    {
        unset($this->data[$field]);
        return $this;
    }
    
    public function build(): array
    {
        return $this->data;
    }
}

// Usage in tests
class ExampleTest extends TestCase
{
    use AssertsDatabase;
    
    public function testExample(): void
    {
        $userData = (new UserTestDataBuilder())
            ->withName('Jane Doe')
            ->withEmail('jane@example.com')
            ->build();
        
        $user = User::factory()->admin()->create();
        $this->assertModelExists($user, ['role' => 'admin']);
    }
}

Development Tools

๐Ÿ”ง PHP Code Sniffer Configuration

<?xml version="1.0"?>
<ruleset name="Custom Coding Standard">
    <description>Custom coding standard based on PSR-12</description>
    
    <!-- Include PSR-12 standard -->
    <rule ref="PSR12"/>
    
    <!-- Additional rules -->
    <rule ref="Generic.PHP.ForbiddenFunctions">
        <properties>
            <property name="forbiddenFunctions" type="array">
                <element key="sizeof" value="count"/>
                <element key="delete" value="unset"/>
                <element key="print" value="echo"/>
                <element key="create_function" value="null"/>
            </property>
        </properties>
    </rule>
    
    <!-- Exclude external dependencies -->
    <exclude-pattern>*/vendor/*</exclude-pattern>
    <exclude-pattern>*/node_modules/*</exclude-pattern>
    <exclude-pattern>*/storage/*</exclude-pattern>
    <exclude-pattern>*/cache/*</exclude-pattern>
    
    <!-- File paths to check -->
    <file>app/</file>
    <file>config/</file>
    <file>database/</file>
    <file>routes/</file>
    <file>tests/</file>
    
    <!-- Show progress -->
    <arg value="p"/>
    
    <!-- Show sniff codes in all reports -->
    <arg value="s"/>
    
    <!-- Use colors in output -->
    <arg name="colors"/>
    
    <!-- Memory and time limits -->
    <ini name="memory_limit" value="128M"/>
    
    <!-- File extensions to check -->
    <arg name="extensions" value="php"/>
</ruleset>

๐Ÿ“Š PHPStan Configuration

# phpstan.neon
parameters:
    level: 8
    paths:
        - app
        - config
        - database
        - routes
        - tests
    
    excludePaths:
        - app/Console/Kernel.php
        - app/Http/Kernel.php
        - storage/*
        - vendor/*
    
    ignoreErrors:
        # Ignore Laravel collection methods
        - '#Call to an undefined method [a-zA-Z0-9\\_]+Collection::[a-zA-Z0-9_]+\(\)#'
        
        # Ignore Eloquent magic methods
        - '#Call to an undefined method [a-zA-Z0-9\\_]+Model::[a-zA-Z0-9_]+\(\)#'
    
    checkMissingIterableValueType: false
    checkGenericClassInNonGenericObjectType: false
    
    stubFiles:
        - stubs/eloquent.stub
        - stubs/collection.stub
    
    bootstrapFiles:
        - phpstan-bootstrap.php
    
    doctrine:
        objectManagerLoader: database/doctrine_loader.php

๐Ÿ”จ PHP-CS-Fixer Configuration

<?php
// .php-cs-fixer.php

$finder = PhpCsFixer\Finder::create()
    ->in(__DIR__)
    ->exclude(['bootstrap/cache', 'storage', 'vendor'])
    ->notPath('*.blade.php');

return (new PhpCsFixer\Config())
    ->setRiskyAllowed(true)
    ->setRules([
        '@PSR12' => true,
        '@PHP81Migration' => true,
        
        // Array notation
        'array_syntax' => ['syntax' => 'short'],
        'trailing_comma_in_multiline' => ['elements' => ['arrays', 'arguments', 'parameters']],
        
        // Import statements
        'ordered_imports' => ['sort_algorithm' => 'alpha'],
        'no_unused_imports' => true,
        'global_namespace_import' => [
            'import_classes' => true,
            'import_constants' => true,
            'import_functions' => true,
        ],
        
        // Method and property declarations
        'visibility_required' => ['elements' => ['method', 'property']],
        'declare_strict_types' => true,
        
        // Control structures
        'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false],
        'concat_space' => ['spacing' => 'one'],
        
        // Comments
        'single_line_comment_style' => ['comment_types' => ['hash']],
        'multiline_comment_opening_closing' => true,
        
        // Whitespace
        'blank_line_after_opening_tag' => true,
        'method_chaining_indentation' => true,
        'no_extra_blank_lines' => [
            'tokens' => [
                'extra', 'throw', 'use', 'use_trait', 'curly_brace_block',
                'square_brace_block', 'parenthesis_brace_block'
            ]
        ],
        
        // Risky rules
        'strict_comparison' => true,
        'strict_param' => true,
        'modernize_types_casting' => true,
        'use_arrow_functions' => true,
    ])
    ->setFinder($finder);

๐Ÿ› XDebug Configuration

; xdebug.ini
zend_extension=xdebug

; Debugging
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_port=9003
xdebug.client_host=host.docker.internal

; Profiling
xdebug.profiler_enable=0
xdebug.profiler_enable_trigger=1
xdebug.profiler_output_dir=/tmp/xdebug

; Coverage
xdebug.coverage_enable=1

; Logging
xdebug.log=/tmp/xdebug.log
xdebug.log_level=7


Quick Reference Commands

Development Commands

# Code style checking
./vendor/bin/phpcs --standard=PSR12 app/
./vendor/bin/phpcbf --standard=PSR12 app/

# Static analysis
./vendor/bin/phpstan analyse app/
./vendor/bin/phpstan analyse --level=8 app/

# Code formatting
./vendor/bin/php-cs-fixer fix

# Testing
./vendor/bin/phpunit
./vendor/bin/phpunit --coverage-html coverage/
./vendor/bin/phpunit --filter=UserTest

Performance Commands

# Profiling with XDebug
php -d xdebug.profiler_enable=1 script.php

# Memory usage
php -d memory_limit=128M script.php

# Opcode cache status
php -m | grep -i opcache

Tags

#php #bestpractices #security #performance #coding-standards #testing #laravel


This guide should be updated as PHP versions and best practices evolve. Always refer to the latest PHP documentation and community standards.