linkfast / exengine-cli
exen: command line tool for the ExEngine microframework (new projects, dev server, generators, extensions).
Requires
- php: >=8.1.0
Requires (Dev)
- linkfast/exengine: ^1.3.0-a1
- phpunit/phpunit: ^13.0
Suggests
- ext-yaml: Lets ExEngine projects read config/application.yml (exen new generates JSON configuration without it).
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-12 02:22:03 UTC
README
Command line tool for the ExEngine microframework: create projects, run the development server, generate code and manage ExEngine extensions.
© LinkFast S.A. (https://linkfast.io) · MIT License
Requirements
- PHP 8.1 or higher
- Composer
- Optional:
ext-yaml(pecl install yaml) so projects can useconfig/application.yml
exen has no Composer dependencies, so installing it globally does not conflict with other global tools.
Installation
composer global require linkfast/exengine-cli:^1.0.0-a1
exen 1.0.0 is in development and published as pre-releases (1.0.0-a1, 1.0.0-a2, ...); the version constraint
lets Composer install them. Projects created with exen new require the pre-releases of ExEngine 1.3.0 and exen the
same way.
Make sure Composer's global bin directory is on your PATH (composer global config bin-dir --absolute
prints it). On Windows, Composer creates exen.bat there automatically.
Projects created with exen new also require exen as a dev dependency. When you run the global exen inside such
a project, it hands over to the project's copy, so every project uses the exen version matching its
dependencies.
Quick start
exen new orders-api
cd orders-api
exen serve
exen new asks for the project settings (package name, version, namespace, servers, ...); answer with Enter to
keep the defaults. Open http://127.0.0.1:8080/Hello/world.
Commands
| Command | Description |
|---|---|
exen new [<directory>] | Create a project (interactive, or from flags) with the web server configurations you choose, and run composer install |
exen new:extension [<directory>] | Create an ExEngine extension package (settings, injectable service, tests), ready to publish |
exen serve | Run the development server the project is made for: RoadRunner or PHP's built-in server |
exen rr:install | Download the RoadRunner server (rr) for this system into the project |
exen rr:init | Create worker.php and .rr.yaml to run the project on RoadRunner |
exen routes | List the routes of the project controllers |
exen doctor | Check PHP, extensions, Composer and the current project |
exen make:controller <Name> | Generate a controller (--rest for a REST controller, Admin/Users for a sub-folder) |
exen make:component <Name> | Generate an injectable component |
exen make:filter <Name> | Generate a request filter (--response for a response filter) |
exen make:dataobject <Name> | Generate a DataObject |
exen make:listener <Name> | Generate a lifecycle listener (--event=ResponseReady) |
exen make:properties <Name> | Generate a #[ConfigProperties] class (--prefix=database) |
exen extension:add <name or vendor/package> | Install an ExEngine extension (first-party by name: monolog, roadrunner) (composer require), activated automatically (--manual-activation to activate it yourself) |
exen extension:list | List the installed ExEngine extensions and how they are activated |
exen extension:remove <name or vendor/package> | Uninstall an ExEngine extension (composer remove) |
exen help <command> shows the arguments and options of each command; exen list shows them all.
exen new
exen new [orders-api] [--package=acme/orders-api] [--description=...] [--project-version=0.1.0] [--namespace=App]
[--api-type=rest,graphql,...] [--php=">=8.1"] [--server=apache,nginx,...] [--rr] [--config-format=yml|json|php]
[--extension=vendor/package ...] [--no-install] [-n|--no-interaction]
In a terminal, exen new asks for every setting not given as a flag, shows a summary and asks for confirmation.
It does not ask anything when --server or --rr is given, with -n / --no-interaction, or without a
terminal (CI, scripts); missing settings then take their defaults (package app/<directory>, version 0.1.0,
namespace App, a REST API on the built-in server).
API types (--api-type, comma-separated, or chosen from the list), kept in composer.json (extra.exengine.api):
| API type | |
|---|---|
rest | HTTP REST: controllers with automatic routing and JSON responses |
openapi | Contract-first REST from an OpenAPI 3 document (code generation coming soon) |
graphql | Schema-first GraphQL with linkfast/exengine-graphql (coming soon) |
asyncapi | Asynchronous message consumers on RoadRunner jobs (coming soon); adds RoadRunner to the servers |
Servers (--server, comma-separated, or chosen from the list):
| Server | Generated files |
|---|---|
builtin | Nothing: PHP's built-in server through exen serve, for development |
apache | public/.htaccess (mod_rewrite front controller) |
nginx | deploy/nginx.conf (server block for PHP-FPM) |
caddy | deploy/Caddyfile (php_fastcgi to PHP-FPM) |
frankenphp | deploy/frankenphp/Caddyfile and public/frankenphp-worker.php (worker mode) |
roadrunner (rr) | worker.php, .rr.yaml; installs linkfast/exengine-roadrunner and the rr binary, PHP 8.2+ |
All of them route URLs without the launcher (/Hello/world, which needs linkfast/exengine 1.3.0-a1+); the classic
/api.php/Hello/world keeps working. --extension (repeatable) installs extensions as exen extension:add does.
composer.json records the development server in extra.exengine.server (roadrunner when it is among the
servers, builtin otherwise), which exen serve runs. RoadRunner projects also get the worker pool per profile:
roadrunner.workers: 2 in application.*, and one worker per CPU core with max-jobs: 500 in application-prod.*.
Creates:
| Path | Contents |
|---|---|
public/api.php | Launcher; the web server document root is public/ |
src/AppConfig.php | Registration of components, filters, listeners and properties classes |
src/Controllers/Hello.php | Sample controller: /Hello/world |
config/application.yml | Properties, plus application-prod.yml for the prod profile |
composer.json | Requires linkfast/exengine; extra.exengine describes the layout for exen |
The configuration format defaults to YAML when ext-yaml is loaded, JSON otherwise.
exen new:extension
exen new:extension [exengine-metrics] [--package=acme/exengine-metrics] [--name=metrics] [--namespace=Acme\Metrics]
[--description=...] [--php=">=8.1"] [--author=...] [--no-install] [-n|--no-interaction]
Creates a Composer package of type exengine-extension, so applications install it with exen extension:add and
ExEngine activates it at boot. In a terminal it asks for the settings not given as flags (--package or -n skip
the questions). The name (by default the directory without exengine-) gives the class names and the settings
prefix:
| Path | Contents |
|---|---|
src/MetricsExtension.php | Extension::register(): binds the settings and registers the service |
src/MetricsProperties.php | #[ConfigProperties('metrics')] settings with their defaults (metrics.enabled) |
src/MetricsService.php | A component applications inject |
tests/ | Boots ExEngine with the extension and checks the settings and the service (vendor/bin/phpunit) |
composer.json | Requires linkfast/exengine; extra.exengine.extension names the extension class |
README.md, CHANGELOG.md, LICENSE | Ready to publish (MIT) |
To try it in an application before publishing, add the folder as a Composer path repository there and run
exen extension:add acme/exengine-metrics:@dev (the generated README shows the commands).
exen serve
exen serve [--host=127.0.0.1] [--port=8080] [--profile=prod] [--rr | --builtin]
Runs the server the project is made for: RoadRunner when composer.json says so (extra.exengine.server:
roadrunner, set by exen new), when the project has AsyncAPI consumers, or when it has worker.php and
.rr.yaml with the RoadRunner extension installed; PHP's built-in server otherwise. --rr and --builtin choose
one explicitly. --profile sets EXENGINE_PROFILES for the application.
PHP's built-in server serves the public/ folder; code changes apply on the next request. Existing files of
public/ are served as they are and every other URL goes to the launcher, so both /Hello/world and
/api.php/Hello/world work.
RoadRunner
RoadRunner runs ExEngine in long-running PHP workers: the application boots once and each worker answers many requests. Set a project up with:
exen extension:add roadrunner # the worker (needs PHP 8.2+ and ext-sockets)
exen rr:install # downloads rr 3.0.0 for this system (--version to choose)
exen rr:init # writes worker.php and .rr.yaml
exen serve --rr # http://127.0.0.1:8080/Hello/world
or create a new project with exen new orders-api --rr, which does all of it (and makes exen serve use
RoadRunner without --rr).
On RoadRunner, exen serve [--host] [--port] [--profile] [--workers=N] [--no-watch] runs rr serve with the
project's .rr.yaml, overriding for development the address, the PHP binary and the log format, and sizing the
worker pool with the roadrunner.workers, roadrunner.max-jobs and roadrunner.max-memory properties of the
active profiles (--workers wins; 2 workers when nothing is set). Because workers keep the application in
memory, exen watches src/, config/ and worker.php and resets the workers (rr reset) when a file changes;
--no-watch turns this off.
exen rr:init writes the same pool settings to .rr.yaml, taken from the prod profile (--profile to choose),
since rr serve uses that file in production; run exen rr:init --force again after changing them. Components
marked #[Service] live as long as their worker (see the
extension's README).
On RoadRunner, URLs work with and without the launcher (/api.php/Hello/world and /Hello/world), files in
public/ are served as static files and PHP files there are never executed. For production run rr serve with
your .rr.yaml (exen doctor checks the setup).
rr:install extracts the release archive with PHP's bundled phar support (no ext-zip needed) and adds the binary
to .gitignore.
Extensions
exen extension:add monolog # activated automatically
exen extension:add monolog --manual-activation # installed, activated by you
exen extension:add monolog --no-config # without adding its default settings
exen extension:add monolog:^1.0 --dev
exen extension:add acme/exengine-metrics # any other ExEngine extension, by Composer package
exen extension:list
exen extension:remove monolog
First-party extensions are installed by name, with the version constraint exen knows for them (a constraint after
: replaces it):
| Name | Package | |
|---|---|---|
monolog | linkfast/exengine-monolog | Monolog as the ExEngine logger |
roadrunner (rr) | linkfast/exengine-roadrunner | RoadRunner workers, with services that live as long as the worker |
Other packages are given by Composer name. exen first asks Composer for the package's type (in Packagist and the
repositories of the project's composer.json) and installs it only if it is an ExEngine extension; any other
library is refused with the composer require command to use instead.
ExEngine extensions are Composer packages of type exengine-extension. Like Spring Boot auto-configurations,
they are activated automatically: once installed, ExEngine discovers them at boot and there is nothing to
register.
With --manual-activation, exen installs the extension but adds its class to exengine.extensions.exclude in
config/application.* (the ExEngine equivalent of spring.autoconfigure.exclude), so it stays inactive until
you activate it in AppConfig:
$this->extension(\ExEngine\Monolog\MonologExtension::class);
Run extension:add again without the flag to switch back to automatic activation. extension:list shows the
activation of each extension (automatic, manual or discovery-disabled) for the active profiles and
environment, and extension:remove also removes the class from the exclude list.
exen edits application.json as data and application.yml as text, keeping comments; a YAML edit is only saved
when ext-yaml confirms the result (otherwise exen shows the change to make). application.php files are never
edited. A profile file with its own exengine.extensions.exclude replaces the base list when that profile is
active; exen warns about it.
Default settings. extension:add also adds the settings the extension declares, with their default values,
to config/application.yml (or .json), so they are visible and ready to edit:
# linkfast/exengine-monolog: default settings, edit them as needed
monolog:
enabled: true
stream: 'php://stderr'
level: debug
exen reads them from the extension itself, using the project's ExEngine: the values passed to
ExtensionContext::defaults() and the defaults of its #[ConfigProperties] classes (placeholders such as
${REDIS_URL} are kept as written). Only missing keys are added; values already in the file are never changed.
In YAML, a commented block is appended for each new section; if the section exists but lacks some keys, exen
shows them instead of editing. Settings without a default value are reported so you can set them. Use
--no-config to skip this step.
extension:remove only removes ExEngine extensions.
Project layout settings
exen reads the project layout from composer.json:
"extra": {
"exengine": {
"launcher": "public/api.php",
"controllers": "src/Controllers",
"namespace": "App\\Controllers",
"source": "src",
"source-namespace": "App",
"config": "config"
}
}
Those are the defaults; adjust them for existing projects with a different layout.
Environment variables
| Variable | Description |
|---|---|
EXEN_COMPOSER | Path to composer, composer.phar or a PHP script to use instead of the one on PATH |
EXEN_RR_BINARY | Path to an rr binary to use instead of the project's (e.g. one installed system-wide) |
EXEN_RR_DOWNLOAD_URL | Base URL (or folder) of RoadRunner releases for rr:install, e.g. a mirror |
EXEN_NO_DELEGATION | Set to always run the global exen, even inside a project with its own copy |
NO_COLOR | Disable colored output |
Development
Tests need PHP 8.4+ (PHPUnit 13) and use the ExEngine core as a dev dependency. To test against a local
checkout of the core without changing composer.json, use a local Composer file (ignored by git):
php -r '$c = json_decode(file_get_contents("composer.json"), true); $c["repositories"] = [["type" => "path", "url" => "../exengine", "options" => ["versions" => ["linkfast/exengine" => "1.3.0"]]]]; file_put_contents("composer.local.json", json_encode($c, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));'
COMPOSER=composer.local.json composer install
php vendor/bin/phpunit
The CI tests against the core branch or tag in EXENGINE_REF (default master); leave it empty to use the core
published on Packagist.
Releasing
- Update
CHANGELOG.md. - Create an annotated tag on
main, without a "v" prefix like the ExEngine core:git tag -a 1.0.0 -m "..." && git push origin 1.0.0. - Packagist publishes the tag (register the repository once at https://packagist.org/packages/submit and enable the GitLab integration).
License
MIT, see LICENSE.