Skip to content

Maintenance release 2.0.1: support PHP 8.1–8.4 and Laravel 10–13 - #87

Merged
ajthinking merged 5 commits into
masterfrom
release/2.0.1
Aug 25, 2026
Merged

Maintenance release 2.0.1: support PHP 8.1–8.4 and Laravel 10–13#87
ajthinking merged 5 commits into
masterfrom
release/2.0.1

Conversation

@ajthinking

Copy link
Copy Markdown
Owner

v2.0.0 shipped 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/laravel at runtime, through a base_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/laravel is no longer a dev dependency. A small Laravel\Sanctum\HasApiTokens stub lets the fixture User stay byte-identical to the real thing without dragging Sanctum's per-version constraints into the matrix.

Also: tests/Pest.php defined a global context() 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@v4 with GitHub's disabled ::set-output removed, covering ten PHP × Laravel combinations plus a PHPStan job.

Fixes

  • ->throw() has silently matched nothing since 2.0.0. php-parser 5 removed Stmt\Throw_; the class map still pointed at it. PHPStan caught it, and there is now a test that fails without the fix.
  • getReflection() caught Exception, not Throwable. A file whose parent class or trait cannot be autoloaded raises an Error, which escaped and killed the entire query rather than skipping that one file. Affects LaravelFile::models(), controllers() and serviceProviders().
  • Seven implicit-nullable parameters that raise deprecation notices on PHP 8.4.
  • commands_root and controllers_root in the shipped config both pointed at app/HTTP/Controllers.

Packaging

Declares a php requirement (there was none), adds the missing LICENSE.md, a changelog and a Dependabot config, moves Archetype\Tests\ out of the production autoloader, and sets minimum-stability to stable. The readme now states supported versions and tells 1.x users why Composer refuses to resolve for them.

Still open, deliberately out of scope

  • The package declares none of its Laravel dependencies — require is just nikic/php-parser, while src/ calls collect(), config(), base_path() and imports half a dozen Illuminate classes. Declaring them could conflict for someone, so it is not patch-safe.
  • PHP 8.4 still emits 47 dynamic-property deprecations per operation from HashInserter writing $node->__object_hash. That is the AST node-identity mechanism, so moving it to setAttribute() is a real refactor.

Both belong in a 2.1.0.

🤖 Generated with Claude Code

ajthinking and others added 2 commits August 25, 2026 12:45
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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: throw maps to Expr\Throw_, getReflection() catches Throwable, corrected commands_root/controllers_root config, and seven ?type nullable-parameter fixes.
  • Rebuilds the test suite around a committed tests/fixtures/laravel skeleton + a HasApiTokens stub, removes the colliding global context() helper, and modernizes phpunit.xml.
  • Rebuilds CI (checkout@v4/cache@v4, $GITHUB_OUTPUT, 10-way matrix + PHPStan) and adds packaging files (LICENSE, CHANGELOG, Dependabot, php requirement).
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.

Comment thread composer.json Outdated
}
],
"require": {
"php": ">=7.4",

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread CHANGELOG.md Outdated
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>
@ajthinking

ajthinking commented Aug 25, 2026

Copy link
Copy Markdown
Owner Author

Impact on statamic/cms

In Console/Commands/Concerns/MakesVueComponents.php (reached from make:fieldtype and make:widget):

try {
    PHPFile::load("addons/{$this->package}/src/ServiceProvider.php")
        ->add()->protected()->property('vite', [
            'input' => ['resources/js/addon.js'],
            'publicDirectory' => 'resources/dist',
        ])
        ->save();
} catch (\Exception $e) {
    $this->comment("Don't forget to configure Vite in your addon's service provider.");
}

Note the catch (\Exception $e). Anything Archetype throws that is not an Exception escapes and kills their command, so that is the sharp edge to protect.

Verification

I installed ajthinking/archetype:2.0.0 and this branch into two otherwise identical projects and ran that exact call against both.

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 allowing archetype ^2.0 is v5.1.0, requiring Laravel ^10.40 || ^11.0, so PHP 8.1 is already their floor. No Statamic user is excluded.
  • getReflection() catching Throwable — strictly reduces the risk to Statamic. It converts an Error that would previously have escaped their catch (\Exception) into a skipped file. Statamic does not call it directly.
  • Stmt\Throw_Expr\Throw_ — Statamic does not use astQuery(), and the old mapping matched nothing anyway.
  • Nullable parameter types?string $x = null is the same effective signature as the implicit string $x = null, so anything implementing Archetype\Drivers\InputInterface stays valid.
  • commands_root / controllers_root config fix — Statamic does not use LaravelFile::controllers(), and neither key was read anywhere in src/.
  • Autoload and minimum-stability changes — 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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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>
@ajthinking

Copy link
Copy Markdown
Owner Author

Follow-up: tested against every live statamic/cms version, and found a gap

The earlier comment tested one configuration. This one covers the whole live Statamic surface, with real installs rather than a reconstructed harness — and it turned up something worth fixing.

The gap: Laravel 13

statamic/cms 6 requires laravel/framework: "^12.40.0 || ^13.0". A real statamic/cms ^6.0 install resolves to Laravel 13.27.0 — so Statamic 6 users are already running Archetype on a Laravel major this package had never been tested against.

Verified it works before claiming it: 163 passing on Laravel 13 with testbench 11, identical to every other leg. Added two legs to the matrix (PHP 8.3 and 8.4 — testbench 11 requires 8.3+), taking it from ten combinations to twelve.

Real installs, per live version

Not a simulation this time — each row is an actual composer update with Archetype 2.0.1 supplied from a path repo, then the Statamic call path executed inside that dependency graph:

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.

@ajthinking ajthinking changed the title Maintenance release 2.0.1: support PHP 8.2–8.4 and Laravel 10–12 Maintenance release 2.0.1: support PHP 8.1–8.4 and Laravel 10–13 Aug 25, 2026
@ajthinking
ajthinking merged commit 6d6b4fc into master Aug 25, 2026
13 checks passed
@ajthinking

Copy link
Copy Markdown
Owner Author

Post-release verification: real app, real command, published package

Closing the loop now that v2.0.1 is on Packagist. This is no longer a harness — it is composer create-project statamic/statamic, the released package pulled from Packagist, and Statamic's actual console command.

Environment: Statamic 6.29.0 · Laravel 13.27.0 · PHP 8.4.1 · ajthinking/archetype v2.0.1 (dist from Packagist, no path repo) · nikic/php-parser 5.8.0

Command run: php please make:addon acme/demo --fieldtype

That is the full path into Archetype — MakeAddonMakeFieldtypegenerateVueComponent()wireUpAddonJs()configureViteInAddonServiceProvider(). It produced the fieldtype class, the Vue component, vite.config.js, package.json, addon.js, and the service provider Archetype edits:

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 ajthinking/archetype:2.0.0 and running the same command. The generated ServiceProvider.php is byte-identical — same MD5 1705efacc9f4837d0d5506c43492c089.

Upgrade path checked too: composer require ajthinking/archetype:^2.0.1 -W on the working 2.0.0 app upgrades cleanly, the app still boots (Statamic 6.29.0), and the generated addon's own test suite passes.

One cosmetic note, not a regression

There is no blank line between ]; and public function bootAddon(). That is how Archetype's property insertion has always behaved — 2.0.0 produces the exact same bytes — so it is pre-existing rather than anything this release introduced. Worth a look someday in the pretty-printer, but it is not a 2.0.1 issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants