Skip to content

CLI scaffolding#2980

Merged
soyuka merged 33 commits into
api-platform:mainfrom
soyuka:feat/cli-scaffold
Jun 12, 2026
Merged

CLI scaffolding#2980
soyuka merged 33 commits into
api-platform:mainfrom
soyuka:feat/cli-scaffold

Conversation

@soyuka

@soyuka soyuka commented Feb 24, 2026

Copy link
Copy Markdown
Member

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 via ComposeOverrideWriter (injects CADDY_SERVER_EXTRA_DIRECTIVES instead of patching symfony-docker), API Platform config writer.
    • PwaScaffold — Next.js app via create-next-app, installs nelmio/cors-bundle on the API, ships a branded landing page that calls parseHydraDocumentation for 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).
    • LaravelConfigPatchernikic/php-parser AST rewrites on config/api-platform.php; preserves comments and surrounding keys.

CLI options

Flag Values Notes
name (arg) identifier project directory
--framework symfony | laravel
--with-docker yes/no Symfony only
--with-pwa yes/no Symfony only; needs node/npx/pnpm
--format jsonld, jsonapi, hal repeatable
--docs swagger_ui, redoc, scalar repeatable; empty disables all UIs

Usage

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=scalar

Highlights from the latest commit

  • Multiselect prompts no longer emit Undefined array key warnings.
  • --docs=scalar now supported (API Platform 4.3+ enable_scalar / scalar.enabled).
  • Laravel patcher now symmetrically toggles swagger_ui, redoc, and scalar (redoc was previously left enabled).
  • Dropped enable_docs: false from the Symfony scaffold — it was a core master switch forcing hideHydraOperation: true and emptying /docs.jsonld#supportedClass, breaking every Hydra client including the bundled PWA.
  • Laravel scaffold ships a branded welcome.blade.php + ES module that calls parseHydraDocumentation to render live resource cards (mirrors the PWA). Patches APP_URL=http://localhost:8000 so the Vite plugin advertises the right URL.

Test plan

  • vendor/bin/phpunit — 37 tests, 67 assertions, all green
  • vendor/bin/phpstan analyse — no errors
  • End-to-end: api-platform my-app --framework=symfony --with-docker --with-pwadocker compose up --wait → PWA renders Greetings card
  • End-to-end: api-platform my-app --framework=laravelnpm run dev + php artisan serve → Blade welcome renders Greetings card (blocked by upstream Laravel ContextAction Content-Type bug; TODO logged in ~/forks/core/TODO.md)

Known upstream issue

api-platform/laravel ContextAction returns Content-Type: application/json on /api/contexts/{shortName} instead of application/ld+json. Breaks any Hydra client; tracked separately.

@rvanlaak

rvanlaak commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

@soyuka are you planning on moving forward with this approach for the template? 🚀

We started experiencing setfacl errors on our containers since some days, which required cherry-picking dunglas/symfony-docker@dab1308 to fix it.

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.

Comment thread Makefile
@soyuka

soyuka commented Apr 2, 2026

Copy link
Copy Markdown
Member Author

yes but we need to review some things, probably use PHP for it instead of GO.
We agreed with @dunglas that it's too hard to maintain this repo in sync with symfony-docker therefore we're going to provide an installation tool instead. It'd be nice to have a way to keep user repositories in sync also. It'll also simplify our onboarding.
Maybe that https://github.com/coopTilleuls/template-sync would be a nice integration.

@rvanlaak

rvanlaak commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

Although template-sync looks interesting, I'd go for the Symfony-way on how Composer recipes can patch existing applications. Agree that keeping templates up to par is not feasible.

@soyuka soyuka force-pushed the feat/cli-scaffold branch 2 times, most recently from d17b2fb to 08ab9be Compare April 29, 2026 13:03
soyuka added 3 commits May 13, 2026 16:08
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.
soyuka added 4 commits May 19, 2026 12:14
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.
Comment thread src/Scaffold/PwaScaffold.php Outdated
Comment thread src/Scaffold/PwaScaffold.php Outdated
@J3m5

J3m5 commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

After my first attempt with the CLI, I encountered these two issues:

  • Selecting the redoc and scalar options for the API documentation returns an error for an invalid value
Details image
API Platform installer
======================

 Project name [my-app]:
 >

 Framework [symfony]:
  [0] symfony
  [1] laravel
 > 0

 Use Docker? (yes/no) [yes]:
 >

 Include Next.js PWA? (yes/no) [no]:
 > yes

 API formats (comma-separated) [jsonld, jsonapi, hal]:
  [0] jsonld
  [1] jsonapi
  [2] hal
 > 0

 API documentation (comma-separated, empty for none) [swagger_ui, redoc, scalar]:
  [0] swagger_ui
  [1] redoc
  [2] scalar
 > 2


 [ERROR] Invalid value "2".


 API documentation (comma-separated, empty for none) [swagger_ui, redoc, scalar]:
  [0] swagger_ui
  [1] redoc
  [2] scalar
 > 1


 [ERROR] Invalid value "1".


 API documentation (comma-separated, empty for none) [swagger_ui, redoc, scalar]:
  [0] swagger_ui
  [1] redoc
  [2] scalar
 > 0

Creating symfony project "my-app"
---------------------------------
  • Creating the PWA with pnpm does not complete and fails, likely due to build approval issues
Details image
Creating Next.js app with create-next-app
$ npx create-next-app pwa --use-pnpm
Need to install the following packages:
[email protected]
Ok to proceed? (y)

Using defaults for unprovided options:

  --ts                    TypeScript (use --js for JavaScript)
  --eslint                ESLint (use --biome for Biome, --no-eslint for None)
  --no-react-compiler     No React Compiler (use --react-compiler for React Compiler)
  --tailwind              Tailwind CSS (use --no-tailwind for No Tailwind CSS)
  --no-src-dir            No src/ directory (use --src-dir for src/ directory)
  --app                   App Router (use --no-app for Pages Router)
  --agents-md             AGENTS.md (use --no-agents-md for No AGENTS.md)
  --import-alias          "@/*"

Creating a new Next.js app in /home/jeremy/workspace/development/tilleuls/api-platform/sandbox/cli-tests/my-app/pwa.

Using pnpm.

Initializing project with template: app-tw


Installing dependencies:
- next
- react
- react-dom

Installing devDependencies:
- @tailwindcss/postcss
- @types/node
- @types/react
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript

Downloading [email protected]: 34.11 MB/34.11 MB, done
Downloading @next/[email protected]: 45.17 MB/45.17 MB, done
Downloading @img/[email protected]: 7.53 MB/7.53 MB, done
Packages: +351
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 427, reused 239, downloaded 117, added 351, done

dependencies:
+ next 16.2.6
+ react 19.2.4 (19.2.6 is available)
+ react-dom 19.2.4 (19.2.6 is available)

devDependencies:
+ @tailwindcss/postcss 4.3.0
+ @types/node 20.19.41 (25.9.1 is available)
+ @types/react 19.2.15
+ @types/react-dom 19.2.3
+ eslint 9.39.4 (10.4.1 is available)
+ eslint-config-next 16.2.6
+ tailwindcss 4.3.0
+ typescript 5.9.3 (6.0.3 is available)

[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: [email protected], [email protected]

Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.

Aborting installation.
  pnpm install has failed.

Installing API Platform frontend libraries
$ pnpm add @api-platform/api-doc-parser github:api-platform/zod @api-platform/ld @api-platform/mercure
[WARN] 1 deprecated subdependencies found: [email protected]
Packages: +28
++++++++++++++++++++++++++++
Progress: resolved 455, reused 358, downloaded 26, added 28, done

dependencies:
+ @api-platform/api-doc-parser 0.16.10
+ @api-platform/ld 1.0.0
+ @api-platform/mercure 1.0.0
+ @api-platform/zod 0.1.0

[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: [email protected], [email protected]

Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.

In ProcessRunner.php line 35:

  Command failed: pnpm add @api-platform/api-doc-parser github:api-platform/zod @api-platform/ld @api-platform/mercure


api-platform [--framework FRAMEWORK] [--with-pwa|--no-with-pwa] [--with-docker|--no-with-docker] [--format FORMAT] [--docs DOCS] [--] [<name>]

Executed with:
Composer version 2.10.0
PHP version 8.4.21
pnpm version 11.5.0
Node.js version 24.16.0

J3m5 added 4 commits June 2, 2026 16:13
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
J3m5 added 4 commits June 2, 2026 16:13
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.
soyuka added 16 commits June 10, 2026 14:34
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.
@soyuka soyuka merged commit f205bde into api-platform:main Jun 12, 2026
4 checks passed
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.

4 participants