What Works Differently

zphp is not a drop-in replacement for every PHP program. There are places where behavior differs from PHP 8.4, either by design or as a current limitation. This page documents the ones you're most likely to notice.

Copy-on-assign vs copy-on-write

PHP uses copy-on-write for arrays: assigning an array to a new variable shares the underlying data until one of them is modified. zphp uses copy-on-assign: the array is fully cloned at the point of assignment.

$a = [1, 2, 3];
$b = $a;  // PHP: shared until modified. zphp: full copy now.

In practice, this rarely matters. The semantics are identical from your code's perspective - both produce independent copies. The difference is when the copy happens, which can affect memory usage if you're assigning very large arrays without modifying them.

Global variables

The global keyword works for reading and writing:

$counter = 0;

function increment() {
    global $counter;
    $counter++;  // this works in zphp
}

increment();
echo $counter; // 1

What doesn't work is creating a reference alias between a local and a global:

$value = 10;

function modify() {
    global $value;
    $ref = &$value;  // reference aliasing - not supported in zphp
    $ref = 20;
}

modify();
echo $value; // PHP: 20. zphp: 10.

Direct reads and writes through the global keyword work. Indirect modification through reference aliases does not.

Pass-by-reference

Pass-by-reference works for simple variables, array element access (string, integer, and variable keys), object property access ($obj->prop), chained property access up to 4 levels ($obj->a->b->c->d), and dynamic property names ($obj->$var).

function modify(&$val) { $val = 'changed'; }

modify($x);                // works
modify($arr['key']);       // works
modify($obj->prop);       // works
modify($obj->a->b);       // works
modify($obj->$dynamicProp); // works

Passing an entire array or object by reference and modifying nested keys inside the function also works:

function set_nested(array &$arr) { $arr['a']['b'] = 99; } // works

Combined property + array access also works:

modify($obj->items['key']); // works

Chains deeper than 4 levels silently skip writeback.

Type hint enforcement

Type hints on function parameters and return values are enforced, matching PHP's behavior. One difference: zphp's fast execution path skips type checking for performance. If your code relies on type errors being thrown in deeply nested hot loops, the behavior may differ.

Auto-vivification

In PHP 8.4, assigning to an index on a scalar value throws a TypeError:

$x = "hello";
$x[] = 1; // PHP 8.4: TypeError

In zphp, this silently fails - the assignment is a no-op. Nested auto-vivification of missing keys (creating intermediate arrays) works correctly.

require and include scope

In PHP, require shares the calling scope. Variables defined in the required file are visible in the caller, and vice versa:

// config.php
$db_host = 'localhost';

// app.php
require 'config.php';
echo $db_host; // PHP: 'localhost'

In zphp, require executes in an isolated scope. Functions and classes are registered globally (as in PHP), but local variables don't cross the boundary. The example above would not work - $db_host would be undefined in app.php.

What still works:

// config.php
return ['host' => 'localhost', 'port' => 3306];

// app.php
$config = require 'config.php'; // return values work fine
// helpers.php
function formatDate($ts) { return date('Y-m-d', $ts); }

// app.php
require 'helpers.php';
echo formatDate(time()); // globally registered functions work

Most modern PHP frameworks (Laravel, Symfony, etc.) use return-based config files and autoloaded classes, both of which work correctly.

strtotime

strtotime() supports common formats, relative modifiers ("next Thursday", "+2 days"), ordinal modifiers, and timezone suffixes (UTC, GMT, EST, PST, numeric offsets, RFC 2822). Timezone parsing is recognition only - internal timestamps are always UTC.

Named arguments

Named arguments work for user-defined functions and approximately 80 common built-in functions. Built-in functions not on this list fall back to positional argument passing.