PHP 8.1 to PHP 8.2: What You Need to Know
If you’re running PHP 8.1 in production, you’ve already experienced the benefits of a modern, type-safe, and performant PHP. We’ve all been there—getting comfortable with a stable version, building solid applications, and then wondering: should we upgrade to the next minor version?
PHP 8.2 is indeed a minor release, but it introduces meaningful improvements in type safety, code clarity, and security. For many teams, the upgrade path is straightforward. For others, certain deprecations will require planning. In this guide, we’ll walk through what PHP 8.2 offers, what you need to watch out for, and how to make an informed decision about upgrading your applications.
New Features in PHP 8.2: Improved Type Safety and Code Clarity
Before we get into the specifics, it’s worth understanding PHP 8.2’s overall direction. This release emphasizes three themes:
- Stronger type safety—catching more errors at compile time rather than runtime
- Cleaner code patterns—reducing boilerplate and ambiguous constructs
- Better security defaults—making it harder to accidentally leak sensitive data
Let’s look at each new feature and how you might use it in your day-to-day development.
Readonly Classes
PHP 8.1 introduced readonly properties—a feature that allowed you to mark individual properties as immutable after construction. PHP 8.2 extends this concept to entire classes. When you declare a class as readonly, all of its properties automatically become readonly, and the class itself becomes deeply read-only—nested objects are also prevented from mutation.
readonly class UserData
{
public function __construct(
public string $name,
public string $email,
) {
}
}
Why does this matter? When you’re building value objects—data structures that represent a specific set of data without behavior that changes that data—immutability helps prevent bugs caused by unintended state changes. For example, if you’re passing user data through multiple layers of your application, readonly classes ensure that no middle layer accidentally modifies the original data. Of course, you’ll still need to decide whether immutability makes sense for your particular use case—some objects genuinely need to be mutable, and that’s perfectly fine.
A practical use case: Suppose you’re building an API that receives user input and passes it through validation, transformation, and storage layers. A readonly DTO (Data Transfer Object) guarantees that once your validation layer creates the object, the transformation layer can’t inadvertently modify it before storage.
Caveat: The readonly modifier only applies to properties defined directly in the class—it doesn’t automatically affect properties inherited from parent classes.
DNF (Disjunctive Normal Form) Types
PHP has supported union types (string|int) since PHP 8.0 and intersection types (Countable&Traversable) since PHP 8.1. But there’s been a gap: what if you need to express “either this intersection type OR that intersection type”? This is where Disjunctive Normal Form (DNF) types come in—they let you combine unions and intersections in a single type declaration.
function processData((Countable&Traversable)|null $data)
{
// ...
}
Why is this useful? In complex systems, you often have multiple valid input shapes. Consider a function that accepts either a fully-loaded repository object (which implements both Countable and Traversable) OR nothing at all (null for optional initialization). Previously, you’d have needed separate parameters or runtime checks. Now you can express this constraint directly in the type system.
Another practical example: A function that accepts either (Logger&EventEmitter)—an object that can both log events and emit them—or string|false for legacy compatibility. The type system can now express these compound alternatives more precisely.
Important: DNF types are only allowed in parameter types, return types, and property types (when combined with readonly). They’re not permitted in standalone type contexts like instanceof checks.
null, true, and false as Standalone Types
PHP 8.2 now allows null, true, and false to be used as standalone types. Previously, you could use them in union types (e.g., string|null), but not by themselves.
function setStatus(true|false $status): null
{
// This function explicitly requires true or false as input
// and guarantees it returns null (perhaps for side effects only)
// ...
return null;
}
When is this useful? Consider a function that deliberately returns nothing (null) because it produces side effects. Declaring : null makes that contract explicit. Similarly, accepting only true|false can enforce boolean parameters—this is more precise than type declarations that accept truthy/falsy values.
A practical scenario: You have a logging function that always returns null because its purpose is side effects:
function logAlert(bool $critical): null
{
$this->logger->alert('System alert', ['critical' => $critical]);
return null; // Explicitly returning nothing
}
This level of precision helps both human readers and static analysis tools catch misuse earlier in the development cycle. Of course, you should use this feature judiciously—most functions that return values should have appropriate return types rather than null unless there’s a specific reason.
New random Extension
PHP 8.2 introduces a new \Random extension that provides a modern, object-oriented API for generating random numbers—a big improvement over the old procedural functions like rand() and mt_rand(). Under the hood, it uses the system’s cryptographically secure random number generator (/dev/urandom on Unix-like systems, CryptGenRandom on Windows) rather than predictable pseudorandom algorithms.
$rng = new \Random\Randomizer();
$randomNumber = $rng->getInt(1, 100);
Why does this matter? The old rand() and mt_rand() functions were never designed for cryptography—they’re predictable given enough output. If you’ve ever used these for session tokens, password reset codes, or any security-sensitive random values, you’ve been vulnerable to attacks. The new random extension provides a drop-in replacement that’s both easier to use and secure by default.
Practical scenarios where this matters:
- Generating API keys or session identifiers
- Creating temporary passwords or tokens
- Implementing fair random selections (e.g., random user sampling)
- Cryptographic operations that require secure random bytes
Note: The extension is enabled by default in PHP 8.2+, but you may need to install the random PECL package on older versions if you want to use it before upgrading. Also, be aware that secure random generation can be slower than mt_rand()—but for most web application use cases, the slight performance difference won’t matter compared to the security benefits.
Constants in Traits
You can now define constants directly in traits—a feature that wasn’t available in previous PHP versions. This might seem like a small syntactic convenience, but it can significantly improve code organization when you’re sharing both behavior and configuration across classes.
trait LogsToFile
{
private const LOG_DIRECTORY = '/var/log/myapp';
private const LOG_LEVEL = 'INFO';
public function log(string $message): void
{
file_put_contents(
self::LOG_DIRECTORY . '/app.log',
'[' . self::LOG_LEVEL . '] ' . $message . PHP_EOL,
FILE_APPEND
);
}
}
class UserService
{
use LogsToFile;
public function createUser(array $data)
{
$this->log('Creating user: ' . $data['email']);
// ...
}
}
Why this is useful: Traits are often used to share implementation details across unrelated classes. By allowing constants, you can bundle configuration with behavior—the constant lives right where it’s used rather than scattered through a separate configuration class or define() calls. Of course, keep in mind that constants defined in traits become part of the using class’s namespace, so be mindful of naming collisions—though that’s already a concern with trait methods.
Sensitive Parameter Value Redaction
PHP 8.2 introduces the \SensitiveParameter attribute, which marks function or method parameters so their values are redacted in stack traces. This addresses a long-standing security issue: when exceptions occur, PHP’s default error logging often includes the full parameter values—exposing passwords, API keys, or other secrets in logs.
use SensitiveParameter;
function login(
string $username,
#[SensitiveParameter] string $password
) {
// Authentication logic here
if ($this->auth->verify($username, $password) === false) {
throw new \Exception('Login failed');
}
}
What happens when an exception is thrown? Without the attribute, your logs might show:
Exception: Login failed in /path/to/auth.php on line 42
Stack trace:
#0 login('user', 'password123') called at ...
With the attribute, the password is replaced with a placeholder:
Exception: Login failed in /path/to/auth.php on line 42
Stack trace:
#0 login('user', '???') called at ...
Why this matters in practice: Many PHP applications log uncaught exceptions by default via set_exception_handler(). In production, these logs might be aggregated to services like Sentry, Papertrail, or CloudWatch. If your function accepts sensitive data and throws an exception, that data could be persisted in multiple locations—where you might not have fine-grained access control. The attribute helps comply with security best practices and regulations like GDPR or PCI-DSS that require minimizing sensitive data in logs.
Important considerations:
- The attribute only affects automatically generated stack traces; it won’t hide data if you manually log parameters yourself
- You should still follow defense-in-depth: validate inputs early, use HTTPS, store passwords hashed, etc.
- For older PHP versions, you can implement the
\Stringableinterface on wrapper objects to achieve similar redaction behavior, though it’s more complex
Deprecations in PHP 8.2: What You Need to Change
Alongside new features, PHP 8.2 deprecates several constructs that have been problematic or have better alternatives. Deprecations don’t break your code immediately—they emit E_DEPRECATED warnings—but they will be removed in a future PHP version (typically PHP 9.0). It’s wise to address them now to avoid future upgrade pain.
${...} String Interpolation
The ${...} style of variable interpolation in double-quoted strings is now deprecated. This syntax dates back to PHP’s early days and can cause ambiguity in complex expressions.
What’s deprecated:
// ❌ Deprecated in PHP 8.2
echo "Hello, ${name}!";
echo "The result is ${$array['key']}";
Recommended alternatives:
// ✓ Use simple variable interpolation
echo "Hello, $name!";
// ✓ Use braces for complex expressions
echo "Hello, {$name}!";
echo "The result is {$array['key']}";
// ✓ Or concatenate
echo "Hello, " . $name . "!";
Impact: This is generally straightforward to fix—most modern PHP code already uses {} or simple interpolation. If you maintain a legacy codebase, search for "${" patterns and update them. Tools like PHP_CodeSniffer with the PHPCompatibility standard can help identify these automatically.
Partially Supported Callables
PHP has allowed strings like "self::method" or "parent::method" as callables for historical reasons, but these have inconsistent behavior with inheritance and visibility. They’re now deprecated.
What’s deprecated:
// ❌ Deprecated
$callable = "self::method";
call_user_func($callable, $arg1, $arg2);
Recommended alternatives:
// ✓ Use Closure::fromCallable (PHP 7.1+)
$callable = Closure::fromCallable([$this, 'method']);
$callable($arg1, $arg2);
// ✓ Or explicit array syntax
$callable = [$this, 'method'];
$callable($arg1, $arg2);
// ✓ For static methods
$callable = [MyClass::class, 'staticMethod'];
Impact: The migration is usually simple, but be aware that Closure::fromCallable creates a true closure with different semantics than the string form (e.g., it binds $this appropriately). Most code will work unchanged with the array syntax, but if you were relying on the quirky behavior of self:: in inheritance hierarchies, you may need to adjust.
utf8_encode() and utf8_decode()
These functions were designed to convert between ISO-8859-1 (Latin-1) and UTF-8, but they’re frequently misapplied to other encodings, leading to data corruption or security vulnerabilities.
What’s deprecated:
// ❌ Deprecated
$utf8 = utf8_encode($latin1_string);
$latin1 = utf8_decode($utf8_string);
Recommended alternative:
// ✓ Use mbstring extension
$utf8 = mb_convert_encoding($latin1_string, 'UTF-8', 'ISO-8859-1');
// ✓ Or iconv
$utf8 = iconv('ISO-8859-1', 'UTF-8', $latin1_string);
Impact: If your application genuinely needs to convert between ISO-8859-1 and UTF-8, migrate to mb_convert_encoding() or iconv(). In many cases, these functions were being used incorrectly to “fix” character encoding issues that should be solved at a different layer (database connection encoding, HTTP headers, etc.). Now is a good time to audit your encoding handling—most modern PHP applications should use UTF-8 end-to-end and may not need these conversions at all.
Additional note: The mbstring extension is widely available and recommended for all multibyte string operations, not just this conversion.
Conclusion
Upgrading to PHP 8.2 is a worthwhile step for any PHP developer upgrading from PHP 8.1. It brings a range of new features that can help you write more expressive and secure code, as well as performance improvements that can make your applications faster. While there are some deprecations to be aware of, they are generally for old and problematic features that have better alternatives.
By understanding these changes, you can make a smooth transition to PHP 8.2. Keep in mind that PHP continues to evolve—PHP 8.3 and 8.4 have since been released with additional features and improvements. For a complete list of changes, always refer to the official PHP 8.2 migration guide.
Sponsored by Durable Programming
Need help with your PHP application? Durable Programming specializes in maintaining, upgrading, and securing PHP applications.
Hire Durable Programming