CRM-13244 - Civi\Core\Container - Allow hooks to modify container. Cache it.
authorTim Otten <totten@civicrm.org>
Thu, 23 Jul 2015 02:00:31 +0000 (19:00 -0700)
committerTim Otten <totten@civicrm.org>
Thu, 17 Sep 2015 22:45:00 +0000 (15:45 -0700)
Conflicts:
composer.json

CRM/Utils/Hook.php
CRM/Utils/System.php
Civi/Core/Container.php
composer.json
composer.lock
tests/phpunit/CiviTest/civicrm.settings.dist.php

index 048f8d3c71393fd98f6fc7095917c7aa0c941026..cf6c688d89f44f6661001e6a19a4dd50daa782e2 100644 (file)
@@ -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 <CRM_Core_FileSearchInterface> $fileSearches
    * @return mixed
index a285329d971032c8c304a57dcdd948f4e9ebee89..8505ba808f7f5ac6abae0c658a937dbc1cc37493 100644 (file)
@@ -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;
index dd9ca9a0d32bf1510e3ce8ef1ba1cc7033469386..d281b06eaae6d769d083149e2f3b8824212fc3a9 100644 (file)
@@ -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'));
index 39232d7fad68971696087fc43f1578d7d9c32d23..799f172e769703d7cd1ccd1f452982243337b1cb 100644 (file)
@@ -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"
   },
index 7450d256de22ba4afdaa07ac3be8ad5d58bcf2b8..bc00e51759e59b621c93b83bdafc314d059ba25d 100644 (file)
@@ -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",
             ],
             "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": {
             },
             "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": "",
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.3-dev"
+                    "dev-master": "2.5-dev"
                 }
             },
             "autoload": {
             ],
             "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": "",
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.3-dev"
+                    "dev-master": "2.5-dev"
                 }
             },
             "autoload": {
             ],
             "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": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.3-dev"
+                    "dev-master": "2.5-dev"
                 }
             },
             "autoload": {
             ],
             "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": {
             ],
             "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",
index e07d5394feb0ea7893403944836fbdaa6a4d2a78..616f0c69ddfb3b8000712135ae579f4507402190 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
 define('CIVICRM_TEST', 1);
+define('CIVICRM_CONTAINER_CACHE', 'auto');
 
 // This file is loaded on all systems running tests. To override settings on
 // your local system, please create "civicrm.settings.local.php" and put