Back to blog

Phase 2 complete: every Laravel utility package at 100%, and five new runtime modules to get there

Phase 2 of the Laravel roadmap is done. Every utility package in the dependency tree, the layer of small libraries that Laravel pulls in transitively before any framework code runs, now passes 100% of its unit tests under Pext. Getting there required five new runtime modules covering the PHP extensions that those packages depend on (ctype, filter, intl, mbstring, and sessions), all of which are now live in the runtime for any project to use.

With Phase 2 closed, the roadmap deliberately deviates for the next stretch: before continuing into Phase 3, we are cross-checking the runtime against two large external projects, PhpSpreadsheet and CodeIgniter 4, to make sure the work we have done holds up outside the Laravel-specific shape.

What "Phase 2 complete" means

Phase 2 of the roadmap is the Pure Utilities layer: self-contained libraries with no I/O or side effects, no framework context, no app-specific assumptions. Things like brick/math, composer/semver, doctrine/inflector, doctrine/lexer, egulias/email-validator, dragonmantank/cron-expression, nette/schema, nette/utils, and the rest of the list visible in the showcase. Every one of them now passes 100% of its unit-test suite under Pext.

This is the floor under everything above it. The Phase 3 packages, the Phase 4 packages, and the Laravel framework itself all sit on top of the utilities. If the utilities are wrong, everything above is wrong. They are now not wrong.

ctype

ctype is the small, fast character-classification extension: ctype_alpha, ctype_digit, ctype_alnum, ctype_space, ctype_lower, ctype_upper, ctype_punct, ctype_xdigit, and the rest. The semantics are deceptively subtle: most of them take a string but check it byte-by-byte against the active C locale, and PHP has corner cases around what happens when you pass an integer (the integer is interpreted as either a byte value or a string depending on its range) that the runtime has to match exactly.

The module covers the full surface, with locale handling delegated to the same locale context the rest of the runtime reads from. Several utility packages, including the validation layer in nette/utils and parts of the rule engine in egulias/email-validator, exercise this directly. With the module in place, those parts move from "skipped" to "passing".

filter

filter is the validation and sanitisation extension: filter_var, filter_input, filter_var_array, plus the long list of FILTER_VALIDATE_* and FILTER_SANITIZE_* constants. It is one of the most "compatibility surface" extensions in PHP, because every flag has subtle behaviour that user code depends on, and getting any of them wrong silently changes what passes validation.

The module covers the validators (FILTER_VALIDATE_INT, FILTER_VALIDATE_FLOAT, FILTER_VALIDATE_EMAIL, FILTER_VALIDATE_URL, FILTER_VALIDATE_IP, FILTER_VALIDATE_DOMAIN, FILTER_VALIDATE_REGEXP, FILTER_VALIDATE_BOOLEAN) and the sanitisers (FILTER_SANITIZE_STRING and its replacements, the email and URL sanitisers, the number sanitisers). The email validator delegates to the same code path the runtime already had after the email-validator work, so the two stay consistent.

intl

intl is the big one. The PHP extension wraps ICU, and the runtime equivalent wraps Node's built-in Intl object plus a few targeted dependencies for the parts Intl does not cover. The module brings Collator, NumberFormatter, MessageFormatter, IntlDateFormatter, Locale, Normalizer, and the transliteration surface, all behind the same class names PHP uses so user code does not change.

Two specifics that mattered. First, Collator has to honour the same locale-aware sorting rules PHP does, which means the wrapper has to pass the right options into Node's Intl.Collator and not the JavaScript defaults. Second, Normalizer needs all four forms (NFC, NFD, NFKC, NFKD) for the Unicode normalisation paths that the utility packages use to canonicalise input.

mbstring

The mbstring runtime module was built out during the email-validator work and detailed in that post. Phase 2 completion is when its surface is treated as stable: every mb_* function exercised by any utility package is now in the module, the encoding configuration is persisted in the runtime context the way PHP holds the locale, and the byte-vs-codepoint distinction that bit the validator parser is enforced at the transpiler level wherever an mb_* offset flows into a byte-oriented function.

sessions

sessions is the runtime equivalent of PHP's session extension: session_start, session_id, session_name, session_regenerate_id, session_destroy, the $_SESSION superglobal, and the SessionHandlerInterface hook surface that frameworks use to plug in their own storage. The implementation reads from and writes to the same centralized storage layer that supports the rest of the runtime, which keeps the semantics correct on the synchronous SAPI and also lays the groundwork for sessions on the future async and multi-threaded SAPIs (see the request model post).

The default file-based session handler is in the module and matches the layout PHP uses, so an application that hands off existing session files keeps working. Custom handlers are loaded through the same interface PHP exposes, so framework-specific drivers (database, Redis, Memcached) drop in without changes.

The detour: PhpSpreadsheet and CodeIgniter 4

Strictly speaking, the next step on the roadmap is Phase 3. We are taking a detour first. The argument for the detour is that 100% on every Phase 2 utility package is a strong signal, but it is a signal measured against one slice of the PHP world (the Laravel dependency tree). Before building further on top, it is worth checking the runtime against two large external projects that exercise different parts of the language and standard library: PhpSpreadsheet for the heavy data-and-IO workload, and CodeIgniter 4 for a second mainstream framework with a different shape from Laravel and Symfony.

Both are in flight. Initial conversion landed last week (see the 18 May recap) and the first end-to-end suite runs are being categorised now. If both clear cleanly, Phase 3 starts on schedule. If one of them surfaces a class of runtime issue the Laravel tree did not, that issue gets fixed before any Phase 3 work begins. The cost of finding a foundational bug now is much smaller than the cost of finding it after another layer of packages has been built on top.