New Features and Improvements in PHP 8.3: A Complete Technical Reference
PHP 8.3 is a major minor release of the PHP language that delivers significant improvements to the JIT compiler, type system, readonly properties, and core array/string functions. Released on November 23, 2023, it introduces typed class constants, json_validate(), array_is_list() refinements, Randomizer additions, and deep-cloning of readonly properties — changes that directly affect application performance, code correctness, and maintainability on production servers.
If you are running PHP-based workloads on a VPS Hosting environment or a Dedicated Server, understanding every PHP 8.3 change is not optional — it is a prerequisite for making informed upgrade decisions, avoiding silent regressions, and extracting measurable performance gains.
What Changed Between PHP 8.2 and PHP 8.3
Before diving into individual features, it is worth establishing the scope of this release. PHP 8.3 is not a breaking rewrite. It is a precision upgrade that closes long-standing gaps in the type system, hardens the JIT pipeline, and adds utility functions that previously required userland workarounds. The table below maps the most impactful changes against their PHP 8.2 equivalents.
| Feature / Behavior | PHP 8.2 | PHP 8.3 |
|---|---|---|
| Typed class constants | Not supported | Fully supported |
json_validate() | Not available | Available natively |
| Readonly property cloning | Not possible | Supported via clone |
array_is_list() | Available | Behavior unchanged, but broader adoption patterns |
| Dynamic class constant fetch | Syntax error | Supported via ClassName::{$const} |
Randomizer::getBytesFromString() | Not available | Available |
Randomizer::getFloat() / nextFloat() | Not available | Available |
#[Override] attribute | Not available | Available |
Deprecation: mt_rand implicit seeding | Not deprecated | Deprecated |
| Fiber stack size ini directive | Not configurable | fiber.stack_size added |
| JIT tracing improvements | Baseline tracing | Improved IR and loop handling |
str_contains with arrays | Not supported | Still not supported (source article error — see below) |
> Critical correction: The source article incorrectly states that str_contains() accepts an array of strings in PHP 8.3. This is factually wrong. str_contains() accepts only two string arguments. Passing an array triggers a TypeError. The correct approach for searching a substring across multiple strings is array_filter() combined with str_contains(), or in_array() for exact matches.
JIT Compilation in PHP 8.3: What Actually Changed
Background: How PHP JIT Works
PHP's JIT compiler, introduced experimentally in PHP 8.0, operates as an extension of the OPcache subsystem. It compiles hot bytecode paths into native machine code at runtime, bypassing the Zend VM interpreter for those paths. PHP 8.3 ships with a substantially revised JIT backend that improves the intermediate representation (IR) used during compilation.
The new IR-based JIT in PHP 8.3 (developed by Dmitry Stogov) replaces the older tracing JIT's code generation layer with a proper SSA-form intermediate representation. This enables better register allocation, dead code elimination, and loop invariant hoisting — optimizations that were structurally impossible in the previous architecture.
Enabling JIT Correctly
The original article shows php -d jit=on script.php, which is incomplete. JIT requires OPcache to be active. The correct minimal configuration for a CLI benchmark or a production php.ini is:
; php.ini
opcache.enable=1
opcache.enable_cli=1
opcache.jit_buffer_size=128M
opcache.jit=tracingFor a web server context (FPM or Apache mod_php), opcache.enable_cli is irrelevant, but opcache.jit_buffer_size must be non-zero or JIT silently disables itself. A common production pitfall is setting jit_buffer_size=0 in a shared php.ini and wondering why JIT has no effect.
When JIT Delivers Measurable Gains
JIT is not universally beneficial. Its gains are concentrated in CPU-bound workloads:
- High-value targets: Mathematical computations, image processing, machine learning inference, game logic, CSV/data parsing loops, cryptographic operations in userland.
- Low-value targets: Typical CRUD web applications where the bottleneck is I/O (database queries, filesystem, network). In these cases, JIT overhead from compilation can slightly increase memory usage with negligible throughput improvement.
- Negative cases: Applications with extremely diverse code paths (large frameworks with heavy reflection) may see JIT thrash the buffer, causing deoptimization overhead.
A practical rule: benchmark with opcache.jit=tracing first. If you see less than 3% improvement on your actual workload, disable JIT to recover the buffer memory for OPcache's opcode cache, which benefits all PHP applications uniformly.
Typed Class Constants
This is arguably the most impactful type-system addition in PHP 8.3 for large codebases.
The Problem It Solves
Prior to PHP 8.3, class constants had no enforced type. A child class could redefine a constant with a completely incompatible type, and PHP would not raise an error until runtime — or not at all, depending on how the constant was consumed.
// PHP 8.2 — no type enforcement
interface StatusCode {
const SUCCESS = 200; // implicitly int
}
class BrokenStatus implements StatusCode {
const SUCCESS = "two hundred"; // silently accepted — a maintenance nightmare
}PHP 8.3 Solution
// PHP 8.3 — type is enforced at definition and inheritance
interface StatusCode {
const int SUCCESS = 200;
}
class BrokenStatus implements StatusCode {
const int SUCCESS = "two hundred";
// Fatal error: Cannot use string as value for typed class constant
// BrokenStatus::SUCCESS of type int
}All scalar types (int, float, string, bool), array, null, union types, and intersection types are valid for constant type declarations. The never and void types are not permitted. This feature integrates cleanly with static analysis tools like PHPStan and Psalm, enabling stricter interface contracts without runtime overhead.
Dynamic Class Constant and Enum Member Fetch
PHP 8.3 allows fetching class constants and enum members using a runtime expression in the ::{} syntax.
class Direction {
const string NORTH = 'north';
const string SOUTH = 'south';
}
$direction = 'NORTH';
echo Direction::{$direction}; // outputs: northPreviously, this required constant() or a match expression, both of which are verbose and error-prone. The new syntax also works with enums:
enum Color {
case Red;
case Blue;
}
$name = 'Red';
$color = Color::{$name}; // Color::RedEdge case to watch: If the variable contains a name that does not correspond to a defined constant or enum case, PHP throws an Error exception — not a warning. Wrap dynamic fetches in a try/catch block or validate with defined() / enum_exists() before use.
The json_validate() Function
Why This Matters in Production
Before PHP 8.3, the idiomatic way to validate a JSON string without decoding it was:
json_decode($input);
$isValid = json_last_error() === JSON_ERROR_NONE;This approach fully decodes the JSON into a PHP structure, allocating memory proportional to the payload size. For validation-only pipelines — API gateways, message queue consumers, webhook receivers — this is wasteful.
PHP 8.3 Native Validation
$payload = '{"user": "alex", "role": "admin"}';
if (json_validate($payload)) {
// safe to decode
$data = json_decode($payload, true);
}
// Invalid JSON
var_dump(json_validate('{invalid}')); // bool(false)json_validate() parses the JSON structure without constructing a PHP value tree. Memory consumption is O(depth) rather than O(size), making it significantly more efficient for large payloads. It also accepts the $depth and $flags parameters consistent with json_decode().
Real-world use case: A webhook receiver processing 50,000 requests per minute can use json_validate() to reject malformed payloads at the edge before any deserialization occurs, reducing CPU and memory pressure substantially.
Readonly Properties: Deep Cloning Support
PHP 8.2 introduced readonly properties but made them impossible to modify even during object cloning. This forced developers into awkward workarounds — factory methods, serialization hacks, or abandoning readonly entirely for value objects.
PHP 8.3 resolves this with the __clone() magic method now being permitted to reassign readonly properties within the cloning context.
class ImmutablePoint {
public function __construct(
public readonly float $x,
public readonly float $y,
) {}
public function withX(float $x): static {
$clone = clone $this;
$clone->x = $x; // Legal in PHP 8.3 within __clone context
return $clone;
}
}
$point = new ImmutablePoint(1.0, 2.0);
$moved = $point->withX(5.0);
echo $moved->x; // 5.0
echo $point->x; // 1.0 — original unchangedThis pattern is foundational for immutable value objects in Domain-Driven Design. Without it, readonly was largely decorative for complex domain models.
The #[Override] Attribute
The #[Override] attribute signals to PHP (and static analysis tools) that a method is intended to override a parent class or interface method. If the parent method does not exist, PHP throws a compile-time error.
class Base {
public function process(): void {}
}
class Child extends Base {
#[Override]
public function process(): void {
// If Base::process() is renamed or removed, this becomes a fatal error
}
}This is particularly valuable in large teams where refactoring a base class can silently break child class overrides. The attribute acts as a compile-time contract, catching a category of bug that previously only surfaced at runtime or through static analysis.
array_is_list() and Correct Array Classification
array_is_list() was introduced in PHP 8.1, not 8.3. However, its correct usage patterns deserve precise documentation because the function is frequently misunderstood.
A PHP array is a list if and only if:
- It is empty, or
- Its keys are consecutive integers starting from
0with no gaps.
var_dump(array_is_list([])); // bool(true)
var_dump(array_is_list([0 => 'a', 1 => 'b'])); // bool(true)
var_dump(array_is_list(['a', 'b', 'c'])); // bool(true)
var_dump(array_is_list([1 => 'a', 0 => 'b'])); // bool(false) — wrong order
var_dump(array_is_list([0 => 'a', 2 => 'b'])); // bool(false) — gap at index 1
var_dump(array_is_list(['key' => 'value'])); // bool(false) — string keyPractical application: When serializing data to JSON, array_is_list() determines whether the output should be a JSON array ([]) or a JSON object ({}). Using it before json_encode() prevents accidental object serialization of numerically-indexed arrays that have had elements removed.
New Randomizer Methods in PHP 8.3
The RandomRandomizer class introduced in PHP 8.2 receives three important additions:
getBytesFromString()
$randomizer = new RandomRandomizer();
$token = $randomizer->getBytesFromString('abcdefghijklmnopqrstuvwxyz0123456789', 16);
echo $token; // e.g., "k3mz9xqp1wvn7yt2"This generates a cryptographically secure random string drawn from a specified alphabet — a pattern required for token generation, OTP codes, and slug creation. Previously, this required a manual loop with random_int().
getFloat() and nextFloat()
$randomizer = new RandomRandomizer();
// Returns a float in [0.0, 1.0)
$value = $randomizer->nextFloat();
// Returns a float in a specified closed or half-open interval
$scaled = $randomizer->getFloat(1.5, 9.5, RandomIntervalBoundary::ClosedOpen);getFloat() uses the γ-section algorithm to produce uniformly distributed floats without the modulo bias that affects naive implementations. This is critical for simulations, probabilistic algorithms, and A/B testing frameworks where distribution uniformity matters.
Deprecations and Removals in PHP 8.3
Understanding what is being phased out is as important as knowing what is being added. Ignoring deprecations now means fatal errors in PHP 9.0.
| Deprecated Feature | Reason | Migration Path |
|---|---|---|
Calling mt_rand() without explicit seed in some contexts | Implicit seeding behavior inconsistency | Use RandomRandomizer |
ReflectionProperty::setValue() without object on non-static | Ambiguous behavior | Pass the target object explicitly |
Passing negative $widths to mb_strimwidth() | Undefined behavior | Validate input before calling |
ldap_connect() with separate host/port args | Deprecated in favor of URI form | Use ldap://host:port URI string |
range() with non-integer step producing floats | Surprising implicit type coercion | Cast step explicitly to float |
Performance Benchmarks: PHP 8.3 vs. Previous Versions
Based on published benchmarks from Kinsta, Phoronix, and the PHP internals team using Symfony Demo, WordPress, and raw Fibonacci/sorting workloads:
| Benchmark | PHP 8.1 | PHP 8.2 | PHP 8.3 |
|---|---|---|---|
| Symfony Demo (req/sec) | ~1,450 | ~1,520 | ~1,610 |
| WordPress (req/sec) | ~1,180 | ~1,240 | ~1,290 |
| Fibonacci (JIT, ms) | ~48 | ~44 | ~38 |
| Mandelbrot (JIT, ms) | ~210 | ~195 | ~170 |
| OPcache only (no JIT) | Baseline | +5% | +8% |
The gains are consistent but not dramatic for I/O-bound applications. The JIT improvements in PHP 8.3 show the most significant delta in pure computation workloads — up to 18% faster than PHP 8.2 on the Mandelbrot benchmark.
Upgrading to PHP 8.3: Practical Server-Side Checklist
If you manage your own server infrastructure — whether on a VPS with cPanel or a bare-metal Dedicated Server — follow this sequence before upgrading production environments.
Pre-Upgrade Steps
- Run
composer outdatedand update all dependencies to versions with declared PHP 8.3 compatibility. - Execute
php -d error_reporting=E_ALL your_app_entrypoint.phpunder PHP 8.3 CLI to surface deprecation notices before they become fatal errors. - Audit any code that calls
str_contains(),str_starts_with(), orstr_ends_with()with non-string arguments — these now throwTypeErrorin strict contexts. - Review any class constants that child classes override — typed constants will cause fatal errors if types are incompatible.
- Check
phpinfo()output after upgrade to confirm OPcache and JIT are active with expected configuration.
php.ini Tuning for PHP 8.3 Production
; Recommended production baseline for PHP 8.3
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.jit=tracing
opcache.jit_buffer_size=64MSetting validate_timestamps=0 disables file modification checks on every request — essential for high-traffic deployments where OPcache invalidation overhead is measurable. Use deployment hooks to call opcache_reset() after code deploys instead.
Post-Upgrade Validation
# Verify active PHP version
php -v
# Confirm JIT is compiled in and active
php -r "var_dump(opcache_get_status()['jit']);"
# Check for deprecation notices in error log
tail -f /var/log/php-fpm/error.log | grep -i deprecatPHP 8.3 and Web Application Security Considerations
PHP 8.3 does not introduce new security primitives, but several changes have indirect security implications:
json_validate()reduces attack surface in input validation pipelines by preventing malformed JSON from reaching deserialization logic.- Typed class constants prevent type confusion attacks where a subclass substitutes an unexpected type for a security-relevant constant (e.g., a permission level or timeout value).
#[Override]attribute prevents silent method shadowing in security-critical base classes, a vector for subtle privilege escalation bugs in plugin architectures.RandomRandomizeradditions replace insecure patterns likesubstr(str_shuffle(implode(range('a','z'))), 0, 16)for token generation.
For applications handling sensitive data, pairing PHP 8.3 with a properly configured TLS stack is non-negotiable. If your hosting environment does not yet have current SSL Certificates deployed, address that before any PHP upgrade.
Choosing the Right Hosting Environment for PHP 8.3
PHP 8.3's JIT compiler and increased memory requirements for typed constant resolution mean that resource-constrained environments may not fully benefit from the upgrade.
- Shared hosting: PHP version availability depends entirely on the provider. If you need PHP 8.3 immediately, Shared Web Hosting plans with PHP version switching give you flexibility without server management overhead.
- VPS: Full control over
php.ini, PHP-FPM pool configuration, OPcache tuning, and JIT buffer sizing. This is the minimum recommended environment for production PHP 8.3 deployments with JIT enabled. - Dedicated servers: Required for high-traffic applications where JIT buffer contention across multiple PHP-FPM workers becomes a bottleneck. A dedicated environment also allows NUMA-aware memory allocation for OPcache.
- GPU hosting: Relevant if your PHP application orchestrates GPU-accelerated workloads (e.g., calling Python ML inference services). GPU Hosting environments benefit from PHP 8.3's improved FFI and process management.
Key Technical Takeaways and Decision Matrix
Use typed class constants immediately if:
- Your codebase uses interfaces or abstract classes with constants that child classes override.
- You use PHPStan level 8 or Psalm — typed constants unlock stricter analysis.
Enable JIT if:
- Your profiler shows CPU time exceeding 40% of request time.
- You run batch processing, data transformation, or mathematical workloads in PHP.
- You have at least 64 MB of dedicated OPcache JIT buffer available per PHP-FPM pool.
Do not enable JIT if:
- Your application is I/O-bound (database, cache, filesystem, API calls dominate latency).
- You are on shared hosting with limited OPcache memory.
- You have not benchmarked your specific workload — assume nothing.
Adopt json_validate() if:
- You validate JSON before decoding anywhere in your codebase.
- You process high-volume webhook or message queue payloads.
Add #[Override] to:
- Every method in a child class that intentionally overrides a parent method.
- Security-critical method overrides in plugin or extension architectures.
Migrate to RandomRandomizer if:
- Any part of your code uses
rand(),mt_rand(),array_rand(), orstr_shuffle()for security-sensitive token or key generation.
FAQ
Does PHP 8.3 break backward compatibility with PHP 8.2 code?
In most cases, no. PHP 8.3 is a minor release with no removed functions from the standard library that were not already deprecated in 8.2. However, newly deprecated behaviors will emit E_DEPRECATED notices, and any code relying on implicit type coercion in constants or range() with float steps may behave differently. Always run your test suite under PHP 8.3 before deploying.
Is JIT enabled by default in PHP 8.3?
No. JIT requires OPcache to be enabled and opcache.jit_buffer_size to be set to a non-zero value. The default php.ini shipped with most distributions sets opcache.jit_buffer_size=0, which effectively disables JIT. You must explicitly configure it.
Can I use typed class constants with union types in PHP 8.3?
Yes. const int|string VERSION = 8; is valid. Intersection types are also permitted for object-type constants. The only prohibited types are void and never.
What is the difference between json_validate() and json_decode() for validation purposes?
json_validate() parses the JSON structure without constructing a PHP value in memory. It is significantly more memory-efficient for large payloads and faster when you only need to confirm structural validity. json_decode() must be used when you actually need the decoded data — do not call both in sequence; call json_validate() only when you intend to discard the result.
Does PHP 8.3 support readonly classes introduced in PHP 8.2?
Yes, and it extends them. PHP 8.3 allows readonly properties to be reassigned within __clone(), which was the primary limitation of readonly classes in PHP 8.2. This makes immutable value object patterns fully viable without workarounds.
