preflow / devtools
Preflow devtools — CLI commands, dev inspector
Requires
- php: >=8.4
- preflow/core: ^0.70 || @dev
- preflow/data: ^0.75 || @dev
- preflow/routing: ^0.1 || @dev
Requires (Dev)
- phpunit/phpunit: ^11.0
Suggests
- preflow/auth: Needed for the auth:token command (bearer tokens for API routes)
Provides
None
Conflicts
- preflow/auth: <0.62
- preflow/core: <0.70
- preflow/data: <0.75
Replaces
None
- dev-main
- v0.76.0
- v0.75.0
- v0.74.0
- v0.73.0
- v0.72.1
- v0.72.0
- v0.71.1
- v0.71.0
- v0.70.0
- v0.69.2
- v0.69.1
- v0.69.0
- v0.68.1
- v0.68.0
- v0.67.0
- v0.66.2
- v0.66.1
- v0.66.0
- v0.65.0
- v0.64.0
- v0.63.2
- v0.63.1
- v0.63.0
- v0.62.0
- v0.61.0
- v0.60.1
- v0.60.0
- v0.59.0
- v0.58.1
- v0.58.0
- v0.57.0
- v0.56.1
- v0.56.0
- v0.55.0
- v0.54.1
- v0.54.0
- v0.53.2
- v0.53.1
- v0.53.0
- v0.52.0
- v0.51.0
- v0.50.0
- v0.49.1
- v0.49.0
- v0.48.5
- v0.48.4
- v0.48.3
- v0.48.2
- v0.48.1
- v0.48.0
- v0.47.1
- v0.47.0
- v0.46.1
- v0.46.0
- v0.45.0
- v0.44.0
- v0.43.0
- v0.42.1
- v0.42.0
- v0.41.2
- v0.41.1
- v0.41.0
- v0.40.0
- v0.39.0
- v0.38.0
- v0.37.0
- v0.36.0
- v0.35.0
- v0.34.1
- v0.34.0
- v0.33.0
- v0.32.0
- v0.31.0
- v0.30.0
- v0.29.0
- v0.28.0
- v0.27.1
- v0.27.0
- v0.26.0
- v0.25.1
- v0.25.0
- v0.24.1
- v0.24.0
- v0.23.0
- v0.22.1
- v0.22.0
- v0.21.0
- v0.20.0
- v0.19.2
- v0.19.1
- v0.19.0
- v0.18.1
- v0.18.0
- v0.17.2
- v0.17.1
- v0.17.0
- v0.16.2
- v0.16.1
- v0.16.0
- v0.15.0
- v0.14.2
- v0.14.1
- v0.14.0
- v0.13.1
- v0.13.0
- v0.12.0
- v0.11.4
- v0.11.3
- v0.11.2
- v0.11.1
- v0.11.0
- v0.10.4
- v0.10.3
- v0.10.2
- v0.10.1
- v0.10.0
- v0.9.1
- v0.9.0
- v0.8.0
- v0.7.0
- v0.6.0
- v0.5.0
- v0.4.1
- v0.4.0
- v0.3.3
- v0.3.2
- v0.3.1
- v0.3.0
- v0.2.3
- v0.2.2
- v0.2.1
- v0.2.0
- v0.1.1
- v0.1.0
This package is auto-updated.
Last update: 2026-11-14 19:29:55 UTC
README
CLI commands for Preflow development. Requires the preflow entry point in your project root (provided by preflow/starter and preflow/starter-folio) or the package binary at vendor/bin/preflow.
Installation
composer require preflow/devtools --dev
Commands
serve — start the dev server
Starts PHP's built-in server rooted at public/.
php preflow serve
# Preflow dev server started on http://localhost:8080
# Press Ctrl+C to stop.
php preflow serve 0.0.0.0 9000
# Binds to 0.0.0.0:9000
migrate — run pending migrations and post-migrate steps
Boots the application (so packages register their migration paths and steps), then:
- runs every pending file from
migrations/and from the paths packages registered (core'spreflow_throttletable among them) on a SQL storage; - runs every registered post-migrate step, such as Folio's table sync for SQL-stored content types.
php preflow migrate
# 2 pending migration(s).
# [folio.tables] created table post
# Done.
php preflow migrate --pretend # list the pending migrations, change nothing
php preflow migrate --storage=sqlite # the storage to migrate (default: sqlite)
- Steps run on every run, not only when a migration was pending, so they must be idempotent. Folio's step is SQLite-only for now (a Folio type on MySQL makes it throw), and creates and adds only; it never drops or renames.
--pretendskips the steps. The command exits 1 withError: ...on stderr when it fails.- A site without SQL storage (JSON only, or no
config/data.php) printsNothing to migrate: no SQL storage configured.and exits 0, so a shared deploy step can always run it. An explicit--storage=<key>that is unknown or not SQL still exits 1.
db:seed [--class=<Name>] — run the seeders
Boots the application (like migrate), then runs the classes in app/Seeds/ in filename order. Each seeder is built by the container, so its constructor can take any service, and gets the container's DataManager:
namespace App\Seeds; use Preflow\Core\Config\Config; use Preflow\Data\DataManager; final class DemoSeeder { public function __construct(private readonly Config $config) {} public function run(DataManager $data): void { // A demo seed must never touch production data. if (!$this->config->get('app.debug')) { echo "Demo data skipped: app.debug is off.\n"; return; } $job = new Job(); $job->uuid = md5('demo-job-1'); // fixed id: rerunning updates, never duplicates $job->title = 'Barista'; $data->save($job); } }
php preflow db:seed
# Running seeders...
# Seeded: DemoSeeder
# Done.
php preflow db:seed --class=DemoSeeder # one seeder; a short name under App\Seeds, or fully qualified
.envis loaded, so a seeder can read$_ENV/getenv().save()validates: the container'sDataManagerhas the validator, so#[Validate]rules apply.- An unknown
--classprintsSeeder <Name> not found.and exits 1; a seeder that throws printsSeeder <Name> failed: <message>and exits 1. - A project without
app/Seeds/printsNo seeders directory found (app/Seeds/).and exits 0.
Upgrading: seeders used to run on a hand-built SQLite connection, with no container, no validation and no .env. They now run through the booted app, so #[Validate] rules apply and seed data that breaks them now fails. Seeders with a constructor that needs arguments now get them from the container instead of failing.
make:component Name — scaffold a component
Creates app/Components/Name/Name.php and app/Components/Name/Name.twig.
php preflow make:component ProductCard
# Component created: app/Components/ProductCard/
# - ProductCard.php
# - ProductCard.twig
The generated PHP class extends Component with empty resolveState() and actions(). The Twig template includes {% apply css %} and {% apply js %} blocks.
make:model Name — scaffold a model
Creates app/Models/Name.php with #[Entity], #[Id], #[Field], and #[Timestamps] attributes. The table name is derived by snake-casing the class name and appending s.
php preflow make:model BlogPost
# Model created: app/Models/BlogPost.php
# (maps to table: blog_posts)
make:model Name --from-type=key — turn a JSON type into a model class
Reads config/models/<key>.json and writes app/Models/Name.php: #[Entity(table, storage, type)],
the id, one property per field, with #[Field], #[Relation] and #[Validate] carrying the data
parts and labels. The JSON file is rewritten to its admin-only part (presentation: label, icon,
list columns, per field readonly, hidden, …), so nothing is defined twice. Same table, same
data, same ids: nothing in the database changes.
php preflow make:model Job --from-type=job
# Model created: app/Models/Job.php
# Admin settings kept in: config/models/job.json
# Check the class, then run your tests: nothing in the database changes.
- Every property is nullable and takes the JSON
default;requiredstays a validation rule, so a stored NULL is not rewritten by a later save. A translatable property isarray|string|null; several assets, blocks, a matrix and a multiple relation are?array; one asset is its id (?string). - A relative entry in
data.model_pathsis under the app's base path. - Before writing, the command loads the generated class and compares the type it derives with the type from the JSON file. Any difference is an error and nothing is written.
- It refuses a class that exists, a key that is already class-backed, a class name PHP reserves
(
Object,List, …) and a type withmatrixableorviews, which a class cannot carry yet. Each error names the key.
make:controller Name — scaffold a controller
Creates app/Controllers/NameController.php with a #[Route] class attribute and a #[Get] index action. Appends Controller to the name if not already present.
php preflow make:controller Article
# Controller created: app/Controllers/ArticleController.php
# (route prefix: /api/article)
make:migration name — create a migration file
Creates a timestamped file in migrations/ with empty up() and down() stubs.
php preflow make:migration create_comments
# Migration created: migrations/2026_04_10_143012_create_comments.php
routes:list — print the route table
Boots the app and prints its routes in the order they match: the component endpoint (answered before routing), then file pages, controllers and the routes packages add (media, Folio). What middleware answers (redirects, sitemap, robots.txt, llms.txt) isn't listed. If the app doesn't boot, it lists the file pages only and says why.
php preflow routes:list
# Registered Routes
# ──────────────────────────────────────────────────────────────────────
# Method Pattern Mode Handler
# ──────────────────────────────────────────────────────────────────────
# ANY /--component/* endpoint Preflow\Htmx\ComponentEndpoint
# GET / component index.twig
# GET /api/places action App\Controllers\Api\PlacesController@index
# ──────────────────────────────────────────────────────────────────────
cache:list — list registered caches
Shows every cache registered with the core cache registry: key, label, number of entries and size.
php preflow cache:list
# Key Label Entries Size
# data.json JSON storage query cache 12 3.1 KB
cache:clear [KEY] — clear framework caches
cache:clear <key> clears one registered cache; cache:clear --all clears
every registered cache. An unknown key exits with 1 and lists the known keys.
Files outside the registered caches are never deleted. The bare form
(cache:clear without arguments) still clears all caches but prints a note;
it will require --all in a later release. An unknown option (anything starting
with -- other than --all) exits with 1 and clears nothing.
cache:list and cache:clear boot the application to read the registry, so
they need what the app needs to boot (.env, config, a reachable database for
SQL drivers). When the boot fails they print the error and exit with 1.
php preflow cache:clear --all
# Cleared data.json: 12 file(s), 3.1 KB.
pages:work [--budget=50] — rebuild queued pages (cron)
With the page cache on (preflow/core's README, "Page cache"), writes forget the pages that
showed what they changed and queue the full pages among them for rebuilding; stale pages and a
deploy's warm-up are queued too. The request that queued them rebuilds for rebuild_budget
seconds after its response; pages:work takes the rest, for up to --budget seconds (50 by
default). Run it every minute from the cron:
* * * * * cd /path/to/site/current && php preflow pages:work >/dev/null 2>&1
The queue's leases keep it and the after-response work from building one page twice. With
the cache off, or app.debug on, it rebuilds nothing.
php preflow pages:work
# Rebuilt 12 page(s) in 1.4 s; 0 queued.
pages:refresh <url> / pages:refresh --all — refresh cached pages
pages:refresh <url> forgets every cached copy of one URL (each locale and htmx fragment
variant; a path such as /about is taken against app.url) and rebuilds the page.
pages:refresh --all forgets every page and queues every known full page without a query.
Both then rebuild for up to --budget seconds (50) and leave the rest to pages:work. A URL of
a host the cache doesn't cache exits with 1. Folio's Settings → Cache has the same two actions.
php preflow pages:refresh /about
# Forgot 1 cached copy of /about.
# Rebuilt 1 page(s); 0 queued.
php preflow pages:refresh --all
# Forgot every page; 240 queued for rebuilding.
# Rebuilt 240 page(s); 0 queued.
cache:clear pages empties the page cache without rebuilding anything (and drops the queue).
optimize [--check] — write the boot manifest
Writes bootstrap/cache/boot.php (atomically): the merged config, the package discovery data
and the catalogs (components, blocks, extensions, models, ...), so web requests with
APP_DEBUG=0 read them instead of scanning. See preflow/core's README, "Boot manifest", for
when a request uses it. Run it on every deploy after composer install and migrations /
folio:extensions:apply, before the release goes live. With APP_DEBUG on it writes none
(and removes an existing one). cache:clear (key boot) removes it.
optimize --check lists the sources changed since the manifest was written and exits with 1;
it exits with 0 when the manifest is current and with 1 when there is none.
php preflow optimize
# Boot manifest written: /srv/site/releases/42/bootstrap/cache/boot.php
# config: 18 file(s); catalogs: core.components, core.controllers, folio.extensions, …
php preflow optimize --check
# Boot manifest is current.
auth:token <email-or-id> — issue, list and revoke API tokens
Needs preflow/auth. Creates a bearer token for a user of the token guard (the auth.api_guard
guard, else the first TokenGuard) and prints it once; only its hash is stored. Use it as
Authorization: Bearer <token> on routes with TokenAuthMiddleware.
php preflow auth:token editor@example.com --name=deploy-bot
php preflow auth:token --list editor@example.com
php preflow auth:token --revoke=<uuid>
--name is a label (default cli).
auth:user <PREFIX> — set a login in .env
Asks for an email and a password (hidden, twice, at least 8 characters) and
writes <PREFIX>_EMAIL and <PREFIX>_PASSWORD_HASH to .env, replacing the
lines when they exist. The prefix is required (capital letters, digits and
underscores); the password and its hash are never printed.
config/auth.php reads the two values, e.g. for a ConfigUserProvider user.
php preflow auth:user ADMIN
php preflow auth:user EDITOR --email=editor@example.com
# Set EDITOR_EMAIL=editor@example.com and EDITOR_PASSWORD_HASH in .env.
Commands from packages and the app
A package brings its own console commands by listing the classes in its composer.json
(constructed without arguments, so they implement Command\CommandInterface):
"extra": { "preflow": { "commands": ["Vendor\\Pkg\\Console\\PruneCommand"] } }
Console reads them from vendor/composer/installed.json, so the site needs no
config/commands.php line (e.g. members:prune from preflow/members). A site's own
config/commands.php is registered after and wins on a name clash. A class that is missing or
not a command is skipped with a warning on stderr.