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]]
- [[#Security Best Practices]]
- [[#Performance Optimization]]
- [[#Code Quality Standards]]
- [[#Error Handling & Logging]]
- [[#Testing Practices]]
- [[#Development Tools]]
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
Related Notes
- Laravel Development Setup
- .NET vs Laravel Complete Developer Guide
- Package Management Strategies
- Performance Optimization Patterns
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.