If you recently upgraded your CI/CD pipeline or local environment to PHP 8.4, you likely encountered a flood of deprecation notices resembling this:
Deprecated: Implicitly marking parameter $param as nullable is deprecated, the explicit nullable type must be used instead
For years, PHP allowed developers to declare a typed parameter with a default null value without explicitly marking the type as nullable. This behavior was convenient but inconsistent with the strict type system PHP has been adopting since version 7.0.
In PHP 8.4, this "magic" behavior is officially deprecated. This guide details exactly why this change was made, how to fix it manually, and how to automate the remediation for large codebases.
The Root Cause: Why PHP 8.4 Removes Implicit Nullability
To understand the fix, we must understand the inconsistency in the type system.
Historically, the PHP engine inferred type definitions based on default values. If you wrote function setDate(DateTime $date = null), PHP interpreted the signature as "Accepts a DateTime object OR null." The engine implicitly converted the type signature to ?DateTime (or DateTime|null) behind the scenes.
However, this created ambiguity in reflection APIs and static analysis. It broke the "Explicit is better than Implicit" rule of software architecture. A type signature reading string $token suggests the variable will always be a string. Allowing null to pass simply because a default value exists undermines the reliability of the type declaration.
PHP 8.4 enforces type purity. If a parameter can be null, the type signature must explicitly state that it allows null.
The Fix: Explicit Type Declarations
The solution involves updating your function signatures to explicitly allow null via the nullable shorthand (?) or Union Types.
Scenario 1: Simple Scalar or Object Types
The most common occurrence is a single type hint combined with a default null.
The Deprecated Code:
class UserProfile {
// ⚠️ Deprecated in PHP 8.4
// The type says 'string', but the default creates an implicit null
public function setDisplayName(string $name = null): void {
$this->name = $name;
}
}
The Fix: Prepend the type with a question mark (?), which is syntactic sugar for "Type OR Null".
class UserProfile {
// ✅ Valid PHP 8.4
// Explicitly nullable string
public function setDisplayName(?string $name = null): void {
$this->name = $name;
}
}
Scenario 2: Union Types
If you are already using Union Types (introduced in PHP 8.0), implicit nullability does not apply the same way. You cannot use the ? shorthand with union types.
The Deprecated Code:
class Logger {
// ⚠️ Deprecated in PHP 8.4
// Accepts array or string, but implicitly allows null
public function log(array|string $message = null): void {
// ...
}
}
The Fix: You must add |null to the union definition.
class Logger {
// ✅ Valid PHP 8.4
// Explicitly includes null in the union
public function log(array|string|null $message = null): void {
if ($message === null) {
return;
}
// ...
}
}
Scenario 3: Constructor Property Promotion
This pattern is ubiquitous in modern Symfony and Laravel applications using Data Transfer Objects (DTOs) or Entities.
The Deprecated Code:
class ProductDTO {
public function __construct(
public string $sku,
// ⚠️ Deprecated
public string $description = null,
) {}
}
The Fix: Apply the nullable modifier directly to the promoted property.
class ProductDTO {
public function __construct(
public string $sku,
// ✅ Valid
public ?string $description = null,
) {}
}
Automating the Fix with Rector
Refactoring thousands of method signatures manually is error-prone and poor use of engineering time. I recommend using Rector, an automated refactoring tool that can instantly upgrade your codebase to comply with PHP 8.4 standards.
If you don't have Rector installed:
composer require rector/rector --dev
Create or update your rector.php configuration file to include the specific rule for explicit nullables.
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\Php84\Rector\Param\ExplicitNullableParamTypeRector;
return RectorConfig::configure()
->withPaths([
__DIR__ . '/src',
__DIR__ . '/tests',
])
// Apply the specific rule for PHP 8.4 implicit nullables
->withRules([
ExplicitNullableParamTypeRector::class,
]);
Run Rector in dry-run mode first to verify changes:
vendor/bin/rector process --dry-run
Once verified, apply the changes:
vendor/bin/rector process
Deep Dive: Inheritance and Interface Compatibility
A critical edge case arises when dealing with inheritance. The Liskov Substitution Principle (LSP) and PHP's covariance/contravariance rules apply strictly here.
If you define an interface in a library that uses the old syntax, and you implement it in your application using the new syntax, PHP generally handles this gracefully because string $a = null was technically interpreted as ?string internally.
However, visual consistency is key. Ensure your interfaces are updated first.
Interface:
interface CacheInterface {
// Update this to ?int
public function setTtl(?int $seconds = null): void;
}
Implementation:
class RedisCache implements CacheInterface {
// This must match the interface signature
public function setTtl(?int $seconds = null): void {
// ...
}
}
If the interface is third-party code that hasn't been updated, your implementation can use ?int without violating the contract, as the child class is allowed to accept a wider range of types (contravariance), and semantically they are identical in PHP < 8.4.
Common Pitfalls
1. The "Optional" Misconception
Do not confuse nullable with optional.
?string $smeans the value can be a string or null, but the argument must be passed.?string $s = nullmeans the value can be string or null, and the argument can be omitted.
Removing the = null default value while adding the ? type hint will break call sites that rely on the default argument.
2. Reflection Logic
If you have meta-programming logic checking ReflectionParameter::allowsNull(), the behavior remains consistent. Both the old syntax and the new syntax return true. However, ReflectionParameter::getType() will now return a ReflectionNamedType or ReflectionUnionType that correctly represents the nullability stringently.
Conclusion
The deprecation of implicit nullable parameters in PHP 8.4 is a move toward a more predictable and robust type system. While it generates "noise" during the upgrade process, the resulting code is clearer and easier for static analysis tools to interpret.
By using the ? prefix or |null union type, you eliminate ambiguity. For larger projects, leverage Rector to automate the transition, ensuring your legacy code remains compatible with the modern PHP ecosystem.