CLI scaffolding#2980
Conversation
91cddaf to
3118c2a
Compare
|
@soyuka are you planning on moving forward with this approach for the template? 🚀 We started experiencing In other words; upstream changes to both symfony-docker and frankenphp configurations take a while to reach this template, so this approach of using another scaffolding route would be a better approach. |
|
yes but we need to review some things, probably use PHP for it instead of GO. |
|
Although |
d17b2fb to
08ab9be
Compare
Removes the legacy distribution (api/, pwa/, helm/, compose.*.yaml, update-deps.sh, e2e/, etc.) and ships api-platform/installer: a Symfony Console CLI that scaffolds new projects on demand. The installer: - offers an interactive wizard or fully non-interactive flag mode (--framework, --with-pwa, --with-docker, --format, --docs) - adds a Docker / no-Docker choice — no-Docker skips the symfony-docker clone + compose patching and points users at `symfony serve` - detects node, npx and pnpm before offering the PWA prompt - ports the Symfony, Laravel and PWA scaffolders, including compose.yaml CADDY_SERVER_EXTRA_DIRECTIVES injection, api_platform.yaml generation, and config/api-platform.php patching - adds an E2E workflow (Symfony no-Docker, Symfony+Docker+PWA, Laravel) on push to main + workflow_dispatch, and gates the release job on it Distribution: `composer global require api-platform/installer` or download the static binary from the GitHub release.
- ChoiceQuestion multiselect default: pass numeric indices to silence "Undefined array key" warnings from SymfonyQuestionHelper. - Add `scalar` as a `--docs` value. SymfonyScaffold writes `enable_scalar`; LaravelConfigPatcher disables `scalar.enabled` when unselected and now also disables `redoc.enabled` (previously a no-op, asymmetric with the Symfony scaffold). - Drop the `enable_docs: false` branch. That flag is a master switch in core that sets `hideHydraOperation: true` and empties /docs.jsonld's supportedClass, breaking every Hydra client including the bundled PWA. - LaravelScaffold ships a branded welcome.blade.php and an ES module that calls parseHydraDocumentation to render live resource cards, mirroring the Next.js PWA. Requires `npm`; installs the four @api-platform/* libs; patches APP_URL=http://localhost:8000 so the Vite plugin advertises the actual \`php artisan serve\` URL. 37 tests, phpstan clean.
…tent compose env - InstallerCommand: error (not warn) when --with-docker or --with-pwa is passed with --framework=laravel; add "scalar" to --docs option description. - SymfonyScaffold: pin symfony-docker to a specific SHA via shallow fetch-by-SHA (uploadpack.allowReachableSHA1InWant) so every install gets identical Docker files; expose SYMFONY_DOCKER_REF for future bumps. - ComposeOverrideWriter: skip COMPOSE_FILE append when an existing line is present so re-running the scaffold doesn't accumulate duplicates. - release.yml gate: prefer the canonical push-triggered E2E run on the tagged SHA; fall back to any successful run for re-tag workflows. - tests: cover all of the above + harden the cwd cast in the multiselect test.
Box reads the phar output path from box.json.dist ("output":
"bin/api-platform.phar"); the -o option does not exist. Remove the
flag from the Makefile and the CI/release workflows so the phar build
succeeds.
Non-interactive runs and the interactive multiselect now pre-select every format (jsonld, jsonapi, hal) and every docs viewer (swagger_ui, redoc, scalar) instead of only jsonld + swagger_ui.
Replace kevingh/box with scripts/build-phar.php using PHP's native Phar class. CI and release workflows now run the script under phar.readonly=0; static binary still ships via static-php-cli's micro:combine. Phar moves out of bin/ into dist/ as a transient build artifact.
Drop the compose.api-platform.yaml sidecar and the COMPOSE_FILE mangling in .env. ComposeOverrideWriter now injects CADDY_SERVER_EXTRA_DIRECTIVES under services.php.environment between ###> api-platform/api-platform ### markers, mirroring Symfony Flex's recipe-block strategy. Pure text patch — upstream comments and key order survive verbatim, idempotent re-runs via marker substring check.
Scaffolds @api-platform/admin into Symfony (sibling admin/ dir) or Laravel (resources/js/admin + Blade route /admin) via Vite. Reuses the existing Flex-style marker policy for idempotent vite.config.js and routes/web.php patches. Local templates only, no remote fetch.
|
After my first attempt with the CLI, I encountered these two issues:
Details
Details
Executed with: |
Why: - The custom allow-empty validator treated numeric choices as invalid strings. - array_filter also dropped the "0" answer, turning swagger_ui into an empty docs selection. - Symfony Console already maps displayed choice indexes to their values, so reusing its validator keeps the prompt behavior consistent. What changed: - Delegate non-empty docs answers to Symfony Console's native ChoiceQuestion validator. - Preserve the displayed numeric choices for swagger_ui, redoc, and scalar. - Cover numeric, textual, combined, and default interactive docs selections. Verifications: - vendor/bin/phpunit --filter InstallerCommandTest - vendor/bin/phpunit - vendor/bin/phpstan analyse --no-progress - manual bin/api-platform interactive probes for docs answers 1, 2, and 0,1,2
Why: - The CLI help advertises that an empty --docs value disables documentation viewers. - Passing --docs= previously failed as an unknown value instead of honoring that explicit opt-out. - The interactive prompt also advertised empty input as none even though Symfony Console uses Enter to accept the displayed default. What changed: - Treat --docs= as an explicit empty docs selection without changing the no-option default. - Reject combinations such as --docs= --docs=redoc as ambiguous. - Remove the misleading empty-for-none wording from the interactive docs prompt. - Cover the empty docs option, ambiguous combinations, and the prompt wording in tests. Verifications: - vendor/bin/phpunit --filter InstallerCommandTest - vendor/bin/phpunit - vendor/bin/phpstan analyse --no-progress - bin/api-platform --docs= probe reaches the expected existing-directory guard - bin/api-platform --docs= --docs=redoc probe reports the ambiguity - interactive bin/api-platform probe shows API documentation (comma-separated) without empty-for-none wording
Why: - pnpm 11 can leave create-next-app projects with pending build-script approvals for sharp and unrs-resolver. - The installer then fails before copying the API Platform PWA page, leaving a partial Next.js app behind. - The required pnpm decision should be explicit and scoped to the packages observed in the generated app. What changed: - Patch create-next-app's pnpm-workspace.yaml placeholders to approve sharp and unrs-resolver builds. - Remove those packages from ignoredBuiltDependencies once they are approved. - Re-run pnpm install only when the workspace file was patched, before adding API Platform frontend libraries. - Cover placeholder patching, unrelated ignored builds, explicit decisions, and idempotence in tests. Verifications: - vendor/bin/phpunit --filter PwaScaffoldTest - vendor/bin/phpunit - vendor/bin/phpstan analyse --no-progress - pnpm --dir /tmp/api-platform-pwa-probe-WAstZx install with pnpm 11.5.0 - pnpm --dir /tmp/api-platform-pwa-probe-WAstZx add @api-platform/api-doc-parser github:api-platform/zod @api-platform/ld @api-platform/mercure
Why: - The PWA template assigned api-doc-parser's nullable required flag to a strict boolean field. - A generated Next.js app with the API Platform template failed tsc --noEmit on that mismatch. - The UI only needs a truthy boolean to decide whether to render the required badge. What changed: - Normalize parsed field required values with Boolean(f.required). - Keep null, undefined, and false values rendered as non-required fields. Verifications: - copied templates/pwa-page.tsx into a generated PWA app/page.tsx fixture and ran ./node_modules/.bin/tsc --noEmit - vendor/bin/phpunit - vendor/bin/phpstan analyse --no-progress
Why: - The PWA scaffold appended a second CORS_ALLOW_ORIGIN after the Nelmio recipe had already configured one. - The appended value could override the recipe value and dropped 127.0.0.1 support. - CORS setup should remain scoped to PWA generation without weakening the Symfony recipe defaults. What changed: - Keep an existing CORS_ALLOW_ORIGIN line unchanged. - Add the Nelmio recipe CORS_ALLOW_ORIGIN fallback only when the variable is missing. - Cover existing recipe values, fallback insertion, and idempotence in PwaScaffold tests. Verifications: - vendor/bin/phpunit --filter PwaScaffoldTest - vendor/bin/phpunit - vendor/bin/phpstan analyse --no-progress - helper probe against ../cli-tests/my-app/api/.env confirms existing CORS_ALLOW_ORIGIN values are left unchanged
Why:
- The E2E workflow used older Node and pnpm versions than the environment that exposed the PWA scaffold failures.
- The previous PWA check only asserted that generic files existed, so incomplete Next.js scaffolds could pass.
- Recent fixes for pnpm build approvals, template replacement, TypeScript, and CORS need CI coverage.
What changed:
- Run the Symfony Docker PWA job on Node 24 and pnpm 11.5.0.
- Verify that the generated PWA page contains the API Platform template and not the default Next.js page.
- Assert that the generated API env contains exactly one CORS_ALLOW_ORIGIN entry.
- Run tsc --noEmit inside the generated PWA before booting Docker.
Verifications:
- ruby -e 'require "yaml"; YAML.load_file(".github/workflows/e2e.yml"); puts "yaml ok"'
- git diff --check
- actionlint .github/workflows/e2e.yml (only pre-existing shellcheck warnings remain)
Why: - Keep GitHub workflow dependencies aligned with the requested major action versions. - Isolate action-version maintenance from installer bug-fix commits. What changed: - Bumped actions/checkout from v4 to v6 in CI, E2E, and release workflows. - Bumped actions/cache from v4 to v5 in CI and release workflows. Verifications: - Parsed the edited workflow YAML files with Ruby. - Ran git diff --check on edited workflow files. - Ran actionlint on the edited workflows; it reports existing shellcheck/runner-label warnings unrelated to these version bumps.
Why: - Soyuka's branch now includes --with-admin, which adds an admin-only CORS setup path. - That new path must keep the same Nelmio fallback value and must not make docs prompt tests depend on local Node/npm availability. What changed: - Rebased the branch on origin/feat/cli-scaffold. - Reused the PWA CORS env patcher from the Symfony admin-only setup path. - Pinned unrelated installer option tests to --with-admin=false. - Added coverage for the admin-only CORS fallback value. Verifications: - Ran targeted PHPUnit tests for InstallerCommandTest, SymfonyScaffoldTest, and PwaScaffoldTest. - Ran the full PHPUnit suite. - Ran PHPStan. - Parsed edited workflow YAML files and ran git diff --check. - Ran actionlint; only existing shellcheck/runner-label warnings remain. - Ran a real interactive bin/api-platform prompt probe through the docs prompt.
LaravelAdminScaffold now writes `VITE_ENTRYPOINT=/api` into the Laravel project's `.env` so the embedded React-admin (served at /admin) reaches the API under `route_prefix=/api`. Without this the admin defaulted to `window.location.origin` and every Hydra context fetch 404'd.
PwaScaffold::run now takes an `$apiEntrypoint` argument and writes it into the Next.js project's `.env.local`. SymfonyScaffold derives the URL from `--with-docker` so a `--with-pwa --no-with-docker` install points the PWA at `http://localhost:8000` instead of the unreachable `https://localhost` Docker default baked into the page template.
`npx create-next-app` is now invoked with `--yes --ts --app` so an interactive TTY user can't pick JavaScript or Pages Router, both of which would crash the downstream `app/page.tsx` lookup. `npx --yes` also auto-confirms the create-next-app fetch.
The previous regex `^APP_URL=http:\/\/localhost$` failed on .env files written with CRLF — `\r` was still consumed by the line content, so the patch silently no-op'd and APP_URL kept pointing at port 80. Adding `\r?` before the line anchor fixes it without affecting LF files.
LaravelConfigPatcher hardcoded `PhpVersion::fromString('8.3')`, which
would reject any newer syntax shipped in future api-platform/laravel
config files. Switching to `PhpVersion::getNewestSupported()` keeps
the patcher current as nikic/php-parser is bumped.
setupCorsForLocalhost silently returned when the Symfony skeleton had no `.env`, so admin-only installs could ship without CORS while the installer reported success. Now it throws the same RuntimeException PwaScaffold raises on the same condition, surfacing the misconfigured skeleton instead of hiding it.
When a `vite.config.js` had `input: ['a', 'b',]` (trailing comma) the patcher concatenated `, $injection` after the captured comma, producing `'b',, /* marker */ '...' /* marker */` — a JS array hole. The patcher now rtrim's whitespace and any trailing comma from the captured slice before adding the separator.
ComposeOverrideWriter hardcoded `services.php` as the only service key that could carry an `environment:` block; upstream symfony-docker may rename it to `frankenphp` (matching the image name). The writer now walks a candidate list (`php`, `frankenphp`) so the injection survives that upstream rename without a manual SHA-bump fix.
SymfonyAdminScaffold now prints the resolved API entrypoint after writing admin/.env, along with a one-line pointer at the file users must edit if their Symfony server runs on a non-default host/port. appendEntrypointEnv is idempotent on subsequent runs, so silently freezing the wrong default left no obvious paper trail.
`'' === $name ||` was dead — `/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/` already requires a leading alnum and so rejects '' on its own. Existing testRejectsInvalidNames covers the empty case.
`npm install` followed by `npm install ...JS_PACKAGES` walked the dependency tree twice. `npm install <pkg>...` installs declared deps from package.json and adds the new packages in a single pass.
`implode(' ', $command)` showed arguments containing spaces unquoted,
so the `$ ...` line printed by ProcessRunner was misleading and not
copy-pasteable. Using `Process::getCommandLine()` applies the
platform's argument escaping so the displayed command matches what
Symfony Process is about to execute.
`null === $nativeValidator` can never hold: ChoiceQuestion's constructor always installs a validator. Replace the dead throw with an `assert()` so PHPStan keeps narrowing the type to non-null.
`getcwd()` may return false on unreadable parents; `chdir(false)` raises TypeError under PHP 8 and masks the real assertion failure. Mirrors the `if (false !== $cwd)` guard already present in testInteractiveMultiselectPromptsEmitNoPhpWarnings.
install.sh verifies the download against this before chmod +x.
file_put_contents results were ignored across the scaffolds: a write failing (permissions, disk full) left a partial project reported as success. FileWriter::write() throws instead; all 13 call sites now use it. build-phar.php similarly guarded file_get_contents so an unreadable file aborts the build instead of corrupting the phar entry. Also import RuntimeException instead of inline \RuntimeException, matching InstallerCommand's existing import style.


Summary
Replaces the legacy distribution repo contents with
api-platform/installer, a single Symfony Console binary that scaffolds a new API Platform project — Symfony or Laravel — with optional Docker (symfony-docker upstream, untouched) and Next.js PWA.What it ships
bin/api-platform— Symfony Console default command (interactive wizard + non-interactive flags).src/Scaffold/SymfonyScaffold— composer create-project, optional Docker viaComposeOverrideWriter(injectsCADDY_SERVER_EXTRA_DIRECTIVESinstead of patching symfony-docker), API Platform config writer.PwaScaffold— Next.js app viacreate-next-app, installsnelmio/cors-bundleon the API, ships a branded landing page that callsparseHydraDocumentationfor live resource discovery.LaravelScaffold— composer create-project +php artisan api-platform:install, AST-based config patching, branded Blade welcome page with the same live-resource UX as the PWA (vanilla JS via Vite,@api-platform/*JS libs preinstalled).LaravelConfigPatcher—nikic/php-parserAST rewrites onconfig/api-platform.php; preserves comments and surrounding keys.CLI options
name(arg)--frameworksymfony|laravel--with-docker--with-pwa--formatjsonld,jsonapi,hal--docsswagger_ui,redoc,scalarUsage
api-platform # interactive wizard api-platform my-app --framework=symfony --with-docker --with-pwa api-platform my-app --framework=laravel --format=jsonld --format=jsonapi api-platform my-app --framework=symfony --docs=scalarHighlights from the latest commit
Undefined array keywarnings.--docs=scalarnow supported (API Platform 4.3+enable_scalar/scalar.enabled).swagger_ui,redoc, andscalar(redoc was previously left enabled).enable_docs: falsefrom the Symfony scaffold — it was a core master switch forcinghideHydraOperation: trueand emptying/docs.jsonld#supportedClass, breaking every Hydra client including the bundled PWA.welcome.blade.php+ ES module that callsparseHydraDocumentationto render live resource cards (mirrors the PWA). PatchesAPP_URL=http://localhost:8000so the Vite plugin advertises the right URL.Test plan
vendor/bin/phpunit— 37 tests, 67 assertions, all greenvendor/bin/phpstan analyse— no errorsapi-platform my-app --framework=symfony --with-docker --with-pwa→docker compose up --wait→ PWA renders Greetings cardapi-platform my-app --framework=laravel→npm run dev+php artisan serve→ Blade welcome renders Greetings card (blocked by upstream LaravelContextActionContent-Type bug; TODO logged in~/forks/core/TODO.md)Known upstream issue
api-platform/laravelContextActionreturnsContent-Type: application/jsonon/api/contexts/{shortName}instead ofapplication/ld+json. Breaks any Hydra client; tracked separately.