PHP Backend Web Development

Modern PHP Development: Best Practices and New Features

M

Maria Garcia

Modern PHP Development: Best Practices and New Features

Explore modern PHP development with PHP 8.3 features, composer packages, and best practices for building robust web applications.

PHP has evolved dramatically over the years, transforming from a simple scripting language into a powerful, modern programming language. With PHP 8.3 and beyond, developers have access to robust features that make building scalable web applications easier than ever.

PHP 8.3 New Features

Typed Class Constants

PHP 8.3 introduces typed class constants, providing better type safety and IDE support.

<?php

class HttpStatus
{
    public const int OK = 200;
    public const int NOT_FOUND = 404;
    public const int SERVER_ERROR = 500;
    
    public const array SUCCESS_CODES = [200, 201, 202];
    public const string DEFAULT_MESSAGE = 'Unknown status';
}

class ApiResponse
{
    public function __construct(
        private int $statusCode = HttpStatus::OK,
        private string $message = HttpStatus::DEFAULT_MESSAGE,
        private array $data = []
    ) {}
    
    public function toArray(): array
    {
        return [
            'status' => $this->statusCode,
            'message' => $this->message,
            'data' => $this->data,
            'success' => in_array($this->statusCode, HttpStatus::SUCCESS_CODES)
        ];
    }
}

Anonymous Readonly Classes

Create immutable data transfer objects with anonymous readonly classes.

<?php

function createUserDto(array $userData): object
{
    return new readonly class($userData['id'], $userData['name'], $userData['email']) {
        public function __construct(
            public int $id,
            public string $name,
            public string $email,
        ) {}
        
        public function toArray(): array
        {
            return [
                'id' => $this->id,
                'name' => $this->name,
                'email' => $this->email,
            ];
        }
    };
}

// Usage
$user = createUserDto([
    'id' => 1,
    'name' => 'John Doe',
    'email' => 'john@example.com'
]);

echo $user->name; // John Doe
// $user->name = 'Jane'; // Error: Cannot modify readonly property

Modern PHP Architecture Patterns

Repository Pattern with Dependency Injection

<?php

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

class DatabaseUserRepository implements UserRepositoryInterface
{
    public function __construct(
        private PDO $database,
        private LoggerInterface $logger
    ) {}
    
    public function findById(int $id): ?User
    {
        try {
            $stmt = $this->database->prepare(
                'SELECT * FROM users WHERE id = :id'
            );
            $stmt->execute(['id' => $id]);
            
            $userData = $stmt->fetch(PDO::FETCH_ASSOC);
            
            return $userData ? User::fromArray($userData) : null;
        } catch (PDOException $e) {
            $this->logger->error('Failed to find user by ID', [
                'id' => $id,
                'error' => $e->getMessage()
            ]);
            throw new UserRepositoryException('User lookup failed', 0, $e);
        }
    }
    
    public function save(User $user): User
    {
        $sql = $user->getId() 
            ? 'UPDATE users SET name = :name, email = :email WHERE id = :id'
            : 'INSERT INTO users (name, email) VALUES (:name, :email)';
            
        $stmt = $this->database->prepare($sql);
        $params = [
            'name' => $user->getName(),
            'email' => $user->getEmail()
        ];
        
        if ($user->getId()) {
            $params['id'] = $user->getId();
        }
        
        $stmt->execute($params);
        
        if (!$user->getId()) {
            $user = $user->withId((int) $this->database->lastInsertId());
        }
        
        return $user;
    }
}

Service Layer with Validation

<?php

class UserService
{
    public function __construct(
        private UserRepositoryInterface $userRepository,
        private ValidatorInterface $validator,
        private EventDispatcherInterface $eventDispatcher
    ) {}
    
    public function createUser(CreateUserRequest $request): User
    {
        // Validate input
        $violations = $this->validator->validate($request);
        if (count($violations) > 0) {
            throw new ValidationException($violations);
        }
        
        // Check if user already exists
        $existingUser = $this->userRepository->findByEmail($request->email);
        if ($existingUser) {
            throw new UserAlreadyExistsException(
                "User with email {$request->email} already exists"
            );
        }
        
        // Create user
        $user = new User(
            id: null,
            name: $request->name,
            email: $request->email,
            createdAt: new DateTimeImmutable(),
            updatedAt: new DateTimeImmutable()
        );
        
        $savedUser = $this->userRepository->save($user);
        
        // Dispatch event
        $this->eventDispatcher->dispatch(
            new UserCreatedEvent($savedUser)
        );
        
        return $savedUser;
    }
    
    public function updateUser(int $id, UpdateUserRequest $request): User
    {
        $user = $this->userRepository->findById($id);
        if (!$user) {
            throw new UserNotFoundException("User with ID {$id} not found");
        }
        
        $violations = $this->validator->validate($request);
        if (count($violations) > 0) {
            throw new ValidationException($violations);
        }
        
        $updatedUser = $user
            ->withName($request->name ?? $user->getName())
            ->withEmail($request->email ?? $user->getEmail())
            ->withUpdatedAt(new DateTimeImmutable());
            
        return $this->userRepository->save($updatedUser);
    }
}

Modern PHP with Composer and PSR Standards

Composer Configuration

{
    "name": "mycompany/awesome-app",
    "description": "A modern PHP application",
    "type": "project",
    "require": {
        "php": "^8.3",
        "symfony/console": "^7.0",
        "symfony/dependency-injection": "^7.0",
        "symfony/event-dispatcher": "^7.0",
        "symfony/validator": "^7.0",
        "monolog/monolog": "^3.0",
        "doctrine/dbal": "^4.0",
        "ramsey/uuid": "^4.7"
    },
    "require-dev": {
        "phpunit/phpunit": "^11.0",
        "phpstan/phpstan": "^1.10",
        "squizlabs/php_codesniffer": "^3.8",
        "rector/rector": "^1.0"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "App\\Tests\\": "tests/"
        }
    },
    "scripts": {
        "test": "phpunit",
        "analyse": "phpstan analyse src tests",
        "cs-check": "phpcs src tests",
        "cs-fix": "phpcbf src tests",
        "rector": "rector process src tests --dry-run",
        "rector-fix": "rector process src tests"
    },
    "config": {
        "sort-packages": true,
        "allow-plugins": {
            "composer/package-versions-deprecated": true
        }
    }
}

PSR-12 Compliant Code Structure

<?php

declare(strict_types=1);

namespace App\Controller;

use App\Service\UserService;
use App\Request\CreateUserRequest;
use App\Exception\ValidationException;
use App\Exception\UserAlreadyExistsException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;

final class UserController
{
    public function __construct(
        private UserService $userService,
        private LoggerInterface $logger
    ) {}
    
    public function create(ServerRequestInterface $request): ResponseInterface
    {
        try {
            $requestData = json_decode(
                $request->getBody()->getContents(),
                true,
                512,
                JSON_THROW_ON_ERROR
            );
            
            $createUserRequest = new CreateUserRequest(
                name: $requestData['name'] ?? '',
                email: $requestData['email'] ?? ''
            );
            
            $user = $this->userService->createUser($createUserRequest);
            
            return $this->jsonResponse([
                'success' => true,
                'data' => $user->toArray(),
                'message' => 'User created successfully'
            ], 201);
            
        } catch (ValidationException $e) {
            return $this->jsonResponse([
                'success' => false,
                'errors' => $e->getViolations(),
                'message' => 'Validation failed'
            ], 422);
            
        } catch (UserAlreadyExistsException $e) {
            return $this->jsonResponse([
                'success' => false,
                'message' => $e->getMessage()
            ], 409);
            
        } catch (\Throwable $e) {
            $this->logger->error('Unexpected error creating user', [
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString()
            ]);
            
            return $this->jsonResponse([
                'success' => false,
                'message' => 'Internal server error'
            ], 500);
        }
    }
    
    private function jsonResponse(array $data, int $status = 200): ResponseInterface
    {
        $response = new \Laminas\Diactoros\Response();
        $response->getBody()->write(json_encode($data, JSON_THROW_ON_ERROR));
        
        return $response
            ->withHeader('Content-Type', 'application/json')
            ->withStatus($status);
    }
}

Testing with PHPUnit

<?php

declare(strict_types=1);

namespace App\Tests\Service;

use App\Service\UserService;
use App\Repository\UserRepositoryInterface;
use App\Request\CreateUserRequest;
use App\Entity\User;
use App\Exception\UserAlreadyExistsException;
use App\Exception\ValidationException;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Validator\ValidatorInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

final class UserServiceTest extends TestCase
{
    private UserService $userService;
    private MockObject|UserRepositoryInterface $userRepository;
    private MockObject|ValidatorInterface $validator;
    private MockObject|EventDispatcherInterface $eventDispatcher;
    
    protected function setUp(): void
    {
        $this->userRepository = $this->createMock(UserRepositoryInterface::class);
        $this->validator = $this->createMock(ValidatorInterface::class);
        $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class);
        
        $this->userService = new UserService(
            $this->userRepository,
            $this->validator,
            $this->eventDispatcher
        );
    }
    
    public function testCreateUserSuccessfully(): void
    {
        // Arrange
        $request = new CreateUserRequest('John Doe', 'john@example.com');
        
        $this->validator
            ->expects($this->once())
            ->method('validate')
            ->with($request)
            ->willReturn([]);
            
        $this->userRepository
            ->expects($this->once())
            ->method('findByEmail')
            ->with('john@example.com')
            ->willReturn(null);
            
        $expectedUser = new User(
            id: 1,
            name: 'John Doe',
            email: 'john@example.com',
            createdAt: new \DateTimeImmutable(),
            updatedAt: new \DateTimeImmutable()
        );
        
        $this->userRepository
            ->expects($this->once())
            ->method('save')
            ->willReturn($expectedUser);
            
        $this->eventDispatcher
            ->expects($this->once())
            ->method('dispatch');
        
        // Act
        $result = $this->userService->createUser($request);
        
        // Assert
        $this->assertSame($expectedUser, $result);
    }
    
    public function testCreateUserThrowsExceptionWhenUserExists(): void
    {
        // Arrange
        $request = new CreateUserRequest('John Doe', 'john@example.com');
        $existingUser = new User(
            id: 1,
            name: 'Existing User',
            email: 'john@example.com',
            createdAt: new \DateTimeImmutable(),
            updatedAt: new \DateTimeImmutable()
        );
        
        $this->validator
            ->method('validate')
            ->willReturn([]);
            
        $this->userRepository
            ->method('findByEmail')
            ->willReturn($existingUser);
        
        // Assert
        $this->expectException(UserAlreadyExistsException::class);
        $this->expectExceptionMessage('User with email john@example.com already exists');
        
        // Act
        $this->userService->createUser($request);
    }
}

Performance Optimization

OPcache Configuration

<?php
// opcache.ini configuration

// Enable OPcache
opcache.enable=1
opcache.enable_cli=1

// Memory settings
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000

// Validation settings
opcache.validate_timestamps=0  // Disable in production
opcache.revalidate_freq=0

// Optimization settings
opcache.save_comments=0
opcache.enable_file_override=1
opcache.optimization_level=0x7FFEBFFF

// JIT settings (PHP 8.0+)
opcache.jit_buffer_size=256M
opcache.jit=tracing

Database Query Optimization

<?php

class OptimizedUserRepository implements UserRepositoryInterface
{
    private array $cache = [];
    
    public function findUsersWithPosts(int $limit = 10): array
    {
        $cacheKey = "users_with_posts_{$limit}";
        
        if (isset($this->cache[$cacheKey])) {
            return $this->cache[$cacheKey];
        }
        
        // Use JOIN instead of N+1 queries
        $sql = "
            SELECT 
                u.id, u.name, u.email,
                p.id as post_id, p.title, p.content
            FROM users u
            LEFT JOIN posts p ON u.id = p.user_id
            ORDER BY u.created_at DESC
            LIMIT :limit
        ";
        
        $stmt = $this->database->prepare($sql);
        $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
        $stmt->execute();
        
        $results = [];
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $userId = $row['id'];
            
            if (!isset($results[$userId])) {
                $results[$userId] = [
                    'id' => $row['id'],
                    'name' => $row['name'],
                    'email' => $row['email'],
                    'posts' => []
                ];
            }
            
            if ($row['post_id']) {
                $results[$userId]['posts'][] = [
                    'id' => $row['post_id'],
                    'title' => $row['title'],
                    'content' => $row['content']
                ];
            }
        }
        
        $this->cache[$cacheKey] = array_values($results);
        
        return $this->cache[$cacheKey];
    }
}

Conclusion

Modern PHP development has come a long way from its early days. With features like:

  • Strong typing with union types, intersection types, and typed properties
  • Improved performance through JIT compilation and OPcache
  • Better architecture with dependency injection and PSR standards
  • Robust ecosystem with Composer and quality packages
  • Modern syntax with match expressions, named arguments, and attributes

PHP 8.3+ provides developers with the tools needed to build scalable, maintainable applications. The key is embracing modern practices, leveraging the type system, and following established patterns and standards.

Whether you’re building APIs, web applications, or CLI tools, modern PHP offers the performance and developer experience needed for professional software development.