What bin/cake plugin list Actually Tells You.
During a CakePHP 4 to 5 upgrade on a long-running application, I needed to review which of our thirty-something
plugins were actually loaded, which were CLI-only, and which were meant to be absent from production.
Application.php had a wall of addPlugin() calls accumulated over years, while composer.json had packages
nobody was sure were still used or were part of obsolete features and the only way to reconcile the two was
opening both files side by side and cross-referencing by hand.
CakePHP 5.1 shipped a command that ends that exercise, and using it on a real app running both plugin-loading mechanisms side by side also surfaced a bigger shift than the command itself: a new way to load and set up plugins in CakePHP 5.
The problem: plugins live in two places, and they drift
In CakePHP 4, "is this plugin loaded" meant reading Application::bootstrap() top to bottom, evaluating every if (PHP_SAPI === 'cli') and if (Configure::read('debug')) guard around it, and hoping nothing was wrapped in a
condition three screens away from the addPlugin() call it protected.
"Is this plugin installed" was a separate question answered by composer.json. The two lists were never guaranteed
to match, and a mismatch isn't automatically a problem, but on an app old enough to have had several maintainers,
there was no reliable way to tell which mismatches mattered without comparing both files by hand.
bin/cake plugin list, added in CakePHP 5.1, answers both questions in one table.
What the command actually shows
| Plugin | Is Loaded | Only Debug | Only CLI | Optional | Version |
|---|---|---|---|---|---|
| CakeDC/Money | — | — | — | — | 2.0.5 |
| Authentication | — | — | — | — | 3.3.7 |
| Authorization | — | — | — | — | 3.5.3 |
| Bake | ✅ | — | ✅ | ✅ | 3.8.1 |
| CakeDC/DbTest | ✅ | ✅ | — | ✅ | 3.0.2 |
| DebugKit | ✅ | ✅ | — | ✅ | 5.2.4 |
| Migrations | ✅ | — | ✅ | — | 5.2.6 |
| Reports | ✅ | — | — | — | 1.4.0 |
That's a trimmed version of the real output on the app I was working on. The first row is worth stopping on:
CakeDC/Money is installed, has a version, and is not loaded anywhere. Grepping src/, templates/, plugins
and config/ for the package's namespace turned up nothing, a dependency nobody remembered adding, still sitting
in composer.json. That's exactly the kind of drift this command exists to catch.
Not every blank in that column means dead weight, though.
Authentication and Authorization show up unloaded on the same app, and they are anything but unused: half a
dozen classes extend Authentication\Authenticator\AbstractAuthenticator and
Authentication\Identifier\AbstractIdentifier, and AuthenticationMiddleware sits in the middleware queue. Neither
package was ever passed to addPlugin(), because neither needs to be. The addPlugin() method registers a plugin's
routes,
templates and bootstrap hooks, and packages like Authentication and Authorization ship none of those. You consume
them as plain PHP classes through Composer's autoloader instead.
Is Loaded tells you whether a package was registered as a CakePHP plugin object, not whether its code runs anywhere.
Before acting on anything the command flags, grep the package's namespace across src/, templates/, plugins
and config/ first. CakeDC/Money earned the removal; Authentication did not.
The bigger change: plugin loading became declarative
The Only Debug, Only CLI and Optional columns are a direct readout of a new configuration format,
and understanding where they come from is the actual payoff of running this command during an upgrade.
In CakePHP 4, gating a plugin was imperative code you wrote yourself:
// CakePHP 4 — src/Application.php
public function bootstrap(): void
{
parent::bootstrap();
if (PHP_SAPI === 'cli') {
$this->addPlugin('Bake');
$this->addPlugin('Migrations');
}
if (Configure::read('debug')) {
$this->addOptionalPlugin('DebugKit');
}
$this->addPlugin('CakeDC/Users');
$this->addPlugin('Reports');
}
Every gate was a hand-written if, and whether a missing plugin was fatal depended on whether you remembered to use
addOptionalPlugin() instead of addPlugin(). Nothing enumerated these rules anywhere; you inferred them by reading
code.
CakePHP 5 replaced this with config/plugins.php, a plain array that Cake\Http\BaseApplication::bootstrap() loads
automatically, before your own Application::bootstrap() body runs:
// config/plugins.php
return [
'Bake' => ['onlyCli' => true, 'optional' => true],
'Migrations' => ['onlyCli' => true],
'DebugKit' => ['onlyDebug' => true, 'optional' => true],
'CakeDC/DbTest' => ['onlyDebug' => true, 'optional' => true],
];
onlyDebug, onlyCli and optional are declared, not scattered through conditionals, and they are precisely the
three columns bin/cake plugin list prints.
Both mechanisms can coexist by design
Adopting the new declarative style is recommended, but nothing forces every addPlugin() call to move to
config/plugins.php, both can coexist at the same time:
public function bootstrap(): void
{
parent::bootstrap();
$this->addPlugin('CakeDC/Users');
$this->addPlugin('CakeDC/Admin');
$this->addPlugin('Reports');
$this->addPlugin('Notifications');
}
// config/plugins.php
return [
'Bake' => ['onlyCli' => true, 'optional' => true],
'Migrations' => ['onlyCli' => true],
'DebugKit' => ['onlyDebug' => true, 'optional' => true],
'CakeDC/DbTest' => ['onlyDebug' => true, 'optional' => true],
'IdeHelper' => ['onlyDebug' => true, 'optional' => true],
];
The tooling plugins are exactly the ones where a wrong load in production matters (nobody wants
DebugKit's toolbar on a live site, or Bake console commands bundled into a deploy that skips --no-dev), so those are
the ones that most benefit from the safety of declarative, testable configuration.
CakePHP 5 supports this split on purpose:
PluginConfig::getInstalledPlugins() merges both sources before the command ever renders a row, so bin/cake plugin list audits the effective state regardless of which mechanism loaded each plugin.
One detail worth knowing before you lean on optional the way the CakePHP core team does for Bake and DebugKit:
check whether the underlying Composer package lives in require or require-dev. cakephp/migrations was in
require on our app deliberately, since migrations need to run from a production deploy step, so it got
onlyCli without optional. cakephp/debug_kit and dereuromark/cakephp-ide-helper were in require-dev, so they
got both onlyDebug and optional: without optional, a --no-dev production install would throw
MissingPluginException the moment the app tried to boot, because the package simply isn't there. onlyDebug alone
doesn't save you from that, it only stops the plugin from loading, it doesn't stop the app from throwing if the
config still lists it and the package is missing.
What can you do in your application?
- Run
bin/cake plugin liston your CakePHP 5 app. - Anything with a version and no
Xin "Is Loaded" is worth a second look, grep its namespace acrosssrc/,templates/,pluginsandconfig/first, before runningcomposer remove. Some packages (Authentication, Authorization) are consumed as plain classes and never get loaded as a plugin, so a blank there isn't proof of dead weight on its own. - For anything still gated by
if (PHP_SAPI === 'cli')orif (Configure::read('debug'))inApplication.php, check whether its package isrequireorrequire-dev, then move it toconfig/plugins.phpwith the matchingonlyCli/onlyDebug/optionalcombination. - You can leave the rest. You don't need to migrate every plugin in one sitting to get value from the command.
If your CakePHP application has accumulated this kind of drift over several years and several maintainers, plugins nobody's sure are still needed, loading logic nobody wants to touch, an upgrade to CakePHP 5 that keeps getting pushed back, this is exactly the kind of work CakeDC does. Get in touch and let's talk about where your project stands and how to move it forward.
Further reading
- [CakePHP 5.1 Migration Guide] (https://book.cakephp.org/5.x/appendices/5-1-migration-guide.html)
- [CakePHP Plugins Cookbook] (https://book.cakephp.org/5.x/en/plugins.html)
- [
Cake\Core\PluginConfigsource] (https://github.com/cakephp/cakephp/blob/5.x/src/Core/PluginConfig.php)