From 40787e18834bb2aeac73434f7ba15da0155c14a5 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Wed, 22 Jul 2015 19:00:31 -0700 Subject: [PATCH] CRM-13244 - Civi\Core\Container - Allow hooks to modify container. Cache it. Conflicts: composer.json --- CRM/Utils/Hook.php | 25 +++ CRM/Utils/System.php | 2 +- Civi/Core/Container.php | 94 ++++++++-- composer.json | 11 +- composer.lock | 168 ++++++++++++++---- .../CiviTest/civicrm.settings.dist.php | 1 + 6 files changed, 249 insertions(+), 52 deletions(-) diff --git a/CRM/Utils/Hook.php b/CRM/Utils/Hook.php index 048f8d3c71..cf6c688d89 100644 --- a/CRM/Utils/Hook.php +++ b/CRM/Utils/Hook.php @@ -1894,6 +1894,31 @@ abstract class CRM_Utils_Hook { ); } + /** + * Modify the CiviCRM container - add new services, parameters, extensions, etc. + * + * @code + * use Symfony\Component\Config\Resource\FileResource; + * use Symfony\Component\DependencyInjection\Definition; + * + * function mymodule_civicrm_container($container) { + * $container->addResource(new FileResource(__FILE__)); + * $container->setDefinition('mysvc', new Definition('My\Class', array())); + * } + * @endcode + * + * Tip: The container configuration will be compiled/cached. The default cache + * behavior is aggressive. When you first implement the hook, be sure to + * flush the cache. Additionally, you should relax caching during development. + * In `civicrm.settings.php`, set define('CIVICRM_CONTAINER_CACHE', 'auto'). + * + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * @see http://symfony.com/doc/current/components/dependency_injection/index.html + */ + public static function container(\Symfony\Component\DependencyInjection\ContainerBuilder $container) { + self::singleton()->invoke(1, $container, self::$_nullObject, self::$_nullObject, self::$_nullObject, self::$_nullObject, self::$_nullObject, 'civicrm_container'); + } + /** * @param array $fileSearches * @return mixed diff --git a/CRM/Utils/System.php b/CRM/Utils/System.php index a285329d97..8505ba808f 100644 --- a/CRM/Utils/System.php +++ b/CRM/Utils/System.php @@ -1771,7 +1771,7 @@ class CRM_Utils_System { * @return bool */ public static function isInUpgradeMode() { - $args = explode('/', $_GET['q']); + $args = explode('/', CRM_Utils_Array::value('q', $_GET)); $upgradeInProcess = CRM_Core_Session::singleton()->get('isUpgradePending'); if ((isset($args[1]) && $args[1] == 'upgrade') || $upgradeInProcess) { return TRUE; diff --git a/Civi/Core/Container.php b/Civi/Core/Container.php index dd9ca9a0d3..d281b06eaa 100644 --- a/Civi/Core/Container.php +++ b/Civi/Core/Container.php @@ -9,10 +9,14 @@ use Doctrine\Common\Cache\FilesystemCache; use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\Driver\AnnotationDriver; use Doctrine\ORM\Tools\Setup; +use Symfony\Component\Config\ConfigCache; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Dumper\PhpDumper; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher; +use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; // TODO use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; @@ -37,20 +41,85 @@ class Container { public static function singleton($reset = FALSE) { if ($reset || self::$singleton === NULL) { $c = new self(); - self::$singleton = $c->createContainer(); + self::$singleton = $c->loadContainer(); } return self::$singleton; } /** + * Find a cached container definition or construct a new one. + * + * There are many weird contexts in which Civi initializes (eg different + * variations of multitenancy and different permutations of CMS/CRM bootstrap), + * and hook_container may fire a bit differently in each context. To mitigate + * risk of leaks between environments, we compute a unique envID + * (md5(DB_NAME, HTTP_HOST, SCRIPT_FILENAME, etc)) and use separate caches for + * each (eg "templates_c/CachedCiviContainer.$ENVID.php"). + * + * Constants: + * - CIVICRM_CONTAINER_CACHE -- 'always' [default], 'never', 'auto' + * - CIVICRM_DSN + * - CIVICRM_DOMAIN_ID + * - CIVICRM_TEMPLATE_COMPILEDIR + * + * @return ContainerInterface + */ + public function loadContainer() { + // Note: The container's raison d'etre is to manage construction of other + // services. Consequently, we assume a minimal service available -- the classloader + // has been setup, and civicrm.settings.php is loaded, but nothing else works. + + $cacheMode = defined('CIVICRM_CONTAINER_CACHE') ? CIVICRM_CONTAINER_CACHE : 'always'; + + // In pre-installation environments, don't bother with caching. + if (!defined('CIVICRM_TEMPLATE_COMPILEDIR') || !defined('CIVICRM_DSN') || $cacheMode === 'never' || \CRM_Utils_System::isInUpgradeMode()) { + return $this->createContainer(); + } + + $envId = md5(implode(\CRM_Core_DAO::VALUE_SEPARATOR, array( + defined('CIVICRM_DOMAIN_ID') ? CIVICRM_DOMAIN_ID : 1, // e.g. one database, multi URL + parse_url(CIVICRM_DSN, PHP_URL_PATH), // e.g. one codebase, multi database + \CRM_Utils_Array::value('SCRIPT_FILENAME', $_SERVER, ''), // e.g. CMS vs extern vs installer + \CRM_Utils_Array::value('HTTP_HOST', $_SERVER, ''), // e.g. name-based vhosts + \CRM_Utils_Array::value('SERVER_PORT', $_SERVER, ''), // e.g. port-based vhosts + // Depending on deployment arch, these signals *could* be redundant, but who cares? + ))); + $file = CIVICRM_TEMPLATE_COMPILEDIR . "/CachedCiviContainer.{$envId}.php"; + $containerConfigCache = new ConfigCache($file, $cacheMode === 'auto'); + + if (!$containerConfigCache->isFresh()) { + $containerBuilder = $this->createContainer(); + $containerBuilder->compile(); + $dumper = new PhpDumper($containerBuilder); + $containerConfigCache->write( + $dumper->dump(array('class' => 'CachedCiviContainer')), + $containerBuilder->getResources() + ); + } + + require_once $file; + $c = new \CachedCiviContainer(); + $c->set('service_container', $c); + return $c; + } + + /** + * Construct a new container. + * * @var ContainerBuilder * @return \Symfony\Component\DependencyInjection\ContainerBuilder */ public function createContainer() { $civicrm_base_path = dirname(dirname(__DIR__)); $container = new ContainerBuilder(); + $container->addCompilerPass(new RegisterListenersPass('dispatcher')); + $container->addObjectResource($this); $container->setParameter('civicrm_base_path', $civicrm_base_path); - $container->set(self::SELF, $this); + //$container->set(self::SELF, $this); + $container->setDefinition(self::SELF, new Definition( + 'Civi\Core\Container', + array() + )); // TODO Move configuration to an external file; define caching structure // if (empty($configDirectories)) { @@ -69,36 +138,36 @@ class Container { // } $container->setDefinition('lockManager', new Definition( - '\Civi\Core\Lock\LockManager', + 'Civi\Core\Lock\LockManager', array() )) ->setFactoryService(self::SELF)->setFactoryMethod('createLockManager'); $container->setDefinition('angular', new Definition( - '\Civi\Angular\Manager', + 'Civi\Angular\Manager', array() )) ->setFactoryService(self::SELF)->setFactoryMethod('createAngularManager'); $container->setDefinition('dispatcher', new Definition( - '\Symfony\Component\EventDispatcher\EventDispatcher', - array() + 'Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher', + array(new Reference('service_container')) )) ->setFactoryService(self::SELF)->setFactoryMethod('createEventDispatcher'); $container->setDefinition('magic_function_provider', new Definition( - '\Civi\API\Provider\MagicFunctionProvider', + 'Civi\API\Provider\MagicFunctionProvider', array() )); $container->setDefinition('civi_api_kernel', new Definition( - '\Civi\API\Kernel', + 'Civi\API\Kernel', array(new Reference('dispatcher'), new Reference('magic_function_provider')) )) ->setFactoryService(self::SELF)->setFactoryMethod('createApiKernel'); $container->setDefinition('cxn_reg_client', new Definition( - '\Civi\Cxn\Rpc\RegistrationClient', + 'Civi\Cxn\Rpc\RegistrationClient', array() )) ->setFactoryClass('CRM_Cxn_BAO_Cxn')->setFactoryMethod('createRegistrationClient'); @@ -135,6 +204,8 @@ class Container { ->setFactoryClass($class)->setFactoryMethod('singleton'); } + \CRM_Utils_Hook::container($container); + return $container; } @@ -146,10 +217,11 @@ class Container { } /** + * @param ContainerInterface $container * @return \Symfony\Component\EventDispatcher\EventDispatcher */ - public function createEventDispatcher() { - $dispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher(); + public function createEventDispatcher($container) { + $dispatcher = new ContainerAwareEventDispatcher($container); $dispatcher->addListener('hook_civicrm_post::Activity', array('\Civi\CCase\Events', 'fireCaseChange')); $dispatcher->addListener('hook_civicrm_post::Case', array('\Civi\CCase\Events', 'fireCaseChange')); $dispatcher->addListener('hook_civicrm_caseChange', array('\Civi\CCase\Events', 'delegateToXmlListeners')); diff --git a/composer.json b/composer.json index 39232d7fad..799f172e76 100644 --- a/composer.json +++ b/composer.json @@ -8,11 +8,12 @@ }, "require": { "dompdf/dompdf" : "0.6.*", - "symfony/dependency-injection": "2.3.*", - "symfony/event-dispatcher": "2.3.*", - "symfony/process": "2.3.*", - "psr/log": "~1.0.0", - "symfony/finder": "2.3.*", + "symfony/config": "~2.5.0", + "symfony/dependency-injection": "~2.5.0", + "symfony/event-dispatcher": "~2.5.0", + "symfony/process": "~2.5.0", + "psr/log": "1.0.0", + "symfony/finder": "~2.5.0", "totten/ca-config": "~13.02", "civicrm/civicrm-cxn-rpc": "~0.15.07.27" }, diff --git a/composer.lock b/composer.lock index 7450d256de..bc00e51759 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "94145c3d8822e929bea514a67dd54f15", + "hash": "7ae864fa67ed95c56a70091935a19c37", "packages": [ { "name": "civicrm/civicrm-cxn-rpc", @@ -257,19 +257,67 @@ ], "time": "2012-12-21 11:40:51" }, + { + "name": "symfony/config", + "version": "v2.5.12", + "target-dir": "Symfony/Component/Config", + "source": { + "type": "git", + "url": "https://github.com/symfony/Config.git", + "reference": "c7309e33b719433d5cf3845d0b5b9608609d8c8e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/Config/zipball/c7309e33b719433d5cf3845d0b5b9608609d8c8e", + "reference": "c7309e33b719433d5cf3845d0b5b9608609d8c8e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "symfony/filesystem": "~2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.5-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\Component\\Config\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Symfony Config Component", + "homepage": "http://symfony.com", + "time": "2015-01-03 08:01:13" + }, { "name": "symfony/dependency-injection", - "version": "v2.3.23", + "version": "v2.5.12", "target-dir": "Symfony/Component/DependencyInjection", "source": { "type": "git", "url": "https://github.com/symfony/DependencyInjection.git", - "reference": "f165ee0e0b3522b5158def22622b2f171a8ecd59" + "reference": "c42aee05b466cc9c66b87ddf7d263402befb6962" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/DependencyInjection/zipball/f165ee0e0b3522b5158def22622b2f171a8ecd59", - "reference": "f165ee0e0b3522b5158def22622b2f171a8ecd59", + "url": "https://api.github.com/repos/symfony/DependencyInjection/zipball/c42aee05b466cc9c66b87ddf7d263402befb6962", + "reference": "c42aee05b466cc9c66b87ddf7d263402befb6962", "shasum": "" }, "require": { @@ -277,7 +325,8 @@ }, "require-dev": { "symfony/config": "~2.2", - "symfony/yaml": "~2.0" + "symfony/expression-language": "~2.4,>=2.4.10", + "symfony/yaml": "~2.1" }, "suggest": { "symfony/config": "", @@ -287,7 +336,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } }, "autoload": { @@ -311,28 +360,31 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "http://symfony.com", - "time": "2014-12-02 19:42:47" + "time": "2015-01-25 04:37:39" }, { "name": "symfony/event-dispatcher", - "version": "v2.3.23", + "version": "v2.5.12", "target-dir": "Symfony/Component/EventDispatcher", "source": { "type": "git", "url": "https://github.com/symfony/EventDispatcher.git", - "reference": "36a40695d94e948d7a85347db0b12ba446c400fa" + "reference": "af6eb6a9a1a3b411facfd8e7e3f82a6be7919c04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/36a40695d94e948d7a85347db0b12ba446c400fa", - "reference": "36a40695d94e948d7a85347db0b12ba446c400fa", + "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/af6eb6a9a1a3b411facfd8e7e3f82a6be7919c04", + "reference": "af6eb6a9a1a3b411facfd8e7e3f82a6be7919c04", "shasum": "" }, "require": { "php": ">=5.3.3" }, "require-dev": { - "symfony/dependency-injection": "~2.0" + "psr/log": "~1.0", + "symfony/config": "~2.0,>=2.0.5", + "symfony/dependency-injection": "~2.0,>=2.0.5,<2.6.0", + "symfony/stopwatch": "~2.3" }, "suggest": { "symfony/dependency-injection": "", @@ -341,7 +393,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } }, "autoload": { @@ -365,21 +417,70 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "http://symfony.com", - "time": "2014-11-30 13:33:44" + "time": "2015-01-29 18:20:43" + }, + { + "name": "symfony/filesystem", + "version": "v2.7.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/Filesystem.git", + "reference": "2d7b2ddaf3f548f4292df49a99d19c853d43f0b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/Filesystem/zipball/2d7b2ddaf3f548f4292df49a99d19c853d43f0b8", + "reference": "2d7b2ddaf3f548f4292df49a99d19c853d43f0b8", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Filesystem Component", + "homepage": "https://symfony.com", + "time": "2015-07-09 16:07:40" }, { "name": "symfony/finder", - "version": "v2.3.23", + "version": "v2.5.12", "target-dir": "Symfony/Component/Finder", "source": { "type": "git", "url": "https://github.com/symfony/Finder.git", - "reference": "d533aea3400dc463c4d0ba9c3ecf40bd80d49dbd" + "reference": "e527ebf47ff912a45e148b7d0b107b80ec0b3cc2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Finder/zipball/d533aea3400dc463c4d0ba9c3ecf40bd80d49dbd", - "reference": "d533aea3400dc463c4d0ba9c3ecf40bd80d49dbd", + "url": "https://api.github.com/repos/symfony/Finder/zipball/e527ebf47ff912a45e148b7d0b107b80ec0b3cc2", + "reference": "e527ebf47ff912a45e148b7d0b107b80ec0b3cc2", "shasum": "" }, "require": { @@ -388,7 +489,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } }, "autoload": { @@ -412,33 +513,30 @@ ], "description": "Symfony Finder Component", "homepage": "http://symfony.com", - "time": "2014-12-02 19:42:47" + "time": "2015-01-03 08:01:13" }, { "name": "symfony/process", - "version": "v2.3.28", + "version": "v2.5.12", "target-dir": "Symfony/Component/Process", "source": { "type": "git", "url": "https://github.com/symfony/Process.git", - "reference": "a8fe947ac58e081f8773e0d160807dcffbff7ed8" + "reference": "00a1308e8b5aec5eba7c8f1708426a78f929be8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Process/zipball/a8fe947ac58e081f8773e0d160807dcffbff7ed8", - "reference": "a8fe947ac58e081f8773e0d160807dcffbff7ed8", + "url": "https://api.github.com/repos/symfony/Process/zipball/00a1308e8b5aec5eba7c8f1708426a78f929be8c", + "reference": "00a1308e8b5aec5eba7c8f1708426a78f929be8c", "shasum": "" }, "require": { "php": ">=5.3.3" }, - "require-dev": { - "symfony/phpunit-bridge": "~2.7" - }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } }, "autoload": { @@ -452,17 +550,17 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" } ], "description": "Symfony Process Component", - "homepage": "https://symfony.com", - "time": "2015-05-01 14:06:45" + "homepage": "http://symfony.com", + "time": "2015-02-08 07:07:45" }, { "name": "totten/ca-config", diff --git a/tests/phpunit/CiviTest/civicrm.settings.dist.php b/tests/phpunit/CiviTest/civicrm.settings.dist.php index e07d5394fe..616f0c69dd 100644 --- a/tests/phpunit/CiviTest/civicrm.settings.dist.php +++ b/tests/phpunit/CiviTest/civicrm.settings.dist.php @@ -1,6 +1,7 @@