Festi CLI
Festi CLI is the name of the command-line interface included with Festi Framework. It provides a number of helpful commands for your use while developing your application.
To use CLI tools, you need to install the festi-team/festi-framework-cli package:
{
"require": {
"festi-team/festi-framework-cli": "dev-develop"
}
}
- Installer
- DSG Converter
- URL Router
- Plugin Manager
- Database Migration
- Database Migration Creator
- React Manager
- Locale Manager
Getting Help
Every command answers --help (or -h) with its usage, options and examples, and ends with the list of all
festi-* commands. Help works from any directory: it never prompts, reads config.php or connects to a database.
./vendor/bin/festi-migrate --help
./vendor/bin/festi-plugin -h
For a command with modes (festi-plugin, festi-react, festi-locale), add --mode to see that mode's options:
./vendor/bin/festi-plugin --help --mode exec
Verbose Output
Every command also accepts --verbose (or -v). A normal run shows only the progress messages; a verbose run adds
debug lines: the shell commands run and their output, each SQL statement that changes the database, the project config
and database driver used, the files written, the migrations skipped as already applied, and the stack trace when the
command fails. Credentials are never printed -- they reach database clients through the environment, not the command.
./vendor/bin/festi-migrate --path ./src/admin/ -v
./vendor/bin/festi-install --verbose
--verbose=n (also false, 0, no) keeps it off, which is how the installer's --verbose y|n keeps working.
The SQL shown is what the tool ran through PHP: INSERT, UPDATE, DELETE and schema statements. Reads are not
shown, and neither is a migration file that festi-migrate hands to the mysql or psql client.
Saving SQL
A table created with festi-dgs --type table, or a plugin or URL registered with festi-plugin, exists only in your
own database. --schema saves the SQL of the run as a migration, so festi-migrate makes the same change everywhere
else. festi-dgs, festi-dgs-exec and festi-plugin accept it.
./vendor/bin/festi-dgs --type table --name active_users --definition-path ./tblDefs/ --schema
./vendor/bin/festi-plugin --mode create --name Expenses --schema=./dumps/develop.sql
--schemaalone creates<path>/dumps/updates<timestamp>.sql, named the same way as byfesti-make-migration. The migration is recorded as applied to your database, because its SQL already ran there.--schema=<file>appends the SQL to that file and keeps what the file holds. Write the value with=:--schema <file>is read as--schemaalone.festi-migrateapplies only files namedupdates<number>.sql. A working file such asdevelop.sqlbecomes a migration withfesti-make-migration --develop.- Do not append to a migration that is already applied: the databases that applied it never run what was added.
The tool warns when the file is one; use
--schemaalone for a new migration instead. - Nothing is saved when the run fails or changes nothing.
- The saved SQL can be replayed on another database. A row that refers to another one finds it by what it says, not by the id your database gave it: the area of a URL rule is linked to the rule by its plugin, pattern and method.
--schemaneeds afesti-team/festi-framework-databasethat reports its statements (IQueryListeners). With an older one the command stops before it changes anything and asks you to update the package.
Installer
The Installer tool enables you to install the framework automatically.
If you use a passphrase, you must first add your password to the keychain using
/usr/bin/ssh-add -K.
Installation using Composer.
Follow these steps to install the framework:
- Create a
composer.jsonfile:
{
"name": "your_company/project_name",
"description": "Description",
"repositories": [
{
"type": "composer",
"url": "https://packages.festi.io/"
}
],
"require": {
"php": ">=8.1",
"festi-team/festi-framework-core": "dev-develop",
"festi-team/festi-framework-database": "dev-develop",
"festi-team/festi-framework-cli": "dev-develop"
},
"minimum-stability": "dev",
"autoload": {
"psr-4": {
"": "src"
}
},
"autoload-dev": {
"psr-4": {
"": ["tests"]
}
},
"require-dev": {
"phpunit/phpunit": "9.*",
"phpunit/php-code-coverage": "9.*",
"phan/phan": "4.x",
"squizlabs/php_codesniffer": "3.*"
}
}
- Run the following command to install dependencies:
composer install
- Remove composer.lock
- Run the following command to install the essential components for your project:
php ./vendor/bin/festi-install
Installation Types
You can choose from several installation types:
| Type | Description |
|---|---|
dashboard |
installs the Jimbo system plugin, which is mainly for the administration panel (default). |
site |
installs the Jimbo system plugin and the Contents plugin. |
api |
installs the RESTful system plugin and sets up the framework to work as a RESTful API |
rpc |
installs the Rpc system plugin and sets up the framework to work as a RPC API. |
async |
installs the Festi Async Framework for creating asynchronous services for IoT, Game, Data Streaming, etc. |
An async project is a daemon started with php src/service/main.php <host> <port>.
It runs on the swoole PHP extension, the only event loop the async framework supports:
install the extension, or install the project with --docker y, whose image brings it.
The installer says so when the PHP it runs on has no swoole.
During installation, you will be prompted to install demo content. If you agree, basic settings and routing for login/registration will be created. In most projects, this is required, so it is recommended to agree.
Writing your own installation type
InstallerTool resolves a type by looking for ucfirst($installType)."Installer"
in the global namespace, so class MyThingInstaller extends Installer plus
--install_type mything is a supported way to add one.
Only these hooks are part of the contract. Override what you need and leave the rest alone:
| Hook | Purpose |
|---|---|
getDefaultThemeName() |
Theme to check out, or null for none. |
getSystemPluginScm() |
Repository of the system plugin. |
getExternalPlugins() |
Extra plugins, as name => repository. |
getDatabaseSchemeFiles() |
Schema dumps to build the migration from. |
getExternalInstallDatabaseSchemePath() |
An extra dump outside the plugin. |
getInstallFilesPaths() |
Directories to copy into the project. |
hasDemoContent() / getDemoPath() |
Whether there is demo content, and where. |
migration() |
How the database is migrated at the end. |
onPrepareOptions() / onStart() |
The options asked for, and the flow itself. |
Everything else protected on Installer is internal. The install mechanics live
in collaborators — InstallPaths, ProjectDirectory, SubmoduleRepository,
ComposerRunner, MigrationScriptWriter and DatabaseCredentials — and are
reached through the installer rather than inherited from it.
Methods that used to be on Installer and moved into those collaborators are
still available through the DeprecatedInstallerApi trait, so an existing custom
installer keeps working. They are deprecated and will be removed in 8.0.
Two are already gone rather than deprecated: createConnection() and
applyDatabaseScheme(), which had no callers anywhere and were the only reason
the installer referenced PDO.
Host Configuration
To properly configure the domain, it should either:
- Point directly to
[PROJECT_NAME]/src/[dashboard|api|site|service|rpc], or - Be set as a symbolic link to the appropriate directory.
Creating a Symbolic Link
Use the following command to create a symbolic link for the domain:
ln -s /path/to/your_project/src/dashboard /home/[USER_NAME]/my-domain.siter.festi.dev
or in the home directory:
ln -s your_project/src/dashboard my-domain.siter.festi.dev
Example Directory Structure
If your virtual host is configured to point to /home/[USER_NAME]/my-domain.siter.festi.dev, the directory structure should look like this:
/home/[USER_NAME]/
│── your_project/
│ ├── dumps/
│ ├── src/
│ │ ├── dashboard/
│ │ ├── api/
│ │ ├── site/
│ │ ├── service/
│ │ ├── rpc/
│ ├── ...
│── my-domain.siter.festi.dev -> your_project/src/dashboard
Install via console with options
Use the following command to install the framework via the console with options:
php vendor/bin/festi-install --db_username [USERNAME] --db_password [PASSWORD] --db_name [NAME] --install_path [PATH]
Here are some available options:
| Option | Description |
|---|---|
--docker |
set to y to install with generate docker files. |
--force |
y to reinstall over an existing project (see Reinstalling with --force). |
--install_path |
path to folder where you do want to install a project. |
--install_type |
this option determines the type of installation to perform. |
--db_host |
the hostname of the database server. |
--db_username |
the username to use when connecting to the database. |
--db_password |
the password to use when connecting to the database. |
--db_name |
the name of the database to use. |
--db_type |
the type of database to use, such as mysql or pgsql. |
--db_port |
the port number to use when connecting to the database. |
--timezone |
the timezone to use for the PHP installation. |
--is_install_demo |
set to y to install basic settings and routing for login/registration . |
--site_host |
the hostname of the site. |
--dashboard_base_http |
the base URL of the dashboard. |
--scm_host |
the hostname of the SCM repository. |
--verbose, -v |
print every command the installer runs and its output (see Verbose Output). |
--docker_php_version |
the version of PHP to use when installing with Docker. |
--docker_server |
the server to use when installing with Docker. |
--docker_swoole_version |
the version of Swoole to use when installing with Docker. |
--docker_service_host |
the service host for async type of project when installing with Docker. |
--docker_service_port |
the service port for async type of project when installing with Docker. |
Reinstalling with --force
Without --force the installer refuses a folder that already holds a project (Instance have installed already.).
With --force y (or true; n keeps it off) it first removes the previous installation, then installs from scratch:
- Removed from the folder: only what the installer creates --
.git,vendor,src,plugins,docker,dumps,composer.json,composer.lock,.gitmodules,.gitignore. Anything else you keep there survives. - Dropped from the database: every table (and, on PostgreSQL and MSSQL, the views, enum types and constraints)
of the configured database, before the migrations run. This cannot be undone; point
--db_nameat the right database. A Docker install is not affected, since its database container starts empty. - Refused: a folder that does not exist,
/, and your home directory -- nothing is removed there.
Add -v to see each removed item and the tables dropped.
Docker files
With --docker y the project gets a docker/ folder; start it with docker compose up --build from there. The PHP
container runs as www-data, not root. So that the mounted project stays writable on a Linux host, www-data takes
your user's UID and GID when the image is built, 1000 by default. If yours differ, export them first:
export UID GID=$(id -g)
docker compose up --build
Apache listens on port 8080 inside the container, and the site is served on http://localhost:8000.
The compose project is named after the project folder, so two generated projects can run on one machine. Give the
second one other host ports, in the shell or in docker/.env:
FESTI_HTTP_PORT=8001 FESTI_DB_PORT=3307 docker compose up --build
The database password is not written to docker-compose.yml, which is committed with the project. The installer
puts it into docker/.env as FESTI_DB_PASSWORD, and the generated .gitignore ignores that file, so a
colleague who clones the project creates it once.
FESTI_DB_PORT is the port the database is reachable on from the host; it defaults to --db_port. The database
image has a fixed version (mysql:8.4, postgres:12). session.cookie_secure is off in docker/php/php.ini,
because the project is served over plain http there; switch it on when the project is behind HTTPS.
Files the installer adds
Next to what the system plugin brings, the installer writes:
.gitignorein the project root, which keepsvendor/,logs/,node_modules/,src/*/local.php(it holds the database password) and.envfiles out of the repository;- for a dashboard or a site,
static/css/styles.css,static/css/print.cssandstatic/images/favicon.ico. The theme links them on every page, and they are the place for the project's own styles.
A file the project already has is not replaced.
Override parameters vie environment variables
You can override parameters using environment variables. For example, you can set:
MYSQL_PATH - path to mysql client
export MYSQL_PATH=/usr/local/opt/mysql-client/bin/mysql
DSG Converter
Enables conversion of a database table to DGS (XML/Array (JSON)) and vice versa.
Usage
./vendor/bin/festi-dgs
Just answer the questions and get the result.
If you need to run with a different version of PHP:
/opt/php80/bin/php ./vendor/bin/festi-dgs
You can also launch it by specifying options:
./vendor/bin/festi-dgs --name users --path [PATH_TO_PROJECT] --definition-path [PATH_TO_DEFINITION_DIR] --type dgs --format xml
This will give you a dgs file [PATH_TO_DEFINITION_DIR]/users.xml created from the table with the name users.
./vendor/bin/festi-dgs --name active_users --definition-path [PATH_TO_DEFINITION_DIR] --type table
This will create a table with the name specified in the name option, using the DGS definition file [PATH_TO_DEFINITION_DIR]/active_users.xml to obtain the schema of the table.
Options Summary
| Option | Description |
|---|---|
name |
table name |
definition-path |
path to create or retrieve DGS definition file |
path |
path to the project root (optional parameter, if not specified, the file will be searched for in the project root). |
config |
path to the project config.php (optional parameter, if not specified, the file will be searched for in the project root). |
type |
what to generate:
|
format |
output file format (xml/array) (optional parameter, defaults to xml). Only works when using dgs type. |
primary-key |
column name for default primary key. Default value is id. |
schema |
save the SQL of the run as a migration, or append it to a file with --schema=<file>. See Saving SQL. |
You can specify either an absolute path (e.g., /home/user/project) or a path relative to the project root (e.g., path/to/project).
DGS Action Executor
Executes a DGS action (insert, edit, remove, list, or any custom action) from the command line against a live Festi project.
Usage
./vendor/bin/festi-dgs-exec
Just answer the questions and get the result as JSON.
You can also launch it by specifying options:
./vendor/bin/festi-dgs-exec --plugin Pages --dgs pages --action list --path [PATH_TO_PROJECT]
Insert a row:
./vendor/bin/festi-dgs-exec --plugin Pages --dgs pages --action insert --values '{"title":"Home","url":"/"}' --path [PATH_TO_PROJECT]
Use a custom system plugin (e.g. when the DGS preamble calls Core::getSystemPlugin()):
./vendor/bin/festi-dgs-exec --plugin Pages --dgs pages --action list --system-plugin MySystemPlugin --path [PATH_TO_PROJECT]
Options Summary
| Option | Description |
|---|---|
plugin |
Plugin name that owns the DGS (required) |
dgs |
DGS identifier — XML filename without .xml inside the plugin's tblDefs/ directory (required) |
action |
Action to execute: insert, edit, remove, list, info, or any custom action defined in XML (required) |
values |
JSON object of field values, e.g. '{"name":"Example"}' (optional, defaults to {}) |
format |
Output format: json (default) or html |
system-plugin |
Plugin to register as the system plugin (optional, defaults to Jimbo). Required when the DGS has PHP preambles that call Core::getSystemPlugin(). |
path |
Path to the project root (optional, defaults to current working directory) |
config |
Path to the project config.php (optional, defaults to [path]/config.php) |
schema |
Save the SQL of the run as a migration, or append it to a file with --schema=<file>. See Saving SQL. |
URL Router
Tells you which plugin method a URL of the project is routed to, so you can open the code behind a page without
reading the festi_url_rules table. It only reads the rules and the code: no request is sent to the method.
Usage
./vendor/bin/festi-route "/test13/"
TestPlugin::onDisplayTest:125
The answer is the class (or trait) that declares the method, the method and the line it starts on. The URL can be pasted from the browser as it is: the host and the query string are ignored.
./vendor/bin/festi-route "https://app.example.com/report/42/?page=2" --area backend --path [PATH_TO_PROJECT]
Add --verbose to see the area, the pattern of the rule and the file:
./vendor/bin/festi-route /test13/ -v
- The URL is compared the way the framework compares it, so
/test13does not match a rule written for/test13/. The command says so when the slash at the end is all that is missing. - Rules are kept per area. Without
--areaevery area of the project is searched. - When several rules match the URL, each is printed with its area and pattern, and the first one the system plugin
compares in an area is marked
used: that is the rule this installation routes the URL by. Another database can return two database rules in a different order. - A project served under a path (
http_base, e.g./app/) usually sets it in itsindex.php, which the command does not run. Give it with--http-base /app/and it is cut off the URL the way the framework does. - A rule whose plugin or method no longer exists is printed as
TestPlugin::onDisplayTest (method not found). - The rules are the ones the system plugin routes a request with: the rules in the database, the ones the plugin
adds itself (
/festi/<store>/resolves to itsonDisplayMain) and$GLOBALS['urlRules']. A rule that a plugin adds while a request is handled, through an event listener, is not listed. - With a
festi-framework-corethat hasIUrlRuleFinder, the system plugin itself is asked which rules match, so the answer is the comparison a request is routed by. With an older core the command compares the rules itself, and the default rules of a system plugin built onApiPlugin(/rpc/<plugin>/<method>/) are not found. - An option the command does not have, or a second URL, is an error.
--are backendis not read as the URLbackend.
Options Summary
| Option | Description |
|---|---|
url |
The URL, when it is not given as the argument (asked for when missing) |
area |
Look only in this area, e.g. backend (optional, defaults to every area of the project) |
system-plugin |
Plugin registered as the system plugin (optional, defaults to Jimbo) |
http-base |
Path the project is served under (optional, defaults to the project's http_base) |
path |
Path to the project root (optional, defaults to current working directory) |
config |
Path to the project config.php (optional, defaults to [path]/config.php) |
Plugin Manager
The Plugin Manager allows creating plugins, their methods (including routing) and their data access code, and setting up a plugin that lives in a repository of its own.
Modes
| Mode | Description |
|---|---|
create |
Creates a folder for the plugin and everything necessary for it. |
url |
Adds a method to the plugin and entries in the festi_url_rules and festi_url_rules2areas tables. |
dgs |
Creates DGS, adds a method for displaying DGS to the plugin and entries in the festi_url_rules and festi_url_rules2areas tables. |
exec |
Invokes a public method on a plugin instance from the command line, with optional JSON-encoded arguments. |
dao |
Adds search, get, add, change and remove methods for tables to the plugin's DataAccessObject, and optionally a values object. |
install |
Adds a plugin from its own git repository as a submodule and applies the SQL of its install folder. |
update |
Applies the new SQL a newer version of such a plugin brought. |
Options Summary
| Option | Description |
|---|---|
mode |
mode |
name |
method's name or table/XML name |
plugin |
plugin's name |
config |
path to the project config.php (optional parameter, if not specified, the file will be searched for in the project root). |
pattern |
RegExp expression for URL |
type |
method type (default, json, ajax) |
area |
the name of the route work area (pulled from festi_url_areas) |
method |
(exec mode) public method on the target plugin to invoke |
args |
(exec mode) JSON list or object of arguments; list spreads positionally, object binds as named arguments |
tables |
(dao mode) table names separated by commas; the first is the main table |
values_object |
(dao mode) y to also create a values object for the main table |
entity |
(dao mode) name of the main entity, used for the values object class |
repository |
(install mode) git repository of the plugin |
dump |
(install and update modes) dumps directory of the project (optional, defaults to [path]/dumps) |
schema |
save the SQL of the run as a migration, or append it to a file with --schema=<file>. See Saving SQL. |
create mode
In this mode, a folder will be created for the plugin and everything necessary for it:
plugins/[PLUGIN_NAME]/[PLUGIN_NAME]Plugin.phpplugins/[PLUGIN_NAME]/[PLUGIN_NAME]Object.phpplugins/[PLUGIN_NAME]/init.phpplugins/[PLUGIN_NAME]/templates/plugins/[PLUGIN_NAME]/tblDefs/
Examples
./vendor/bin/festi-plugin
- Enter the mode name - create
- Enter the desired plugin name - Expenses
As a result, the following files will be obtained:
plugins/Expenses/ExpensesPlugin.phpplugins/Expenses/ExpensesObject.phpplugins/Expenses/init.phpplugins/Expenses/templates/plugins/Expenses/tblDefs/
Settings can also be specified in launch parameters:
./vendor/bin/festi-plugin --mode create --name Expenses
In addition, the corresponding entry will be added to the festi_plugins table.
url mode
In this mode, will be created DGS, a method will be added to the plugin and entries in the festi_url_rules and festi_url_rules2areas tables.
Method types:
- default - onDisplay[Name]
- ajax - onAjax[Name]
- json - onJson[Name]
Examples
./vendor/bin/festi-plugin
- Enter the mode name - url
- Enter the plugin name - Incomes
- Enter the URL rule - ~^/income/([0-9]+)/$~
- Enter the desired method name - Income
- Enter the route work area - backend
- Enter the method type - default
As a result, the plugin will receive a method:
/**
* @urlRule ~^/income/([0-9]+)/$~
* @section none
* @area backend
* @userType user
*/
public function onDisplayIncome(Response &$response)
{
// code ...
return true;
}
Parameters can also be specified in the launch arguments:
./vendor/bin/festi-plugin --mode url --plugin Incomes --pattern "~^/income/([0-9]+)/$~" --name Income --area backend --type default
dgs mode
In this mode, a method for displaying DGS will be added to the plugin and entries in the festi_url_rules and festi_url_rules2areas tables.
If the XML file is not available, it will be automatically generated from the database table using the DSG Converter.
Examples
./vendor/bin/festi-plugin
- Enter the mode name - dgs
- Enter the table (DGS) name - users
- Enter the plugin name - Users
- Enter the desired URL - ~/admin/users/~
As a result, we will get a file:
- plugins/Users/tblDefs/user.xml
And in the plugin, we will have a method:
/**
* @urlRule ~/admin/users/~
* @section none
* @area backend
* @userType user
*/
public function onDisplayUsers(Response &$response)
{
$store = $this->createStoreInstance("users");
$store->onRequest($response);
return true;
}
exec mode
Invokes a single public method on a plugin instance from the command line. Use this whenever you'd otherwise add a one-off bin script just to call $plugin->doSomething(...) — for example, batch jobs, ad-hoc maintenance, or driving plugin features from CI without writing per-task scaffolding. The mode bootstraps the full Festi runtime, resolves the plugin via Core::getPluginInstance(), runs the method, and prints the return value.
The dispatch guard uses is_callable([$plugin, $method]), which respects calling-scope visibility — only the plugin's genuine public API is reachable. Private, protected, and inherited framework lifecycle methods (e.g. onInitRequest, signinByToken) are rejected with a precise error.
Output marshaling
nullreturn → no output (silent success).- Scalar return (string, int, bool, float) → printed raw.
- Array or object return → printed JSON-encoded with
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE.
Argument forms (--args)
--args accepts a JSON value:
- Omitted or
[]→ no-argument call:$plugin->method(). - JSON list (e.g.
[2, 5]) → spread positionally:$plugin->method(2, 5). - JSON object (e.g.
{"format":"webp","quality":85}) → spread as PHP 8.1+ named arguments, bound by parameter name (order-independent):$plugin->method(format: 'webp', quality: 85).
Examples
Run a no-arg method:
./vendor/bin/festi-plugin --mode exec --plugin Contents --method bundleAssets
Pass positional arguments:
./vendor/bin/festi-plugin --mode exec --plugin Reports --method generate --args '[2026,3]'
Pass named arguments — order-independent:
./vendor/bin/festi-plugin --mode exec --plugin Images --method optimize --args '{"quality":85,"format":"webp"}'
Options
| Option | Description |
|---|---|
mode |
exec |
plugin |
Plugin name (e.g. Contents, Reports). Resolved via Core::getPluginInstance(). Required (prompted if missing). |
method |
Public method on the plugin. Required (prompted if missing). |
args |
JSON list or object of arguments. Optional; defaults to no arguments. |
path |
Project path. Defaults to the current working directory. |
config |
Path to a custom config.php. Defaults to [path]/config.php. |
dao mode
Writes the data access code of a plugin for the tables it owns. The tables must already exist in the project database.
./vendor/bin/festi-plugin --mode dao --plugin Shop --tables orders,orders_items --values_object y --entity Order
The first table is the main one. Its methods have the short names; the methods of every other table carry the table name:
| Table | Methods added to ShopObject |
|---|---|
orders (main) |
search(), get(), add(), change(), remove() |
orders_items |
searchOrdersItems(), getOrdersItems(), addOrdersItems(), changeOrdersItems(), removeOrdersItems() |
public function search(array $search = [], array $orderBy = []): array
{
$sql = "SELECT * FROM orders";
return $this->select($sql, $search, $orderBy);
} // end search
public function add(array $values): int
{
return (int) $this->insert('orders', $values);
} // end add
A method the class already has is skipped, so the mode can be run again when the plugin gets another table.
add() returns the identifier the database assigned to the new row, so the table needs an auto-increment key (AUTO_INCREMENT, SERIAL or IDENTITY). On PostgreSQL add() fails for a table without one.
Values object
With --values_object y the mode also creates Domain/Model/<Entity>ValuesObject.php in the plugin folder, in the namespace Plugin\<Plugin>\Domain\Model, and imports it in the DataAccessObject. The class has one FIELD_* constant, one typed getter and one setter per column of the main table; nullable columns get nullable types. An entity id is named the Festi way: id gives getID(), id_user gives FIELD_USER_ID and getUserID().
The class is found by the project's class loader. A project that loads its plugins from a Composer class map needs composer dump-autoload after the file is created. search() and get() of the main table then return values objects instead of arrays (get() returns null when no row matches).
namespace Plugin\Shop\Domain\Model;
class OrderValuesObject extends ValuesObject
{
public const FIELD_ID = 'id';
public const FIELD_CUSTOMER_ID = 'id_customer';
/**
* @return int
*/
public function getID(): int
{
return $this->get(static::FIELD_ID);
} // end getID
/**
* @param int|null $value
*
* @return void
*/
public function setCustomerID(?int $value): void
{
$this->set(static::FIELD_CUSTOMER_ID, $value);
} // end setCustomerID
// ...
}
The entity name defaults to the singular of the main table (orders gives Order, orders_items gives OrderItem). An existing values object is never overwritten: it is meant to be edited by hand afterwards, for domain names of getters and for enum constants.
Options
| Option | Description |
|---|---|
mode |
dao |
plugin |
Plugin name. Required (prompted if missing). |
tables |
Table names separated by commas, the main table first. Required (prompted if missing). |
values_object |
y or n. Prompted if missing; the default answer is n. |
entity |
Name of the main entity. Prompted when a values object is wanted; defaults to the singular of the main table. |
path |
Project path. Defaults to the current working directory. |
config |
Path to a custom config.php. Defaults to [path]/config.php. |
install and update modes: a plugin with its own repository
A plugin that lives in a repository of its own, such as Contents, is set up in an existing project with one command:
./vendor/bin/festi-plugin --mode install --path ./src/dashboard/ --dump ./dumps/ \
--plugin Contents --repository [email protected]:FestiPlugins/PHP_Festi_Plugin_Contents.git
It does three things:
- Adds the repository to the plugins directory as a git submodule, the way
festi-installadds plugins. The project has to be a git repository. - Copies the plugin's
install/install.<database type>.sqlinto the dumps directory as a newupdates<timestamp>.sql. A plugin whose SQL does not add it tofesti_pluginsgets that row in the same migration, and a plugin without any SQL gets a migration with only that row. - Applies the migrations of the project that the database does not have yet, the new one last, as
festi-migratedoes.
The SQL goes through a migration of the project on purpose: festi-migrate reads only the dumps directory, so this is
how every other database of the project gets the plugin. Commit the submodule and the new migration together.
--plugin is the name of the plugin class without Plugin; the repository has to contain <name>Plugin.php. When
anything fails, the plugin folder and the migration are removed again. A plugin that has SQL for other databases but
not for the one of the project is refused.
A plugin keeps its later database changes in one growing file, install/update.<database type>.sql. After you take a
newer version of the plugin, apply what it brought:
git submodule update --remote src/dashboard/plugins/Contents
./vendor/bin/festi-plugin --mode update --path ./src/dashboard/ --dump ./dumps/ --plugin Contents
When the update file is not the one the project took last, the whole file becomes a new migration and is applied. When it is the same, the command says that there are no new updates and writes nothing. It knows this from a comment at the top of each migration it writes, so do not remove that line:
-- festi-plugin: Contents install/update.mysql.sql sha256=ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb
The whole update file runs again, not only its new part, so it has to be written to be safe on a database that
already has the older part. See Idempotency Rules. For a plugin that was set
up by festi-install or by hand, the first update takes the whole file and says so.
--schema is refused in these two modes: their SQL is already saved as a migration.
Connecting to the database
By default, the config.php file located in the project root is used to establish a database connection.
If you need to use a different file, you need to run the script with the config parameter:
./vendor/bin/festi-plugin --config="cron/config.php"
Database Migration
To migrate the database, follow these steps:
- Create a folder for dumps in the root of the project, for example,
dump:
/dump/init.sql
/dump/updates1.sql
/dump/updates2.sql
/dump/updatesN.sql
- Use the
./vendor/bin/festi-migratecommand:
./vendor/bin/festi-migrate
If the config.php file is not located in the root directory of the repository:
./vendor/bin/festi-migrate --path ./src/admin/
./vendor/bin/festi-migrate --path ./src/admin/ --dump ./dump/
./vendor/bin/festi-migrate --path ./src/admin/ --dump ./dump/ --backup ./backups/
If you need to update the crontab, create a file in the following format:
# CRONTAB
*/5 * * * * %PHP% %PROJECT_PATH%/crons/email_worker.php --id 2
*/5 * * * * %PHP% %PROJECT_PATH%/plugins/Queues/queues.php --id 3 --name Balance
*/5 * * * * %PHP% %PROJECT_PATH%/plugins/Queues/queues.php --id 4 --name Tasks
Then, run the migration with parameters for the crontab:
./vendor/bin/festi-migrate --crontab ./dump/crontab.txt --crontab-user user
You can also update the version of the project's static content (festi_settings->js_version) during the migration by using the --up-static-version option.
See Database Migrations for the full guide to authoring migration files.
Parameters in Migrations
A migration can take a value that differs between installations, or that must not be committed, from config.php
in the dumps directory. The migration names the value as %%name%%:
INSERT INTO ticketing_systems (caption, redirect_uri, client_id, client_secret)
VALUES ('Discord', 'https://%%host%%/callback/discord/', '%%discord_client_id%%', '%%discord_client_secret%%');
<?php
// dumps/config.php
return [
'host' => 'app.example.com',
'discord_client_id' => getenv('DISCORD_CLIENT_ID'),
'discord_client_secret' => getenv('DISCORD_CLIENT_SECRET'),
];
- The token has two percent signs on each side, so
LIKE '%name%'in a migration is left alone. - The value is inserted as it is. The migration writes the quotes around it, and a value with a quote in it has
to be escaped in
config.php. - A
%%name%%thatconfig.phpdoes not define stopsfesti-migratebefore the migration is applied, and so does a migration with parameters in a dumps directory without aconfig.php. Nothing is recorded as applied. - A migration without parameters needs no
config.phpand is applied as before. config.phpusually holds secrets: keep it out of git and commit adumps/config.dist.phpthat lists the keys.- While a migration with parameters is applied, its filled-in SQL exists as
updates<number>.rendered.<id>.tmpnext to it, so the dumps directory must be writable. The file is removed when the migration ends. --verboselists the names of the parameters a migration used. When a migration is applied through PHP rather than themysqlorpsqlclient, a verbose run also shows its statements with the values in them.- PHP migrations and
festi-mergedo not fill parameters in: a merged file keeps its%%name%%tokens.
Using PHP Scripts for Database Migration
Sometimes pure SQL is insufficient for writing migration queries, especially when data needs to be corrected in production. For such cases, you can create an updatesN.php file containing a class named UpdatesN inherited from core\util\MigrationUpdates:
use core\util\MigrationUpdates;
class Updates2 extends MigrationUpdates
{
protected function onStart(): void
{
$core = Core::getInstance();
$search = array(
'type' => 'catalog'
);
$sql = "SELECT * FROM contents";
$catalogs = $core->db->select($sql, $search);
$removeIDs = array();
foreach ($catalogs as $row) {
if (!empty($row['path']) && !file_exists($row['path'])) {
$removeIDs[] = $row['id'];
}
}
if ($removeIDs) {
$search = array(
'id&IN' => $removeIDs
);
$core->db->delete("contents", $removeIDs);
}
}
}
Docker
If you use database in docker, you should specify the container name:
./vendor/bin/festi-migrate --path ./src/admin/ --dump ./dump/ --backup ./backups/ --docker db_postgres
Also, you need add dumps folder as volume to container:
postgres:
container_name: db_postgres
image: postgres:16.0-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_DATABASE}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
volumes:
- db_data:/var/lib/postgresql/data:delegated
- ./dumps:/var/www/app/dumps:cached # <-- here
Database Migration Creator
To create a new database migration, use the command:
./vendor/bin/festi-make-migration
The file is created in the project's dumps/ folder and named
updates<unix timestamp>.sql, for example updates1789628617.sql. If the
dumps/ folder does not exist yet, it is created.
Do not rename the file: festi-migrate looks for migrations matching
updates<number>.<sql|php>, and a file with any other name is skipped without
a warning.
If the config.php file is not located in the root directory of the
repository:
./vendor/bin/festi-make-migration --path ./src/admin/
./vendor/bin/festi-make-migration --path ./src/admin/ --dump ./dumps/
An explicitly given --dump must already exist. Unlike the default path it is
not created, because a path that does not exist is almost always a typo, and a
migration written where festi-migrate never looks for it is worse than a
failed command.
The --php option is also available, to create a PHP migration:
./vendor/bin/festi-make-migration --dump ../../dumps/ --php
For convenience you can add a shortcut to composer.json:
{
"scripts": {
"make-migration": "./vendor/bin/festi-make-migration --dump ../../dumps"
}
}
composer make-migration
From a working file
While a feature is written, its SQL usually collects in one working file, for
example with --schema=./dumps/develop.sql (see Saving SQL).
festi-migrate does not apply a file with that name. Before the branch is
merged, turn it into the next migration:
./vendor/bin/festi-make-migration --develop
./vendor/bin/festi-make-migration --develop=./sql/feature.sql --dump ./dumps/
./vendor/bin/festi-make-migration --develop --not-applied
--developalone takesdevelop.sqlfrom the dumps folder.--develop=<file>takes another file; write the value with=. A file name after a space is refused, because it would not be read anddevelop.sqlwould be renamed.- The file is renamed to
updates<unix timestamp>.sqlin the dumps folder. Its content is not changed, and the working file no longer exists afterwards. - The migration is recorded as applied to your database, because its SQL
already ran there.
festi-migratethen runs it only on the other databases. Use--not-appliedfor a file you wrote by hand and never ran. - When your database has no
schema_migrationtable yet, nothing is recorded and the command says so: the nextfesti-migrateruns the file there. - When the record cannot be written, the file stays renamed and the error
gives the
INSERTstatement to run by hand. - A file that does not exist, is empty, is not
.sqlor is already namedupdates<number>.sqlis refused, and nothing is renamed.--developcannot be used together with--php.
The migration number is a Unix timestamp rather than a readable date because it
is stored in the schema_migration.version column, which is declared as a
32-bit int in all three supported databases. Ten digits fit there; the fourteen
of YmdHis do not. This stops working on 19 January 2038, and the fix then is
to widen the column to bigint in the three src/migrate/*MigrateAdapter.php
adapters.
Merge Migrations Files
The festi-merge tool simplifies the process of combining multiple migration files into fewer, larger files. This is particularly helpful for improving the speed of database migrations during CI/CD deployments and unit testing.
Usage
To merge migration files, use the following command:
./vendor/bin/festi-merge --path ../../dumps
Options
--path: (Required) The path to the folder containing migration files.--output: (Optional) The path where the merged files will be stored. If not specified, amergedfolder will be created in the input folder.
By merging migration files: - Faster CI/CD Deployments: Consolidated files reduce execution time for database migrations. - Optimized Testing: Accelerates database setup during unit tests. - Improved Development Workflow: Reduces overhead in managing large numbers of migration files.
Utils
CliUtils
Helper class that supports printing messages to the console with a color scheme.
Usage
$tableName = CliUtils::cin("Table Name:");
CliUtils::i("Information");
$type = CliUtils::cin("What do you want to create?", array("a", "b"), "b");
Sometimes you need to mock output/input for unit tests:
CliUtils::$outputCallback = function ($message) {
};
CliUtils::$inputCallback = function (string $label, ?array $enum = null, string $default = '') {
return 'test';
};
CliUtils::d() prints a debug line that is shown only in verbose mode. AbstractTool switches the mode on when a tool
is started with --verbose or -v; a test can do the same with CliUtils::setVerbose(true), and should switch it
back off in tearDown() because the mode is process-wide.
CliUtils::d('SQL: '.$sql); // shown only with --verbose
CliUtils::exception($exp); // message always, stack trace only with --verbose
FAQ
PHP Version Error
If you encounter PHP version errors such as:
PHP Fatal error: Composer detected issues in your platform: Your Composer dependencies require a PHP version ">= 8.0.2". You are running 5.4.16. in ...
There are two ways to resolve this:
- Update the PHP version on your server to meet the requirements.
- Use an environment variable to specify the path to the required PHP version:
export PHP_PATH="/php_path"
Unit Tests
Local
export DB_HOST=127.0.0.1
cd tests/
../vendor/bin/phpunit Tools/DgsConverterTest.php
Docker
cd tests/docker
make start
docker compose run -e DB_HOST=festi-cli-mysql-db php-cli /var/www/vendor/bin/phpunit -c /var/www/tests/phpunit.xml
The stack belongs to this project alone: the compose project is festi-cli and
the containers are festi-cli-app, festi-cli-mysql-db, festi-cli-postgres-db
and festi-cli-mssql-db, so it can run next to the test stack of another Festi
project. The databases are published on a free loopback port that Docker picks;
docker compose port mysql-db 3306 prints it if you want to connect from the host.
The tests install a whole Festi project, thousands of files. The compose file
sets FESTI_TEST_INSTALL_DIR=/tmp/festi-cli-install so that project lives on
the container's own filesystem rather than the macOS bind mount, where writing
and deleting it took tens of seconds per test. Without the variable it goes to
tests/Resources/install/, as it does in CI.
Access to private Festi packages
The Festi packages on packages.festi.io are published without dist archives,
so Composer clones them from gitlab.varteq.com. The test container reaches
them over HTTPS with a read-only token. No credential is built into the image.
- Create a GitLab personal access token with the
read_repositoryscope only (https://gitlab.varteq.com/-/user_settings/personal_access_tokens). - Store it in the macOS login Keychain, which survives a re-clone of the
checkout.
-wwith no value prompts for the token without echoing it:
The Makefile looks it up by both account and service, so the entry has to belong tosecurity add-generic-password -U -a "$USER" -s GITLAB_PAT -w$USERas written above. Three sources are consulted, in this order:
| Order | Source |
|---|---|
| 1 | the environment — GITLAB_PAT=... make start overrides for one run |
| 2 | tests/docker/.env, if you prefer a file |
| 3 | the macOS login Keychain |
Without any of the three, make start stops with an error naming the
chain rather than building an image that cannot clone.
3. Start the stack as usual:
make start
.env is optional: the database values default to the ones in
.env.example, so only the token has to be supplied.
The container entrypoint rewrites the [email protected]: remotes to
HTTPS using that token, so Composer and the installer can clone private
packages. To revoke access, delete the token in GitLab; nothing needs
rebuilding. If you would rather use your own SSH key, forward your agent into
the container instead and leave GITLAB_PAT unset.
5. Run test:
docker compose run -e DB_HOST=festi-cli-mysql-db php-cli /var/www/vendor/bin/phpunit -c /var/www/tests/phpunit.xml /var/www/tests/Tools/DgsConverterTest.php
React Manager
React Manager creates a React app for a Festi project. The app is a Vite project whose build is
written straight into the Festi project as main.js and main.css.
See ReactJS in a Plugin for the full procedure in a project: the directories, the page with the app, and the data for the app.
Modes
| Mode | Description |
|---|---|
create |
Creates the app and, on request, a page for it in a plugin. |
deploy |
Only for an app made with Create React App by an earlier version: copies its finished build. See below. |
Options Summary
| Option | Description |
|---|---|
mode |
create or deploy (asked for when missing) |
name |
the name of the app (asked for when missing) |
path |
the directory of the React app. Defaults to ./react/<name> for create and to the current directory for deploy |
plugin |
(create mode) the plugin that gets a page showing the app |
target |
(create mode) the folder of the project a build is written to. With plugin it defaults to <plugin>/static/<name>-static |
config |
(create mode) the project's config.php, needed to find the plugin. Defaults to ./config.php |
create mode
./vendor/bin/festi-react --mode create --name app --plugin Services --config ./src/dashboard/config.php
Run from the project root, this:
- Creates the app in
./react/app/:package.json,vite.config.js,index.htmland the starter source. - Adds a method to the plugin
Services(you are asked for its name, URL pattern, area and type, as infesti-plugin --mode url) and the templatetemplates/app.phtml, which loadsmain.cssandmain.js. - Points the build of the app at
plugins/Services/static/app-static/, the folder that page takes the files from.
Then install the dependencies and build:
cd react/app
npm install
npm run build
npm run watch builds again on every change. No file of the app holds a path of your machine: vite.config.js
names the build folder relative to itself, so the app can be committed and built by anyone.
Keep the app outside the directory the web server serves. It holds the source and node_modules; only the build
has to be reachable from a browser. ./react/<name> in the project root is such a place, and create warns when
--path is inside a served directory.
Without --plugin and --target you are asked whether to append the app to a plugin. To create only the app,
name the build folder yourself:
./vendor/bin/festi-react --mode create --name app --path ./react/app --target ./src/dashboard/static/app-static
The API of the project
src/rpc/request.js of the app sends JSON-RPC requests. It reads the address of the API and the token of the
signed-in user from the data-endpoint and data-access-token attributes of the root element. The method added to
the plugin assigns both to the template as empty strings; fill them in there.
deploy mode
This mode is for an app that an earlier version of the CLI created with Create React App. It does not build
anything: it copies the files of a finished build (<path>/build/static) to the folder ../<name> next to the app,
under names without the content hash (main.2cb2b1b3.js becomes main.js).
npm run build
./vendor/bin/festi-react --mode deploy --name app-static --path [PATH_TO_APP_DIR]
An app created with the create mode of this version does not need it: its build already goes to the right folder
under the stable names.
Locale Manager
Locale Manager is a command-line utility that simplifies the process of managing localization files for a project. It allows you to easily initiate a new po file and generate a mo file from it.
Modes
Locale Manager has two modes:
| Mode | Description |
|---|---|
init |
Use this mode to create a new po file for your project. The utility will scan your project files and create a new po file with all the translatable strings it finds. |
generate |
Use this mode to generate a mo file based on an existing po file. The utility will compile the po file into a mo file that can be used in your project. |
Options Summary
| Option | Description |
|---|---|
mode |
Specifies the mode that Locale Manager should run in (init or generate). |
path |
Specifies the path to the root of the project that Locale Manager should operate on. This is an optional parameter; if not specified, Locale Manager will search for the project in the current working directory. |
lang |
Specifies the language you want to create a mo file for. You can specify the language using its ISO 639-1 language code (e.g., en for English, fr for French). |
init mode
This mode creates a new po file for your project.
The utility will scan your project files with extention php, phtml, xml and create a new po file with all the translatable strings it finds.
Also it gets caption from festi_menues and append to po file.
Examples
To use this mode, run the following command:
./vendor/bin/festi-locale --mode=init --path=/path/where/create/project/
This command will initiate a new po file for your project at the specified path at locale folder.
Or you can run the following command:
./vendor/bin/festi-locale
This will prompt you for the following information:
- Enter the mode name -
init - Enter the full project path. If you don't specify a path, the utility will use the current directory -
/path/where/create/project/
generate mode
This mode generates a mo file based on an existing po file. The utility will compile the po file into a mo file that can be used in your project.
Examples
To use this mode, run the following command:
./vendor/bin/festi-locale --mode=generate --path=/path/where/create/project/ --lang=en
This command will generate a mo file for the English language in your project at the specified path at locale folder.
Or you can run the following command:
./vendor/bin/festi-locale
This will prompt you for the following information:
- Enter the mode name -
generate - Enter the full project path. If you don't specify a path, the utility will use the current directory -
/path/where/create/project/ - Enter the language -
en