PHP 8 - Modern Features and Best Practices for Developers
PHP Modern Features
backendPHP 8 - Modern Features and Best Practices
PHP has undergone a remarkable transformation in recent years. Versions 8.0, 8.1, and 8.2 delivered a suite of features that bring the language on par with the most modern backend technologies available today. If you are still writing PHP 5 or 7 style code, this guide will show you why it is time to modernize and how to take full advantage of what PHP 8 has to offer.
Named Arguments - Self-Documenting Function Calls#
One of the most anticipated additions in PHP 8.0 was named arguments. They allow you to pass values to a function by explicitly specifying the parameter name, which dramatically improves code readability.
The Problem with Positional Arguments#
// PHP 7 - what do these values mean?
array_slice($array, 0, 3, true);
setcookie('session', $value, 0, '/', '', true, true);
Without consulting the documentation, it is nearly impossible to tell what each argument represents.
The Named Arguments Solution#
// PHP 8 - crystal clear
array_slice(array: $array, offset: 0, length: 3, preserve_keys: true);
setcookie(
name: 'session',
value: $value,
path: '/',
secure: true,
httponly: true,
);
Named arguments work beautifully with default values - you can skip parameters that do not need to change:
function createUser(
string $name,
string $email,
string $role = 'user',
bool $active = true,
?string $avatar = null,
string $locale = 'en',
) {
// ...
}
// Only set what you need
$user = createUser(
name: 'Jane Smith',
email: 'jane@example.com',
locale: 'de',
);
Union Types and Intersection Types - Precise Type Declarations#
PHP 8.0 introduced union types, and PHP 8.1 expanded the type system with intersection types. Together, they form a powerful mechanism for expressing data expectations.
Union Types (PHP 8.0)#
A union type declares that a parameter or return value can be one of several types:
function processInput(string|int|float $value): string|false
{
if (is_numeric($value)) {
return number_format((float) $value, 2, '.', ',');
}
return false;
}
// Nullable shorthand
function findUser(int $id): User|null
{
// Equivalent to ?User
return User::find($id);
}
Intersection Types (PHP 8.1)#
Intersection types require a value to implement multiple interfaces simultaneously:
interface Loggable
{
public function toLogEntry(): string;
}
interface Serializable
{
public function serialize(): string;
}
// Parameter must implement BOTH interfaces
function processEntity(Loggable&Serializable $entity): void
{
logger()->info($entity->toLogEntry());
cache()->put('entity', $entity->serialize());
}
Disjunctive Normal Form Types (PHP 8.2)#
PHP 8.2 allows combining union and intersection types in a single expression:
function handleInput((Stringable&Countable)|string $input): string
{
if (is_string($input)) {
return $input;
}
return (string) $input;
}
Match Expression - A Modern Alternative to Switch#
The match expression replaces the switch statement with a more concise and safer syntax. Unlike switch, match uses strict comparison (===), does not require break statements, and returns a value directly.
// Legacy approach with switch
switch ($statusCode) {
case 200:
case 201:
$message = 'Success';
break;
case 404:
$message = 'Not Found';
break;
case 500:
$message = 'Server Error';
break;
default:
$message = 'Unknown Status';
}
// Modern approach with match
$message = match($statusCode) {
200, 201 => 'Success',
404 => 'Not Found',
500 => 'Server Error',
default => 'Unknown Status',
};
match is especially powerful when combined with enums and in more complex decision scenarios:
$result = match(true) {
$age < 13 => 'child',
$age < 18 => 'teenager',
$age < 65 => 'adult',
default => 'senior',
};
Enums - Native Enumerated Types (PHP 8.1)#
PHP 8.1 finally introduced native enums, eliminating the need for constant-based workarounds or third-party libraries.
Pure Enums#
enum Status
{
case Draft;
case Published;
case Archived;
}
function updateArticle(int $id, Status $status): void
{
// Type-safe - invalid values cannot be passed
}
updateArticle(1, Status::Published);
Backed Enums - Enums with Scalar Values#
enum Color: string
{
case Red = '#FF0000';
case Green = '#00FF00';
case Blue = '#0000FF';
// Enums can have methods
public function label(): string
{
return match($this) {
self::Red => 'Red',
self::Green => 'Green',
self::Blue => 'Blue',
};
}
}
// Create from value
$color = Color::from('#FF0000'); // Color::Red
$color = Color::tryFrom('#FFFFFF'); // null - no exception
// Access value and name
echo Color::Red->value; // #FF0000
echo Color::Red->name; // Red
echo Color::Red->label(); // Red
Enums with Interfaces#
interface HasDescription
{
public function description(): string;
}
enum PaymentMethod: string implements HasDescription
{
case CreditCard = 'credit_card';
case BankTransfer = 'bank_transfer';
case PayPal = 'paypal';
public function description(): string
{
return match($this) {
self::CreditCard => 'Credit or debit card',
self::BankTransfer => 'Bank wire transfer',
self::PayPal => 'PayPal payment',
};
}
public function processingTime(): string
{
return match($this) {
self::CreditCard => 'instant',
self::BankTransfer => '1-2 business days',
self::PayPal => 'instant',
};
}
}
Fibers - Lightweight Concurrency (PHP 8.1)#
Fibers are a user-level concurrency mechanism that allows code execution to be suspended and resumed. They serve as the foundation for modern asynchronous PHP frameworks such as AMPHP and ReactPHP.
$fiber = new Fiber(function (): void {
$value = Fiber::suspend('first suspension');
echo "Received: $value\n";
$value = Fiber::suspend('second suspension');
echo "Received: $value\n";
});
// Start the fiber
$result = $fiber->start();
echo $result . "\n"; // "first suspension"
// Resume with a value
$result = $fiber->resume('hello');
echo $result . "\n"; // "second suspension"
$fiber->resume('world');
Practical Example - Concurrent HTTP Requests#
function asyncHttpRequest(string $url): Fiber
{
return new Fiber(function () use ($url): array {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
// Suspend fiber - allow other tasks to run
Fiber::suspend();
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'status' => $httpCode,
'body' => json_decode($response, true),
];
});
}
// Launch multiple requests "concurrently"
$fibers = [
asyncHttpRequest('https://api.example.com/users'),
asyncHttpRequest('https://api.example.com/products'),
asyncHttpRequest('https://api.example.com/orders'),
];
foreach ($fibers as $fiber) {
$fiber->start();
}
// Collect results
$results = [];
foreach ($fibers as $fiber) {
$results[] = $fiber->resume();
}
In practice, fibers are most commonly used under the hood by frameworks, but understanding how they work enables you to write more efficient asynchronous code.
Readonly Properties and Classes (PHP 8.1 / 8.2)#
Readonly Properties (PHP 8.1)#
Readonly properties can only be assigned once - typically in the constructor. Any subsequent modification attempt triggers an error:
class Invoice
{
public function __construct(
public readonly string $number,
public readonly float $amount,
public readonly DateTimeImmutable $issuedAt,
public readonly string $currency = 'USD',
) {}
}
$invoice = new Invoice(
number: 'INV/2025/001',
amount: 1500.00,
issuedAt: new DateTimeImmutable(),
);
echo $invoice->number; // INV/2025/001
// $invoice->number = 'INV/2025/002'; // Error: Cannot modify readonly property
Readonly Classes (PHP 8.2)#
PHP 8.2 allows marking an entire class as readonly - all properties automatically become readonly:
readonly class Money
{
public function __construct(
public float $amount,
public string $currency,
) {}
public function add(Money $other): self
{
if ($this->currency !== $other->currency) {
throw new InvalidArgumentException('Currencies must match');
}
return new self($this->amount + $other->amount, $this->currency);
}
public function multiply(float $factor): self
{
return new self($this->amount * $factor, $this->currency);
}
}
$price = new Money(100.00, 'USD');
$tax = $price->multiply(0.07);
$total = $price->add($tax);
echo "$total->amount $total->currency"; // 107 USD
Readonly classes are a perfect fit for the Value Object pattern and help you build immutable data structures.
Constructor Promotion - Less Boilerplate, More Clarity#
Constructor promotion, introduced in PHP 8.0, dramatically reduces the amount of code needed to define simple classes:
// PHP 7 - lots of repetition
class Product
{
private string $name;
private float $price;
private string $sku;
private int $stock;
public function __construct(
string $name,
float $price,
string $sku,
int $stock
) {
$this->name = $name;
$this->price = $price;
$this->sku = $sku;
$this->stock = $stock;
}
}
// PHP 8 - concise and readable
class Product
{
public function __construct(
private string $name,
private float $price,
private string $sku,
private int $stock = 0,
) {}
public function isAvailable(): bool
{
return $this->stock > 0;
}
}
Combining constructor promotion with readonly yields extremely compact DTOs and value objects:
readonly class OrderDTO
{
public function __construct(
public string $customerEmail,
public array $items,
public float $totalAmount,
public string $currency = 'USD',
public ?string $couponCode = null,
) {}
}
Null-safe Operator - Safe Object Navigation#
The ?-> operator eliminates cascading null checks that used to clutter codebases:
// PHP 7 - chain of null checks
$country = null;
if ($user !== null) {
$address = $user->getAddress();
if ($address !== null) {
$city = $address->getCity();
if ($city !== null) {
$country = $city->getCountry();
}
}
}
// PHP 8 - one line
$country = $user?->getAddress()?->getCity()?->getCountry();
The null-safe operator combines well with other PHP 8 features:
// Combined with named arguments and match
$shippingLabel = match($order?->getShippingMethod()?->getType()) {
'express' => 'Express shipping',
'standard' => 'Standard shipping',
'pickup' => 'In-store pickup',
default => 'No shipping info available',
};
// Null-safe with method calls and fallback
$length = $user?->getProfile()?->getBio()?->length() ?? 0;
First-Class Callable Syntax (PHP 8.1)#
PHP 8.1 introduced an elegant syntax for creating function and method references using the (...) operator:
// Legacy approach
$lengths = array_map(function ($str) {
return strlen($str);
}, $strings);
// PHP 8.1 - first-class callable
$lengths = array_map(strlen(...), $strings);
// Works with object methods
class StringHelper
{
public function capitalize(string $str): string
{
return mb_strtoupper(mb_substr($str, 0, 1)) . mb_substr($str, 1);
}
}
$helper = new StringHelper();
$capitalized = array_map($helper->capitalize(...), ['php', 'javascript', 'python']);
// ['Php', 'Javascript', 'Python']
// Static methods
$filtered = array_filter($items, Validator::isValid(...));
// Building a processing pipeline
$pipeline = [
trim(...),
strtolower(...),
htmlspecialchars(...),
];
$result = array_reduce(
$pipeline,
fn($carry, $fn) => $fn($carry),
$input,
);
JIT Compiler - Supercharged PHP Performance#
The Just-In-Time (JIT) compiler in PHP 8.0 represents a fundamental shift in how PHP code is executed. JIT compiles PHP into native machine code, delivering dramatic performance gains in certain workloads.
Configuring JIT#
; php.ini
opcache.enable=1
opcache.jit_buffer_size=256M
opcache.jit=1255
; JIT modes:
; 1205 - function JIT (safe, good starting point)
; 1235 - tracing JIT with optimizations
; 1255 - tracing JIT with full optimizations (max performance)
When Does JIT Make a Difference?#
JIT delivers the greatest gains in CPU-intensive code:
// Mathematical operations - up to 3-4x speedup
function fibonacci(int $n): int
{
if ($n <= 1) return $n;
return fibonacci($n - 1) + fibonacci($n - 2);
}
// Image processing, cryptography, ML algorithms
// - anything CPU-bound benefits from JIT
// Typical web applications (I/O-bound) see smaller gains,
// but JIT is still worth enabling for overall improvement
PHP 8 vs PHP 7 Benchmarks#
For typical web applications, PHP 8 with JIT offers:
- Symfony/Laravel applications: 5-15% faster response times
- String operations: up to 30% faster processing
- Mathematical computations: up to 300% speedup
- Memory usage: comparable to PHP 7.4, slight increase due to JIT buffer
Additional Features Worth Knowing#
Attributes (PHP 8.0)#
#[Route('/api/users', methods: ['GET'])]
#[Middleware('auth')]
public function listUsers(): JsonResponse
{
// ...
}
#[Deprecated('Use newMethod() instead')]
public function oldMethod(): void
{
// ...
}
The never Return Type (PHP 8.1)#
function redirect(string $url): never
{
header("Location: $url");
exit;
}
function throwNotFound(): never
{
throw new NotFoundException();
}
Constants in Interfaces and Traits (PHP 8.2)#
enum DatabaseDriver: string
{
case MySQL = 'mysql';
case PostgreSQL = 'pgsql';
case SQLite = 'sqlite';
public function defaultPort(): int
{
return match($this) {
self::MySQL => 3306,
self::PostgreSQL => 5432,
self::SQLite => 0,
};
}
}
Practical Example - A Modern Service Class#
Let us combine all the features we have covered into a single, real-world example:
readonly class OrderService
{
public function __construct(
private OrderRepository $orders,
private PaymentGateway $payments,
private NotificationService $notifications,
private LoggerInterface $logger,
) {}
public function placeOrder(OrderDTO $dto): Order
{
$order = new Order(
items: $dto->items,
total: new Money(amount: $dto->totalAmount, currency: $dto->currency),
status: OrderStatus::Pending,
createdAt: new DateTimeImmutable(),
);
$paymentResult = match($dto->paymentMethod) {
PaymentMethod::CreditCard => $this->payments->chargeCard(order: $order),
PaymentMethod::BankTransfer => $this->payments->initTransfer(order: $order),
PaymentMethod::PayPal => $this->payments->processPayPal(order: $order),
};
$order = $paymentResult?->isSuccessful()
? $order->withStatus(OrderStatus::Paid)
: $order->withStatus(OrderStatus::PaymentFailed);
$this->orders->save($order);
$this->notifications->send(
recipient: $dto->customerEmail,
template: $order->status->notificationTemplate(),
context: ['order' => $order],
);
$this->logger->info('Order placed', [
'orderId' => $order->id,
'status' => $order->status->value,
'amount' => "$order->total->amount $order->total->currency",
]);
return $order;
}
}
Migrating to PHP 8 - Where to Start#
- Update your dependencies - check library compatibility with
composer why-not php ^8.2 - Enable strict types - add
declare(strict_types=1)to every file - Use static analysis tools - PHPStan and Psalm will detect issues early
- Migrate incrementally - start with new files and refactor existing code as you go
- Enable JIT - configure opcache for your production environment
Summary#
PHP 8 is not a minor update - it is a fundamental evolution in how PHP code is written. Named arguments, union types, enums, readonly classes, match expressions, and the JIT compiler create an ecosystem where PHP stands shoulder to shoulder with languages like TypeScript and Kotlin.
Modern PHP is type-safe, expressive, performant, and genuinely enjoyable to work with. If you have not yet adopted these features, there has never been a better time to start.
Need help migrating to PHP 8 or building a modern backend application? The team at MDS Software Solutions Group specializes in creating high-performance, scalable solutions using PHP, Node.js, and .NET. Get in touch with us - we will help you harness the full potential of modern technologies.
Team of programming experts specializing in modern web technologies.