Command line application for Phalcon.
See the README for installation, usage and configuration.
Aliases are shown in brackets. Run crest with no arguments, or crest list,
for the same listing from the tool itself. In a project, the listing shows only
the commands for the flavor of the project. crest --help shows the help of list.
An unknown option with no command, or a crest.php that is not valid, stops the
listing with an error.
| Command | Description |
|---|---|
about (info, i) |
environment and version report |
config:show |
the project configuration crest resolved, and where each value came from |
container:list |
services registered in the project container |
down |
stop and remove the project containers |
event:list |
listeners attached to the project events manager |
init |
write crest.php for an existing project |
install |
install composer dependencies in the project container |
list (commands, enumerate) |
the available commands |
make:action |
create an ADR action for a route |
make:command |
create a crest command |
make:controller |
create an MVC controller |
make:middleware |
create an ADR middleware |
make:model |
create an MVC model from a database table, or from each table with --all (relations come only from foreign keys) |
make:module |
create an MVC module |
make:provider |
create a service provider |
make:responder |
create an ADR responder |
make:scaffold |
create MVC CRUD pages for a database table |
new |
create an ADR or MVC project |
route:list |
every route the application answers |
serve (server) |
start PHP's built-in web server for the project |
stub:publish |
copy packaged stubs into the project for editing |
up |
start the project containers |
make:command works in each flavor. make:provider works in ADR and MVC projects,
with a stub for each flavor. make:controller, make:model, make:module and
make:scaffold are for MVC projects only. These commands are for ADR projects only: make:action,
make:middleware, make:responder, route:list, container:list and
event:list. In a project of a different flavor, they stop with an error:
crest: make:responder does not support the 'mvc' flavor of this project; it supports: adr
new writes an ADR or an MVC project that runs:
crest new my-app
crest new my-app --flavor=mvc
An ADR project has public/index.php, the front controller src/AppFront.php,
crest.php, composer.json, the docker files, and src/Action/Get.php, which answers
GET /. For an MVC project, see "The MVC project".
| Option | Purpose |
|---|---|
--flavor=adr|mvc |
the flavor of the project; defaults to adr |
--template-engine=<engine> |
the view engine of an MVC project: phtml, the default and the only engine for now. An ADR project has no views, so new stops when you give it |
--namespace=<name> |
root namespace for the generated code; defaults to App |
--php=<major.minor> |
PHP version for composer.json and the Dockerfile; defaults to 8.4, and must be 8.1 or later |
--phalcon=v5|v6 |
v5 requires the C extension, 5.18 or later; v6 the phalcon/phalcon package; defaults to v5 |
--runtime=host|docker |
where the project commands run (the runtime key of crest.php); defaults to docker |
--service=<name> |
the docker compose service, for docker; defaults to app |
--database=none|mariadb|mysql|pgsql|sqlite |
the database of the project; defaults to none. See "The database" |
--camel-case |
for MVC: property names in camelCase (models.camelCase of crest.php). See "Names" |
--force |
write into a directory that is not empty, and overwrite files with the same names |
In a terminal, new asks for each value that no option gives: the flavor, the
namespace, the PHP version, the Phalcon version, the runtime (docker with the
service app, or host), and the database. For an MVC project with a database, it
also asks if the property names of the models are in camelCase. There is no question
for the view engine while there is only one. An empty answer takes the default.
--no-interaction, or a run without a terminal (CI, a pipe), asks nothing and takes
the defaults. The options stay, for CI.
The project goes into the working directory, or into --directory if you give
one. By default, new runs nothing: no composer, no docker, no network. It
prints the next steps:
cd my-app
./crest up docker compose up -d
./crest install composer install in the app container
./crest is a launcher script that new writes into the project. It needs docker
only: PHP on the host is not necessary. up, down and install run docker compose on
the host. All other commands run vendor/bin/crest in the service of the project (app,
or the --service of new). Before ./crest install, they stop with
crest: run './crest install' first. Without a terminal (CI, a pipe), the launcher
adds -T to docker compose exec. On Windows, run it in WSL2. With a global crest,
crest up and crest install do the same. With rootless docker, set UID=0 and GID=0
in .env: rootless docker maps root in the container to your user.
The crest commands of the launcher get CREST_HOST_ROOT, the host folder of the
project, so crest shows host paths, not the paths in the container. The hand-off of a
global crest does the same. The launcher runs vendor/bin/crest. If you change
vendor-dir or bin-dir in composer.json, change the crest= line at the top of
./crest.
crest down stops and removes the containers. up --build rebuilds the image
first, and down --volumes also removes the named volumes.
Without docker, run composer install, then crest serve. It runs
php -S 127.0.0.1:8080 -t public .htrouter.php in the project root, with the
same router and document root as the container. The port is --port, else
APP_PORT from the environment, else APP_PORT in the project .env, else
8080. docker compose reads APP_PORT in the same order. serve reads .env
as docker compose does: the last APP_PORT line wins, and export,
APP_PORT: <port>, quotes and # comments are allowed. It does not expand
${...}. Such a value stops serve with an error. serve finds the root
from a subdirectory. A global crest passes it to the crest in vendor/, as
it does the other project commands. It stops before PHP starts if
.htrouter.php or <vendor-dir>/autoload.php is missing.
The server uses the PHP that runs crest. PHP options on the command line, for
example -d extension=phalcon.so, do not reach it. Put such settings in
php.ini.
Press Ctrl+C to stop the server. If the server continues to run, for example after crest was stopped in a different way, stop its PHP process:
pkill -f -- '-S 127.0.0.1:8080'
Use your port if it is not 8080.
new, up, down and install run before the project has a vendor/. Run
them with a crest outside the project, for example one that you install with
composer global require phalcon/crest. On a host with docker and no PHP, run
up, down and install with ./crest.
On a host with docker and no PHP, one command creates a project, starts it and installs it. See "With docker only" in the README:
curl -fsSL https://phalcon.io/crest-setup | sh -s -- my-app
The generated project requires phalcon/crest as a dev dependency. The
commands that work on the project, for example make:action and
route:list, need the autoloader and the Phalcon of the project. After
crest install or composer install, the project has its own crest in
vendor/. A global crest passes these commands to it, with the same
arguments, and returns its exit status:
crest make:action GET /hello
In the container, there is no global crest. Run the crest of the project:
docker compose exec app vendor/bin/crest make:action GET /hello
On a host without PHP, ./crest does the same:
./crest make:action GET /hello
new, init, up, down, install and --version always run in the crest that you
type. crest finds the project by its crest.php: the file that --config names, or the
nearest crest.php above the working directory or --directory. up, down and
install run docker compose in that folder. Without crest.php, crest stops:
crest: no crest.php found; run 'crest init'
If the project has no <bin-dir>/crest (by default vendor/bin/crest), crest stops
too:
crest: /path/to/my-app has no vendor/bin/crest; run 'crest install' or 'composer install' first
crest: /path/to/my-app does not require phalcon/crest; run 'composer require --dev phalcon/crest'
With 'runtime' => ['type' => 'docker', 'service' => 'app'] in crest.php (what new
writes), a global crest runs the project commands in the container:
docker compose exec app vendor/bin/crest .... A v5 project then works on a host
without ext-phalcon. serve is the exception: it always runs on the host, because
PHP's built-in server must listen there. With docker, crest up serves the project.
The docker runtime has two conditions. The service must mount the project folder, with
vendor/ in it: crest looks for vendor/bin/crest on the host first. And the
working_dir of the service must be the project root: vendor/bin/crest runs there. The
compose file of new does both. install runs composer in the service of the docker
runtime. With the host runtime, it uses the service of the key, else app.
crest reads vendor-dir and bin-dir as composer does: COMPOSER_VENDOR_DIR and
COMPOSER_BIN_DIR, then config in composer.json, then vendor and
<vendor-dir>/bin. The hand-off runs <bin-dir>/crest, and serve checks
<vendor-dir>/autoload.php. With the docker runtime, composer runs in the container, so
crest reads only composer.json, and the two folders must be relative. crest does not
expand ~ or $VAR in these values: use a relative or an absolute path. A custom
vendor-dir or bin-dir needs this version of crest both as the global crest and in the
project: an older crest looks only for vendor/bin/crest.
The files come from the project-* stubs. To change them, publish them by
name in the directory that the project goes into (the working directory, or
--directory), then edit the copies:
crest stub:publish project-front
A project stub needs no crest.php. --flavor gives its flavor, as for new;
the default is adr:
crest stub:publish project-front --flavor=mvc
The project stubs that are the same in both flavors are in
resources/stubs/shared/, and a publish puts them there. A copy in
resources/stubs/<flavor>/ wins over the shared copy for that flavor, and a publish
of the shared copy prints a note that names it. Crest looks for a stub in the project
(resources/stubs/<flavor>/, then resources/stubs/shared/), then in the package, in
the same order. When the
flavor does not have the stub and another flavor does, the error names the
option to use, for example pass --flavor=mvc. A
publish with no name leaves the project stubs out, because they do nothing
inside a project. It also leaves the fragment stubs (fragment-*) out: they
hold pieces of the framework API and the guidance text that the generators
print, and a copy does not get the changes of later releases. Publish a
fragment by name to change it.
You can change each project stub on its own. The names that the stubs and
crest must agree on come from placeholders: {{ documentRoot }},
{{ router }}, {{ portVariable }}, {{ port }}, {{ front }},
{{ source }} and {{ autoloader }}. Keep these placeholders where the
packaged copy has them. Then serve, init and the generated files agree.
project-front, project-config and project-index must also agree with
each other on the namespace. new decides where each file goes: a stub
cannot move its file. A published copy must use only the placeholders of the
packaged copy. If a placeholder has no value, new stops before it writes a
file.
In an MVC project, {{ controllerNamespace }}, {{ controllerPath }},
{{ providerNamespace }}, {{ providerPath }} and {{ viewsPath }} come from
the default paths of the flavor. A stub that depends on the view engine has the
engine name in its name: project-phtml-provider, project-phtml-layout and
project-phtml-index.
crest new my-app --flavor=mvc writes the layout of the Phalcon sample
applications (vokuro, invo):
| Path | Contents |
|---|---|
public/index.php |
the web entry point, as in an ADR project |
src/AppFront.php |
the front controller: FactoryDefault, the providers and Phalcon\Mvc\Application |
src/Providers/ |
DispatcherProvider (the default namespace of the controllers), UrlProvider (the base URI of the links and the redirects) and ViewProvider (the views folder and the phtml engine) |
src/Controllers/ |
IndexController, which answers GET / |
src/Controllers/Base/ |
CrestControllerBase, the base that crest owns (see "Generated bases") |
themes/default/ |
the layout index.phtml and the view index/index.phtml |
crest.php has 'flavor' => 'mvc' and the paths controller, form, model,
module, provider and views. To add a service, run crest make:provider Mailer. It
writes src/Providers/MailerProvider.php, a provider that implements
Phalcon\Di\ServiceProviderInterface, and prints the line to add to
AppFront::providers().
A URL with no controller stops in the dispatcher ("handler class cannot be
loaded"). With display_errors off, PHP answers 500. With it on, PHP shows the
error in the page, with status 200. Add a not-found page if your project needs
one, for example a dispatcher listener such as the NotFoundPlugin of invo.
--database gives the project a database, in each flavor:
crest new my-app --flavor=mvc --database=mysql
| Choice | The service in docker-compose.yml |
The PHP extension |
|---|---|---|
none |
none; the default | none |
mariadb |
database, with the image mariadb:11.4 |
pdo_mysql |
mysql |
database, with the image mysql:8.4 |
pdo_mysql |
pgsql |
database, with the image postgres:18 |
pdo_pgsql |
sqlite |
none: the database is the file storage/database.sqlite |
pdo_sqlite, which the php image has |
With a database, new also writes:
-
DbProvider.phpin the provider folder (src/Providersfor MVC,src/Providerfor ADR). It registers thedbservice. The MVC front adds it inproviders(). The ADR front gets aregisterProviders()override that calls it. -
The connection values at the end of
.env:DB_HOST=database DB_PORT=3306 DB_NAME=my_app DB_USERNAME=my_app DB_PASSWORD=secret DB_PUBLISHED_PORT=3306DB_NAMEis the project name, with_in place of-, cut to 64 characters.DB_USERNAMEis the same name, cut to 32 characters. Forsqlite,.envhas onlyDB_NAME=storage/database.sqlite, relative to the project root. -
The service
databaseindocker-compose.yml, with a named volume for its data and a health check. The app service starts when the database accepts connections, socrest make:modelworks right aftercrest upandcrest install. The database listens on the host only at127.0.0.1, on the portDB_PUBLISHED_PORT: other computers cannot connect to it. -
The PDO extension in
resources/docker/Dockerfile. -
phalcon/phqlincomposer.json, for an MVC project with Phalcon v6 (see "Generating models"). -
For
sqlite: the empty filestorage/database.sqlite(an empty file is a valid SQLite database),storage/.gitkeep, and/storage/database.sqlitein.gitignore.--forcedoes not replace a database file that exists: git ignores the file, so nothing can restore its data.
docker compose gives the values of .env to the app container. PHP on the host does not
read .env: there, DbProvider uses its defaults, 127.0.0.1 and the published port.
DbProvider reads each value from getenv(), then from $_ENV, then takes the default.
A loader such as phpdotenv also reads .env on the host, and there DB_HOST=database is
not a known host. Then give PHP on the host DB_HOST=127.0.0.1 and DB_PORT (the
published port) in its environment: getenv() wins over $_ENV. To run the project
with PHP on the host, start only the database, and install the PDO extension on the
host:
docker compose up -d database
If the port is in use on the host, change DB_PUBLISHED_PORT in .env. Then PHP on the
host needs DB_PORT with the same value in its environment. The MySQL and MariaDB images
do not accept the user root, so a project with the name root needs another
DB_USERNAME.
The db service is shared, and it connects when the project uses it for the first time,
for example at the first query of a model. Thus the project answers GET / when the
database does not run, and a wrong value gives the connection error at the first use of
db.
With mysql, mariadb or pgsql, the app service cannot have the name database:
crest: the service 'database' is the name of the database service; choose another service name
In an ADR project with a database, the front already has registerProviders(). For a new
provider, add only the provide() line that crest make:provider prints.
The service and the .env lines come from the project stubs project-compose-mysql,
project-compose-mariadb, project-compose-pgsql, project-env-database and
project-env-sqlite. To change one, for example the image, publish it by name in the
directory that the projects go into:
crest stub:publish project-compose-mysql
A copy of a project stub that you published before the database option does not have the
new placeholders: {{ dependsOn }}, {{ databaseService }}, {{ databaseInstall }},
{{ databaseEnv }}, {{ databaseIgnore }}, {{ phql }}, {{ dbUse }},
{{ dbProvider }}, {{ registerProviders }} and {{ models }}. When the project needs
one, new stops before it writes a file, and names the copy:
crest: the published stub /path/to/resources/stubs/shared/project-compose.stub has no {{ databaseService }}; publish it again with crest stub:publish project-compose --flavor=adr --force, and make your changes in it again
Without a database and without --camel-case, such a copy works as before.
new can run commands after it writes the files, for example crest up and
crest install. Publish the project-config stub in the folder that the projects go
into, and add the key new to the copy:
crest stub:publish project-config
'new' => [
['crest', 'up'],
['crest', 'install'],
],
With the host runtime, use ['composer', 'install'] instead. With the docker runtime,
composer runs in the container: the host often has no Phalcon, or an older PHP, and then
composer install on the host stops.
Each entry is one command, as a list of words. There is no shell: pipes, && and
variables do not work. When the first word is crest, the crest that runs new also runs
the command. Use crest, not ./crest: crest looks for a relative path from the folder
where you type the command, not from the project. new runs the commands in order, in
the project folder, and prints Running: <command> before each one. At the end, it prints
Ran 2 commands from the 'new' key of crest.php.
The first command that fails stops the list, and new returns its exit status:
crest: 'composer install' failed with exit status 2; the files in my-app/ stay
A program that is not in the PATH also stops the list, with exit status 1:
crest: 'composer' was not found; install it or add it to the PATH
new reads the key from the crest.php that it writes. Thus it checks the key after it
writes the files. If the key is not a list of commands, or if PHP cannot parse the file,
new stops before the first command, and the files stay. The error names the published
stub that new used. Change the stub, not the crest.php of the project, then run new
again with --force:
crest: my-app/crest.php: 'new' entry 2 in crest.php must be a list of strings, e.g. ['crest', 'up']; the files in my-app/ stay; fix /path/to/resources/stubs/shared/project-config.stub and run new again with --force
new also checks that the written crest.php has the flavor of the project. Keep
'flavor' => '{{ flavor }}' in a published project-config: a copy with a fixed
flavor stops new in the same way, before the first command.
The key stays in the crest.php of the project. No other command reads it, and
config:show does not show it. init uses the same stub: it writes the key, but runs
nothing. Without the key, new runs nothing.
The one-line setup (see "With docker only" in the README) runs new in a container
without docker. There, crest up fails. Do not use the key with it.
composer require --dev phalcon/crest
crest init
init writes crest.php in the nearest folder with a composer.json. It asks these
questions, in this order, each with a proposal:
-
The namespace. The proposal is the namespace of the first psr-4 entry whose folder exists. The answer must be in the psr-4 map (
App\Apiunder"App\\": "src/"is), because the generators get their namespaces from it. The other proposals follow the answer: its folder is the psr-4 folder of the longest prefix, for examplesrc/Api. -
The flavor:
adrormvc. For each flavor,initcounts the default folders of that flavor (item 4) that the project has, and proposes the flavor with the most. A tie givesadr. A project withsrc/Controllers,src/Providersandthemes/vokurogetsmvc.--flavoranswers the question. -
The front controller:
<namespace>\AppFrontwhenAppFront.phpis in that folder. Answernonefor no front controller. -
A folder for each path of the flavor:
action,command,middleware,providerandresponderfor ADR;command,controller,form,model,module,providerandviewsfor MVC. The question says if the folder exists. A class folder is in the folder of the namespace. An answer must be a relative folder in a psr-4 folder, and its folder names must give a usable namespace:src/my-actionsgivesApp\my-actions, which PHP cannot use. A leading./is removed.viewsholds no classes. It is a relative folder in the project, with no psr-4 check. Each part of the answer must be a folder name, soinitrefuses an absolute folder, an empty part (//),.and... The proposal isthemes/default; when that folder does not exist andthemes/has one folder only,initproposes that folder (themes/vokuroin vokuro). -
The runtime: docker when the project has
compose.yaml,compose.yml,docker-compose.yamlordocker-compose.yml, with the serviceapp, or the first service. For docker, the service.
Press Enter to accept a proposal. --no-interaction accepts all of them. --force
overwrites an existing crest.php. If the project does not require crest, init prints
the composer require line.
For an MVC project, init also writes the models key of crest.php, with
'camelCase' => false, so the models of the project keep their property names. See
"Names".
init needs a psr-4 entry in composer.json, because the generators get their
namespaces from it. Without one, init stops and tells you to add one, for example
"App\\": "app/", and to run composer dump-autoload.
container:list, event:list, make:model and make:scaffold read state that
exists only once the application has registered it, so they start your front
controller.
Name it in crest.php:
return [
'bootstrap' => App\Front\ApiFront::class,
];The class is constructed with the project root - take it if you need it - and
has to declare boot(). There is no base class and no interface; boot() is
the whole contract:
use Phalcon\Container\Container;
final class ApiFront
{
public function __construct(private readonly string $root)
{
}
public function boot(): Container
{
$container = new Container();
// register your services
return $container;
}
}boot() must return an object, and what that object has to satisfy depends on
the command:
| Command | Requires the returned object to |
|---|---|
container:list |
implement Phalcon\Contracts\Container\Service\Collection and Enumerable |
event:list |
implement Collection, and hold a registered Phalcon\Events\Manager |
make:model, make:scaffold |
implement Phalcon\Di\DiInterface, and hold a db service that is a Phalcon\Db\Adapter\AdapterInterface |
Phalcon\Container\Container implements both contracts, so returning one is
enough for container:list.
For event:list, "registered" means a definition or an existing instance. A
manager the container would merely autowire does not count - crest reports that
the bootstrap registers none, rather than listing zero listeners off a fresh
instance it created itself.
Everything else - the other generators, about, config:show and
route:list - reads the filesystem and keeps working on a project that does
not currently run.
make:action takes an HTTP method and a route path:
crest make:action GET /company/all
crest make:action GET /company/{id}
The class name comes from the framework's routing convention, so the file lands
where the router will look for it. Placeholders must come last: /album/{id}/edit
is rejected, with /album/edit/{id} suggested instead.
| Option | Purpose |
|---|---|
--responder=json|view |
which packaged shape to render; defaults to json |
--stub=<name> |
render a named stub instead; cannot be combined with --responder |
--template=<name> |
template the view responder renders; defaults to <path>/index |
--force |
overwrite an existing action |
A view action names a template but does not create one. Renderer::render()
takes a name rather than a path, so the directory and the extension belong to
your renderer, and crest prints the name instead of guessing at a file.
In an MVC project, make:controller writes a controller and the base class that
it extends:
crest make:controller Users
Created src/Controllers/Base/CrestControllerBase.php
Created src/Controllers/UsersController.php
UsersController extends CrestControllerBase, which extends
Phalcon\Mvc\Controller, and has an empty indexAction(). Crest then prints
what the dispatcher needs: its default namespace, the URL of indexAction()
with the default routes (GET /users), and the name of its view
(users/index). The views folder and the engine belong to your project.
A project from crest new --flavor=mvc already has CrestControllerBase and
IndexController, and its dispatcher has the default namespace.
The name must give a class that the dispatcher can find from a URL. Crest stops
on users (the URL /users looks for UsersController), on User_Profiles,
and on a name with the suffix two times, for example UserControllerController
or Usercontroller. UserController gives UserController.
To write the controller into a module, add --module (see "Generating
modules"):
crest make:controller Users --module=admin
Created src/Modules/Admin/Controllers/Base/CrestControllerBase.php
Created src/Modules/Admin/Controllers/UsersController.php
Each module has its own CrestControllerBase. The Module.php of the module
sets the default namespace of the dispatcher, so crest prints the URL with the
routes of make:module (GET /admin/users/index) and the views folder of the
module instead. The module must exist: else crest stops, and tells you to run
make:module first.
Crest owns CrestControllerBase in src/Controllers/Base/. Do not edit it: its
docblock starts with Generated by crest., and crest writes the file again
when the stub of the base changes. You own the controllers, and crest writes
each one only once. Each module has its own copy, in the Controllers/Base/
folder of the module (src/Modules/Admin/Controllers/Base/), with the same
rules: to get its new base, add --module=<key> to make:controller.
-
To get the new base after an upgrade of crest, run
make:controlleragain on an existing controller. Crest keeps the controller and writes the base:Updated src/Controllers/Base/CrestControllerBase.php Kept src/Controllers/UsersController.php: it exists; pass --force to replace it -
--forcereplaces the controller with a new one. It never replaces a base without theGenerated by crest.line: crest keeps that file and prints a note. To get the crest base back, delete the file or add theGenerated by crest.line to it. -
Put the code that your controllers share into your own class that extends
CrestControllerBase. Publishcontroller.stub(crest stub:publish controller) and change itsuseline and itsextends, so that new controllers extend your class. -
A published
controller-base.stubwins over the packaged one, so the upgrades of crest no longer reach your base. Keep theGenerated by crest.line in your copy, so that crest writes the base again when you change the stub.
In an MVC project, make:model reads a table from the database of the project,
and writes a model and the base class that it extends. Crest writes relations
only from FOREIGN KEY constraints (see "Relations"):
crest make:model users
Created src/Models/Base/UsersBase.php
No relations for users: the table has no foreign keys, and no foreign keys point to it. Relations come only from foreign keys.
Created src/Models/Users.php
Crest reads the table through the db service of your project, so it starts
your front controller: crest.php must name it in bootstrap (see "Commands
that boot the project"). A project from crest new --flavor=mvc --database=<choice>
has the db service (see "The database"). For a project without one, add a
provider that registers it (crest make:provider Db).
With the phalcon/phalcon package (Phalcon v6), a model also needs
phalcon/phql, because a model uses PHQL to find rows. The extension (Phalcon
v5) has PHQL. crest new --database adds the package to composer.json. For
another project, install it:
composer require phalcon/phql:1.0.x-dev
Users extends UsersBase, which extends Phalcon\Mvc\Model. The base has a
property for each column, in the order of the table, and an initialize() that
sets the table:
public ?int $id = null;
public ?string $name = null;| Column type | Property type |
|---|---|
| the integer types | ?int |
FLOAT, REAL, DOUBLE |
?float |
BOOLEAN on PostgreSQL |
?bool |
all others, also DECIMAL, the dates and text |
?string |
The column type is the type that Phalcon reads from the database. MySQL and
MariaDB keep a BOOLEAN column as TINYINT(1), so it gives ?int. On SQLite,
Phalcon reads DOUBLE and BOOLEAN columns as VARCHAR, so they give
?string.
Each property is nullable, with the default null, because a new model has no
values yet. The class name comes from the table name: crest makes the first
letter of each part between _ uppercase. user_profiles gives
UserProfiles. To give a table another class, or a column another property
name, see "Names".
When the table changes, run make:model again. Crest writes the base again,
and keeps your model:
Updated src/Models/Base/UsersBase.php
No relations for users: the table has no foreign keys, and no foreign keys point to it. Relations come only from foreign keys.
Kept src/Models/Users.php: it exists; pass --force to replace it
The rules of "Generated bases" apply to the base of a model too: do not edit
it (you can change only the aliases of its relations and the names in its
PropertyName attributes, see "Relations" and "Names"), and --force
replaces the model, but never a base without the
Generated by crest. line. If you add initialize() to your model, call
parent::initialize() first.
Crest stops, and writes no file, when:
- the table name gives no class name (
user-profiles), or a name that PHP reserves (list), andmodels.classesgives it no class (see "Names"); - the project has no
dbservice, the service is not a Phalcon database adapter, or it cannot connect; - the table does not exist (in the schema of
models.schema, when you set it); models.schemahas a value, and thedbservice is SQLite (see "The schema");- a column name is not a property name (
first name), or it is the name of a property ofPhalcon\Mvc\Model(container), and no--propertyor attribute gives it another name: the error gives the--propertyto use; - the base is not valid PHP, or crest cannot use a
RelationAliasor aPropertyNameattribute in it (see "Relations" and "Names"); - the
modelskey ofcrest.phpis not correct, or--propertynames a column that the table does not have (see "Names"); - a published stub has no
{{ columnMap }}or{{ property }}, and the base needs acolumnMap()(see "Names"), or the publishedmodel-basehas no{{ schema }}, andmodels.schemahas a value (see "The schema"); --modulenames a module that does not exist (see "Generating modules").
make:model is for MVC projects. An ADR project uses repositories, not Active
Record models, so the command stops there.
To write the models into a module, add --module (see "Generating modules"):
crest make:model users --module=admin
Created src/Modules/Admin/Models/Base/UsersBase.php
No relations for users: the table has no foreign keys, and no foreign keys point to it. Relations come only from foreign keys.
Created src/Modules/Admin/Models/Users.php
The models and their bases go into the Models folder of the module, in its
namespace (App\Modules\Admin\Models). --module works with one table and
with --all. A relation names a model in the same module, so write the related
tables into the same module. The lines that tell you to run make:model again
name the option, for example
Run make:model --module=admin for customers too: .... Crest reads the tables
through the db service of the project, also when the module sets its own
db.
Crest gives each model the class of its table name, and each property the
name of its column. Two settings in crest.php and one option change the
names:
return [
'flavor' => 'mvc',
'bootstrap' => App\AppFront::class,
'models' => [
'classes' => ['users' => 'User', 'user-profiles' => 'UserProfile'],
'camelCase' => true,
],
];The models key has a third setting, schema, which does not change the names
(see "The schema").
In an MVC project, crest config:show shows the key: the rows camelCase and
schema, each with its origin, and a table with each line of classes, sorted
by table:
ITEM VALUE ORIGIN
...
service app default
camelCase true declared
schema none default
PATH LOCATION ORIGIN
...
TABLE CLASS
user-profiles UserProfile
users User
When the key is not correct, config:show shows the error in a models row and
exits with 0, and make:model stops on it:
service app default
models crest.php: models.camelCase must be true or false declared
Classes. classes gives a class to a table. Write a line only for each
table whose class you want to change: the other tables keep the class of their
name. Crest does not make a singular class name: users gives Users, not
User. For User, write the line 'users' => 'User'. The table name must be
exact, with the same case. A line also gives a class to a table whose name gives
none (user-profiles), or gives a name that PHP reserves (list), also with
--all. With --all, a line for a table that the database does not have gives a
note:
Note: models.classes names the table Users, which the database does not have; did you mean users?
When you add a line for a table that has a model, crest writes a new base and a
new model (UserBase.php, User.php), and keeps the old files. A note names
the old files, and the tables that have relations with the table:
Note: the old files of users stay: src/Models/Users.php and src/Models/Base/UsersBase.php; move your code to User, delete them, then run make:model for invoices
Move your code to the new model, delete the old files, and run make:model for
the tables of the note. Crest prints the note on each run until you delete the
old files. With --all, the note names only the related tables that the run
does not write, such as the tables of --exclude. The note also names a
related table that has no model yet: make:model then writes a model for it.
The new base does not get the PropertyName attributes of the old base: give
the names again with --property.
No note comes when the old base does not have the Generated by crest. line.
No note comes also when another table gets the old class (for example, with
the line 'members' => 'Users'), because the old base can be the base of that
table.
camelCase. With 'camelCase' => true, each property gets the camelCase form
of its column: crest splits the column at each _, makes the first letter of
each part uppercase, and then makes the first letter of the name lowercase.
first_name gives firstName, and name stays name. The other letters do
not change, so a column in capitals gives an odd name (USER_ID gives uSERID):
give it a name with --property. The setting applies to each table, on each
run.
Warning: when you change
camelCasein a project that has models, the next run ofmake:modelrenames the properties of each base, and the code that uses the old names stops working. Crest prints aRenamedline for each property that changes.
One property. --property gives a name to a column of one table:
crest make:model users --property="first name=firstName,last_name=lastName"
Separate the columns with commas, and give all the names to one --property:
if you give it two times, crest uses only the last one. A column whose name has
a comma, or a space at the start or the end, cannot get a name. --property
works only for one table, not with --all.
Crest keeps each name of --property in a PropertyName attribute on the
base:
#[\Crest\Attribute\PropertyName('first name', name: 'firstName')]
abstract class UsersBase extends ModelOn the next runs, you do not give --property again: crest reads the
attribute. You can change the name in the attribute, and crest uses it on the
next run. To use the name of camelCase, or the column name, again, delete the
attribute line and run make:model again. When the table no longer has the
column, crest drops the attribute:
Dropped the property name firstName of column 'first name': the table has no such column
Crest reads the PropertyName attributes as it reads the RelationAlias
attributes (see "Relations"). Crest stops, and writes no file, when a
PropertyName attribute does not have two plain strings (the column and the
name), when two attributes have the same column, or when the name in an
attribute is not a usable property name. The message names the file and the
line.
The order of the names: --property, then the attribute, then camelCase, then
the column name.
columnMap(). When a property does not have the name of its column, the
base gets a columnMap() with each column and its property. Phalcon uses it to
read and write the rows, but only when the setting orm.column_renaming is on,
as it is by default. The relations use the property names of the two tables:
with camelCase, the belongsTo() of invoices has 'customerId'.
When a name changes. When a property gets another name (a change of
camelCase, a new --property, or a changed attribute), crest prints a line
for it. When a relation of another table uses the property, crest names that
table:
Updated src/Models/Base/InvoicesBase.php
Renamed $customer_id to $customerId: change the code that uses it
Run make:model for customers too: its relations use the property names of invoices
Kept src/Models/Invoices.php: it exists; pass --force to replace it
With --all, crest writes all the tables again, so it names only the tables of
--exclude (see "All the tables"). It does not name a table that fails later.
The summary lists each table that failed: run make:model for it again after
the fix.
Published stubs. model-base has the placeholder {{ columnMap }}, and
fragment-model-property has {{ property }}. A copy that you published before
does not have them. When a base needs a columnMap(), crest stops:
crest: the published stub .../resources/stubs/mvc/model-base.stub has no {{ columnMap }}; publish it again with crest stub:publish model-base --force, and make your changes in it again
Crest writes the columnMap() with the stub fragment-model-column-map, which
has the placeholder {{ pairs }}. On the next run, crest reads the old property
names from this columnMap(). It reads only the form that it writes: a body
with only return [...];, and a plain string on each side of each =>. When
the base has another form (a published fragment-model-column-map, or a change
by hand), crest still writes the base, but it cannot find the renamed
properties. It prints no Renamed line, and prints:
Note: crest cannot read columnMap() of src/Models/Base/CustomersBase.php, so it cannot list the renamed properties
Then find the code that uses the properties of that table yourself.
make:model --all writes a model and its base for each table of the database,
in the order of the table names. For each table, crest prints a Table line,
then the lines of make:model <table>. At the end, crest prints a summary:
crest make:model --all
Table customers
Created src/Models/Base/CustomersBase.php
Created src/Models/Customers.php
Table invoices
Created src/Models/Base/InvoicesBase.php
Created src/Models/Invoices.php
2 tables: 2 models created, 0 kept, 0 failed.
Each table works as with make:model <table>: another run writes the bases
again and keeps the models, and --force replaces the models. When the
database has no tables to write, crest prints No tables to write., and the
exit status is 0.
With models.schema, --all writes the tables of that schema (see "The
schema"), and the line names the schema:
No tables to write in the schema 'billing'.
PostgreSQL gives no tables for a schema that does not exist, so a schema name with a typo gives this line. On MySQL and MariaDB, a schema that does not exist stops crest:
crest: the schema 'billing' cannot be read: SQLSTATE[42000]: Syntax error or access violation: 1049 Unknown database 'billing'
--all writes no models for views, and no models for the internal tables of
SQLite (for example, sqlite_sequence). For a view, run make:model <view>.
This works on MySQL, MariaDB and PostgreSQL. On SQLite, make:model <view>
stops with the "not found" error, because Phalcon finds only tables there.
To skip tables, give their names to one --exclude, separated by commas:
crest make:model --all --exclude=audit_log,phalcon_migrations
Give all the names to one --exclude. If you give --exclude two times,
crest uses only the last one.
Each name must be the exact name of a table, with the same case. A name that the database does not have stops crest before it writes a file. When the name differs from a table only in case, the error gives the name of the table:
crest: --exclude: the database has no table Users; did you mean users?
For the name of a view or an internal SQLite table, crest prints a note, and
continues. A skipped table still gives its relations: with
--exclude=invoices, the base of customers has its hasMany() to
Invoices.
A table fails when its name gives no class name (user-profiles) or a name
that PHP reserves (list) (a line of models.classes gives such a table a
class), when a column is not a property name and has no name from an
attribute, or when crest cannot use its base (see the list above). A table
also fails when it
gives the same class as another table, also with a different case
(UserProfiles and user_profiles, or USERS and users): crest writes
none of these tables. Crest prints the reason, and continues with the next
table. At the end, crest lists the tables that failed, so that you can write
their models yourself:
Table user-profiles
Failed: 'user-profiles' gives no usable class name; expected a table name like 'users' or 'user_profiles'
Table users
Created src/Models/Base/UsersBase.php
No relations for users: the table has no foreign keys, and no foreign keys point to it. Relations come only from foreign keys.
Created src/Models/Users.php
2 tables: 1 model created, 0 kept, 1 failed.
Failed tables:
user-profiles: 'user-profiles' gives no usable class name; expected a table name like 'users' or 'user_profiles'
The exit status is 1 when a table fails, and 0 when crest writes or keeps each
table. Crest prints all these lines to stdout, also the Failed lines, so a
log of the output (> log.txt) has them in order. An error that is not about
one table stops crest, and goes to stderr: for example, no db service, a
connection that breaks, or a stub that crest cannot render. The files of the
tables before the error stay, and the next run continues from them.
Crest writes relations only from FOREIGN KEY constraints. It does not read the
names of the columns: a column customer_id with no FOREIGN KEY constraint
gives no relation. MySQL tables with the MyISAM engine do not keep foreign
keys, so they give no relations. When a table has no foreign keys, and no
foreign key of another table points to it, make:model prints:
No relations for users: the table has no foreign keys, and no foreign keys point to it. Relations come only from foreign keys.
With no foreign keys, write the relations in your model, in initialize()
after parent::initialize():
public function initialize(): void
{
parent::initialize();
$this->belongsTo(
'customer_id',
Customers::class,
'id',
['alias' => 'Customer']
);
}With foreign keys, the base gets a belongsTo() for each foreign key of the
table, and a hasMany() for each foreign key of another table that points to
the table. With these tables:
CREATE TABLE customers (id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(50) NOT NULL);
CREATE TABLE invoices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_id INTEGER NOT NULL REFERENCES customers(id),
total DECIMAL(10,2)
);the base of invoices gets:
$this->belongsTo(
'customer_id',
\App\Models\Customers::class,
'id',
['alias' => 'Customer']
);and the base of customers gets:
$this->hasMany(
'id',
\App\Models\Invoices::class,
'customer_id',
['alias' => 'Invoices']
);Read a relation through its alias: $invoice->Customer, $customer->Invoices,
or $invoice->getRelated('Customer'). The related class is in the folder of
your models, also when you did not generate its model yet. Run make:model
for each table.
The aliases:
- A
belongsTo()uses the key column without_id:customer_idgivesCustomer. If the column has no_id, or the key has more than one column, the alias is the related class (Customers). - A
hasMany()uses the class of the other table (Invoices). If that table has more than one key to this table, the alias addsByand the alias of the key: a tablemessageswithsender_idandrecipient_idgivesMessagesBySenderandMessagesByRecipient.
Each relation has a RelationAlias attribute on the base, with the key of the
relation and its alias:
/**
* Generated by crest. Do not edit this file: crest writes it again when you
* run make:model invoices. Put your code into the model that extends it.
*
* You can change the alias in a RelationAlias attribute: crest keeps it when
* it writes this file again.
*/
#[\Crest\Attribute\RelationAlias('belongsTo invoices(customer_id) -> customers(id)', alias: 'Customer')]
abstract class InvoicesBase extends ModelTo change an alias, change the alias: argument, and run make:model for the
table again. Crest writes the base with your alias, and keeps it on each later
run. An alias in an attribute does not change when another key to the same
table comes, or when a later version of crest has other rules. Change only the
alias: argument: crest writes the rest of the base again. If you delete the
base, the aliases go back to the rules above.
When the next base has no relation for the key of an attribute, crest drops the attribute and prints one line for it, so that you know that the alias is gone. This happens when a key column gets a new name, when the key gets another target, or when the table name has another case on another server:
Dropped the alias Buyer of 'belongsTo invoices(buyer_id) -> customers(id)': the database has no such key
Crest reads the attributes with the PHP tokenizer, so a formatter can change
their layout: line breaks, comments, quotes, named arguments, or a use import
of Crest\Attribute\RelationAlias. Each argument must be a plain string: in
single quotes, or in double quotes with no backslash. A constant, or two
strings joined with ., is not a plain string. The attribute class does not
need to exist when your application runs, so an install without crest
(composer install --no-dev) works.
Crest stops, and writes no file, when the base is not valid PHP, when a
RelationAlias attribute does not have two plain strings (the key and the
alias), when an alias is not a valid name, when two attributes have the same
key, when an alias of an attribute has the name of a property of the table, or
when two aliases of attributes are the same (the case is ignored). The message
names the file and the line.
When another table gets a new key to a table, run make:model for that table
again, so that its base gets the new hasMany(). Or run make:model --all,
which writes the bases of all the tables again.
Crest skips a relation that it cannot write, and prints one line for it:
Skipped the relation belongsTo invoices(customer_id) -> customers(id): the alias Customer has the name of a property
Crest skips a relation when:
- the key does not name the referenced columns (SQLite accepts
REFERENCES customers); - the related table gives no class name, or a name that PHP reserves;
- crest cannot read the base of the related table, for example because an attribute in it does not have two plain strings. The line gives the error;
- a key column of the related table has no usable property name, and no
--propertyor attribute gives it one (see "Names"); - the alias has the name of a property of the table (the case is ignored);
- two or more relations have the same alias (the case is ignored). Crest skips all of them, but not a relation whose alias comes from an attribute;
- the key points to a table of another schema: the reason is
the table customers is in another schema(see "The schema").
Crest writes the other relations. When the reason is the alias (the name of a
column, or two relations with the same alias), you can give the skipped
relation an alias: add an attribute with the key from the line, and run
make:model again:
#[\Crest\Attribute\RelationAlias('belongsTo invoices(customer_id) -> customers(id)', alias: 'Client')]
For the other reasons, an attribute does nothing: crest drops it, with a
Dropped line.
When crest cannot read the base of the related table, correct that base as the
line says, and run make:model again. When a key column of the related table
has no usable property name, give it a name in a run for the related table, and
then run make:model for this table again:
Skipped the relation hasMany notes(it's_id) -> users(id): column 'it's_id' of table 'notes' has no usable property name
crest make:model notes --property="it's_id=userId"
crest make:model users
You can also add a skipped relation in your model, in initialize() after
parent::initialize().
Composite keys work on MySQL and PostgreSQL: the fields are arrays, for example
['order_id', 'line_no']. SQLite gives one key for each column of a composite
key, so crest does not support SQLite composite keys.
An alias with the name of a get method of Phalcon\Mvc\Model, for example
Messages (getMessages()) or Source (getSource()), works as a property
($user->Messages) and through getRelated('Messages'), but not as
$user->getMessages(): PHP calls the method of the model.
Crest reads the tables of the default schema of the connection. To read the
tables of another schema, give its name in the models key of crest.php:
'models' => [
'schema' => 'billing',
],Then make:model, make:model --all and make:scaffold read the tables, the
columns and the foreign keys of that schema, and each base calls setSchema()
after setSource(). So the model reads and writes the rows of
billing.invoices:
public function initialize(): void
{
$this->setSource('invoices');
$this->setSchema('billing');
}The value must be a string that is not empty and not only white space. Crest
uses it as you write it: on PostgreSQL, write the name with the case that the
server keeps. A null value is the same as no value. The schema applies to all
the tables of the project, also with --module. config:show shows it in the
schema row (see "Names").
When you change the value, the next run reads the tables of the new schema, and
writes the base of each table of the run with the new value. A table that the
new schema does not have stops make:model:
crest: table 'invoices' was not found in the schema 'billing' of the 'db' service
Crest deletes no file. When you remove the key, the next run writes each base
with no setSchema().
A model keeps its schema until crest writes its base again. So after you add,
change or remove the key, run make:model --all: a related model whose base
crest did not write again reads the old schema, with no error.
SQLite has no schemas. When the db service is SQLite, and models.schema has
a value, crest stops:
crest: crest.php: models.schema is 'billing', but SQLite has no schemas; remove the key
When models.schema has a value, a published model-base must have the
placeholder {{ schema }}. A copy that you published before does not have it,
and crest stops with the message of "Published stubs" (see "Names"). With no
models.schema, a published copy works as before.
A key to another schema. Crest writes a relation only to a table of the
schema that it reads. A foreign key to a table of another schema gives no
relation, also with no models.schema:
Skipped the relation belongsTo invoices(customer_id) -> customers(id): the table customers is in another schema
Crest finds such a key from the list of the tables: the referenced table is not
a table of the schema. A base from an older version of crest can have a
relation like that. The next run removes it, and the code that uses its alias
stops working. A RelationAlias attribute for the key gets a Dropped line.
If you need the relation, write it in your model.
The limits:
- When the schema also has a table or a view with the name of the referenced table, crest cannot see the difference. It writes the relation to the model of that table or view in the schema.
- A key from a table of another schema to a table of this schema gives no
hasMany(): crest does not read the other schemas. - On PostgreSQL, until cphalcon 5.24.0 and the tag of
phalcon/phalconafterv6.0.0RC4: when two schemas have the same table with the same key column, Phalcon gives one key with the columns of the two keys. Crest then writes a relation with the column two times, or aSkippedline, for that key. To fix it, give one of the two keys another name, or use the release with the fix.
In an MVC project, make:module writes a module: a part of the application
with its own controllers, models and views, for example admin or reports.
All the modules are in the module path of crest.php (src/Modules), and
each module has the same structure:
crest make:module admin
Created src/Modules/Admin/Module.php
Created src/Modules/Admin/Controllers
Created src/Modules/Admin/Models
Created src/Modules/Admin/views
| Path | Contents |
|---|---|
src/Modules/Admin/Module.php |
App\Modules\Admin\Module, which implements Phalcon\Mvc\ModuleDefinitionInterface |
src/Modules/Admin/Controllers/ |
the controllers (crest make:controller Index --module=admin) |
src/Modules/Admin/Models/ |
the models (crest make:model users --module=admin) |
src/Modules/Admin/views/ |
the views of the module |
After the Created lines, crest prints the lines to add to your front
controller and your router (see below).
The folder is the name with the first letter of each part between _ in
uppercase: admin and Admin give Admin, and admin_panel gives
AdminPanel. The key of the module is the folder in lowercase: admin, or
adminpanel. You use the key in registerModules(), in the routes and in
--module. Crest finds a module from its key, so --module=adminpanel finds
the folder AdminPanel. A name that does not give a class name (my module,
admin-panel) stops the command.
Module.php is yours: add the services of the module in registerServices().
Crest writes it one time. When it exists, make:module stops; --force
replaces only Module.php, and the folders and their files stay. There is no
config file: a module that needs settings reads them from .env in
registerServices(), as the providers of the project do.
Crest does not change your front controller or your router. Register the
module in run() of AppFront, before handle():
public function run(): int
{
$uri = (string) ($_SERVER['REQUEST_URI'] ?? '/');
$application = new Application($this->boot());
$application->registerModules([
'admin' => ['className' => \App\Modules\Admin\Module::class],
'common' => ['className' => \App\Modules\Common\Module::class],
]);
$response = $application->handle($uri);
if ($response instanceof ResponseInterface) {
$response->send();
}
return 0;
}Then send requests to the modules with routes. Write a provider for the router
(crest make:provider Router), and add new RouterProvider(), to
providers():
public function register(DiInterface $di): void
{
$di->setShared('router', function (): Router {
// The default routes stay for the controllers of the project.
$router = new Router();
$router->add('/admin', [
'module' => 'admin',
'controller' => 'index',
'action' => 'index',
]);
$router->add('/admin/:controller', [
'module' => 'admin',
'controller' => 1,
'action' => 'index',
]);
$router->add('/admin/:controller/:action/:params', [
'module' => 'admin',
'controller' => 1,
'action' => 2,
'params' => 3,
]);
// A route that does not follow the pattern of a module.
$router->add('/login', [
'module' => 'common',
'controller' => 'session',
'action' => 'login',
]);
return $router;
});
}Router is Phalcon\Mvc\Router. A request with no module in its route uses
the dispatcher and the views of the project, as before.
registerServices() of Module.php changes two services that the providers
of the project set:
- the default namespace of the dispatcher becomes the namespace of the
controllers of the module (
App\Modules\Admin\Controllers); - the views folder becomes the
views/folder of the module.
So a page of the module uses only the views of the module, and shows no
layout of the project. Add a layout in the views/ folder of the module. For
one views folder for the whole application, change the setViewsDir() line of
Module.php to the views folder of the project.
A project with no dispatcher or view service must change
registerServices().
A relation of a model in a module names a model in the same module. To share
models between modules, keep them in the model path: run make:model with
no --module. make:model --module reads the tables through the db service
of the project, also when the module sets its own db.
In an MVC project, make:scaffold writes CRUD pages for one table of the
database: a model, a form, a controller and three views. In a project from
crest new --flavor=mvc --database=<choice>, with a table users:
crest make:scaffold users
Created src/Models/Base/UsersBase.php
No relations for users: the table has no foreign keys, and no foreign keys point to it. Relations come only from foreign keys.
Created src/Models/Users.php
Created src/Forms/Base/UsersFormBase.php
Created src/Forms/UsersForm.php
Created src/Controllers/UsersController.php
Created themes/default/users/index.phtml
Created themes/default/users/new.phtml
Created themes/default/users/edit.phtml
Crest then prints what the pages need: the default namespace of the dispatcher, the URL of the list, and the base URI.
| File | Owner | A second run |
|---|---|---|
src/Models/Base/UsersBase.php |
crest | writes it again |
src/Models/Users.php |
you | keeps it, also with --force |
src/Forms/Base/UsersFormBase.php |
crest | writes it again |
src/Forms/UsersForm.php |
you | keeps it, unless --force |
src/Controllers/Base/CrestControllerBase.php |
crest | writes it again when its stub changes |
src/Controllers/UsersController.php |
you | keeps it, unless --force |
themes/default/users/index.phtml |
you | keeps it, unless --force |
themes/default/users/new.phtml |
you | keeps it, unless --force |
themes/default/users/edit.phtml |
you | keeps it, unless --force |
So when the table gets a new column, run make:scaffold again. Crest writes the
two bases again, and the list and the new and edit pages show the column,
because the views loop over the form. The rules of "Generated bases" apply to the
two bases. make:model --force replaces the model.
With models.schema, the pages read the table of that schema (see "The
schema").
All the names come from the class of the model, so one line of models.classes
changes all of them (see "Names"):
| Name | users |
user_profiles |
users with 'users' => 'User' |
|---|---|---|---|
| Model | Users |
UserProfiles |
User |
| Form | UsersForm |
UserProfilesForm |
UserForm |
| Form base | UsersFormBase |
UserProfilesFormBase |
UserFormBase |
| Controller | UsersController |
UserProfilesController |
UserController |
| URL and views folder | users |
user-profiles |
user |
The name of a field is the property of its column, and its label comes from the
column: first_name gives the label First name.
| URL | Method | Result |
|---|---|---|
/users |
any | the list, with 10 rows on each page, and the search form |
/users/search |
any | the list of the rows that match the query |
/users/new |
any | an empty form |
/users/create |
POST | adds a row, and goes to the list |
/users/edit/<id> |
any | the form of the row |
/users/save/<id> |
POST | changes the row, and goes to the list |
/users/delete/<id> |
POST | deletes the row, and goes to the list |
- A request to
create,saveordeletethat is not a POST goes to the list, and changes nothing. - When the model refuses a value, the form shows again, with the messages of the model and the values that the user typed. When the model refuses a delete, the list shows its messages.
createdoes not change a row that exists: a primary key that is in the table gives the message of the model, and the form shows again.- A row that does not exist gives
Users was not found, and the list.
To write the pages into a module, add --module (see "Generating modules"):
crest make:scaffold users --module=admin
The model, the form and the controller go into the Models, Forms and
Controllers folders of the module, in its namespace, and the views go into
views/users/. The pages are at /admin/users, after you add the registration
and the routes that make:module prints. The module must exist.
Crest does these checks before it writes a file, so a run that stops changes nothing. It stops when:
- the project is not an MVC project;
--modulenames a module that does not exist;- the table name gives no class name, and
models.classesgives it no class; - the dispatcher cannot find a controller of that class from a URL (see "Generating controllers");
- no psr-4 entry covers the
form,controllerormodelpath; - the table does not exist;
- the table has no primary key, or its primary key has more than one column;
make:modelstops on the table (see "Generating models");- a property has the name of an accessor of
Phalcon\Mvc\Model(see below); - the
editform has no fields, for example when the table has only its primary key.
A column with the name of a Model accessor. Phalcon\Mvc\Model has methods
such as setSource(), getSchema() and getMessages(). A form calls
set<Name>() and get<Name>() when they exist, so a column source calls
Model::setSource(), and each save fails. Crest stops, and gives the command that
gives the column another property name:
crest: column 'source' gives the property 'source', and Phalcon\Mvc\Model has a method with that name; give it another name with make:model sources --property=source=<name>
Run that make:model command, then run make:scaffold again.
The form has one field for each column, in the order of the table:
| Column | Field | Filter | Other |
|---|---|---|---|
| The identity column | none | ||
| A primary key that is not identity | as the rows below | as the rows below | on the new form only |
Name is email (any case) |
Email |
email |
|
BIGINTEGER, INTEGER, MEDIUMINTEGER, SMALLINTEGER, TINYINTEGER |
Numeric |
int |
|
DOUBLE, FLOAT |
Numeric |
float |
step="any" |
DECIMAL |
Numeric |
trim |
step="any" |
BOOLEAN |
Select |
bool |
the options No and Yes, and an empty option |
DATE |
Date |
trim |
|
TEXT, TINYTEXT, MEDIUMTEXT, LONGTEXT, JSON, JSONB |
TextArea |
trim |
|
BLOB, TINYBLOB, MEDIUMBLOB, LONGBLOB, BINARY, VARBINARY, BYTEA |
none | crest prints Skipped <column>: a binary column has no field in the form |
|
| Each other type | Text |
trim |
The column type is the type that Phalcon reads from the database (see "Generating
models"). An input that is empty or has only whitespace is null, and the model decides
if the column can be null: a NOT NULL column gives the message <property> is required.
The search form has the fields of the new form, and uses the rules of
Phalcon\Mvc\Model\Criteria::fromInput(): LIKE for a VARCHAR column, and =
for each other type. An empty field is not in the search. On PostgreSQL, partial
text for a DATE, a TIMESTAMP or a UUID column gives an error page.
The links and the redirects use the url service. With no base URI, Phalcon makes
one from PHP_SELF, and the links of a page such as /users/edit/5 are not
correct. A project from crest new sets / in UrlProvider. For an application in
a subfolder, change that line, for example to /shop/. Another project must set
the base URI:
$url->setBaseUri('/');
- The pages have no login and no CSRF check. Add them before you deploy the pages.
- There is no success message after a redirect, because a message that survives a
redirect needs a session. When your project has a
sessionservice, call$this->flashSession->success('Saved')before the redirect, and add<?php $this->flashSession->output() ?>to the view. - A foreign key is a number field, not a list of the related rows.
- A
DELETEthat a foreign key refuses gives an error page. - A column with the name
pageis also the parameter of the page in the URL. - With the
phalcon/phalconpackage (Phalcon v6), a project needsphalcon/phql.crest new --databaseadds it (see "Generating models").
A package adds commands to crest in its composer.json:
"extra": {
"crest": {
"commands": {
"demo:greet": "Vendor\\Demo\\GreetCommand"
}
}
}crest reads this block from each installed package, and from the root
project. crest make:command writes a command and prints its block.
An unknown command gives a hint when crest knows the package that provides its prefix:
crest: unknown command 'migration:run'; provided by phalcon/migrations
There is no hint when a registered command has that prefix: the package is installed, and only the name is wrong.
A command extends Crest\Console\Command\Command and declares two methods:
use Crest\Console\Command\Command;
use Crest\Console\Input;
use Crest\Console\Output;
use Crest\Console\Parsing\Definition;
final class GreetCommand extends Command
{
public function define(): Definition
{
return Definition::for('demo:greet', 'Greet someone')
->argument('name', true, 'Who to greet')
->option('shout', 'Use capital letters');
}
public function handle(Input $input, Output $output): int
{
$output->line('Hello ' . $input->argumentString('name'));
return 0;
}
}crest makes the command with new $class(), so the constructor must work
without arguments. Each command also gets the global options (--config,
--directory, --trace, --help, --quiet, --no-interaction).
These classes are the API for a package. Each has @api in its docblock:
| Class | Use |
|---|---|
Crest\Console\Command\Command |
the base class of a command |
Crest\Console\Parsing\Definition |
for(), argument() and option() declare the arguments and options |
Crest\Console\Input |
the values of the call |
Crest\Console\Output |
line(), success(), error(), write(), table(), ask() and choice() |
Crest\Console\Exceptions\Exception |
throw it to stop with one clean error line |
Crest\Command\Project |
Project::context($input) gives the project of the call |
Crest\Project\ProjectContext |
root(), path($key) and namespaceFor($key) |
Project::context() finds the project by the rule of the other commands:
the file that --config names, or the nearest crest.php above
--directory or the working directory. Without crest.php, the command
stops with the crest init hint.
A public element with @internal in its docblock is not part of the API,
and it can change in any release. Tools that read the tag, for example an
IDE or a static-analysis rule, can report its use from another package. Do
not extend Crest\Command\ProjectCommand: it is the base of the commands of
crest, and it changes with them.