Introduction to PHP 8
Explore the new features and improvements in PHP 8, including JIT compiler and union types.
Explore the new features and improvements in PHP 8, including JIT compiler and union types.

Introduction: PHP has powered the web for decades, evolving from a simple scripting language into a mature, feature-rich platform for building modern applications. PHP 8 represents the most significant leap forward in the language's history, bringing groundbreaking features that enhance performance, improve developer experience, and modernize the codebase. With the JIT compiler, union types, attributes, named arguments, and dozens of other improvements, PHP 8 closes the gap with other modern languages while maintaining the simplicity and accessibility that made PHP popular in the first place.
PHP 8 is a major update of the PHP language. It contains many new features and optimizations including named arguments, union types, attributes, constructor property promotion, match expression, nullsafe operator, JIT compiler, and improvements in the type system, error handling, and consistency. Released in November 2020, PHP 8 builds upon the foundation of PHP 7.x with enhanced performance and developer-friendly syntax that makes writing clean, maintainable code easier than ever.
Understanding the benefits helps you make an informed decision about upgrading.
JIT Compiler: The Just-In-Time compiler can provide significant performance boosts for CPU-intensive operations, mathematical calculations, and long-running processes. While web applications may see modest improvements, the JIT shines in scenarios like image processing, machine learning, and scientific computing.
Optimized Core: Even without JIT, PHP 8 includes numerous optimizations that improve overall performance compared to PHP 7.4.
Modern Syntax: Features like match expressions, named arguments, and constructor property promotion reduce boilerplate and make code more expressive.
Better Type System: Union types, mixed type, and static return type provide more flexibility and safety when defining APIs.
Attributes: Replace docblock annotations with native, type-safe metadata that can be validated at compile time.
Stricter Type Checking: PHP 8 catches more type-related errors at compile time, reducing runtime bugs.
Improved Error Messages: Better error messages and stack traces make debugging easier.
Consistency: Many inconsistencies in the language have been addressed, making behavior more predictable.
PHP 8 is actively maintained with security updates and new features. PHP 7.4 reached end-of-life in November 2022, meaning no more security updates. Upgrading ensures your applications remain secure and supported.
Let's get PHP 8 installed on your system.
php -v
# PHP 7.4.x or earlier
Ubuntu/Debian:
# Add repository
sudo apt update
sudo apt install software-properties-common
sudo add-apt-repository ppa:ondrej/php
# Install PHP 8.3 (latest stable)
sudo apt update
sudo apt install php8.3
# Install common extensions
sudo apt install php8.3-{cli,fpm,mysql,xml,curl,gd,mbstring,zip}
# Verify installation
php -v
# PHP 8.3.x
macOS (using Homebrew):
# Install PHP 8.3
brew install php@8.3
# Link to make it default
brew link php@8.3
# Verify
php -v
Windows:
Download from https://windows.php.net/download/ and follow the installation wizard.
Docker:
FROM php:8.3-fpm
RUN docker-php-ext-install pdo pdo_mysql mysqli
WORKDIR /var/www/html
# Find php.ini location
php --ini
# Common settings to adjust
# memory_limit = 256M
# upload_max_filesize = 64M
# post_max_size = 64M
# max_execution_time = 300
# Install Composer
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
# Verify
composer --version
The Just-In-Time compiler is the most talked-about feature in PHP 8.
JIT compiles PHP code to machine code at runtime, potentially improving performance for CPU-intensive operations. Unlike traditional interpretation, JIT can optimize hot code paths and eliminate overhead.
; php.ini
opcache.enable=1
opcache.jit_buffer_size=100M
opcache.jit=1255
; JIT modes:
; 0 - Disabled
; 1201 - Minimal JIT (tracing)
; 1205 - Optimal (function)
; 1255 - Maximum (tracing + function)
// CPU-intensive calculations
function fibonacci(int $n): int {
if ($n <= 1) return $n;
return fibonacci($n - 1) + fibonacci($n - 2);
}
// Image processing
$image = imagecreatefromjpeg('input.jpg');
imagefilter($image, IMG_FILTER_GRAYSCALE);
imagejpeg($image, 'output.jpg');
// Mathematical operations
function calculatePi(int $iterations): float {
$pi = 0;
for ($i = 0; $i < $iterations; $i++) {
$pi += pow(-1, $i) / (2 * $i + 1);
}
return $pi * 4;
}
// Typical web applications (I/O bound)
$users = DB::table('users')
->where('active', true)
->get();
// Most web requests are limited by database/network, not CPU
// Check JIT status
var_dump(opcache_get_status()['jit']);
// Example output shows:
// - enabled: true
// - on: true
// - buffer_size: 104857600
// - buffer_free: 98765432
Union types allow you to specify that a variable can be one of several types.
class User {
// Property can be int or string
private int|string $id;
// Parameter can be array or object
public function setData(array|object $data): void {
// Process data
}
// Return type can be int or float
public function getPrice(): int|float {
return $this->price;
}
}
// Nullable union type
function findUser(int $id): User|null {
$user = DB::find($id);
return $user ?: null;
}
// Multiple types with null
function process(string|int|null $value): bool {
if ($value === null) {
return false;
}
// Process value
return true;
}
class ApiResponse {
// Can return array of data or error object
public function getData(): array|Error {
if ($this->hasError()) {
return new Error($this->errorMessage);
}
return $this->data;
}
// Multiple union types
public function process(
int|float $number,
string|array $config,
bool|null $verbose = null
): string|array {
// Implementation
return $result;
}
}
// false as a type
function save(): bool|false {
try {
// Save operation
return true;
} catch (Exception $e) {
return false;
}
}
// mixed type (any type)
function process(mixed $value): mixed {
// Can accept and return any type
return $value;
}
Named arguments allow you to pass values to a function based on parameter name rather than position.
// Traditional positional arguments
function createUser($name, $email, $age, $active = true) {
// ...
}
createUser('John', 'john@example.com', 30, false);
// Named arguments (PHP 8)
createUser(
name: 'John',
email: 'john@example.com',
age: 30,
active: false
);
function renderPage(
string $title,
string $content,
bool $sidebar = true,
bool $comments = true,
string $theme = 'light'
) {
// ...
}
// Skip middle parameters
renderPage(
title: 'My Page',
content: 'Page content',
theme: 'dark' // sidebar and comments use defaults
);
// Before: What do these booleans mean?
setcookie('user_session', $sessionId, 3600, '/', '', true, true);
// After: Crystal clear
setcookie(
name: 'user_session',
value: $sessionId,
expires_or_options: 3600,
path: '/',
domain: '',
secure: true,
httponly: true
);
$params = [
'name' => 'Jane',
'email' => 'jane@example.com',
'age' => 28
];
// Unpack array as named arguments
createUser(...$params);
// Mix positional and named
createUser('John', ...$params);
// Database query builder
$users = DB::table('users')
->select(columns: ['id', 'name', 'email'])
->where(column: 'active', operator: '=', value: true)
->orderBy(column: 'created_at', direction: 'desc')
->limit(count: 10)
->get();
// HTML helper
echo htmlspecialchars(
string: $userInput,
flags: ENT_QUOTES | ENT_HTML5,
encoding: 'UTF-8',
double_encode: false
);
Attributes provide a native way to add metadata to classes, methods, properties, and parameters.
#[Attribute]
class Route {
public function __construct(
public string $path,
public string $method = 'GET'
) {}
}
#[Attribute(Attribute::TARGET_METHOD)]
class Middleware {
public function __construct(
public array $middlewares
) {}
}
#[Route('/api/users', 'GET')]
class UserController {
#[Route('/api/users/{id}', 'GET')]
#[Middleware(['auth', 'verified'])]
public function show(int $id): User {
return User::find($id);
}
#[Route('/api/users', 'POST')]
#[Middleware(['auth'])]
public function store(Request $request): User {
return User::create($request->all());
}
}
function getRoutes(string $className): array {
$reflection = new ReflectionClass($className);
$routes = [];
foreach ($reflection->getMethods() as $method) {
$attributes = $method->getAttributes(Route::class);
foreach ($attributes as $attribute) {
$route = $attribute->newInstance();
$routes[] = [
'path' => $route->path,
'method' => $route->method,
'handler' => [$className, $method->getName()]
];
}
}
return $routes;
}
// Usage
$routes = getRoutes(UserController::class);
// Deprecated attribute
class User {
#[Deprecated('Use getFullName() instead')]
public function getName(): string {
return $this->name;
}
public function getFullName(): string {
return $this->firstName . ' ' . $this->lastName;
}
}
// Override attribute
class Child extends Parent {
#[Override]
public function process(): void {
// Must exist in parent
}
}
#[Attribute]
class Validate {
public function __construct(
public ?int $min = null,
public ?int $max = null,
public ?string $pattern = null
) {}
}
class CreateUserRequest {
#[Validate(min: 3, max: 50)]
public string $name;
#[Validate(pattern: '/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/')]
public string $email;
#[Validate(min: 18, max: 120)]
public int $age;
}
// Validator implementation
function validate(object $object): array {
$errors = [];
$reflection = new ReflectionClass($object);
foreach ($reflection->getProperties() as $property) {
$attributes = $property->getAttributes(Validate::class);
foreach ($attributes as $attribute) {
$validate = $attribute->newInstance();
$value = $property->getValue($object);
if ($validate->min && strlen($value) < $validate->min) {
$errors[] = "{$property->getName()} too short";
}
// More validation...
}
}
return $errors;
}
Reduce boilerplate when defining class properties through constructor parameters.
class User {
private string $name;
private string $email;
private int $age;
public function __construct(
string $name,
string $email,
int $age
) {
$this->name = $name;
$this->email = $email;
$this->age = $age;
}
}
class User {
public function __construct(
private string $name,
private string $email,
private int $age
) {}
// That's it! Properties are automatically created and assigned
}
class Product {
private float $discount;
public function __construct(
private string $name,
private float $price,
private int $stock,
bool $featured = false // Not promoted
) {
$this->discount = $featured ? 0.1 : 0;
}
}
class BlogPost {
public function __construct(
private string $title,
private string $content,
private string $author = 'Anonymous',
private bool $published = false,
private ?DateTime $publishedAt = null
) {}
}
// Usage
$post = new BlogPost(
title: 'My Post',
content: 'Post content'
);
class Payment {
public function __construct(
private int|string $orderId,
private float $amount,
private string|array $metadata = []
) {}
}
class CreateUserCommand {
public function __construct(
private string $name,
private string $email,
private string $password,
private array $roles = ['user'],
private bool $active = true
) {}
public function execute(): User {
return User::create([
'name' => $this->name,
'email' => $this->email,
'password' => bcrypt($this->password),
'roles' => $this->roles,
'active' => $this->active
]);
}
}
Match is a more powerful, type-safe alternative to switch statements.
// Old switch
$result = '';
switch ($status) {
case 'pending':
$result = 'Processing';
break;
case 'completed':
$result = 'Done';
break;
case 'failed':
$result = 'Error';
break;
default:
$result = 'Unknown';
}
// New match
$result = match ($status) {
'pending' => 'Processing',
'completed' => 'Done',
'failed' => 'Error',
default => 'Unknown'
};
// match uses === (strict comparison)
$value = '1';
// This returns "string"
$result = match ($value) {
1 => 'integer',
'1' => 'string',
};
// switch would return "integer" (loose comparison)
$httpCode = 404;
$message = match ($httpCode) {
200, 201, 202 => 'Success',
400, 401, 403 => 'Client Error',
404 => 'Not Found',
500, 502, 503 => 'Server Error',
default => 'Unknown Status'
};
$price = match ($user->role) {
'guest' => $basePrice,
'member' => $basePrice * 0.9,
'premium' => $basePrice * 0.8,
'admin' => 0,
};
// With function calls
$discount = match ($user->tier) {
'bronze' => calculateBronzeDiscount(),
'silver' => calculateSilverDiscount(),
'gold' => calculateGoldDiscount(),
default => 0
};
$price = 150;
$category = match (true) {
$price < 50 => 'budget',
$price < 100 => 'mid-range',
$price < 200 => 'premium',
$price >= 200 => 'luxury',
};
function process(mixed $value): string {
return match (true) {
is_int($value) => "Integer: $value",
is_string($value) => "String: $value",
is_array($value) => "Array with " . count($value) . " items",
is_object($value) => "Object of class " . get_class($value),
default => "Unknown type"
};
}
$route = match ($path) {
'/' => new HomeController(),
'/about' => new AboutController(),
'/contact' => new ContactController(),
default => throw new NotFoundException("Route not found: $path")
};
The nullsafe operator provides a cleaner way to access properties and methods on potentially null values.
// Nested null checks
$country = null;
if ($user !== null) {
if ($user->getAddress() !== null) {
$country = $user->getAddress()->getCountry();
}
}
// Ternary hell
$country = $user ? ($user->getAddress() ? $user->getAddress()->getCountry() : null) : null;
// Clean and readable
$country = $user?->getAddress()?->getCountry();
// If any part is null, the whole expression returns null
class User {
public ?Profile $profile = null;
}
class Profile {
public ?Address $address = null;
}
class Address {
public ?string $city = null;
}
$user = new User();
// Traditional
$city = $user->profile !== null &&
$user->profile->address !== null
? $user->profile->address->city
: null;
// Nullsafe
$city = $user->profile?->address?->city;
class Order {
private ?Customer $customer = null;
public function getCustomer(): ?Customer {
return $this->customer;
}
}
class Customer {
public function getName(): string {
return 'John Doe';
}
}
$order = new Order();
// Returns null if customer doesn't exist
$customerName = $order->getCustomer()?->getName();
// Works with array access too
$value = $data['user']?->profile?->settings['theme'] ?? 'default';
// API response processing
$city = $response?->data?->address?->city ?? 'Unknown';
// Database relationships
$authorName = $post?->author?->name ?? 'Anonymous';
// Configuration
$debugMode = $config?->app?->debug ?? false;
// Session data
$userId = $session?->user?->id;
// Short-circuits on null
$result = $user?->getProfile()?->save();
// If $user is null, getProfile() is never called
// Use with caution in assignments
$user?->profile = new Profile(); // Not allowed
$user->profile = new Profile(); // Use this instead
// Combine with null coalescing
$name = $user?->name ?? 'Guest';
WeakMap allows creating maps where object keys are held weakly, preventing memory leaks.
$map = new WeakMap();
$object = new stdClass();
$map[$object] = 'some data';
// Access
echo $map[$object]; // 'some data'
// When $object is no longer referenced, it's automatically garbage collected
unset($object);
// WeakMap entry is automatically removed
class DataCache {
private WeakMap $cache;
public function __construct() {
$this->cache = new WeakMap();
}
public function get(object $key): mixed {
return $this->cache[$key] ?? null;
}
public function set(object $key, mixed $value): void {
$this->cache[$key] = $value;
}
}
// Usage
$cache = new DataCache();
$user = User::find(1);
$cache->set($user, ['processed' => true]);
// When $user is no longer needed, cache is auto-cleared
class ObjectMetadata {
private WeakMap $metadata;
public function __construct() {
$this->metadata = new WeakMap();
}
public function setMetadata(object $object, array $data): void {
$this->metadata[$object] = $data;
}
public function getMetadata(object $object): ?array {
return $this->metadata[$object] ?? null;
}
}
$tracker = new ObjectMetadata();
$product = new Product();
$tracker->setMetadata($product, [
'views' => 100,
'last_viewed' => new DateTime()
]);
PHP 8 introduces new string functions for common operations.
// Check if string contains substring
$text = "Hello, World!";
// Before
if (strpos($text, 'World') !== false) {
echo "Found";
}
// PHP 8
if (str_contains($text, 'World')) {
echo "Found";
}
// Case-sensitive
str_contains('Hello', 'hello'); // false
$url = "https://example.com";
// Before
if (substr($url, 0, 5) === 'https') {
echo "Secure";
}
// PHP 8
if (str_starts_with($url, 'https')) {
echo "Secure";
}
// Examples
str_starts_with('Hello World', 'Hello'); // true
str_starts_with('Hello World', 'World'); // false
$filename = "document.pdf";
// Before
if (substr($filename, -4) === '.pdf') {
echo "PDF file";
}
// PHP 8
if (str_ends_with($filename, '.pdf')) {
echo "PDF file";
}
// Examples
str_ends_with('image.png', '.png'); // true
str_ends_with('image.png', '.jpg'); // false
// URL validation
function isSecureUrl(string $url): bool {
return str_starts_with($url, 'https://');
}
// File type checking
function isPdfFile(string $filename): bool {
return str_ends_with($filename, '.pdf');
}
// Email validation helper
function isValidEmail(string $email): bool {
return str_contains($email, '@') && str_contains($email, '.');
}
// Path checking
function isAbsolutePath(string $path): bool {
return str_starts_with($path, '/') || str_contains($path, ':\\');
}
PHP 8 brings several improvements to the type system.
class Model {
public static function create(array $data): static {
return new static($data);
}
}
class User extends Model {
// Returns User instance, not Model
}
$user = User::create(['name' => 'John']);
// $user is typed as User, not Model
// Explicit mixed type
function process(mixed $value): mixed {
return $value;
}
// Replaces unclear docblocks
/**
* @param mixed $value
* @return mixed
*/
function oldWay($value) {
return $value;
}
// Void: function returns nothing
function logMessage(string $message): void {
file_put_contents('log.txt', $message);
// No return statement
}
// Never: function never returns (throws or exits)
function redirect(string $url): never {
header("Location: $url");
exit;
}
function fail(string $message): never {
throw new Exception($message);
}
PHP 8 improves error handling and reporting.
// throw can now be used as an expression
// In arrow functions
$fn = fn() => throw new Exception('Error');
// In null coalescing
$value = $data['key'] ?? throw new InvalidArgumentException('Missing key');
// In ternary
$result = $condition ? $value : throw new Exception('Invalid condition');
// In match
$result = match ($type) {
'a' => processA(),
'b' => processB(),
default => throw new Exception('Unknown type')
};
// Before: must capture exception variable
try {
riskyOperation();
} catch (Exception $e) {
// Don't use $e
logError();
}
// PHP 8: omit variable if not needed
try {
riskyOperation();
} catch (Exception) {
logError();
}
// PHP 8 provides more informative error messages
// Before:
// "Undefined variable: name"
// PHP 8:
// "Undefined variable $name in /path/file.php:42"
// Type errors are clearer too
function greet(string $name): void {
echo "Hello, $name";
}
greet(123);
// PHP 8: "greet(): Argument #1 ($name) must be of type string, int given"
Several other improvements enhance PHP 8.
function createUser(
string $name,
string $email,
int $age, // Trailing comma allowed
) {
// ...
}
// Also in closures
$fn = function(
$a,
$b,
$c, // Trailing comma
) {
// ...
};
$object = new User();
// Before
$className = get_class($object);
// PHP 8
$className = $object::class;
class Product implements Stringable {
public function __construct(
private string $name,
private float $price
) {}
public function __toString(): string {
return "{$this->name} - \${$this->price}";
}
}
// Type hint with Stringable
function display(string|Stringable $value): void {
echo $value;
}
// Object-based token API
$tokens = PhpToken::tokenize('<?php echo "Hello";');
foreach ($tokens as $token) {
echo $token->getTokenName();
}
Upgrade your existing PHP 7.x applications to PHP 8.
# Check compatibility with rector
composer require --dev rector/rector
vendor/bin/rector init
# Or use PHPStan
composer require --dev phpstan/phpstan
vendor/bin/phpstan analyse src
1. Parameter Type Validation:
// PHP 7.4: null accepted even with type hint
function greet(string $name) {
echo "Hello, $name";
}
greet(null); // Works in 7.4
// PHP 8: TypeError
greet(null); // Fatal error
2. String/Number Comparisons:
// PHP 7.4
0 == 'hello'; // true
// PHP 8
0 == 'hello'; // false (saner comparison)
3. @ Error Suppression:
// PHP 7.4: suppresses all errors
@file_get_contents('missing.txt');
// PHP 8: fatal errors not suppressed
@file_get_contents('missing.txt'); // Still throws on fatal
4. Resource to Object:
// PHP 7.4: curl_init() returns resource
$ch = curl_init();
is_resource($ch); // true
// PHP 8: returns CurlHandle object
$ch = curl_init();
$ch instanceof CurlHandle; // true
// Update nullable types
// Before
function process(?string $value = null) {}
// Better
function process(?string $value) {}
// Use union types
// Before
/**
* @param string|int $id
*/
function find($id) {}
// After
function find(string|int $id) {}
// Use constructor promotion
// Before
class User {
private string $name;
public function __construct(string $name) {
$this->name = $name;
}
}
// After
class User {
public function __construct(
private string $name
) {}
}
Make the most of PHP 8 features.
<?php
declare(strict_types=1);
// Enables strict type checking
function add(int $a, int $b): int {
return $a + $b;
}
add(1, 2); // OK
add('1', '2'); // TypeError
// Be specific about what you accept
function process(string|int $id): User {
if (is_int($id)) {
return User::find($id);
}
return User::findByUuid($id);
}
// Clear and self-documenting
$user = createUser(
name: 'John',
email: 'john@example.com',
active: true
);
// More concise and type-safe
$message = match ($status) {
Status::PENDING => 'Processing',
Status::COMPLETE => 'Done',
Status::FAILED => 'Error',
};
// Less boilerplate
class DTO {
public function __construct(
public readonly string $name,
public readonly int $age,
) {}
}
PHP 8 represents a massive leap forward for the PHP language, bringing modern features that rival those found in newer programming languages while maintaining backward compatibility where possible. The JIT compiler improves performance for CPU-intensive tasks, while features like union types, attributes, named arguments, and match expressions make code more expressive and maintainable.
For developers, PHP 8 reduces boilerplate through constructor property promotion, improves null handling with the nullsafe operator, and provides better type safety throughout. The improved error messages and stricter type system help catch bugs earlier in development.
PHP 8 is not just an update—it's a transformation. Embrace these features, write cleaner code, and build better applications!