403c682f08c82664c17b92af013d8340ded89e33
[civicrm-core.git] / CRM / Extension / ClassLoader.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 * Class CRM_Extension_ClassLoader
14 */
15 class CRM_Extension_ClassLoader {
16
17 /**
18 * List of class-loader features that are valid in this version of Civi.
19 *
20 * This may be useful for some extensions which enable/disable polyfills based on environment.
21 */
22 const FEATURES = ',psr0,psr4,';
23
24 /**
25 * @var CRM_Extension_Mapper
26 */
27 protected $mapper;
28
29 /**
30 * @var CRM_Extension_Container_Interface
31 */
32 protected $container;
33
34 /**
35 * @var CRM_Extension_Manager
36 */
37 protected $manager;
38
39 /**
40 * @var \Composer\Autoload\ClassLoader
41 */
42 protected $loader;
43
44 /**
45 * CRM_Extension_ClassLoader constructor.
46 * @param \CRM_Extension_Mapper $mapper
47 * @param \CRM_Extension_Container_Interface $container
48 * @param \CRM_Extension_Manager $manager
49 */
50 public function __construct(\CRM_Extension_Mapper $mapper, \CRM_Extension_Container_Interface $container, \CRM_Extension_Manager $manager) {
51 $this->mapper = $mapper;
52 $this->container = $container;
53 $this->manager = $manager;
54 }
55
56 public function __destruct() {
57 $this->unregister();
58 }
59
60 /**
61 * Registers this instance as an autoloader.
62 * @return CRM_Extension_ClassLoader
63 */
64 public function register() {
65 // In pre-installation environments, don't bother with caching.
66 $cacheFile = (defined('CIVICRM_DSN') && !defined('CIVICRM_TEST') && !\CRM_Utils_System::isInUpgradeMode())
67 ? $this->getCacheFile() : NULL;
68
69 if (file_exists($cacheFile)) {
70 [$classLoader, $mixinLoader, $bootCache] = require $cacheFile;
71 $cacheUpdate = NULL;
72 }
73 else {
74 $classLoader = $this->buildClassLoader();
75 $mixinLoader = (new CRM_Extension_MixinScanner($this->mapper, $this->manager, $cacheFile !== NULL))->createLoader();
76 $bootCache = new CRM_Extension_BootCache();
77 // We don't own Composer\Autoload\ClassLoader, so we clone to prevent register() from potentially leaking data.
78 // We do own MixinLoader, and we want its state - like $bootCache - to be written.
79 $cacheUpdate = $cacheFile ? [clone $classLoader, clone $mixinLoader, $bootCache] : NULL;
80 }
81
82 $classLoader->register();
83 $mixinLoader->run($bootCache);
84
85 if ($cacheUpdate !== NULL) {
86 // Save cache after $mixinLoader has a chance to fill $bootCache.
87 $export = var_export(serialize($cacheUpdate), 1);
88 file_put_contents($cacheFile, sprintf("<?php\nreturn unserialize(%s);", $export));
89 }
90
91 $this->loader = $classLoader;
92 return $classLoader;
93 }
94
95 /**
96 * @return \Composer\Autoload\ClassLoader
97 * @throws \CRM_Extension_Exception
98 * @throws \Exception
99 */
100 public function buildClassLoader() {
101 $loader = new \Composer\Autoload\ClassLoader();
102
103 $statuses = $this->manager->getStatuses();
104 foreach ($statuses as $key => $status) {
105 if ($status !== CRM_Extension_Manager::STATUS_INSTALLED) {
106 continue;
107 }
108 self::loadExtension($loader, $this->mapper->keyToInfo($key), $this->mapper->keyToBasePath($key));
109 }
110
111 return $loader;
112 }
113
114 public function unregister() {
115 if ($this->loader) {
116 $this->loader->unregister();
117 $this->loader = NULL;
118 }
119 }
120
121 public function refresh() {
122 $this->unregister();
123 $file = $this->getCacheFile();
124 if (file_exists($file)) {
125 unlink($file);
126 }
127 $this->register();
128 }
129
130 /**
131 * Add a newly installed extension to the active classloader.
132 *
133 * NOTE: This is intended for use by CRM/Extension subsystem during installation.
134 *
135 * @param \CRM_Extension_Info $info
136 * @param string $path
137 */
138 public function installExtension(CRM_Extension_Info $info, string $path): void {
139 $file = $this->getCacheFile();
140 if (file_exists($file)) {
141 unlink($file);
142 }
143 if ($this->loader) {
144 self::loadExtension($this->loader, $info, $path);
145 }
146 }
147
148 /**
149 * Read the extension metadata configure a classloader.
150 *
151 * @param \Composer\Autoload\ClassLoader $loader
152 * @param \CRM_Extension_Info $info
153 * @param string $path
154 */
155 private static function loadExtension(\Composer\Autoload\ClassLoader $loader, CRM_Extension_Info $info, string $path): void {
156 if (!empty($info->classloader)) {
157 foreach ($info->classloader as $mapping) {
158 switch ($mapping['type']) {
159 case 'psr0':
160 $loader->add($mapping['prefix'], CRM_Utils_File::addTrailingSlash($path . '/' . $mapping['path']));
161 break;
162
163 case 'psr4':
164 $loader->addPsr4($mapping['prefix'], $path . '/' . $mapping['path']);
165 break;
166 }
167 }
168 }
169 }
170
171 /**
172 * @return string
173 */
174 protected function getCacheFile() {
175 $formatRev = '_2';
176 $envId = \CRM_Core_Config_Runtime::getId() . $formatRev;
177 $file = \Civi::paths()->getPath("[civicrm.compile]/CachedExtLoader.{$envId}.php");
178 return $file;
179 }
180
181 }