Skip to content

The command-line tool

Quiote ships a command-line tool, quiote, built on Symfony Console. Its built-in commands scaffold a new application (new) and pieces inside one (make:module, make:action, make:middleware, make:job), run a local dev server (serve), list an app’s routes (routes:list), print framework/app info (about), compile caches ahead of time (routes:compile, cache:warmup), and derive an API spec (openapi:generate). Plugins can contribute more (see Writing your own command).

The CLI comes with the framework, so install Quiote first. Every command except new also needs an application to point at (see Application-aware commands below); new runs anywhere because it only writes files.

When Quiote is installed as a dependency, the binary is at vendor/bin/quiote. From a checkout of the framework itself it’s bin/quiote.

Terminal window
vendor/bin/quiote list # show all available commands
vendor/bin/quiote <command> --help # options for one command
vendor/bin/quiote --version

list, help, and --version come from Symfony Console; the Quiote-specific commands are below.

CommandPurposeNeeds an app?
newScaffold a new Quiote applicationNo (writes files only)
serveRun a local development serverYes (locates pub/; doesn’t boot)
make:moduleScaffold a module directory treeYes
make:actionScaffold an Action (and matching View/Template)Yes
make:middlewareScaffold a PSR-15 middleware classYes
make:jobScaffold a queue Job classYes
routes:listList routes from the app’s routing serviceYes
aboutPrint framework and application infoYes
routes:compileCompile route/module introspection data into cache/introspection/app.jsonYes
cache:warmupCompile and cache configuration ahead of time so workers start warmYes
openapi:generateDerive an OpenAPI document from routes and validators. See OpenAPI generationYes

Plugin-contributed commands — queue:work and queue:failed:* from quioteframework/queue, schedule:run from quioteframework/scheduler, mcp:* from quioteframework/mcp, cassette:list/cassette:show/cassette:fetch/cassette:prune/replay from quioteframework/replay — appear in list once their package is installed and the plugin enabled.

Every command except new and serve bootstraps a real application, so it needs to find one. new doesn’t — it only writes files and never boots the framework. serve needs to locate an app (to find its pub/ directory) but deliberately doesn’t boot it, since the server process it starts will do that itself.

The app-aware commands all accept:

OptionDefaultEffect
--app-dir$QUIOTE_APP_DIR, else an upward searchPath to the application directory.
--env$QUIOTE_ENV, else developmentEnvironment to bootstrap.

App-directory resolution order: --app-dir, then $QUIOTE_APP_DIR, then a .quiote.json marker file ({"app_dir": "...", "env": "..."}) found by walking up from the current directory, then an upward search from the current directory for a Config/settings.* file. If none is found, the command errors and tells you to pass --app-dir, set $QUIOTE_APP_DIR, or run from inside an app directory. In practice, running the command from your project root just works.

Creates a self-contained, runnable application: a Default module with Index, About, Boom, and Contact actions (the last routed via a #[Route] attribute rather than in AppRouting, to demonstrate both routing styles), the config needed to boot (settings, factories, databases, output_types) plus a PHP AppRouting routing class, and a FrankenPHP-ready pub/index.php.

Terminal window
vendor/bin/quiote new my-app
Argument / optionDefaultEffect
path (argument, required)Directory to create the application in.
--namespaceAppPSR-4 namespace prefix for the app (e.g. App, SampleApp). Must start with an uppercase letter.
--config-formatphpFormat for the generated settings file: php, yaml, or xml. (The scaffold deliberately mixes formats: factories is always YAML and databases/output_types are always XML, so a generated app exercises all three config drivers.)
--runtimeAlso scaffold an entrypoint for a persistent worker runtime: roadrunner (writes worker.php and .rr.yaml) or swoole (writes swoole.php). See Deployment.
--force, -fWrite into a directory that already exists and is non-empty.
Terminal window
# A YAML-configured app under a custom namespace
vendor/bin/quiote new ./shop --namespace Shop --config-format yaml

If the target directory exists and isn’t empty, the command refuses unless you pass --force. The generated app has no composer.json of its own — its front controller registers a PSR-4 autoloader for its own namespace and locates a vendor/autoload.php that has Quiote in it.

After scaffolding, the command prints the next steps:

Terminal window
cd my-app
php -S localhost:8000 -t pub pub/index.php # quick smoke test
# or, with FrankenPHP:
frankenphp php-server --root pub

Or let serve pick whichever of those is available for you:

Terminal window
cd my-app
vendor/bin/quiote serve

The generated app serves GET /, GET /about, GET /contact, and GET /boomboom deliberately throws, so you can see error handling (set core.developer_exceptions true in Config/settings.* for the Whoops page). See Your first application for a walkthrough of what it generates.

new prints a php -S command for you to run by hand; serve runs one for you, and reaches every runtime Quiote supports through a single entry point.

Terminal window
vendor/bin/quiote serve
vendor/bin/quiote serve --port=9000
vendor/bin/quiote serve --runtime=roadrunner
OptionDefaultEffect
--hostlocalhostHost to bind.
--port8000Port to bind.
--runtimeautoWhich server to run: auto, frankenphp, php, roadrunner, swoole.

Under auto, the command looks for a frankenphp binary on PATH. If it finds one it runs frankenphp php-server --listen {host}:{port} --root {app}/pub; otherwise it falls back to php -S {host}:{port} -t {app}/pub with a note that FrankenPHP is recommended for anything beyond a quick local check, since php -S is single-threaded.

--runtime also accepts the two off-SAPI runtimes, so one command covers every deployment shape:

RuntimeRunsWhere the listen address comes fromNeeds
frankenphpfrankenphp php-server--host / --portfrankenphp on PATH
phpphp -S--host / --portnothing
roadrunnerrr serve from the app root.rr.yamlrr on PATH or at vendor/bin/rr, plus a .rr.yaml
swoolethe app’s swoole.phpworker.swoole.* settingsext-swoole, plus a swoole.php

For RoadRunner and Swoole, --host/--port deliberately do not apply — that runtime’s own config owns the address — and the command says so when it starts. Each missing prerequisite is reported as a specific, actionable error (how to install the binary, or which quiote new --runtime=… invocation generates the missing entrypoint) rather than a generic failure.

Every branch wraps the server’s own binary or the app’s entrypoint script as a child process, never an in-process Kernel::run() — the console has already bootstrapped the app in its own process, so serving inline would bootstrap twice. The server runs in the foreground until Ctrl-C, like any dev server.

Scaffolding inside an app: the make:* commands

Section titled “Scaffolding inside an app: the make:* commands”

Where new scaffolds a whole application from nothing, the four make:* commands scaffold within one that already exists. They all take the standard --app-dir/--env options plus --force (-f) to overwrite an existing file, and all validate names as PHP class-name segments before writing anything.

Terminal window
vendor/bin/quiote make:module Blog --with-index
vendor/bin/quiote make:action Post --module=Blog --methods=GET,POST --output-types=html,json
vendor/bin/quiote make:middleware RequestId --phase=before_action --priority=10
vendor/bin/quiote make:job SendWelcomeEmail --retryable

A module in Quiote is a directory convention, not a class — so this creates the tree (Modules/{Name}/{Actions,Views,Templates}) rather than generating any code.

OptionDefaultEffect
--with-indexAlso seed an IndexAction/View/Template trio, so the new module isn’t left completely empty. Equivalent to following up with make:action Index --methods=GET.
--force, -fOverwrite existing files (only meaningful with --with-index).

Generates the Action and, unless --no-view, a matching View and Template.

OptionDefaultEffect
--moduleDefaultModule to create the action in.
--methodsGETComma-separated HTTP verbs the action handles.
--output-typeshtmlComma-separated output types the View should support.
--no-viewGenerate only the Action — no View or Template.
--force, -fOverwrite existing files.

--methods takes real HTTP verbs (GET, HEAD, OPTIONS, TRACE, POST, PUT, PATCH, DELETE) and maps them through the same HttpMethodMapper convention ActionResolver dispatches against, so the generated methods are executeRead() / executeWrite() / executeUpdate() / executeRemove() — not executeGet(). An unknown verb is rejected with the supported list. See Actions and views.

--output-types maps each type to an execute{OutputType}() method on the generated View — json becomes executeJson(). For json, xml and text, the command additionally provisions the output type into Config/output_types.xml if it isn’t declared yet, as a php-rendered <output_type> entry with a sensible Content-Type. Any other type name still gets its View method stub, plus a warning that you need to add a matching output_types.xml entry by hand. --output-types is incompatible with --no-view (there would be nothing to attach it to) and says so.

The generated template is written in the syntax of whichever renderer your app configures for html, via Renderer::getStarterTemplate() and that renderer’s default extension — so a Twig-configured app gets PostSuccess.twig, not a .php file Twig would never execute. If the configured renderer offers no starter template, the command writes none and warns, naming the file and extension for you to author by hand.

Generates a PSR-15 MiddlewareInterface class in Middleware/, carrying a #[Quiote\Middleware\Attribute\Middleware] attribute built from your ordering options.

OptionDefaultEffect
--phasebefore_actionOne of bootstrap, pre_routing, pre, routing, before_action, action, after_action, finalize.
--priority0Ordering priority within the phase.
--afterRun after this middleware class/name.
--beforeRun before this middleware class/name.
--force, -fOverwrite an existing file.

There is no separate registration step: attribute scanning picks up app-owned middleware automatically. See Writing custom middleware.

Generates a Quiote\Queue\Job implementation in Jobs/ — just a handle() method — or, with --retryable, a Quiote\Queue\RetryableJob that also stubs maxAttempts() and backoffSeconds(int $attempt).

OptionDefaultEffect
--retryableImplement RetryableJob instead of Job, for a per-job retry policy.
--force, -fOverwrite an existing file.

If quioteframework/queue isn’t installed, the command still writes the file but notes inline that the generated class won’t autoload until you composer require the package.

Lists every route the app’s configured routing service knows about — whatever the class named for the routing factory role exposes, whether declared in Routing::build(), via #[Route] attributes, or both merged together. It is a read-only view of the live result, not a second opinion. See Routing.

Terminal window
vendor/bin/quiote routes:list
Name Path Methods Module Action Output type Source
------- ------------- -------- -------- -------- ------------ ----------
index / ANY Default Index File
about /about ANY Default About File
contact /contact GET Default Contact Attribute

Columns:

  • Methods — the HTTP methods the route accepts, or ANY if unrestricted.
  • SourceAttribute if the route’s name was declared via a #[Route] attribute, File for anything else (Routing::build(), a programmatic builder, and so on).

Options (in addition to --app-dir / --env):

OptionDefaultEffect
--contextcore.default_context, else webContext to resolve the routing service from.
--moduleOnly show routes for this module (case-insensitive).
--actionOnly show routes resolving to this action (case-insensitive).
--sortnameSort by name, path, module, or action.
--jsonOutput JSON instead of a table.
Terminal window
vendor/bin/quiote routes:list --module Blog --sort path
vendor/bin/quiote routes:list --json

Diagnostics and exit code: the command independently scans #[Route] attributes and reports authoring problems (e.g. duplicate route names or paths) as warnings or errors above the table. If any diagnostic is an error, the command exits non-zero — useful in CI to catch route conflicts.

Bootstraps the app and prints a short diagnostic table: Quiote version, application directory, environment, module directory, and namespace prefix.

Terminal window
vendor/bin/quiote about

It takes the standard --app-dir / --env options and nothing else. It’s the simplest way to confirm the CLI can locate and boot your application.

cache:warmup — precompiling config and routing

Section titled “cache:warmup — precompiling config and routing”

Compiles the app’s configuration ahead of time so a freshly started worker doesn’t pay the first-request cost of parsing, validating and XSL-transforming every config file.

Terminal window
vendor/bin/quiote cache:warmup
vendor/bin/quiote cache:warmup --check # CI guard: verify, don't write
OptionDefaultEffect
--contextcore.default_context, else webContext to warm.
--checkVerify the compiled routing matcher is up to date without writing, and exit non-zero on drift.

It warms three things:

  1. Compiled config — every default config file (settings, factories, output_types, databases, translation, …). Optional files that are simply absent are reported as skipped, not as errors.
  2. The routing IRAttributeRouteScanner’s scan result, so AttributeRouting::build() can skip the live #[Route] scan. Only applies to attribute-based routing, and is skipped with a note otherwise.
  3. The compiled route matcher.

The config backend is auto-detected the same way the runtime picks it: if QUIOTE_USE_APCU_CONFIG_CACHE is defined and true, the APCu path runs; otherwise the on-disk cache under {app_dir}/cache/config is populated. APCu is per-process shared memory, so warming it from a detached CLI (where apc.enable_cli is typically 0) doesn’t help — warm the file backend there and let the worker’s QUIOTE_APCU_PREWARM hydrate APCu at boot.

Running this on every deploy is what makes the two production trust switches safe to enable — see core.config_check_freshness and core.routing.trust_compiled_ir. --check in CI catches a compiled matcher that has drifted from the source routes.

Because the CLI is Symfony Console, a custom command is a standard Symfony Command. The one Quiote-specific piece is the base class Quiote\Console\Command\AbstractAppCommand, which handles bootstrapping a real application so your command has access to config, the context, and the DI container.

<?php
declare(strict_types=1);
namespace App\Console;
use Quiote\Config\Config;
use Quiote\Context;
use Quiote\Console\Command\AbstractAppCommand;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\{InputInterface, InputOption};
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(name: 'cache:prune', description: 'Prune expired cache entries')]
final class PruneCacheCommand extends AbstractAppCommand
{
protected function configure(): void
{
$this->configureAppOptions(); // adds --app-dir and --env
$this->addOption('context', null, InputOption::VALUE_REQUIRED, 'Context to use', 'web');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->bootstrapApp($input); // Config + Context now available
$io = new SymfonyStyle($input, $output);
$context = Context::getInstance((string) $input->getOption('context'));
// resolve services via $context->getContainer()->get(...), read Config::get(...), etc.
$io->success('Done.');
return self::SUCCESS;
}
}

The essentials:

  • #[AsCommand(name:, description:)] names the command (this is also how it’s de-duplicated).
  • configure() — call $this->configureAppOptions() to get the standard --app-dir / --env, then add your own arguments and options.
  • bootstrapApp($input) — call this first in execute(). It resolves the app directory, reads --env, boots the framework, and registers a fallback autoloader for your app’s namespace. After it returns, Config::get() and Context::getInstance() work.
  • execute() returns self::SUCCESS or self::FAILURE. Use SymfonyStyle for tables, titles, and status output.

A command that only writes files and never needs the framework (like a scaffolder) can extend Symfony’s Command directly and skip bootstrapApp().

There is no directory scan for commands — you contribute yours through a plugin, with PluginRegistrar::command():

$registrar->command(\App\Console\PruneCacheCommand::class);

Once a plugin registers it, the CLI picks it up (after the app is bootstrapped) and vendor/bin/quiote cache:prune runs. This is the same seam plugins use to ship their own commands, so an authentication or health-check plugin can add commands to your CLI without you wiring anything.