CRM-17957 - Allow extensions to register with PHP classloader (PSR-4)
authorTim Otten <totten@civicrm.org>
Wed, 3 Feb 2016 03:16:54 +0000 (20:16 -0700)
committerTim Otten <totten@civicrm.org>
Wed, 3 Feb 2016 03:47:25 +0000 (20:47 -0700)
For example, in info.xml, add:

```
  <classloader>
    <psr4 prefix="Civi\Foo\Bar\" path="src"  />
  </classloader>
```

CRM/Extension/ClassLoader.php [new file with mode: 0644]
CRM/Extension/Info.php
CRM/Extension/System.php
Civi/Core/Container.php

diff --git a/CRM/Extension/ClassLoader.php b/CRM/Extension/ClassLoader.php
new file mode 100644 (file)
index 0000000..7097d8d
--- /dev/null
@@ -0,0 +1,132 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.7                                                |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2015                                |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM.                                    |
+ |                                                                    |
+ | CiviCRM is free software; you can copy, modify, and distribute it  |
+ | under the terms of the GNU Affero General Public License           |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception.   |
+ |                                                                    |
+ | CiviCRM is distributed in the hope that it will be useful, but     |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of         |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.               |
+ | See the GNU Affero General Public License for more details.        |
+ |                                                                    |
+ | You should have received a copy of the GNU Affero General Public   |
+ | License and the CiviCRM Licensing Exception along                  |
+ | with this program; if not, contact CiviCRM LLC                     |
+ | at info[AT]civicrm[DOT]org. If you have questions about the        |
+ | GNU Affero General Public License or the licensing of CiviCRM,     |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing        |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ *
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC (c) 2004-2015
+ * $Id$
+ *
+ */
+class CRM_Extension_ClassLoader {
+
+  /**
+   * @var CRM_Extension_Mapper
+   */
+  protected $mapper;
+
+  /**
+   * @var CRM_Extension_Container_Interface
+   */
+  protected $container;
+
+  /**
+   * @var CRM_Extension_Manager
+   */
+  protected $manager;
+
+  /**
+   * @var \Composer\Autoload\ClassLoader
+   */
+  protected $loader;
+
+  /**
+   * CRM_Extension_ClassLoader constructor.
+   * @param \CRM_Extension_Mapper $mapper
+   * @param \CRM_Extension_Container_Interface $container
+   * @param \CRM_Extension_Manager $manager
+   */
+  public function __construct(\CRM_Extension_Mapper $mapper, \CRM_Extension_Container_Interface $container, \CRM_Extension_Manager $manager) {
+    $this->mapper = $mapper;
+    $this->container = $container;
+    $this->manager = $manager;
+  }
+
+  public function __destruct() {
+    if ($this->loader) {
+      $this->loader->unregister();
+      $this->loader = NULL;
+    }
+  }
+
+  /**
+   * Registers this instance as an autoloader.
+   * @return $this
+   */
+  public function register() {
+    // In pre-installation environments, don't bother with caching.
+    if (!defined('CIVICRM_TEMPLATE_COMPILEDIR') || !defined('CIVICRM_DSN') || \CRM_Utils_System::isInUpgradeMode()) {
+      return $this->buildClassLoader()->register();
+    }
+
+    $envId = \CRM_Core_Config_Runtime::getId();
+    $file = CIVICRM_TEMPLATE_COMPILEDIR . "/CachedExtLoader.{$envId}.php";
+    if (file_exists($file)) {
+      $loader = require $file;
+    }
+    else {
+      $loader = $this->buildClassLoader();
+      $ser = serialize($loader);
+      file_put_contents($file,
+        sprintf("<?php\nreturn unserialize(%s);", var_export($ser, 1))
+      );
+    }
+    return $loader->register();
+  }
+
+  /**
+   * @return \Composer\Autoload\ClassLoader
+   * @throws \CRM_Extension_Exception
+   * @throws \Exception
+   */
+  public function buildClassLoader() {
+    $loader = new \Composer\Autoload\ClassLoader();
+
+    $statuses = $this->manager->getStatuses();
+    foreach ($statuses as $key => $status) {
+      if ($status !== CRM_Extension_Manager::STATUS_INSTALLED) {
+        continue;
+      }
+      $path = $this->mapper->keyToBasePath($key);
+      $info = $this->mapper->keyToInfo($key);
+      if (!empty($info->classloader)) {
+        foreach ($info->classloader as $mapping) {
+          switch ($mapping['type']) {
+            case 'psr4':
+              $loader->setPsr4($mapping['prefix'], $path . '/' . $mapping['path']);
+              break;
+          }
+          $result[] = $mapping;
+        }
+      }
+    }
+
+    return $loader;
+  }
+
+}
index b5bf1049decda3c3edb56ad43662622cd696af6e..d0d8c94c051d4b2690d34485a8b50152d2d51200 100644 (file)
@@ -44,6 +44,13 @@ class CRM_Extension_Info {
   public $label = NULL;
   public $file = NULL;
 
+  /**
+   * @var array
+   *   Each item is a specification like:
+   *   array('type'=>'psr4', 'namespace'=>'Foo\Bar', 'path'=>'/foo/bar').
+   */
+  public $classloader = array();
+
   /**
    * Load extension info an XML file.
    *
@@ -124,6 +131,16 @@ class CRM_Extension_Info {
         }
         ksort($this->urls);
       }
+      elseif ($attr === 'classloader') {
+        $this->classloader = array();
+        foreach ($val->psr4 as $psr4) {
+          $this->classloader[] = array(
+            'type' => 'psr4',
+            'prefix' => (string) $psr4->attributes()->prefix,
+            'path' => (string) $psr4->attributes()->path,
+          );
+        }
+      }
       else {
         $this->$attr = CRM_Utils_XML::xmlObjToArray($val);
       }
index 9ffccd35e6c4c0d3052d8d51158b87ce326a633b..92e5c36e7c576387619e8adf24de7005b85faa8c 100644 (file)
@@ -43,6 +43,11 @@ class CRM_Extension_System {
   private $browser = NULL;
   private $downloader = NULL;
 
+  /**
+   * @var CRM_Extension_ClassLoader
+   * */
+  private $classLoader;
+
   /**
    * The URL of the remote extensions repository.
    *
@@ -182,6 +187,16 @@ class CRM_Extension_System {
     return $this->mapper;
   }
 
+  /**
+   * @return \CRM_Extension_ClassLoader
+   */
+  public function getClassLoader() {
+    if ($this->classLoader === NULL) {
+      $this->classLoader = new CRM_Extension_ClassLoader($this->getMapper(), $this->getFullContainer(), $this->getManager());
+    }
+    return $this->classLoader;
+  }
+
   /**
    * Get the service for enabling and disabling extensions.
    *
index 68404a6028ddd14ac9c05a42d1d3c3ad55841f0d..3a443f3759eb422fc2503693f5e4ab89100f5fbb 100644 (file)
@@ -379,6 +379,7 @@ class Container {
       \CRM_Core_DAO::init($runtime->dsn);
       \CRM_Utils_Hook::singleton(TRUE);
       \CRM_Extension_System::singleton(TRUE);
+      \CRM_Extension_System::singleton(TRUE)->getClassLoader()->register();
 
       $c = new self();
       \Civi::$statics[__CLASS__]['container'] = $c->loadContainer();