Back to blog

Week in review: 04 May 2026

A recap of what shipped during the week of 04 May. Six items this week: a partial audit of the arrays module, doctrine/lexer brought to 100%, egulias/email-validator to 92% on the back of two new runtime modules (pext-network and pext-multibyte-string), hamcrest/hamcrest-php to 90%, myclabs/deep-copy to 83% with a full implementation of PHP's clone operator, and a round of datetime improvements driven by dragonmantank/cron-expression.

Partial audit of the arrays module

The arrays module went into a structured audit this week, following the same pattern that cleared the math module the week prior: enumerate every function it exposes, cross-check against the PHP reference, and exercise each one from both the PHP-side and JavaScript-side test suites. The surface is large (sorting, searching, mapping, filtering, slicing, walking, combining, set-like operations, the SPL iterators that sit on top), so the audit is split across multiple passes.

This first pass focused on the high-frequency functions and on the corner cases that have caused regressions before: key preservation across array_filter and array_values, the difference between array_merge and the + operator on integer keys, sort stability, callback signatures, and the by-reference variants of array_walk and usort. The remaining functions, plus the SPL bridges, are queued for next week.

doctrine/lexer at 100%

doctrine/lexer is now at 100% on its test suite, up from 76% last week. The library is small but central: it backs doctrine/annotations, the DQL parser, and several other Doctrine projects. The remaining failures were all in one corner: reflection over class constants. The lexer relies on enumerating constants of a token-type class and matching them by value, and a few reflection paths around constant visibility, inherited constants, and constant value types were not yet a perfect match for PHP.

Those gaps are closed. doctrine/lexer is the fourth Pure Utilities package at 100%, after brick/math, composer/semver, and doctrine/inflector.

egulias/email-validator at 92%

egulias/email-validator is the most widely used email-validation library in PHP and is the validator behind symfony/mime and Laravel's email rule. Getting the suite running surfaced two missing pieces in the runtime, both of which are now packaged as new modules.

pext-network is a new runtime module for DNS-related operations: checkdnsrr, dns_get_record, gethostbyname, and friends. The validator uses these to check MX records and host reachability, and there was no Pext module that owned the surface until now. The module wraps Node's dns module and normalises return shapes to match PHP's idiosyncratic conventions (the trailing dot on FQDN responses, the integer record-type constants, the array layout returned by dns_get_record).

pext-multibyte-string is a new runtime module for the mb_* family: mb_strlen, mb_substr, mb_strpos, mb_convert_encoding, mb_detect_encoding, and the rest. The validator uses these throughout its parser to walk the local-part and domain by codepoint rather than by byte, so it cannot be tested without them. The module sits alongside the byte-oriented string functions, with PHP's encoding semantics preserved at the boundary (see last week's note on string encodings).

With both modules in place the validator is at 92%. The remaining 8% is split across two clusters: UTF-8 handling in a few low-level paths where the parser is still treating multibyte sequences as if they were single bytes, and the carriage-return character in folded-header parsing where the tokeniser disagrees with PHP on the CR vs CRLF split. Both are in flight.

hamcrest-php at 90%

hamcrest/hamcrest-php, the matcher library that backs Mockery and a number of older PHPUnit assertion styles, is at 90%. Three things stand between it and 100%, all of them feature work on the transpiler rather than gaps in any single runtime module.

The first is case-insensitive static property access. PHP's class member lookup is case-insensitive for methods and constants but case-sensitive for properties, except in a handful of legacy paths that Hamcrest exercises directly. The transpiler is producing a strict-case lookup, which mismatches the runtime's behaviour. The second is dynamic namespace parsing: Hamcrest constructs matcher class names by concatenating a base namespace with a variable suffix, then resolves them. The transpiler's namespace handling currently assumes static names in several places, and those spots need to be widened. The third is instantiation of anonymous classes through a factory path, which is a feature the transpiler does not yet emit correctly when the anonymous class is constructed via reflection rather than via the literal new class syntax.

myclabs/deep-copy at 83% and a full clone implementation

myclabs/deep-copy is at 83%. The headline change for this package is not the percentage but what landed underneath: a full implementation of PHP's clone operator in Pext, with the same semantics PHP itself uses. Properties are copied shallow by default, __clone is invoked on the new instance, references are preserved correctly, and the interaction with typed properties, readonly properties, and properties holding resources is all matched against the reference.

Two specific object types drove most of the remaining work: DateTime and DatePeriod. Both have custom __clone behaviour in PHP that the runtime was not yet replicating exactly, and both are heavily exercised by deep-copy's test suite. The fixes are now in the datetime module (see the next section).

The remaining 17% is mostly in the corners of the matcher API around closures captured during cloning and a few cases involving SPL collections. Those are queued behind the next arrays-module pass.

Datetime improvements for cron-expression

dragonmantank/cron-expression sits on the datetime module and is sensitive to every quirk in DateTime / DateTimeImmutable / DateInterval / DateTimeZone. Working it to a higher pass rate this week drove a round of fixes to the module: arithmetic across DST boundaries, immutability invariants on DateTimeImmutable (every mutating method must return a new instance and leave the receiver untouched), the exact return shape of DateTime::diff, and the carry rules in DateInterval when months and days overflow.

The same fixes feed back into deep-copy and into any user code that touches date arithmetic, so the work was shared overhead rather than cron-specific. cron-expression's remaining gaps are now in expression parsing, not in datetime semantics.