Search by

linkfast / exengine-cli

gchiappe

exen: command line tool for the ExEngine microframework (new projects, dev server, generators, extensions).

Package info

gitlab.com/linkfast/oss/exengine-cli

Issues

pkg:composer/linkfast/exengine-cli

Statistics

Installs: 24

Dependents: 0

Suggesters: 0

Stars: 0

1.0.0-a3 2026-10-12 02:16 UTC

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 use config/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

CommandDescription
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 serveRun the development server the project is made for: RoadRunner or PHP's built-in server
exen rr:installDownload the RoadRunner server (rr) for this system into the project
exen rr:initCreate worker.php and .rr.yaml to run the project on RoadRunner
exen routesList the routes of the project controllers
exen doctorCheck 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:listList 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
restHTTP REST: controllers with automatic routing and JSON responses
openapiContract-first REST from an OpenAPI 3 document (code generation coming soon)
graphqlSchema-first GraphQL with linkfast/exengine-graphql (coming soon)
asyncapiAsynchronous message consumers on RoadRunner jobs (coming soon); adds RoadRunner to the servers

Servers (--server, comma-separated, or chosen from the list):

ServerGenerated files
builtinNothing: PHP's built-in server through exen serve, for development
apachepublic/.htaccess (mod_rewrite front controller)
nginxdeploy/nginx.conf (server block for PHP-FPM)
caddydeploy/Caddyfile (php_fastcgi to PHP-FPM)
frankenphpdeploy/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:

PathContents
public/api.phpLauncher; the web server document root is public/
src/AppConfig.phpRegistration of components, filters, listeners and properties classes
src/Controllers/Hello.phpSample controller: /Hello/world
config/application.ymlProperties, plus application-prod.yml for the prod profile
composer.jsonRequires 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:

PathContents
src/MetricsExtension.phpExtension::register(): binds the settings and registers the service
src/MetricsProperties.php#[ConfigProperties('metrics')] settings with their defaults (metrics.enabled)
src/MetricsService.phpA component applications inject
tests/Boots ExEngine with the extension and checks the settings and the service (vendor/bin/phpunit)
composer.jsonRequires linkfast/exengine; extra.exengine.extension names the extension class
README.md, CHANGELOG.md, LICENSEReady 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):

NamePackage
monologlinkfast/exengine-monologMonolog as the ExEngine logger
roadrunner (rr)linkfast/exengine-roadrunnerRoadRunner 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

VariableDescription
EXEN_COMPOSERPath to composer, composer.phar or a PHP script to use instead of the one on PATH
EXEN_RR_BINARYPath to an rr binary to use instead of the project's (e.g. one installed system-wide)
EXEN_RR_DOWNLOAD_URLBase URL (or folder) of RoadRunner releases for rr:install, e.g. a mirror
EXEN_NO_DELEGATIONSet to always run the global exen, even inside a project with its own copy
NO_COLORDisable 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

  1. Update CHANGELOG.md.
  2. 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.
  3. Packagist publishes the tag (register the repository once at https://packagist.org/packages/submit and enable the GitLab integration).

License

MIT, see LICENSE.