Merge branch '5.51'
[civicrm-core.git] / setup / src / Setup.php
1 <?php
2 namespace Civi;
3
4 use Civi\Setup\Event\CheckAuthorizedEvent;
5 use Civi\Setup\Event\CheckRequirementsEvent;
6 use Civi\Setup\Event\CheckInstalledEvent;
7 use Civi\Setup\UI\Event\UIConstructEvent;
8 use Civi\Setup\Event\InitEvent;
9 use Civi\Setup\Event\InstallDatabaseEvent;
10 use Civi\Setup\Event\InstallFilesEvent;
11 use Civi\Setup\Event\UninstallDatabaseEvent;
12 use Civi\Setup\Event\UninstallFilesEvent;
13 use Civi\Setup\Exception\InitException;
14 use Psr\Log\NullLogger;
15 use Civi\Core\CiviEventDispatcher;
16
17 class Setup {
18
19 const PROTOCOL = '1.1';
20
21 const PRIORITY_START = 2000;
22 const PRIORITY_PREPARE = 1000;
23 const PRIORITY_MAIN = 0;
24 const PRIORITY_LATE = -1000;
25 const PRIORITY_END = -2000;
26
27 private static $instance;
28
29 /**
30 * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
31 */
32 protected $dispatcher;
33
34 /**
35 * @var \Civi\Setup\Model
36 */
37 protected $model;
38
39 /**
40 * @var \Psr\Log\LoggerInterface
41 */
42 protected $log;
43
44 /**
45 * @var string|null
46 */
47 protected $pendingAction = NULL;
48
49 // ----- Static initialization -----
50
51 /**
52 * The initialization process loads any `*.civi-setup.php` files and
53 * fires the `civi.setup.init` event.
54 *
55 * @param array $modelValues
56 * List of default configuration options.
57 * Recommended fields: 'srcPath', 'cms'
58 * @param callable $pluginCallback
59 * Function which manipulates the list of plugin files.
60 * Use this to add, remove, or re-order callbacks.
61 * function(array $files) => array
62 * Ex: ['hello' => '/var/www/plugins/hello.civi-setup.php']
63 * @param \Psr\Log\LoggerInterface $log
64 */
65 public static function init($modelValues = array(), $pluginCallback = NULL, $log = NULL) {
66 if (!defined('CIVI_SETUP')) {
67 define('CIVI_SETUP', 1);
68 }
69
70 self::$instance = new Setup();
71 self::$instance->model = new \Civi\Setup\Model();
72 self::$instance->model->setValues($modelValues);
73 self::$instance->dispatcher = new CiviEventDispatcher();
74 self::$instance->dispatcher->setDispatchPolicy([
75 '/^civi\.setup\./' => 'run',
76 '/^civi\.setupui\./' => 'run',
77 '/./' => 'fail',
78 ]);
79 self::$instance->log = $log ? $log : new NullLogger();
80
81 $pluginDir = dirname(__DIR__) . '/plugins';
82 $pluginFiles = array();
83 foreach (['*.civi-setup.php', '*/*.civi-setup.php'] as $pattern) {
84 foreach ((array) glob("$pluginDir/$pattern") as $file) {
85 $key = substr($file, strlen($pluginDir) + 1);
86 $key = preg_replace('/\.civi-setup\.php$/', '', $key);
87 $pluginFiles[$key] = $file;
88 }
89 }
90 ksort($pluginFiles);
91
92 if ($pluginCallback) {
93 $pluginFiles = $pluginCallback($pluginFiles);
94 }
95
96 foreach ($pluginFiles as $pluginFile) {
97 self::$instance->log->debug('[Setup.php] Load plugin {file}', array(
98 'file' => $pluginFile,
99 ));
100 require $pluginFile;
101 }
102
103 $event = new InitEvent(self::$instance->getModel());
104 self::$instance->getDispatcher()->dispatch('civi.setup.init', $event);
105 // return $event; ...or... return self::$instance;
106 }
107
108 /**
109 * Assert that this copy of civicrm-setup is compatible with the client.
110 *
111 * @param string $expectedVersion
112 * @throws \Exception
113 */
114 public static function assertProtocolCompatibility($expectedVersion) {
115 if (version_compare(self::PROTOCOL, $expectedVersion, '<')) {
116 throw new InitException(sprintf("civicrm-setup is running protocol v%s. This application expects civicrm-setup to support protocol v%s.", self::PROTOCOL, $expectedVersion));
117 }
118 list ($actualFirst) = explode('.', self::PROTOCOL);
119 list ($expectedFirst) = explode('.', $expectedVersion);
120 if ($actualFirst > $expectedFirst) {
121 throw new InitException(sprintf("civicrm-setup is running protocol v%s. This application expects civicrm-setup to support protocol v%s.", self::PROTOCOL, $expectedVersion));
122 }
123 }
124
125 /**
126 * Assert that the "Setup" subsystem is running.
127 *
128 * This function is mostly just a placeholder -- in practice, if
129 * someone makes a failed call to `assertRunning()`, it will probably
130 * manifest as an unknown class/function. But this gives us a pretty,
131 * one-line, syntactically-valid way to make the assertion.
132 */
133 public static function assertRunning() {
134 if (!defined('CIVI_SETUP')) {
135 exit("Installation plugins must only be loaded by the installer.\n");
136 }
137 }
138
139 /**
140 * @return Setup
141 */
142 public static function instance() {
143 if (self::$instance === NULL) {
144 throw new InitException('\Civi\Setup has not been initialized.');
145 }
146 return self::$instance;
147 }
148
149 /**
150 * @return \Psr\Log\LoggerInterface
151 */
152 public static function log() {
153 return self::instance()->getLog();
154 }
155
156 /**
157 * @return \Symfony\Component\EventDispatcher\EventDispatcherInterface
158 */
159 public static function dispatcher() {
160 return self::instance()->getDispatcher();
161 }
162
163 // ----- Logic ----
164
165 /**
166 * Determine whether the current CMS user is authorized to perform
167 * installation.
168 *
169 * @return \Civi\Setup\Event\CheckAuthorizedEvent
170 */
171 public function checkAuthorized() {
172 $event = new CheckAuthorizedEvent($this->getModel());
173 return $this->getDispatcher()->dispatch('civi.setup.checkAuthorized', $event);
174 }
175
176 /**
177 * Determine whether the local environment meets system requirements.
178 *
179 * @return \Civi\Setup\Event\CheckRequirementsEvent
180 */
181 public function checkRequirements() {
182 $event = new CheckRequirementsEvent($this->getModel());
183 return $this->getDispatcher()->dispatch('civi.setup.checkRequirements', $event);
184 }
185
186 /**
187 * Determine whether the setting and/or schema are already installed.
188 *
189 * @return \Civi\Setup\Event\CheckInstalledEvent
190 */
191 public function checkInstalled() {
192 $event = new CheckInstalledEvent($this->getModel());
193 return $this->getDispatcher()->dispatch('civi.setup.checkInstalled', $event);
194 }
195
196 /**
197 * Create the settings file.
198 *
199 * @return \Civi\Setup\Event\InstallFilesEvent
200 */
201 public function installFiles() {
202 if ($this->pendingAction !== NULL) {
203 throw new InitException(sprintf("Cannot begin action %s. Already executing %s.", __FUNCTION__, $this->pendingAction));
204 }
205 $this->pendingAction = __FUNCTION__;
206
207 try {
208 $event = new InstallFilesEvent($this->getModel());
209 return $this->getDispatcher()->dispatch('civi.setup.installFiles', $event);
210 }
211 finally {
212 $this->pendingAction = NULL;
213 }
214 }
215
216 /**
217 * Create the database schema.
218 *
219 * @return \Civi\Setup\Event\InstallDatabaseEvent
220 */
221 public function installDatabase() {
222 if ($this->pendingAction !== NULL) {
223 throw new InitException(sprintf("Cannot begin action %s. Already executing %s.", __FUNCTION__, $this->pendingAction));
224 }
225 $this->pendingAction = __FUNCTION__;
226
227 try {
228 $event = new InstallDatabaseEvent($this->getModel());
229 return $this->getDispatcher()->dispatch('civi.setup.installDatabase', $event);
230 }
231 finally {
232 $this->pendingAction = NULL;
233 }
234 }
235
236 /**
237 * Remove the settings file.
238 *
239 * @return \Civi\Setup\Event\UninstallFilesEvent
240 */
241 public function uninstallFiles() {
242 if ($this->pendingAction !== NULL) {
243 throw new InitException(sprintf("Cannot begin action %s. Already executing %s.", __FUNCTION__, $this->pendingAction));
244 }
245 $this->pendingAction = __FUNCTION__;
246
247 try {
248 $event = new UninstallFilesEvent($this->getModel());
249 return $this->getDispatcher()->dispatch('civi.setup.uninstallFiles', $event);
250 }
251 finally {
252 $this->pendingAction = NULL;
253 }
254 }
255
256 /**
257 * Remove the database schema.
258 *
259 * @return \Civi\Setup\Event\UninstallDatabaseEvent
260 */
261 public function uninstallDatabase() {
262 if ($this->pendingAction !== NULL) {
263 throw new InitException(sprintf("Cannot begin action %s. Already executing %s.", __FUNCTION__, $this->pendingAction));
264 }
265 $this->pendingAction = __FUNCTION__;
266
267 try {
268 $event = new UninstallDatabaseEvent($this->getModel());
269 return $this->getDispatcher()->dispatch('civi.setup.uninstallDatabase', $event);
270 }
271 finally {
272 $this->pendingAction = NULL;
273 }
274 }
275
276 /**
277 * Create a page-controller for a web-based installation form.
278 *
279 * @return \Civi\Setup\UI\Event\UIConstructEvent
280 */
281 public function createController() {
282 $event = new UIConstructEvent($this->getModel());
283 return $this->getDispatcher()->dispatch('civi.setupui.construct', $event);
284 }
285
286 // ----- Accessors -----
287
288 /**
289 * @return \Symfony\Component\EventDispatcher\EventDispatcherInterface
290 */
291 public function getDispatcher() {
292 return $this->dispatcher;
293 }
294
295 /**
296 * @return \Civi\Setup\Model
297 */
298 public function getModel() {
299 return $this->model;
300 }
301
302 /**
303 * @return \Psr\Log\LoggerInterface
304 */
305 public function getLog() {
306 return $this->log;
307 }
308
309 /**
310 * @return NULL|string
311 * The name of a pending installation action, or NULL if none are active.
312 * Ex: 'installDatabase', 'uninstallFiles'
313 */
314 public function getPendingAction() {
315 return $this->pendingAction;
316 }
317
318 }