milpa / devtools
The coa generate-verify-inspect developer loop for the Milpa PHP framework: deterministic controller/entity scaffolding (Make engine), framework health + plugin-manifest + capability-graph validators, and BaseController/Doctrine-entity convention verifiers.
Requires
- php: >=8.3
- composer-runtime-api: ^2.2
- doctrine/orm: ^3
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.65
- milpa/core: *
- phpstan/phpdoc-parser: ^2.3
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
This package is auto-updated.
Last update: 2026-07-08 12:02:21 UTC
README
Milpa DevTools
The generate-verify-inspect developer loop for the Milpa PHP framework: deterministic controller/entity scaffolding (the Make engine), boot-time doctors, and architectural validators — capability graphs, plugin manifests, boundary rules — that run in-process, with Composer-safe root resolution.
milpa/devtools is what coa calls when you type coa:make, coa:doctor, coa:validate, or
coa:inspect — the engine behind that whole CLI surface, extracted so it runs the same in-process
whether it's driven by a real host app's console or by your own tests. Generate deterministic,
convention-following scaffolding; verify it against the exact runtime rules the framework
enforces; inspect a plugin ecosystem's capability graph and manifests for problems before they
become a boot-time failure. No exec() to a script at a hardcoded path, no assumption about
install depth — just classes you can new and call.
Install
composer require milpa/devtools
Quick example: generate, then read what you got
EntityGenerator turns the --fields DSL coa:make entity accepts into a fully-formed Doctrine
entity — as a string, in memory, with zero disk I/O of its own (that's WriteGuard's job, so a
caller can inspect, diff, or dry-run a generation before anything touches the filesystem):
use Milpa\DevTools\Make\GenerationContext; use Milpa\DevTools\Make\Generators\EntityGenerator; $context = new GenerationContext( plugin: 'InventoryPlugin', name: 'Product', options: ['fields' => 'name:string:120,price:decimal:10,2,active:bool'], root: '/path/to/host-app', ); $result = (new EntityGenerator())->generate($context); echo $result->files[0]->path; // -> /path/to/host-app/plugins/InventoryPlugin/Entities/Product.php echo $result->files[0]->contents;
produces (verbatim, this is a real run — see What's inside for the full field DSL):
<?php declare(strict_types=1); namespace Milpa\Plugins\InventoryPlugin\Entities; use Doctrine\ORM\Mapping as ORM; use Milpa\Support\UuidGenerator; #[ORM\Entity] #[ORM\Table(name: 'products')] class Product { use UuidGenerator; #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: 'integer')] private int $id; #[ORM\Column(type: 'string', length: 36, unique: true)] private string $uuid; #[ORM\Column(type: 'string', length: 120)] private string $name; #[ORM\Column(type: 'decimal', precision: 10, scale: 2)] private string $price; #[ORM\Column(type: 'boolean')] private bool $active; public function __construct() { $this->uuid = self::generateUuid(); } // ... getId()/getUuid(), plus a get*()/set*() pair per field (elided here — see the // generated file above for the full, real output). }
$result->verifyKind is 'entity' and $result->verifyTarget is the class's FQCN — the exact two
values VerifyRunner::run() needs to close the loop (see below) once the file is actually written.
Composer-safe root resolution: RootResolver
Every coa:* devtools command needs one thing before it can do anything else: the Milpa host
application's filesystem root — the directory holding its composer.json, plugins/,
scripts/. Computing that as dirname(__DIR__, N) from a command's own file only works while the
command lives at a fixed depth relative to the host; the moment this package is Composer-vendored
(vendor/milpa/devtools/..., any install depth, a global install), that walk lands under vendor/
instead — silently, since plugins/ or scripts/ just aren't found under the wrong root (or worse,
a same-named directory is found there instead).
RootResolver::resolve() tries three strategies, in order, and throws
RootNotFoundException — never a plausible-looking wrong path — if none of them lands:
- An explicit root passed to the constructor. Host wiring always wins — e.g. a container binding the app root once from a known-good source, or a test fixture.
Composer\InstalledVersions::getRootPackage()['install_path']. The Composer-canonical answer to "where is the application that required me" — correct regardless of install depth, path-repo dev install vs. a real registry install, valid the instant Composer's generated autoloader is on the include path, which it always is for any Composer-managed PHP process (composer-runtime-apiis a real dependency of this package, not an optional one — see Requirements).- Walk up from
getcwd()looking for the nearest ancestorcomposer.json— a last-resort fallback for the pathological case where Composer's own runtime API is unavailable.
use Milpa\DevTools\Support\RootResolver; // Tier 1: explicit root wins outright. (new RootResolver('/srv/my-milpa-app'))->resolve(); // -> '/srv/my-milpa-app' // Tier 2 (no explicit root): Composer\InstalledVersions::getRootPackage()['install_path']. (new RootResolver())->resolve(); // -> the absolute path of whatever application actually required milpa/devtools — // correct whether that's a path-repo dev install or vendor/milpa/devtools in production.
The loop: generate → verify → inspect
| Layer | Namespace | What it does |
|---|---|---|
| Generate | Make |
GeneratorInterface implementations (ControllerGenerator, EntityGenerator) render a .php.stub template into a PlannedFile (path + contents, no I/O yet). FieldParser reads the --fields DSL; WriteGuard refuses to clobber an existing file unless --force; VerifyRunner closes the loop by running the matching verifier against the freshly written class, in-process. |
| Verify | Verify |
ControllerVerifier / EntityVerifier reflect an already-autoloaded class and check it against the framework's real runtime conventions — extends BaseController, calls parent::__construct(), correct #[ORM\Column] nullability, no debug output, no duplicate routes, and more. A VerificationResult never throws for a violation; it collects errors (fail the run) and warnings (advisory only). |
| Inspect | Validators |
PluginManifestValidator checks one milpa.json against the plugin manifest shape. CapabilityGraphValidator checks an entire plugin ecosystem: every hard requires must be satisfied by some plugin's provides, and the dependency graph must be acyclic (unmet suggests degrade, they never fail). ProviderImplementsValidator autoloads every declared provider and asserts it really implements what it claims. BoundaryValidator runs host-supplied BoundaryRules (which directories may not reference which namespaces) — the engine is generic, the rules are yours. |
A validator example, real output — a manifest with a non-semver version fails with a precise,
addressable message instead of a generic "invalid manifest":
use Milpa\DevTools\Validators\PluginManifestValidator; file_put_contents('/tmp/milpa.json', json_encode([ 'name' => 'acme/inventory', 'version' => '1.0', // not semver — must be x.y.z 'type' => 'Mixed', 'namespace' => 'Milpa\\Plugins\\InventoryPlugin', 'entrypoint' => 'InventoryPlugin.php', ])); $result = (new PluginManifestValidator())->validate('/tmp/milpa.json'); $result->ok(); // false $result->errors; // ["version must be semver: '1.0'"]
The generated code targets your host app's conventions, not this package's. ControllerGenerator
and ControllerVerifier both know the exact FQCNs Milpa\app\Providers\BaseController /
HttpResponse and the #[Route] attribute — that convention belongs to a real Milpa host
application, not to milpa/devtools itself, which ships zero use imports of those classes
(the .stub templates reference them as generated-code text, not real dependencies). coa:make controller scaffolds a class that targets those FQCNs; ControllerVerifier closes the loop by
checking generated output against that same convention.
What's inside
| Namespace | What it provides |
|---|---|
Milpa\DevTools\Make |
GeneratorInterface, GenerationContext/GenerationResult/PlannedFile, ControllerGenerator/EntityGenerator, FieldParser/FieldSpec (the --fields DSL: name:type[:mods], ? prefix for nullable, enum:<Enum>, <name>:belongsTo:<Target>), StubRenderer, WriteGuard, VerifyRunner |
Milpa\DevTools\Verify |
VerifierInterface, VerificationResult, ControllerVerifier, EntityVerifier |
Milpa\DevTools\Validators |
PluginManifestValidator, CapabilityGraphValidator, ProviderImplementsValidator, BoundaryValidator (+ BoundaryRule/BoundaryRuleResult/BoundaryReport) and each validator's typed result |
Milpa\DevTools\Support |
RootResolver/RootNotFoundException, ClassNameExtractor (file path → FQCN, no autoloading — lets a CLI accept either) |
Every public symbol carries a DocBlock; the full field DSL, every generator/verifier check, and every validator's exact error messages are documented at the source and in the API reference.
Requirements
- PHP ≥ 8.3
- No runtime
milpa/*dependencies —milpa/coreappears only inrequire-dev(docs tooling). doctrine/orm^3 — a genuine dependency:EntityVerifierreflects real#[ORM\Column]/#[ORM\JoinColumn]attributes, it doesn't just pattern-match their namescomposer-runtime-api^2.2 — the documented way to depend onComposer\InstalledVersions, whichRootResolveruses as its second resolution tier
Documentation
Full API reference: getmilpa.github.io/devtools — generated straight from the source DocBlocks and dressed with the Milpa design system.
Contributing
Contributions are welcome — see CONTRIBUTING.md. Please report security issues via SECURITY.md, and note that this project follows a Code of Conduct.
License
Apache-2.0 © TeamX Agency.
Milpa is designed, built, and maintained by TeamX Agency.