Config methods - markstory/cakephp GitHub Wiki
Configuration and dependency management are closely intertwined in CakePHP and most other web frameworks. Since CakePHP doesn't currently have a general purpose application wide DI container we've handled dependency injection/construction in a variety of ways:
- Many of the static facade classes had a
config()
method that allowed for configuration and DI. - Other classes used supplementary classes like
DATABASE_CONFIG
andEmailConfig
. - Some features use properties like
$components
,$helpers
,$tasks
and$actsAs
. These properties are generally used in application layers where the constructed objects have a very regular shape. The properties acts as a friendly DSL for doing dependency management and configuration. - Other settings are managed through
Configure
.
Earlier in the development of 3.0, I proposed and executed changes to move all non-property related configuration into Configure. This had some benefits but some serious drawbacks as well:
- Spooky action at a distance. Changing configuration in one class could (but not always) impact behavior in another class.
- Methods on Cache/Log did strange things. For example
Cache::drop()
didn't really drop the adapter unless you also removed the configure values. - Easy to get configuration out of sync. Editing Configure's data didn't and couldn't refresh the downstream objects. This makes the spooky action at a distance harder to understand.
- Tight coupling. Tons of code started being very coupled to the basically global symbols contained in Configure.
Returning back to config() methods
I think config/dependency injection methods have some nice advantages.
- Easy to understand.
- Always consistent, there is only one source of truth.
- Easy to directly inject constructed objects with minimal fuss.
- Fewer dependencies.
The role of the new config methods should follow a few guidelines to keep features consistent and keep separation from other types of configuration like that under the App.*
. Config methods should:
- Be used by classes that act as registries or facades for various adapters/implementations.
- Take a name and configuration data. This data is used to construct instances as needed.
- Take an instantiated object and use it.
- Take only a name to allow reading configuration data back out.
- If an implementor accepts classnames they should be supplied as the
className
key. Implementors may allow other aliases for this value e.g.engine
. This value can either be a fully resolved classname or use Plugin.Name and be resolved withApp::classname()
. - Take an array of key => values to configure multiple adapters at once. This is important for integrating with configuration files.
- Have a corresponding
drop
method to remove adapters.
These guidelines give an interface that looks like:
<?php
config($name, $settings = null)
drop($name)
Some samples using Cache
as an example class:
<?php
// Toggle caching (Replaces Cache.disable in Configure).
Cache::disable();
Cache::enable();
Cache::enabled();
// Configure an adapter using data only.
Cache::config('short', [
'className' => 'File', // Could also be 'engine' for BC reasons?
'path' => TMP,
'duration' => '+10 minutes'
]);
// Inject an adapter
Cache::config('short', $myCache);
// Inject a factory method. This allows deferred construction
// of objects requiring specific initialization logic.
Cache::config('short', function () {
return new Cake\Cache\Adapter\FileCache();
});
// Configure multiple adapters at once.
Cache::config(Configure::read('Cache'));
// Read the configuration data for an adapter
Cache::config('short');
Once a name has been configured, attempting to reconfigure it will raise an exception. If you need to reconfigure an adapter, it must first be dropped and then reconfigured.
Classes that could use this interface
- Cache
- Log
- Email - Transports and email profiles should be handled separately with separate methods. Perhaps
config()
andconfigTransport()
. - ConnectionManager
- Error and Exception handling (kind of). These static classes can easily be replaced with instances.
Additional Examples
Email configuration would separate transports and delivery profiles. This would allow configuration to stay DRY and profiles could re-use defined transports:
<?php
// Configuring email (App/Config/email.php)
use Cake\Network\Email\Email;
// Add a transport, it would not be loaded/constructed until used.
Email::configTransport('gmail', [
'className' => 'Smtp',
'username' => 'user',
'password' => 'secret',
'host' => 'mail.google.com',
]);
Email::config('gmail', [
'from' => '[email protected]',
'transport' => 'gmail', // Points to the transport config name.
]);
Error and exception handling currently use Configure but really don't need to. Instead they could be replaced with instances and instance methods. This would both simplify testing and the clarity/simplicity in error handling. Error handling could instead look like:
<?php
// Configure error handler. (App/Config/error.php)
use Cake\Error\ErrorHandler;
use Cake\Error\ConsoleErrorHandler;
use Cake\Error\ExceptionHandler;
use Cake\Error\ConsoleExceptionHandler;
$options = [
'log' => true,
'level' => E_ALL & ~E_DEPRECATED
];
// Static class is no more.
if (php_sapi() == 'cli') {
$errorHandler = new ConsoleErrorHandler($options);
} else {
$errorHandler = new ErrorHandler($options);
}
$errorHandler->register();
$options = [
'log' => true,
'skipLog' => [],
'renderer' => 'Cake\Error\ExceptionRenderer',
];
if (php_sapi() == 'cli') {
$exceptionHandler = new ConsoleExceptionHandler($options);
} else {
$exceptionHandler = new ExceptionHandler($options);
}
$exceptionHandler->register();