Merge pull request #20750 from mattwire/viewrecur
[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(['/^civi\.setup\./' => 'run', '/./' => 'fail']);
75 self::$instance->log = $log ? $log : new NullLogger();
76
77 $pluginDir = dirname(__DIR__) . '/plugins';
78 $pluginFiles = array();
79 foreach (['*.civi-setup.php', '*/*.civi-setup.php'] as $pattern) {
80 foreach ((array) glob("$pluginDir/$pattern") as $file) {
81 $key = substr($file, strlen($pluginDir) + 1);
82 $key = preg_replace('/\.civi-setup\.php$/', '', $key);
83 $pluginFiles[$key] = $file;
84 }
85 }
86 ksort($pluginFiles);
87
88 if ($pluginCallback) {
89 $pluginFiles = $pluginCallback($pluginFiles);
90 }
91
92 foreach ($pluginFiles as $pluginFile) {
93 self::$instance->log->debug('[Setup.php] Load plugin {file}', array(
94 'file' => $pluginFile,
95 ));
96 require $pluginFile;
97 }
98
99 $event = new InitEvent(self::$instance->getModel());
100 self::$instance->getDispatcher()->dispatch('civi.setup.init', $event);
101 // return $event; ...or... return self::$instance;
102 }
103
104 /**
105 * Assert that this copy of civicrm-setup is compatible with the client.
106 *
107 * @param string $expectedVersion
108 * @throws \Exception
109 */
110 public static function assertProtocolCompatibility($expectedVersion) {
111 if (version_compare(self::PROTOCOL, $expectedVersion, '<')) {
112 throw new InitException(sprintf("civicrm-setup is running protocol v%s. This application expects civicrm-setup to support protocol v%s.", self::PROTOCOL, $expectedVersion));
113 }
114 list ($actualFirst) = explode('.', self::PROTOCOL);
115 list ($expectedFirst) = explode('.', $expectedVersion);
116 if ($actualFirst > $expectedFirst) {
117 throw new InitException(sprintf("civicrm-setup is running protocol v%s. This application expects civicrm-setup to support protocol v%s.", self::PROTOCOL, $expectedVersion));
118 }
119 }
120
121 /**
122 * Assert that the "Setup" subsystem is running.
123 *
124 * This function is mostly just a placeholder -- in practice, if
125 * someone makes a failed call to `assertRunning()`, it will probably
126 * manifest as an unknown class/function. But this gives us a pretty,
127 * one-line, syntactically-valid way to make the assertion.
128 */
129 public static function assertRunning() {
130 if (!defined('CIVI_SETUP')) {
131 exit("Installation plugins must only be loaded by the installer.\n");
132 }
133 }
134
135 /**
136 * @return Setup
137 */
138 public static function instance() {
139 if (self::$instance === NULL) {
140 throw new InitException('\Civi\Setup has not been initialized.');
141 }
142 return self::$instance;
143 }
144
145 /**
146 * @return \Psr\Log\LoggerInterface
147 */
148 public static function log() {
149 return self::instance()->getLog();
150 }
151
152 /**
153 * @return \Symfony\Component\EventDispatcher\EventDispatcherInterface
154 */
155 public static function dispatcher() {
156 return self::instance()->getDispatcher();
157 }
158
159 // ----- Logic ----
160
161 /**
162 * Determine whether the current CMS user is authorized to perform
163 * installation.
164 *
165 * @return \Civi\Setup\Event\CheckAuthorizedEvent
166 */
167 public function checkAuthorized() {
168 $event = new CheckAuthorizedEvent($this->getModel());
169 return $this->getDispatcher()->dispatch('civi.setup.checkAuthorized', $event);
170 }
171
172 /**
173 * Determine whether the local environment meets system requirements.
174 *
175 * @return \Civi\Setup\Event\CheckRequirementsEvent
176 */
177 public function checkRequirements() {
178 $event = new CheckRequirementsEvent($this->getModel());
179 return $this->getDispatcher()->dispatch('civi.setup.checkRequirements', $event);
180 }
181
182 /**
183 * Determine whether the setting and/or schema are already installed.
184 *
185 * @return \Civi\Setup\Event\CheckInstalledEvent
186 */
187 public function checkInstalled() {
188 $event = new CheckInstalledEvent($this->getModel());
189 return $this->getDispatcher()->dispatch('civi.setup.checkInstalled', $event);
190 }
191
192 /**
193 * Create the settings file.
194 *
195 * @return \Civi\Setup\Event\InstallFilesEvent
196 */
197 public function installFiles() {
198 if ($this->pendingAction !== NULL) {
199 throw new InitException(sprintf("Cannot begin action %s. Already executing %s.", __FUNCTION__, $this->pendingAction));
200 }
201 $this->pendingAction = __FUNCTION__;
202
203 try {
204 $event = new InstallFilesEvent($this->getModel());
205 return $this->getDispatcher()->dispatch('civi.setup.installFiles', $event);
206 }
207 finally {
208 $this->pendingAction = NULL;
209 }
210 }
211
212 /**
213 * Create the database schema.
214 *
215 * @return \Civi\Setup\Event\InstallDatabaseEvent
216 */
217 public function installDatabase() {
218 if ($this->pendingAction !== NULL) {
219 throw new InitException(sprintf("Cannot begin action %s. Already executing %s.", __FUNCTION__, $this->pendingAction));
220 }
221 $this->pendingAction = __FUNCTION__;
222
223 try {
224 $event = new InstallDatabaseEvent($this->getModel());
225 return $this->getDispatcher()->dispatch('civi.setup.installDatabase', $event);
226 }
227 finally {
228 $this->pendingAction = NULL;
229 }
230 }
231
232 /**
233 * Remove the settings file.
234 *
235 * @return \Civi\Setup\Event\UninstallFilesEvent
236 */
237 public function uninstallFiles() {
238 if ($this->pendingAction !== NULL) {
239 throw new InitException(sprintf("Cannot begin action %s. Already executing %s.", __FUNCTION__, $this->pendingAction));
240 }
241 $this->pendingAction = __FUNCTION__;
242
243 try {
244 $event = new UninstallFilesEvent($this->getModel());
245 return $this->getDispatcher()->dispatch('civi.setup.uninstallFiles', $event);
246 }
247 finally {
248 $this->pendingAction = NULL;
249 }
250 }
251
252 /**
253 * Remove the database schema.
254 *
255 * @return \Civi\Setup\Event\UninstallDatabaseEvent
256 */
257 public function uninstallDatabase() {
258 if ($this->pendingAction !== NULL) {
259 throw new InitException(sprintf("Cannot begin action %s. Already executing %s.", __FUNCTION__, $this->pendingAction));
260 }
261 $this->pendingAction = __FUNCTION__;
262
263 try {
264 $event = new UninstallDatabaseEvent($this->getModel());
265 return $this->getDispatcher()->dispatch('civi.setup.uninstallDatabase', $event);
266 }
267 finally {
268 $this->pendingAction = NULL;
269 }
270 }
271
272 /**
273 * Create a page-controller for a web-based installation form.
274 *
275 * @return \Civi\Setup\UI\Event\UIConstructEvent
276 */
277 public function createController() {
278 $event = new UIConstructEvent($this->getModel());
279 return $this->getDispatcher()->dispatch('civi.setupui.construct', $event);
280 }
281
282 // ----- Accessors -----
283
284 /**
285 * @return \Symfony\Component\EventDispatcher\EventDispatcherInterface
286 */
287 public function getDispatcher() {
288 return $this->dispatcher;
289 }
290
291 /**
292 * @return \Civi\Setup\Model
293 */
294 public function getModel() {
295 return $this->model;
296 }
297
298 /**
299 * @return \Psr\Log\LoggerInterface
300 */
301 public function getLog() {
302 return $this->log;
303 }
304
305 /**
306 * @return NULL|string
307 * The name of a pending installation action, or NULL if none are active.
308 * Ex: 'installDatabase', 'uninstallFiles'
309 */
310 public function getPendingAction() {
311 return $this->pendingAction;
312 }
313
314 }