1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the puli/manager package. |
5
|
|
|
* |
6
|
|
|
* (c) Bernhard Schussek <[email protected]> |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace Puli\Manager\Module; |
13
|
|
|
|
14
|
|
|
use Exception; |
15
|
|
|
use Puli\Manager\Api\Context\ProjectContext; |
16
|
|
|
use Puli\Manager\Api\Environment; |
17
|
|
|
use Puli\Manager\Api\FileNotFoundException; |
18
|
|
|
use Puli\Manager\Api\InvalidConfigException; |
19
|
|
|
use Puli\Manager\Api\Module\InstallInfo; |
20
|
|
|
use Puli\Manager\Api\Module\Module; |
21
|
|
|
use Puli\Manager\Api\Module\ModuleCollection; |
22
|
|
|
use Puli\Manager\Api\Module\ModuleFile; |
23
|
|
|
use Puli\Manager\Api\Module\ModuleManager; |
24
|
|
|
use Puli\Manager\Api\Module\NameConflictException; |
25
|
|
|
use Puli\Manager\Api\Module\RootModule; |
26
|
|
|
use Puli\Manager\Api\Module\RootModuleFile; |
27
|
|
|
use Puli\Manager\Api\Module\UnsupportedVersionException; |
28
|
|
|
use Puli\Manager\Api\NoDirectoryException; |
29
|
|
|
use Puli\Manager\Assert\Assert; |
30
|
|
|
use Webmozart\Expression\Expr; |
31
|
|
|
use Webmozart\Expression\Expression; |
32
|
|
|
use Webmozart\PathUtil\Path; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* Manages the module repository of a Puli project. |
36
|
|
|
* |
37
|
|
|
* @since 1.0 |
38
|
|
|
* |
39
|
|
|
* @author Bernhard Schussek <[email protected]> |
40
|
|
|
*/ |
41
|
|
|
class ModuleManagerImpl implements ModuleManager |
42
|
|
|
{ |
43
|
|
|
/** |
44
|
|
|
* @var ProjectContext |
45
|
|
|
*/ |
46
|
|
|
private $context; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* @var string |
50
|
|
|
*/ |
51
|
|
|
private $rootDir; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @var RootModuleFile |
55
|
|
|
*/ |
56
|
|
|
private $rootModuleFile; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @var ModuleFileStorage |
60
|
|
|
*/ |
61
|
|
|
private $moduleFileStorage; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* @var ModuleCollection |
65
|
|
|
*/ |
66
|
|
|
private $modules; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Loads the module repository for a given project. |
70
|
|
|
* |
71
|
|
|
* @param ProjectContext $context The project context. |
72
|
|
|
* @param ModuleFileStorage $moduleFileStorage The module file storage. |
73
|
|
|
* |
74
|
|
|
* @throws FileNotFoundException If the install path of a module not exist. |
75
|
|
|
* @throws NoDirectoryException If the install path of a module points to a file. |
76
|
|
|
* @throws InvalidConfigException If a configuration file contains invalid configuration. |
77
|
|
|
* @throws NameConflictException If a module has the same name as another loaded module. |
78
|
|
|
*/ |
79
|
53 |
|
public function __construct(ProjectContext $context, ModuleFileStorage $moduleFileStorage) |
80
|
|
|
{ |
81
|
53 |
|
$this->context = $context; |
82
|
53 |
|
$this->rootDir = $context->getRootDirectory(); |
83
|
53 |
|
$this->rootModuleFile = $context->getRootModuleFile(); |
84
|
53 |
|
$this->moduleFileStorage = $moduleFileStorage; |
85
|
53 |
|
} |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* {@inheritdoc} |
89
|
|
|
*/ |
90
|
12 |
|
public function installModule($installPath, $name = null, $installerName = InstallInfo::DEFAULT_INSTALLER_NAME, $env = Environment::PROD) |
91
|
|
|
{ |
92
|
12 |
|
Assert::string($installPath, 'The install path must be a string. Got: %s'); |
93
|
12 |
|
Assert::string($installerName, 'The installer name must be a string. Got: %s'); |
94
|
12 |
|
Assert::oneOf($env, Environment::all(), 'The environment must be one of: %2$s. Got: %s'); |
95
|
12 |
|
Assert::nullOrModuleName($name); |
96
|
|
|
|
97
|
11 |
|
$this->assertModulesLoaded(); |
98
|
|
|
|
99
|
11 |
|
$installPath = Path::makeAbsolute($installPath, $this->rootDir); |
100
|
|
|
|
101
|
11 |
|
foreach ($this->modules as $module) { |
102
|
11 |
|
if ($installPath === $module->getInstallPath()) { |
103
|
11 |
|
return; |
104
|
|
|
} |
105
|
|
|
} |
106
|
|
|
|
107
|
10 |
|
if (null === $name && $moduleFile = $this->loadModuleFile($installPath)) { |
108
|
|
|
// Read the name from the module file |
109
|
6 |
|
$name = $moduleFile->getModuleName(); |
110
|
|
|
} |
111
|
|
|
|
112
|
8 |
|
if (null === $name) { |
113
|
1 |
|
throw new InvalidConfigException(sprintf( |
114
|
|
|
'Could not find a name for the module at %s. The name should '. |
115
|
|
|
'either be passed to the installer or be set in the "name" '. |
116
|
1 |
|
'property of %s.', |
117
|
|
|
$installPath, |
118
|
1 |
|
$installPath.'/puli.json' |
119
|
|
|
)); |
120
|
|
|
} |
121
|
|
|
|
122
|
7 |
|
if ($this->modules->contains($name)) { |
123
|
1 |
|
throw NameConflictException::forName($name); |
124
|
|
|
} |
125
|
|
|
|
126
|
6 |
|
$relInstallPath = Path::makeRelative($installPath, $this->rootDir); |
127
|
6 |
|
$installInfo = new InstallInfo($name, $relInstallPath); |
128
|
6 |
|
$installInfo->setInstallerName($installerName); |
129
|
6 |
|
$installInfo->setEnvironment($env); |
130
|
|
|
|
131
|
6 |
|
$module = $this->loadModule($installInfo); |
132
|
|
|
|
133
|
6 |
|
$this->assertNoLoadErrors($module); |
134
|
5 |
|
$this->rootModuleFile->addInstallInfo($installInfo); |
135
|
|
|
|
136
|
|
|
try { |
137
|
5 |
|
$this->moduleFileStorage->saveRootModuleFile($this->rootModuleFile); |
138
|
|
|
} catch (Exception $e) { |
139
|
|
|
$this->rootModuleFile->removeInstallInfo($name); |
140
|
|
|
|
141
|
|
|
throw $e; |
142
|
|
|
} |
143
|
|
|
|
144
|
5 |
|
$this->modules->add($module); |
145
|
5 |
|
} |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* {@inheritdoc} |
149
|
|
|
*/ |
150
|
8 |
|
public function renameModule($name, $newName) |
151
|
|
|
{ |
152
|
8 |
|
$module = $this->getModule($name); |
153
|
|
|
|
154
|
8 |
|
if ($name === $newName) { |
155
|
2 |
|
return; |
156
|
|
|
} |
157
|
|
|
|
158
|
6 |
|
if ($this->modules->contains($newName)) { |
159
|
2 |
|
throw NameConflictException::forName($newName); |
160
|
|
|
} |
161
|
|
|
|
162
|
4 |
|
if ($module instanceof RootModule) { |
163
|
2 |
|
$this->renameRootModule($module, $newName); |
164
|
|
|
} else { |
165
|
2 |
|
$this->renameNonRootModule($module, $newName); |
166
|
|
|
} |
167
|
2 |
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* {@inheritdoc} |
171
|
|
|
*/ |
172
|
4 |
|
public function removeModule($name) |
173
|
|
|
{ |
174
|
|
|
// Only check that this is a string. The error message "not found" is |
175
|
|
|
// more helpful than e.g. "module name must contain /". |
176
|
4 |
|
Assert::string($name, 'The module name must be a string. Got: %s'); |
177
|
|
|
|
178
|
4 |
|
$this->assertModulesLoaded(); |
179
|
|
|
|
180
|
4 |
|
if ($this->rootModuleFile->hasInstallInfo($name)) { |
181
|
2 |
|
$installInfo = $this->rootModuleFile->getInstallInfo($name); |
182
|
2 |
|
$this->rootModuleFile->removeInstallInfo($name); |
183
|
|
|
|
184
|
|
|
try { |
185
|
2 |
|
$this->moduleFileStorage->saveRootModuleFile($this->rootModuleFile); |
186
|
1 |
|
} catch (Exception $e) { |
187
|
1 |
|
$this->rootModuleFile->addInstallInfo($installInfo); |
188
|
|
|
|
189
|
1 |
|
throw $e; |
190
|
|
|
} |
191
|
|
|
} |
192
|
|
|
|
193
|
3 |
|
$this->modules->remove($name); |
194
|
3 |
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* {@inheritdoc} |
198
|
|
|
*/ |
199
|
3 |
|
public function removeModules(Expression $expr) |
200
|
|
|
{ |
201
|
3 |
|
$this->assertModulesLoaded(); |
202
|
|
|
|
203
|
3 |
|
$installInfos = $this->rootModuleFile->getInstallInfos(); |
204
|
3 |
|
$modules = $this->modules->toArray(); |
205
|
|
|
|
206
|
3 |
|
foreach ($this->modules->getInstalledModules() as $module) { |
207
|
3 |
|
if ($expr->evaluate($module)) { |
208
|
3 |
|
$this->rootModuleFile->removeInstallInfo($module->getName()); |
209
|
3 |
|
$this->modules->remove($module->getName()); |
210
|
|
|
} |
211
|
|
|
} |
212
|
|
|
|
213
|
3 |
|
if (!$installInfos) { |
|
|
|
|
214
|
|
|
return; |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
try { |
218
|
3 |
|
$this->moduleFileStorage->saveRootModuleFile($this->rootModuleFile); |
219
|
1 |
|
} catch (Exception $e) { |
220
|
1 |
|
$this->rootModuleFile->setInstallInfos($installInfos); |
221
|
1 |
|
$this->modules->replace($modules); |
222
|
|
|
|
223
|
1 |
|
throw $e; |
224
|
|
|
} |
225
|
2 |
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* {@inheritdoc} |
229
|
|
|
*/ |
230
|
1 |
|
public function clearModules() |
231
|
|
|
{ |
232
|
1 |
|
$this->removeModules(Expr::true()); |
233
|
1 |
|
} |
234
|
|
|
|
235
|
|
|
/** |
236
|
|
|
* {@inheritdoc} |
237
|
|
|
*/ |
238
|
10 |
|
public function getModule($name) |
239
|
|
|
{ |
240
|
10 |
|
Assert::string($name, 'The module name must be a string. Got: %s'); |
241
|
|
|
|
242
|
10 |
|
$this->assertModulesLoaded(); |
243
|
|
|
|
244
|
10 |
|
return $this->modules->get($name); |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
/** |
248
|
|
|
* {@inheritdoc} |
249
|
|
|
*/ |
250
|
1 |
|
public function getRootModule() |
251
|
|
|
{ |
252
|
1 |
|
$this->assertModulesLoaded(); |
253
|
|
|
|
254
|
1 |
|
return $this->modules->getRootModule(); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
/** |
258
|
|
|
* {@inheritdoc} |
259
|
|
|
*/ |
260
|
24 |
|
public function getModules() |
261
|
|
|
{ |
262
|
24 |
|
$this->assertModulesLoaded(); |
263
|
|
|
|
264
|
|
|
// Never return he original collection |
265
|
24 |
|
return clone $this->modules; |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
/** |
269
|
|
|
* {@inheritdoc} |
270
|
|
|
*/ |
271
|
4 |
|
public function findModules(Expression $expr) |
272
|
|
|
{ |
273
|
4 |
|
$this->assertModulesLoaded(); |
274
|
|
|
|
275
|
4 |
|
$modules = new ModuleCollection(); |
276
|
|
|
|
277
|
4 |
|
foreach ($this->modules as $module) { |
278
|
4 |
|
if ($expr->evaluate($module)) { |
279
|
4 |
|
$modules->add($module); |
280
|
|
|
} |
281
|
|
|
} |
282
|
|
|
|
283
|
4 |
|
return $modules; |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
/** |
287
|
|
|
* {@inheritdoc} |
288
|
|
|
*/ |
289
|
11 |
|
public function hasModule($name) |
290
|
|
|
{ |
291
|
11 |
|
Assert::string($name, 'The module name must be a string. Got: %s'); |
292
|
|
|
|
293
|
11 |
|
$this->assertModulesLoaded(); |
294
|
|
|
|
295
|
11 |
|
return $this->modules->contains($name); |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
/** |
299
|
|
|
* {@inheritdoc} |
300
|
|
|
*/ |
301
|
1 |
View Code Duplication |
public function hasModules(Expression $expr = null) |
|
|
|
|
302
|
|
|
{ |
303
|
1 |
|
$this->assertModulesLoaded(); |
304
|
|
|
|
305
|
1 |
|
if (!$expr) { |
306
|
1 |
|
return !$this->modules->isEmpty(); |
307
|
|
|
} |
308
|
|
|
|
309
|
1 |
|
foreach ($this->modules as $module) { |
310
|
1 |
|
if ($expr->evaluate($module)) { |
311
|
1 |
|
return true; |
312
|
|
|
} |
313
|
|
|
} |
314
|
|
|
|
315
|
1 |
|
return false; |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
/** |
319
|
|
|
* {@inheritdoc} |
320
|
|
|
*/ |
321
|
1 |
|
public function getContext() |
322
|
|
|
{ |
323
|
1 |
|
return $this->context; |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
/** |
327
|
|
|
* Loads all modules referenced by the install file. |
328
|
|
|
* |
329
|
|
|
* @throws FileNotFoundException If the install path of a module not exist. |
330
|
|
|
* @throws NoDirectoryException If the install path of a module points to a |
331
|
|
|
* file. |
332
|
|
|
* @throws InvalidConfigException If a module is not configured correctly. |
333
|
|
|
* @throws NameConflictException If a module has the same name as another |
334
|
|
|
* loaded module. |
335
|
|
|
*/ |
336
|
52 |
|
private function loadModules() |
337
|
|
|
{ |
338
|
52 |
|
$this->modules = new ModuleCollection(); |
339
|
52 |
|
$this->modules->add(new RootModule($this->rootModuleFile, $this->rootDir)); |
340
|
|
|
|
341
|
52 |
|
foreach ($this->rootModuleFile->getInstallInfos() as $installInfo) { |
342
|
|
|
// Catch and log exceptions so that single modules cannot break |
343
|
|
|
// the whole repository |
344
|
51 |
|
$this->modules->add($this->loadModule($installInfo)); |
345
|
|
|
} |
346
|
52 |
|
} |
347
|
|
|
|
348
|
|
|
/** |
349
|
|
|
* Loads a module for the given install info. |
350
|
|
|
* |
351
|
|
|
* @param InstallInfo $installInfo The install info. |
352
|
|
|
* |
353
|
|
|
* @return Module The module. |
354
|
|
|
*/ |
355
|
52 |
|
private function loadModule(InstallInfo $installInfo) |
356
|
|
|
{ |
357
|
52 |
|
$installPath = Path::makeAbsolute($installInfo->getInstallPath(), $this->rootDir); |
358
|
52 |
|
$moduleFile = null; |
359
|
52 |
|
$loadError = null; |
360
|
|
|
|
361
|
|
|
try { |
362
|
52 |
|
$moduleFile = $this->loadModuleFile($installPath); |
363
|
7 |
|
} catch (InvalidConfigException $loadError) { |
|
|
|
|
364
|
6 |
|
} catch (UnsupportedVersionException $loadError) { |
|
|
|
|
365
|
4 |
|
} catch (FileNotFoundException $loadError) { |
|
|
|
|
366
|
1 |
|
} catch (NoDirectoryException $loadError) { |
|
|
|
|
367
|
|
|
} |
368
|
|
|
|
369
|
52 |
|
$loadErrors = $loadError ? array($loadError) : array(); |
370
|
|
|
|
371
|
52 |
|
return new Module($moduleFile, $installPath, $installInfo, $loadErrors); |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
/** |
375
|
|
|
* Loads the module file for the module at the given install path. |
376
|
|
|
* |
377
|
|
|
* @param string $installPath The absolute install path of the module |
378
|
|
|
* |
379
|
|
|
* @return ModuleFile|null The loaded module file or `null` if none |
380
|
|
|
* could be found. |
381
|
|
|
*/ |
382
|
52 |
|
private function loadModuleFile($installPath) |
383
|
|
|
{ |
384
|
52 |
|
if (!file_exists($installPath)) { |
385
|
4 |
|
throw FileNotFoundException::forPath($installPath); |
386
|
|
|
} |
387
|
|
|
|
388
|
51 |
|
if (!is_dir($installPath)) { |
389
|
2 |
|
throw new NoDirectoryException(sprintf( |
390
|
2 |
|
'The path %s is a file. Expected a directory.', |
391
|
|
|
$installPath |
392
|
|
|
)); |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
try { |
396
|
50 |
|
return $this->moduleFileStorage->loadModuleFile($installPath.'/puli.json'); |
397
|
4 |
|
} catch (FileNotFoundException $e) { |
398
|
|
|
// Modules without module files are ok |
399
|
1 |
|
return null; |
400
|
|
|
} |
401
|
|
|
} |
402
|
|
|
|
403
|
52 |
|
private function assertModulesLoaded() |
404
|
|
|
{ |
405
|
52 |
|
if (!$this->modules) { |
406
|
52 |
|
$this->loadModules(); |
407
|
|
|
} |
408
|
52 |
|
} |
409
|
|
|
|
410
|
6 |
|
private function assertNoLoadErrors(Module $module) |
411
|
|
|
{ |
412
|
6 |
|
$loadErrors = $module->getLoadErrors(); |
413
|
|
|
|
414
|
6 |
|
if (count($loadErrors) > 0) { |
415
|
|
|
// Rethrow first error |
416
|
1 |
|
throw reset($loadErrors); |
417
|
|
|
} |
418
|
5 |
|
} |
419
|
|
|
|
420
|
2 |
|
private function renameRootModule(RootModule $module, $newName) |
421
|
|
|
{ |
422
|
2 |
|
$moduleFile = $module->getModuleFile(); |
423
|
2 |
|
$previousName = $moduleFile->getModuleName(); |
424
|
2 |
|
$moduleFile->setModuleName($newName); |
425
|
|
|
|
426
|
|
|
try { |
427
|
2 |
|
$this->moduleFileStorage->saveRootModuleFile($this->rootModuleFile); |
428
|
1 |
|
} catch (Exception $e) { |
429
|
1 |
|
$moduleFile->setModuleName($previousName); |
430
|
|
|
|
431
|
1 |
|
throw $e; |
432
|
|
|
} |
433
|
|
|
|
434
|
1 |
|
$this->modules->remove($module->getName()); |
435
|
1 |
|
$this->modules->add(new RootModule($moduleFile, $module->getInstallPath())); |
436
|
1 |
|
} |
437
|
|
|
|
438
|
2 |
|
private function renameNonRootModule(Module $module, $newName) |
439
|
|
|
{ |
440
|
2 |
|
$previousInstallInfo = $module->getInstallInfo(); |
441
|
|
|
|
442
|
2 |
|
$installInfo = new InstallInfo($newName, $previousInstallInfo->getInstallPath()); |
443
|
2 |
|
$installInfo->setInstallerName($previousInstallInfo->getInstallerName()); |
444
|
|
|
|
445
|
2 |
|
foreach ($previousInstallInfo->getDisabledBindingUuids() as $uuid) { |
446
|
1 |
|
$installInfo->addDisabledBindingUuid($uuid); |
447
|
|
|
} |
448
|
|
|
|
449
|
2 |
|
$this->rootModuleFile->removeInstallInfo($module->getName()); |
450
|
2 |
|
$this->rootModuleFile->addInstallInfo($installInfo); |
451
|
|
|
|
452
|
|
|
try { |
453
|
2 |
|
$this->moduleFileStorage->saveRootModuleFile($this->rootModuleFile); |
454
|
1 |
|
} catch (Exception $e) { |
455
|
1 |
|
$this->rootModuleFile->removeInstallInfo($newName); |
456
|
1 |
|
$this->rootModuleFile->addInstallInfo($previousInstallInfo); |
457
|
|
|
|
458
|
1 |
|
throw $e; |
459
|
|
|
} |
460
|
|
|
|
461
|
1 |
|
$this->modules->remove($module->getName()); |
462
|
1 |
|
$this->modules->add(new Module( |
463
|
1 |
|
$module->getModuleFile(), |
464
|
1 |
|
$module->getInstallPath(), |
465
|
|
|
$installInfo, |
466
|
1 |
|
$module->getLoadErrors() |
467
|
|
|
)); |
468
|
1 |
|
} |
469
|
|
|
} |
470
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.