Civi v4.7 introduces some overhauls to the core CiviCRM development framework. Some of the planning discussions can be found in the forum, but now that it's merged and stablized a bit, I wanted to give a walk-through for other developers. A few highlights:
-
Core service objects (such as the settings manager, logger, and JS/CSS resource manager) should be accessed through a new facade, simply named
Civi
. A few examples of using this facade:
Civi::log()->info('Hello, log!'); Civi::settings()->get('versionCheck'); Civi::service('civi_api_kernel')->getEntityNames(3); Civi::resources()->addScriptFile('org.example.mymodule', 'ex.js'); Civi::$statics[__CLASS__]['tmpCacheData'] = ...;
-
All remaining settings from
civicrm_domain.config_backend
have been migrated to the settings framework. These may be modified, inspected, and overridden using the settings framework. -
Path and URL settings may include path-variables, such as
[civicrm.files]
or[cms.root]
. This simplifies migration/redeployment. - Hook functions are cached to avoid redundant scans.
The remainder of this article will explore the details in more depth.
In-depth: Statics, singletons, containers, and the Civi
facade
Among developers, a lot of ink (and even more bytes, ad nauseum) have been spent discussing singletons, static functions/variables, global functions/variables, containers, and dependency-injection. I'm not looking to re-state that discussion here, but the Civi
facade is a response to that debate.
The Civi
facade provides access to the core services of the Civi framework. For example, to a write a log message, one might lookup the log service and call the error(), warning(), or info() function:
Civi::log()->info('Hello, log!');
Civi::log()
is a simple, one-line function that locates and returns the log service -- and nearly every function in Civi::
follows that same pattern. If you've worked with Drupal 8, the Drupal
facade is very similar.
Of course, syntactically, log()
looks like a static function -- and static functions are bad, right? Sort of. A function like CRM_Core_Error::debug_log_message()
is bad because:
-
(In)flexibility: It's impossible to swap out the log implementation (without either patching core or replacing the entire
CRM/Core/Error.php
). -
(Un)testability: It's complicated to reset any hidden state (eg static variables) used by
CRM_Core_Error
.
In v4.5+, we have a dependency injection container, so we can skip these problems, right? We could use the container to inject the log
as-needed. However, this comes with a problem: there are hundreds of thousands of lines of code which don't currently use the container. Moreover, third-party developers write custom scripts and integrations which aren't part of the container. The container cannot inject log
in these cases. Instead, you have to lookup the service:
Civi\Core\Container::singleton()->get('log')
Unfortunately, that notation is also problematic. It's hard to remember and discover service names (log
vs logs
vs logger
vs psr_log
vs civi_system_log
), and the returned item has no obvious class or interface -- so an IDE cannot provide typehints or drilldown. And it's pretty verbose.
The Civi
facade balances these interests:
-
In older code and external code, one accesses the log via the
Civi
facade. The facade has typehints and documentation, and it's fairly pithy. - In newer container-aware code, one injects the log service.
- The container and the facade provide exactly the same log service.
-
In testing, all the services and statics (whether used via facade or container) can be reset at once (by clearing
Civi::$statics
).
Of course, there are several services like log()
. The following sections will drill-down on a few of the most important ones, comparing old code patterns (based on statics/singletons) and new patterns (based on the facade). The old patterns will still work - but should be viewed as deprecated.
Please note that, in terms of support and compatibility, the Civi
facade should be regarded as an API -- we should be conscientious about adding, changing, removing, and documenting functions in the facade.
In-depth: Civi::log()
// Old pattern
CRM_Core_Error::debug_log_message("Egads!");
CRM_Core_Error::debug_var('something_bad_with_var', $var);
// New pattern
Civi::log()->error("Egads");
Civi::log()->error("There is something bad with {var}", array(
'var' => $var,
));
Notes:
-
The old pattern logged all messages as errors. The new pattern admits different log levels (e.g.
warning()
,notice()
,debug()
). - The log service is based on the standardized PSR-3 Logger Interface.
- The current implementation uses Civi's existing log file, but it could be swapped with any other PSR-3-compliant logger.
-
When putting in data, use the
{var}
notation instead of literally plugging in the$var
variable. This pretty-prints the variable, and (in the future) it will allow the message to be translated/localized.
In-depth: Civi::settings()
// Old pattern
CRM_Core_BAO_Setting::getItem(
CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
'versionCheck', NULL, 1)
// New pattern
Civi::settings()->get('versionCheck')
Notes:
-
The
Civi::settings()
helper provides access to all settings in the current domain. - The old function accepted group-name and component_id, but these were not consistently or reliably used, and the name effectively needed to be unique. The new function simplifies. If you must access the group-name/component_id, these are still available in the settings metadata.
- Default values are loaded from the setting metadata.
- To avoid the overhead of reloading/rescanning the full setting metadata on every page-request, the default values are retained in a cache.
In-depth: Civi::service($id)
// Old pattern
Civi\Core\Container::singleton()
->get('civi_api_kernel')
->getEntityNames(3);
// New pattern
Civi::service('civi_api_kernel')
->getEntityNames(3);
For core, documented, well-supported services like logs and settings, it's preferrable to use a static function (Civi::log()
or Civi::settings()
). However, if you need to lookup some alternative service (perhaps an esoteric, limited-purpose, or undocumented service), then you can find it by name.
In-depth Civi::resources()
// Old pattern
CRM_Core_Resources::singleton()->addScriptFile('org.example.mymodule', 'ex.js');
// New pattern
Civi::resources()->addScriptFile('org.example.mymodule', 'ex.js');
No functional change -- just a cleaner notation.
In-depth: Civi::$statics
// Old pattern
public static function foo() {
static $mydata;
if ($mydata === NULL) {
$mydata = ...;
}
}
// New pattern
public static function foo() {
if (!isset(Civi::$statics[__CLASS__]['mydata'])) {
Civi::$statics[__CLASS__]['mydata'] = ...;
}
}
A common optimization is to temporarily cache data in a static variable. The static variable will be retained for the length of the current page-request -- and then resets automatically.
In unit-testing, one must explicitly reset static variables to ensure consistent/predictable results, but resetting static variables is either manual or slow when they're distributed helterskelter. Instead, use Civi::$statics
. This is slightly more verbose, but it's faster and simpler in unit-testing.
In-depth: CRM_Core_Config
as facade
In previous versions, the $config
object was constructed by merging data from:
-
CRM_Core_Config
,CRM_Core_Config_Variables
,CRM_Core_Config_Defaults
-
define()
d constants and server properties -
civicrm_domain.config_backend
-
civicrm_setting
- Various bits of filter/helper logic
The $config
object is used quite pervasively and must be maintained for compatibility. However, to ensure that we properly migrated all data from civicrm_domain.config_backend
to civicrm_setting
, we must be able to definitively audit all the properties in $config
.
In v4.7, this has been reworked. $config
still mixes data from multiple sources, but there's a map of all properties which describes where each piece of data comes from: CRM_Core_Config_MagicMerge::getPropertyMap()
In-depth: Global $civicrm_setting
The global variable $civicrm_setting
is used to override CiviCRM settings. v4.7 should be backward compatible, but if you manipulate $civicrm_setting
in an extension or module, then check out the change log:
https://docs.civicrm.org/dev/en/latest/api/changes/#470-global-civicrm_setting-multiple-changes
In-depth: hook_civicrm_container
/**
* @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
*/
function example_civicrm_container($container) {
$container->addResource(new \Symfony\Component\Config\Resource\FileResource(__FILE__));
$container->findDefinition('dispatcher')->addMethodCall('addListener',
array(\Civi\Token\Events::TOKEN_REGISTER, 'example_register_tokens')
));
}
Civi v4.7 allows extensions and modules to add or modify services in the container. For more discussion about the concepts and functions in ContainerBuilder $container
object, check the documentation for the Symfony DependencyInjection Component.
Note: I don't expect this interface to see a lot of usage right now -- we need more polished examples of working with $container (e.g. loading YAML files and annotations) and, for the moment, it only has a few specific use-cases (e.g. manipulating the API kernel and the schedule-reminder system). However, I expect we'll see more interesting things come from this in future versions.
Comments
Thanks for the insightful article, Tim. It's a very much welcome change to the core, thanks to everyone who put effort into this!
I think links to the new classes on Github would be quite useful, though.
I like this. Has a Joomla-esque feel.