v2.1.0: add the archetype command line - #90
Open
ajthinking wants to merge 4 commits into
Open
Conversation
Archetype has only ever been reachable from PHP. This adds a command line
over the same engine — 26 operations, each an Artisan command under
`archetype:`, plus an `archetype` binary that finds the application and
forwards to it. Nothing here reimplements AST rewriting; every mutation
goes through the existing PHPFile/LaravelFile endpoints.
Three properties hold for every operation that writes, because a caller
that cannot rely on them has to read the file back and loses the point of
asking:
- the file is re-rendered and compared, so a change that matched nothing
exits non-zero instead of reporting a success that wrote nothing;
- the answer carries a diff of what changed;
- a change already applied reports SKIP, which makes them safe to repeat.
Every operation takes a single target — a path, a class name, or a
directory, where a directory means every class beneath it, narrowed with
--extends / --implements / --uses-trait / --matching. Every operation
takes --json; every mutation takes --dry-run and --no-diff.
Two operations exist because the PHP API cannot express them:
set-array-key edits the array a method returns, which is where rules(),
toArray() and casts() keep their contents, and set-casts writes to
whichever casting mechanism a model already uses rather than adding a
second one beside the first. add-relation covers all eleven Eloquent
relation types with pivot tables, explicit keys and withPivot.
Alongside, a few endpoints now reach constructs they previously matched
but silently ignored: className(), classConstant(), useTrait() and
property() work on any class-like, and implements() on classes and enums.
Before this, adding an interface to an enum reported success and wrote
nothing. The PSR-2 printer also prints `function name(): Type` rather
than `function name() : Type`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjUMv5rFTVJMr6F1J7bx7x
The previous commit widened five endpoints from `class` to `classLike`,
changed the PSR-2 printer's return-type spacing, added enum() and
enumCase() to the query builder, and put --json on archetype:errors.
None of that belongs in a change whose job is to add a command line, and
one of them was an outright regression: on a file declaring both an
interface and a class, className() started answering about the interface
and property() started writing into it, which is not valid PHP.
All of it is reverted. `src/` is now the new `src/Console` tree plus two
lines in the service provider, and nothing an existing user calls behaves
differently.
The console pays for that in reach, and says so rather than pretending
otherwise. The endpoints it drives address `class` declarations, so the
property, constant, interface, trait, parent-class and rename operations
refuse an enum, interface or trait outright:
ERR app/Enums/Status.php archetype:add-implements only works on
classes, and this is an enum
Refusing up front rather than after the fact matters for the three
operations that import a name before using it. add-implements on an enum
used to write the import, fail to add the interface, and report OK — a
half-done change is indistinguishable from a whole one once the file has
changed, so the render-and-compare check cannot catch it.
inspect, show, find, the method operations, add-case and set-array-key
have no such limit; they read and write the declaration through the
console's own code whatever it is.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjUMv5rFTVJMr6F1J7bx7x
The first cut invented a second vocabulary: set-property, add-to-property,
rename-class, add-use, set-extends. Half of those rename an endpoint that
already had a name, which means anyone who knows the PHP API has to learn
the CLI's dialect of it too. The directive system was sitting right there
and maps onto flags almost exactly.
So a command named after an endpoint is now that endpoint. Same
arguments, same directives as flags, same result: `archetype property
<target> fillable nickname --add` is `$file->add()->property('fillable',
'nickname')`. Give a value and it writes, give none and it reads.
property className extends implements namespace use useTrait
classConstant methodNames make
fillable hidden visible guarded unguarded casts dates table
connection timestamps
hasOne hasMany belongsTo belongsToMany
Operations with names of their own are the ones with no PHP equivalent —
inspect, show, find, set-array-key, add-case, the method operations,
apply, and the seven relationship types LaravelFile does not cover. The
operation map prints in those two halves so the rule is visible rather
than documented somewhere else.
Two tests hold the line: every operation listed as an endpoint must be a
real method on LaravelFile, and `archetype hasMany <target> Task` must
produce byte-identical output to `$file->hasMany('Task')`. With no
options given the relationship commands call the endpoint rather than
reimplementing it.
Two bugs fell out of the rewrite, both from reading a value off a file
that already carried a directive — the endpoints read directives off the
file, so `$file->add()->use()` as a getter appends instead of answering.
State is now read from the syntax tree, and directives are applied at the
write itself. The second was mine from the first cut: useTrait() answers
with Node\Name objects, so class_basename() on them returned "Name" and
the already-uses check never matched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjUMv5rFTVJMr6F1J7bx7x
Constants in traits are PHP 8.2, and this package supports 8.1, so the directive names move from the trait that uses them to a class of their own. CI caught it; my local PHP is 8.4 and the lowest I can install here is 8.2, so nothing on this machine would have. Neither PHPStan with phpVersion 80100 nor php-parser's version-aware parser flags the construct, so there is no local guard to add — the 8.1 job in CI is the check. A scan for the other 8.2-8.4 additions found nothing else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AjUMv5rFTVJMr6F1J7bx7x
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Archetype has only ever been reachable from PHP. This adds a command line over the same engine, aimed at callers who work a command at a time — a terminal, a script, an AI agent.
./vendor/bin/archetype fillable app/Models/User.php # is the same as php artisan archetype:fillable app/Models/User.phpAn operation named after an endpoint is that endpoint
Same arguments, same directives — as flags — same result. Give a value and it writes; give none and it reads. There is one vocabulary to learn, not two, and nothing is renamed on the way through.
$file->property('table')archetype property <target> table$file->property('table', 'gdpr_users')archetype property <target> table gdpr_users$file->add()->property('fillable', 'nickname')archetype property <target> fillable nickname --add$file->remove()->property('table')archetype property <target> table --remove$file->empty()->property('fillable')archetype property <target> fillable --empty$file->private()->property('key', 'v')archetype property <target> key v --private$file->className()archetype className <target>$file->full()->className()archetype className <target> --full$file->add()->use([...])archetype use <target> ... --add$file->hasMany('Task')archetype hasMany <target> TaskA test asserts that every operation listed as an endpoint is a real method on
LaravelFile, so the two cannot drift. Another asserts thatarchetype hasMany <target> Taskproduces output byte-identical to$file->hasMany('Task')— with no options given, the command calls the endpoint rather than reimplementing it.The operations
Endpoints — these mirror the PHP API.
property<target> <name> [<value>]--add --remove --empty --clear --public --protected --private --staticclassName<target> [<NewName>]--fullextends<target> [<Class>]implements<target> [<Interface>...]--addnamespace<target> [<Namespace>]--removeuse<target> [<FQCN>...]--adduseTrait<target> [<Trait>...]--addclassConstant<target> <NAME> [<value>]--add --remove --empty --clearmethodNames<target>make<name>--file --extends= --implements= --trait= --forceerrorsfillablehiddenvisibleguardedunguardedcastsdates<target> [<value>]--add --remove --empty --cleartableconnectiontimestamps<target> [<value>]hasOnehasMany<target> <Related>--name= --foreign-key= --local-key=belongsTo<target> <Related>--name= --foreign-key= --owner-key=belongsToMany<target> <Related>--table= --with-pivot= --with-timestamps --using=The console's own — these have no PHP equivalent.
inspect<target> [meta|traits|uses|consts|cases|props|methods|relations]...show<target> <method>find[<dir>]--type=all|models|controllers|providers|migrationsset-array-key<target> <method> <key> [<value>]--append --removerules(),toArray(),casts()add-case<target> <Name> [<value>]add-method<target> --code=<php>replace-method<target> <name> --code=<php>remove-method<target> <name>apply[<file>]hasOneThroughhasManyThrough<target> <Related> --through=LaravelFiledoes not covermorphOnemorphManymorphToManymorphedByMany<target> <Related> --morph-name=morphTo<target> [--morph-name=]archetypewith no arguments prints exactly this list, in these two halves, so the naming rule is visible rather than documented elsewhere.The contract
Three properties hold for every operation that writes, because a caller that cannot rely on them has to read the file back, which throws away the reason for asking:
SKIP, notOKand not an error.OKDRYSKIPERROne target, three meanings
Directory targets narrow with
--extends,--implements,--uses-traitand--matching. Those options are rejected on a single-file target rather than quietly ignored.Every operation but
errorstakes--json. Every mutation takes--dry-runand--no-diff.Values, read the way they were meant
nullable|dateis a valid PHP expression — a bitwise or of two constants — and never what someone typing a validation rule meant. So a bare word is a string, and PHP is assumed only where the text announces it: a bracket, a quote, a$variable, a call, aClass::constant, a number or a boolean.The PHP API is untouched
src/is the newsrc/Consoletree plus two lines in the service provider. No endpoint, printer or query-builder change. Nothing an existing user calls behaves differently, which is what makes this a minor.The console pays for that in reach, and says so. The endpoints address
classdeclarations, so the endpoint-named operations refuse an enum, interface or trait:Refusing before writing matters for the three operations that import a name before using it.
implementson an enum would otherwise write the import, fail to add the interface, and reportOK— once the file has changed, a half-done change is indistinguishable from a whole one, so render-and-compare cannot catch it.inspect,show,find, the method operations,add-caseandset-array-keyhave no such limit — they read and write the declaration through the console's own code, whatever it is.Two smaller judgement calls worth a look:
useTrait,implementsandextendsadd the import when given a fully qualified name, since a name used without one is never valid PHP.--no-importopts out.archetype castsrefuses on a model that declares Laravel 11'scasts()method rather than writing$castsbeside it, and points atset-array-key.Test-harness changes
Two files outside
src/, both so console tests can write in place rather than into the isolated.outputdirectory:tests/Pest.php— points the output root at the application fortests/Feature/Console.tests/TestCase.php— a guard so cleanup never emptiesbase_path(). Worth having regardless.Tests
Console tests run through Artisan the way a caller runs them — one command line in, an exit code and the raw output back — plus six that exercise the
archetypebinary against a stub application, including that backslashes in a class name survive the shell.The risky and incomplete ones are pre-existing. PHPStan is clean.
Not in this PR
The natural next step is exposing the same operations over MCP, so they sit in an agent's tool list next to its file tools rather than behind a shell. Separate piece of work; changes nothing here.
🤖 Generated with Claude Code
https://claude.ai/code/session_01AjUMv5rFTVJMr6F1J7bx7x