15%

Save 15% on All Hosting Services

Test your skills and get Discount on any hosting plan

Use code:

Skills
Get Started
05.12.2023

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 / BehaviorPHP 8.2PHP 8.3
Typed class constantsNot supportedFully supported
json_validate()Not availableAvailable natively
Readonly property cloningNot possibleSupported via clone
array_is_list()AvailableBehavior unchanged, but broader adoption patterns
Dynamic class constant fetchSyntax errorSupported via ClassName::{$const}
Randomizer::getBytesFromString()Not availableAvailable
Randomizer::getFloat() / nextFloat()Not availableAvailable
#[Override] attributeNot availableAvailable
Deprecation: mt_rand implicit seedingNot deprecatedDeprecated
Fiber stack size ini directiveNot configurablefiber.stack_size added
JIT tracing improvementsBaseline tracingImproved IR and loop handling
str_contains with arraysNot supportedStill 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=tracing

For 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: north

Previously, 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::Red

Edge 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 unchanged

This 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:

  1. It is empty, or
  2. Its keys are consecutive integers starting from 0 with 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 key

Practical 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 FeatureReasonMigration Path
Calling mt_rand() without explicit seed in some contextsImplicit seeding behavior inconsistencyUse RandomRandomizer
ReflectionProperty::setValue() without object on non-staticAmbiguous behaviorPass the target object explicitly
Passing negative $widths to mb_strimwidth()Undefined behaviorValidate input before calling
ldap_connect() with separate host/port argsDeprecated in favor of URI formUse ldap://host:port URI string
range() with non-integer step producing floatsSurprising implicit type coercionCast 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:

BenchmarkPHP 8.1PHP 8.2PHP 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 outdated and update all dependencies to versions with declared PHP 8.3 compatibility.
  • Execute php -d error_reporting=E_ALL your_app_entrypoint.php under PHP 8.3 CLI to surface deprecation notices before they become fatal errors.
  • Audit any code that calls str_contains(), str_starts_with(), or str_ends_with() with non-string arguments — these now throw TypeError in 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=64M

Setting 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 deprecat

PHP 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.
  • RandomRandomizer additions replace insecure patterns like substr(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(), or str_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.

15%

Save 15% on All Hosting Services

Test your skills and get Discount on any hosting plan

Use code:

Skills
Get Started