Maintenance release 2.0.1: support PHP 8.1–8.4 and Laravel 10–13 - #87
Conversation
Support PHP 8.2-8.4 and Laravel 10-12, and prove it in CI. v2.0.0 shipped the php-parser 5 port having never been tested against any PHP or Laravel version currently in use; the workflow still targeted PHP 7.4/8.0 with Laravel 6/7/8 and had not run since May 2024. No API changes. Nothing here can break existing usage. Testing - Pin the application skeleton the suite asserts against into tests/fixtures/laravel instead of copying one out of vendor/laravel/laravel at runtime. Laravel 11 deleted most of the files the suite relied on (app/Console/Kernel.php, app/Exceptions/Handler.php, RouteServiceProvider, the middleware), so the old approach could not run on current Laravel at all. Drops the laravel/laravel dev dependency. - Stop defining a global context() helper in tests/Pest.php. Laravel 11 ships its own, and the collision took the suite down before a single test ran. - Rebuild the CI matrix: ten PHP x Laravel combinations, checkout@v4, cache@v4, and no more of GitHub's disabled ::set-output. Add a PHPStan job. - Migrate phpunit.xml to the PHPUnit 10+ schema. Fixes - Map the 'throw' AST node to Expr\Throw_. php-parser 5 removed Stmt\Throw_, so ->throw() had silently matched nothing since 2.0.0. Caught by PHPStan, now covered by a test. - Catch Throwable rather than Exception in getReflection(). A file whose parent class or trait cannot be autoloaded raises an Error, which escaped and killed the whole query instead of skipping that one file. Affects LaravelFile::models(), controllers() and serviceProviders(). - Declare seven implicit-nullable parameters explicitly, silencing the deprecation notices they raise on PHP 8.4. - Correct commands_root and controllers_root in the shipped config, which both pointed at app/HTTP/Controllers. Packaging - Declare a php requirement. There was none. - Add LICENSE.md. The package has always been MIT; the file was missing. - Add CHANGELOG.md and .github/dependabot.yml. - Move Archetype\Tests\ out of the production autoloader. - Set minimum-stability to stable. - Refresh .gitattributes, which referenced files that no longer exist and missed ones that do. Readme now states supported versions and tells 1.x users why Composer refuses to resolve for them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Composer 2.9 refuses to install any package with an open security advisory. Laravel 10 and 11 are both past security support, so every release in those branches is flagged and the whole matrix leg fails at dependency resolution — before a single test runs. Only Laravel 12 installs unmodified. Turn the policy off for the 10 and 11 legs. This is a compatibility matrix rather than a deployment, and enough applications are still on those versions that we want the coverage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The new "php": ">=7.4" constraint is below the actual PHP 8.0+ requirement (str_contains) and the documented 8.1 floor, allowing installs where the code fatals.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This maintenance release (2.0.1) brings the previously-untested php-parser 5 port up to date with modern stacks: PHP 8.2–8.4 and Laravel 10–12. Since v2.0.0 shipped without ever running against a currently-supported version, the CI workflow and test harness were broken; this PR rebuilds both, pins a Laravel application skeleton as a test fixture (replacing the runtime copy out of vendor/laravel/laravel), and fixes a handful of correctness issues surfaced along the way (the ->throw() class map pointing at the removed Stmt\Throw_, getReflection() only catching Exception instead of Throwable, wrong config roots, and PHP 8.4 implicit-nullable deprecations). Packaging metadata (LICENSE, changelog, Dependabot, php requirement) is also added.
Changes:
- Fixes real bugs:
throwmaps toExpr\Throw_,getReflection()catchesThrowable, correctedcommands_root/controllers_rootconfig, and seven?typenullable-parameter fixes. - Rebuilds the test suite around a committed
tests/fixtures/laravelskeleton + aHasApiTokensstub, removes the colliding globalcontext()helper, and modernizesphpunit.xml. - Rebuilds CI (
checkout@v4/cache@v4,$GITHUB_OUTPUT, 10-way matrix + PHPStan) and adds packaging files (LICENSE, CHANGELOG, Dependabot,phprequirement).
File summaries
| File | Description |
|---|---|
| src/Traits/PHPParserClassMap.php | Points throw at Expr\Throw_ (php-parser 5). |
| src/Endpoints/PHP/ReflectionProxy.php | Catches Throwable so autoload Errors skip one file instead of killing the query. |
| src/config/archetype.php | Corrects commands_root/controllers_root paths. |
| src/PHPFile.php, src/Endpoints/PHP/Namespace_.php, src/Endpoints/EndpointProvider.php, src/Drivers/InputInterface.php, src/Drivers/FileInput.php, src/Support/AST/ASTQueryBuilder.php, src/Traits/PHPParserPropertyMap.php | Explicit ?type params to silence PHP 8.4 deprecations. |
| tests/TestCase.php | Copies pinned fixture skeleton instead of climbing into vendor/. |
| tests/Pest.php | Removes colliding no-op context()/describe() helpers. |
| tests/Support/TestablePHPFile.php | Nullable test-helper params. |
| tests/Unit/Support/AST/ASTQueryBuilderTest.php | Adds throw test; context()→describe(). |
| tests/stubs/laravel-sanctum.php | HasApiTokens stub for the fixture User. |
| tests/fixtures/laravel/** | Pinned Laravel application skeleton used as test input. |
| composer.json | Adds php requirement, drops laravel/laravel, moves test autoload to dev, minimum-stability: stable. |
| .github/workflows/tests.yml | 10-way PHP×Laravel matrix + PHPStan job, checkout@v4/cache@v4, $GITHUB_OUTPUT. |
| .github/dependabot.yml | New Dependabot config. |
| phpunit.xml | Migrated to PHPUnit 10+ schema. |
| phpstan.neon | Excludes tests/fixtures/*. |
| CHANGELOG.md, LICENSE.md, readme.md, .gitattributes, .gitignore | Packaging/documentation updates. |
Review details
- Files reviewed: 47/48 changed files
- Comments generated: 2
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| } | ||
| ], | ||
| "require": { | ||
| "php": ">=7.4", |
There was a problem hiding this comment.
Agreed, and fixed in bcc3d53 — but set to ^8.1 rather than >=8.1.
The upper bound is deliberate. PHP 9 removes dynamic properties, and HashInserter still writes $node->__object_hash onto every php-parser node — that is the AST node-identity mechanism, so it is a real refactor rather than a patch fix (deferred to 2.1.0). Until that lands, PHP 9 fatals, and it is better for Composer to decline the install than to hand someone a package that breaks at runtime. ^8.1 is also the ecosystem convention.
Checked that 8.1 excludes nobody downstream before picking it: the earliest statamic/cms release allowing archetype ^2.0 is v5.1.0, which requires laravel/framework ^10.40 || ^11.0 and therefore PHP 8.1 at the lowest. Also updated the readme "Requirements" section, which still claimed PHP 7.4 and Laravel 7.
Copilot was right that ">=7.4" was untrue. HasIO calls str_contains(), which is PHP 8.0, and nothing below 8.1 is tested or even installable for the dev tooling. Set "php": "^8.1" rather than ">=8.1". The upper bound is deliberate: PHP 9 removes dynamic properties, and the AST node-identity mechanism in HashInserter still writes $node->__object_hash. Better for Composer to decline the install than to hand someone a package that fatals. Verified this excludes nobody downstream. The earliest statamic/cms release allowing archetype ^2.0 is v5.1.0, which requires Laravel ^10.40 || ^11.0 and therefore PHP 8.1 at the lowest. A real statamic/cms ^5.1 resolution against this branch locks archetype 2.0.1. Also corrects the changelog, which said "Six" implicit-nullable parameters while listing seven, and updates the readme requirements section, which still claimed PHP 7.4 and Laravel 7. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Impact on
|
| Scenario | 2.0.0 | this branch |
|---|---|---|
Fresh addon, no $vite property |
OK | OK — identical output |
Existing $vite property (overwrite) |
OK | OK — identical output |
| Empty class body | OK | OK — identical output |
| Malformed PHP | FileParseError |
FileParseError |
| Missing file | ErrorException |
ErrorException |
The generated ServiceProvider.php is byte-identical between the two. Both failure paths still throw an Exception subclass, so Statamic's catch still catches them and nothing escapes as a bare Throwable.
Resolution also checked end to end: a real statamic/cms ^5.1 install against this branch resolves and locks ajthinking/archetype (2.0.1) with nikic/php-parser (v5.8.0).
Why each change is safe for them
php: ^8.1— earliest Statamic release allowingarchetype ^2.0is v5.1.0, requiring Laravel^10.40 || ^11.0, so PHP 8.1 is already their floor. No Statamic user is excluded.getReflection()catchingThrowable— strictly reduces the risk to Statamic. It converts anErrorthat would previously have escaped theircatch (\Exception)into a skipped file. Statamic does not call it directly.Stmt\Throw_→Expr\Throw_— Statamic does not useastQuery(), and the old mapping matched nothing anyway.- Nullable parameter types —
?string $x = nullis the same effective signature as the implicitstring $x = null, so anything implementingArchetype\Drivers\InputInterfacestays valid. commands_root/controllers_rootconfig fix — Statamic does not useLaravelFile::controllers(), and neither key was read anywhere insrc/.- Autoload and
minimum-stabilitychanges — root-only, or affect a namespace (Archetype\Tests\) that was never shipped in the dist archive.
One thing this release does not reach, and a warning
Statamic 4 and 3.4 pin ajthinking/archetype: ^1.0.3 with no || ^2.0. Those installs can never receive 2.0.1; they are on 1.1.5 permanently.
That changes my earlier recommendation to mark 1.1.5 abandoned on Packagist. Doing so would put a deprecation warning in front of every Statamic 4 user, none of whom have an upgrade path within Archetype — the fix would have to come from Statamic widening its constraint. Recommend we do not abandon 1.x. A note in the readme (already in this PR) is the right level.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The change spans package-distribution manifest edits, a full CI rewrite, shipped-config default changes, and a large new fixture skeleton, so final human review is warranted despite no concrete defects being found.
Review details
- Files reviewed: 47/48 changed files
- Comments generated: 0 new
- Review effort level: Balanced
statamic/cms 6 requires laravel/framework "^12.40.0 || ^13.0", and a real statamic/cms ^6.0 install resolves to Laravel 13.27.0 — so downstream users are already running Archetype there, untested. Verified it works before adding: 163 passing on Laravel 13 with testbench 11, same as every other leg. Widens the testbench dev constraint to allow ^11.0 and adds two legs (PHP 8.3 and 8.4; testbench 11 requires 8.3+). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up: tested against every live
|
| Statamic | resolved | Laravel | archetype | php-parser | result |
|---|---|---|---|---|---|
^6.0 |
6.29.0 | 13.27.0 | 2.0.1 | 5.8.0 | all 5 scenarios identical to 2.0.0 |
^5.0 |
5.74.4 | 12.68.0 | 2.0.1 | 5.8.0 | all 5 scenarios identical to 2.0.0 |
^5.1 |
(earliest allowing ^2.0) |
10 / 11 | 2.0.1 | 5.8.0 | resolves |
^4.0 |
4.58.3 | 9 / 10 | cannot | — | pinned to ^1.0.3, 2.x unreachable |
^3.4 |
3.4.17 | 8 / 9 | cannot | — | pinned to ^1.0.3, 2.x unreachable |
The five scenarios are: fresh addon with no $vite property, existing $vite to overwrite, empty class body, malformed PHP, and missing file. Output is byte-identical to Archetype 2.0.0 in every case, and both failure paths still throw Exception subclasses so Statamic's catch (\Exception $e) still catches them.
Statamic 4 and 3.4 were confirmed by resolution, not assumption — Composer refuses 2.0.1 against them, exactly as their ^1.0.3 pin implies. Those two lines are also stale (last releases Sep 2024 and Jan 2024), so the live surface for Archetype 2.x is Statamic 5 and 6.
What this does not cover
I executed the Archetype call inside the real Statamic dependency graph, not Statamic's make:fieldtype command end to end — that path shells out to npm install and prompts interactively. The Archetype-facing behaviour is fully covered; the command wrapper around it is not.
Post-release verification: real app, real command, published packageClosing the loop now that v2.0.1 is on Packagist. This is no longer a harness — it is Environment: Statamic 6.29.0 · Laravel 13.27.0 · PHP 8.4.1 · Command run: That is the full path into Archetype — class ServiceProvider extends AddonServiceProvider
{
protected $vite = [
'input' => [
'resources/js/addon.js',
],
'publicDirectory' => 'resources/dist',
];
public function bootAddon()
{
//
}
}Compared against 2.0.0 by building a second identical app pinned to Upgrade path checked too: One cosmetic note, not a regressionThere is no blank line between |
v2.0.0shipped the php-parser 5 port in May 2024 having never been tested against any PHP or Laravel version currently in use — the workflow still targeted PHP 7.4/8.0 with Laravel 6/7/8, and it has not run since. The package works on a modern stack; we just had no evidence of it.This is maintenance only. No API changes, and nothing here can break existing usage.
Verified locally on PHP 8.4 against Laravel 10, 11 and 12 — 163 passing on each, PHPStan clean.
Testing
The suite used to copy an application skeleton out of
vendor/laravel/laravelat runtime, through abase_path('./../../../../vendor/...')path climb. Laravel 11 deleted most of the files it relied on —app/Console/Kernel.php,app/Exceptions/Handler.php,RouteServiceProvider, the middleware — so on current Laravel the suite could not run at all, and its exact-file-count assertions were hostage to whatever the skeleton looked like that year.It now runs against a pinned skeleton committed at
tests/fixtures/laravel.laravel/laravelis no longer a dev dependency. A smallLaravel\Sanctum\HasApiTokensstub lets the fixtureUserstay byte-identical to the real thing without dragging Sanctum's per-version constraints into the matrix.Also:
tests/Pest.phpdefined a globalcontext()helper. Laravel 11 ships its own, and the collision took the whole suite down before a single test ran.CI is rebuilt on
checkout@v4/cache@v4with GitHub's disabled::set-outputremoved, covering ten PHP × Laravel combinations plus a PHPStan job.Fixes
->throw()has silently matched nothing since 2.0.0. php-parser 5 removedStmt\Throw_; the class map still pointed at it. PHPStan caught it, and there is now a test that fails without the fix.getReflection()caughtException, notThrowable. A file whose parent class or trait cannot be autoloaded raises anError, which escaped and killed the entire query rather than skipping that one file. AffectsLaravelFile::models(),controllers()andserviceProviders().commands_rootandcontrollers_rootin the shipped config both pointed atapp/HTTP/Controllers.Packaging
Declares a
phprequirement (there was none), adds the missingLICENSE.md, a changelog and a Dependabot config, movesArchetype\Tests\out of the production autoloader, and setsminimum-stabilitytostable. The readme now states supported versions and tells 1.x users why Composer refuses to resolve for them.Still open, deliberately out of scope
requireis justnikic/php-parser, whilesrc/callscollect(),config(),base_path()and imports half a dozenIlluminateclasses. Declaring them could conflict for someone, so it is not patch-safe.HashInserterwriting$node->__object_hash. That is the AST node-identity mechanism, so moving it tosetAttribute()is a real refactor.Both belong in a 2.1.0.
🤖 Generated with Claude Code