On this page
CLI reference
Gacela ships a small CLI that helps you build, inspect and tune the modules in your application.
Every command below runs through vendor/bin/gacela. Run it without arguments to list the installed commands, or run
vendor/bin/gacela help <command> for all of one command's options.
The binary walks up from the current directory to the nearest vendor/autoload.php and bootstraps with that project
root. Like other Composer tools, it works from anywhere in the tree. gacela.php, setAppModulePaths() and the cache
directory always resolve against the project root, never against the directory you run from. Before 2.1 the command
looked only in the working directory, and failed after a single cd src.
Project setup
init
Create the gacela.php bootstrap file that every other command needs:
vendor/bin/gacela init [--force|-f]--force overwrites an existing file.
agents:install Since 2.5
Point the project's AGENTS.md at the guide Gacela ships for coding agents. The command writes between markers that
only it rewrites. See coding agents.
vendor/bin/gacela agents:installModule discovery
list:modules
List every module found under your project namespaces.
vendor/bin/gacela list:modules [--detailed|-d] [-j|--json] [<filter>]filter: substring to narrow the output-d,--detailed: show each module's contents in detail-j,--json: output machine-readable JSON
To limit which directories this command (and debug:modules, cache:warm, doctor) scans, use
setAppModulePaths().
Any class that descends from AbstractFacade marks a module, not only a direct child. A project with its own base
Facade in between (ShopFacade extends AppBaseFacade extends AbstractFacade) used to vanish from list:modules,
doctor, debug:graph and cache:warm, and nothing reported it.
With no appModulePaths configured, the scan starts at the project root. It skips vendor, node_modules and any
hidden directory before entering them, rather than walking them and throwing the results away. The rule is narrow on
purpose: the scan enters every other directory. Assuming a project's build/ or data/ holds no modules is how
discovery starts silently missing them. An appModulePaths entry inside a skipped directory still works, because the
scan never filters the configured root itself.
debug:modules
Walk every discovered module and inspect the constructor of each pillar (Facade, Factory, Config, Provider). It sits
between list:modules (the structure) and debug:dependencies (one class in depth).
vendor/bin/gacela debug:modules [--detail|-d] [--check] [-j|--json] [<filter>]- By default, the output groups by module, with resolvable and unresolvable counts per pillar.
--detailincludes every parameter, not only the unresolvable ones.--checkexits non-zero when a pillar has a parameter the container cannot satisfy, for CI.-j,--jsonreports as a JSON document instead of text.filteraccepts a namespace substring (e.g.App\\Shop) or a directory (e.g.src/).
debug:dependencies
Inspect one class's constructor and report whether the container can resolve each parameter.
vendor/bin/gacela debug:dependencies <class|file> [--tree] [-j|--json]- Takes a fully qualified class name, or the path to a PHP file that declares the class.
- Tags each parameter:
bound → target,autowirable,has default, orunresolvablewith a reason. - Tags parameters with
#[Inject]asinject, and shows the override concrete inline when there is one. --treeappends the transitive dependency graph, after applying bindings and contextual bindings. Nodes are markedbinding,instance,autowiredorunresolvable; cycles are shown and cut.-j/--json: report as a JSON document instead of text.
debug:module
Inspect one module: its resolved Facade, Factory, Config and Provider, the container bindings it registers, and its
dependency tree. Compare debug:modules (all modules, whether each pillar's dependencies resolve) and debug:dependencies (one class).
vendor/bin/gacela debug:module <module> [-j|--json] [-t|--tree]module: module name, or a part of it (required)-j,--json: output machine-readable JSON-t,--tree: only print the dependency tree
debug:graph
Draw the module dependency graph of the whole app: which module imports which, with an edge for each cross-module Facade use.
vendor/bin/gacela debug:graph [<filter>] [-f|--format=text|mermaid|graphviz|json] [-j|--json] [--check]filter: only include modules matching this substring-f,--format:text(default),mermaid,graphviz, orjson-j,--json: shorthand for--format=json--check: exit non-zero when an unreviewed dependency cycle exists--allowed-cycles <file>: JSON allowlist of reviewed cycles and their reasons--rules <file>: Since 2.2 exit non-zero on a dependency your module rules file forbids. You cannot combine it with a filter argument: in a narrowed graph, a rule about a filtered-out module looks the same as a rule about a module that no longer exists.-c,--compare-to <graph.json>: diff the current graph against saved JSON output. It writes only the diff, so it refuses--check,--rulesand--allowed-cycles: run the check as its own step. Since 2.7
With --check, --format=json writes the findings as a report instead of lines, for a CI job that needs more than an
exit code: undeclared cycles, stale allow-list entries, forbidden dependencies and unknown rule namespaces. Since 2.2
Use the mermaid and graphviz formats for architecture diagrams, and --check in CI.
See Failing on dependency cycles for the allowlist format and
the CI comparison workflow.
The command reads imports with PHP's tokenizer, not line by line. Grouped (use App\Shop\{A, B};), multiline and
aliased imports all produce edges, and so do use function and use const. A leading \ on an import and an uppercase
USE no longer hide a dependency. Modules resolve through a name index instead of comparing every import against every
module, which keeps the scan cheap on large graphs.
debug:container
Inspect the container's user bindings and plugins only. Framework-internal services are left out.
vendor/bin/gacela debug:container [<class>] [-s|--stats] [-t|--tree] [-j|--json]- No arguments (or
-s,--stats): print container statistics: registered services, frozen services, factory services, bindings, cached dependencies, and process memory usage. <class>(or-t,--treewith a class): show the dependency tree for that fully qualified class name. A class implies--tree;--treewithout a class is an error.-s,--statsalways wins:debug:container SomeClass --statsprints statistics, not the dependency tree, even with a class given.-j,--json: report as a JSON document instead of text.
debug:provides Since 2.3
Find which Provider declares an id with #[Provides]. debug:module cannot
answer this, because it needs you to know the module already.
vendor/bin/gacela debug:provides [<id>] [-j|--json]id: only ids containing this text; omit it to list every declaration-j,--json: output machine-readable JSON
The table shows the id, the module, the Provider and the method. Two modules that declare the same id do not collide, because each resolves through its own container, so both rows stay. That is the reason to look here at all.
It lists only ids declared with the attribute. Finding the ids a Provider registers in code with $container->set()
would mean running the Provider.
debug:plugins Since 2.5
List the members of each plugin stack and the ids of each tag, in the order they are
read, plus the #[AsListener] methods. Each entry shows where it was declared: gacela.php (packages included) or the
attribute.
vendor/bin/gacela debug:plugins [-j|--json]Ids that a module's Provider tags at runtime stay in that module's container, so the command does not list them. A
#[Plugin] that names a stack nobody declared is listed as never read.
debug:events Since 2.4
List every event the framework can dispatch, plus your project's own. It shows which ones have a listener, and which are dispatched on the class-resolution hot path.
vendor/bin/gacela debug:events [<filter>] [-l|--listened] [-j|--json]filter: only events whose class name contains this text-l,--listened: only events something listens to-j,--json: output machine-readable JSON
A specific listener matches by inheritance, so a registration can cover an event without naming it. The listener
column names the target that does. The command finds project events under the paths discovery already walks: a class
that implements GacelaEventInterface or is named *Event. It marks them project. It also says when
disableEventListeners() is in effect or a custom dispatcher is installed, because a supplied dispatcher passes events
on to a bus the command cannot see into.
#[AsListener] methods appear beside the gacela.php listeners, marked as such, and
as attributeListeners in --json. --listened counts an event that only they handle. They never match framework
events, because the framework's dispatch sites do not read them. Since 2.6
Caching & production
cache:warm
Pre-resolve every module class, and write the persistent caches and, optionally, the merged configuration cache. Run it once per deploy in production.
vendor/bin/gacela cache:warm [-c|--clear] [-a|--attributes]-c,--clear: clear the existing cache before warming (the same as runningcache:clearfirst)-a,--attributes: pre-scan and cache#[ServiceMap]attributes, and store the#[Plugin],#[Tag]and#[AsListener]members Since 2.5
cache:warm batches file writes with AbstractPhpFileCache::beginBatch() / commitBatch() and flushes with an atomic
rename(). One write replaces the old N modules × 4 resolvers full-file rewrites.
Exit code. As of 2.1 the command exits non-zero when module discovery fails or any module fails to warm, so a broken
deploy step does not show green. Before, it always exited 0 and printed the failures as warnings. It writes the merged
configuration cache only when the file cache is enabled.
When it writes no merged configuration cache, it says why: a config source changed in the same second, or the cache directory is not writable. The next bootstrap writes it. Since 2.6
cache:clear
Remove every Gacela cache file.
vendor/bin/gacela cache:clearIt clears the project's class-name, custom-service and merged-config cache files, the cacheable-method entries, and the container's in-process reflection memos.
Configuration health
doctor
Run health checks on the environment and the wiring, each with a hint on how to fix what it finds. The built-in checks
include cache staleness, suffix mismatches and filename/class mismatches, plus any ModuleHealthCheckInterface
registered through GacelaConfig::addHealthCheck(). It also checks the
declared config schema, and reports a published stub that
lost a placeholder or sits under a name the scaffolder never reads. Since 2.2
vendor/bin/gacela doctor [<filter>] [--strict] [--only-problems] [--format=text|json] [-j|--json]filter: limit module-scoped checks to a namespace substring.- By default, warnings still exit
0.--strictmakes warnings fail too; use it in CI. --only-problems: report only the checks that found something.--format:text(default) orjson.-j,--json: shorthand for--format=json.
The staleness check also covers the merged configuration cache. It compares it against every file ConfigLoader
would read: base patterns, environment patterns and local overrides. A cache written by cache:warm keeps serving old
values after a config/*.php file changes, while every class-name entry stays fresh. That is how doctor used to
report "all cache entries are fresh" on a stale configuration.
validate:config
Check the current Gacela configuration for errors and best practices.
vendor/bin/gacela validate:config [--strict] [--format=text|json] [-j|--json]- Warns when
gacela.phpis missing. - Walks every registered binding and warns on a type mismatch, with the expected interface or class, the actual type chain, and a hint on the fix.
- Checks interface-keyed bindings too (they used to be skipped).
- Accepts non-class binding keys (plain string ids such as
'db.dsn') instead of reporting them as non-existent. - Checks the configuration against the declared schema, and exits non-zero when a declared key is missing or has the wrong type. Since 2.2
--strict: exit with a failure code on warnings too, for CI.--format:text(default) orjson;-j/--jsonis short for--format=json.
debug:config
Print the merged configuration your modules get as a table, after resolving every config/*.php file and environment
override.
vendor/bin/gacela debug:config [<filter>] [--format=text|json] [-j|--json]filter: only show keys that contain this substring.--format:text(default) orjson;-j/--jsonis short for--format=json.- It reads
Config::getAllValues(), so it shows exactly what your modules see at runtime. - Each key is marked
declared,undeclaredormissingagainst the declared schema. The table flags the keys the schema does not cover, and lists a declared key that nothing provides, even though it has no value to show. Since 2.2
Profiling
profile:report
Build a performance report from the in-memory Profiler. Enable the profiler (Profiler::getInstance()->enable())
early in your bootstrap, run your code, then dump the report. Profiling documents the Profiler API
and how spans work.
vendor/bin/gacela profile:report [-f|--format=table|json|summary] [-j|--json] [-s|--sort=duration|memory|operation]-f,--format:table(default),json, orsummary.-j/--jsonis short for--format=json.-s,--sort:duration(default),memory, oroperation.
--format=json writes entries, stats and, since 2.3, unfinished: the operations started and never stopped,
keyed operation:subject, with a count of how many of each are still open.
{
"entries": [],
"stats": {},
"unfinished": { "db-query:orders": 1 }
}The profiler ignores a stop() whose operation or subject does not match its start(), because it has no start time
to measure from. Before 2.3 the span vanished from the report and looked like code nobody had instrumented. Naming the
unmatched start turns "my operation is missing" into the typo that caused it. The text report says the same.
The profiler keeps a stack of start times per operation, so nested and recursive spans each record their own duration.
Starting an operation that was already running used to overwrite the outer timestamp and merge both into one entry.
disable() drops every span still running: a span left open while profiling is off can never close, and would pair a
later stop() with a stale start.
Code generation
make:file
Generate a Facade, Factory, Config, Provider, or any combination of them.
vendor/bin/gacela make:file [-s|--short-name] [-f|--force] [--dry-run] <path> <filenames>...path: file path, for exampleApp/TestModule/TestSubModulefilenames: any combination offacade,factory,config,provider-s,--short-name: drop the module prefix from the generated class name-f,--force: replace files that already exist Since 2.3--dry-run: report the files that would be written, and write nothing Since 2.3
The command refuses a kind Gacela does not have, instead of guessing: Repository exits 1 instead of producing a
Factory. Abbreviations of the four pillars still work.
vendor/bin/gacela make:file App/TestModule facade factory providerBoth generators find the target directory by stripping only the matched psr-4 prefix from the path. Replacing the
namespace text everywhere also rewrote it inside the module name, so App/Application against App\ => src/ produced
src/srclication. A psr-4 entry mapped to a list of directories works: the generators use the first, because Composer
looks there first.
make:module
Generate a full module: Facade, Factory, Config, and Provider.
vendor/bin/gacela make:module [-s|--short-name] [-t|--template=basic|service|minimal] [--minimal] [--with-tests] [-f|--force] [--dry-run] <path>-s,--short-name: drop the module prefix from the generated class name-t,--template:basic(four pillars),service(four pillars plus a wired Domain service), orminimal(Facade and Factory only).--minimal: shorthand for--template=minimal.--with-tests: also scaffold aGacelaTestCase-based facade test (only valid with--template=service).-f,--force: replace files that already exist Since 2.3--dry-run: report the files that would be written, and write nothing Since 2.3
Both generators check every target before writing any. Without --force, a run that would replace an existing file
writes nothing, names the files in the way, and exits 1. Since 2.3
vendor/bin/gacela make:module -s App/TestModulevendor/bin/gacela make:module --template=service --with-tests App/Checkoutstubs:publish Since 2.2
make:module and make:file generate from templates that ship with Gacela. stubs:publish copies them into the
project (stubs/gacela/ by default; use GacelaConfig::setStubsDir() to put them elsewhere), so make:* generates
your house style:
vendor/bin/gacela stubs:publish # every stub
vendor/bin/gacela stubs:publish --template=basic # one template set
vendor/bin/gacela stubs:publish --force # replace ones already published
vendor/bin/gacela stubs:publish --dry-run # report what would be published, write nothingFrom then on, each generated file uses the project's stub when there is one, and the built-in template when there is
not. This works per file, so publishing your Facade stub does not freeze the Factory at the version you copied.
Without --force, the command overwrites nothing already published: it is a file somebody changed on purpose.
Every stub substitutes $NAMESPACE$, $MODULE_NAME$ and $CLASS_NAME$. doctor reports a published stub
that lost $NAMESPACE$ or $CLASS_NAME$, and one filed under a name the scaffolder does not read. Otherwise an edit
that never takes effect looks exactly like one that did.
ide:meta Since 2.3
Generate editor metadata for getProvidedDependency() from the
#[Provides] attributes, so the editor resolves a string id to a real type.
vendor/bin/gacela ide:meta [--dry-run]--dry-run: report what would change, write nothing
It writes .phpstorm.meta.php/gacela.meta.php. The directory form is on purpose: a project with a hand-written
.phpstorm.meta.php file keeps it.
An id that two Providers register with different types is listed, not written. The editor map covers the whole
application, while getProvidedDependency() reads the calling module's container, so there is no single answer to
write. Saying nothing would make the id look unsupported.
Static analysis needs none of this. Both analysers already type a class-string key; this covers the string ids they cannot.
dto:generate Since 2.3
Write the immutable class for every data shape declared with
declareDtoSchema(): typed getters, with*() copies, toArray() and fromArray().
vendor/bin/gacela dto:generate [--dry-run] [--check]--dry-run: report what would change, write nothing--check: like--dry-run, and exit non-zero when a class would be written
Unlike make:*, this generates a derived file, not a starting point. The declaration is the source of truth and
the class is regenerated from it, so nobody edits the result. Regenerating an unchanged declaration gives a
byte-identical file.
The command places each class through the project's own psr-4 autoload map, longest matching prefix first. When no
prefix covers a shape's namespace, it reports the shape and exits non-zero instead of writing it somewhere else.
Run --check in CI to fail on a declaration nobody regenerated. See DTO schema for the
declaration syntax and the shape of the generated class.
migrate:service-map Since 2.4
Write the #[ServiceMap] attribute for every pillar accessor still resolved from a @method
docblock, across the whole project in one run. That resolution is deprecated in 2.x and removed in 3.0. The runtime
notice only fires for accessors a run reaches, so a migration driven by notices covers only the code paths your tests
run.
vendor/bin/gacela migrate:service-map [<filter>] [--dry-run]filter: only files whose path contains this text--dry-run: report what would change and write nothing
The command adds only the attribute and, when missing, its import. Nothing else in the file moves, and a second run
changes nothing. It leaves alone an accessor whose @method type is not a single class name (A|B, ?A, array<A>,
self).