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\Package; |
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\NoDirectoryException; |
20
|
|
|
use Puli\Manager\Api\Package\InstallInfo; |
21
|
|
|
use Puli\Manager\Api\Package\NameConflictException; |
22
|
|
|
use Puli\Manager\Api\Package\Package; |
23
|
|
|
use Puli\Manager\Api\Package\PackageCollection; |
24
|
|
|
use Puli\Manager\Api\Package\PackageFile; |
25
|
|
|
use Puli\Manager\Api\Package\PackageManager; |
26
|
|
|
use Puli\Manager\Api\Package\RootPackage; |
27
|
|
|
use Puli\Manager\Api\Package\RootPackageFile; |
28
|
|
|
use Puli\Manager\Api\Package\UnsupportedVersionException; |
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 package repository of a Puli project. |
36
|
|
|
* |
37
|
|
|
* @since 1.0 |
38
|
|
|
* |
39
|
|
|
* @author Bernhard Schussek <[email protected]> |
40
|
|
|
*/ |
41
|
|
|
class PackageManagerImpl implements PackageManager |
42
|
|
|
{ |
43
|
|
|
/** |
44
|
|
|
* @var ProjectContext |
45
|
|
|
*/ |
46
|
|
|
private $context; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* @var string |
50
|
|
|
*/ |
51
|
|
|
private $rootDir; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @var RootPackageFile |
55
|
|
|
*/ |
56
|
|
|
private $rootPackageFile; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @var PackageFileStorage |
60
|
|
|
*/ |
61
|
|
|
private $packageFileStorage; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* @var PackageCollection |
65
|
|
|
*/ |
66
|
|
|
private $packages; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Loads the package repository for a given project. |
70
|
|
|
* |
71
|
|
|
* @param ProjectContext $context The project context. |
72
|
|
|
* @param PackageFileStorage $packageFileStorage The package file storage. |
73
|
|
|
* |
74
|
|
|
* @throws FileNotFoundException If the install path of a package not exist. |
75
|
|
|
* @throws NoDirectoryException If the install path of a package points to a file. |
76
|
|
|
* @throws InvalidConfigException If a configuration file contains invalid configuration. |
77
|
|
|
* @throws NameConflictException If a package has the same name as another loaded package. |
78
|
|
|
*/ |
79
|
53 |
|
public function __construct(ProjectContext $context, PackageFileStorage $packageFileStorage) |
80
|
|
|
{ |
81
|
53 |
|
$this->context = $context; |
82
|
53 |
|
$this->rootDir = $context->getRootDirectory(); |
83
|
53 |
|
$this->rootPackageFile = $context->getRootPackageFile(); |
84
|
53 |
|
$this->packageFileStorage = $packageFileStorage; |
85
|
53 |
|
} |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* {@inheritdoc} |
89
|
|
|
*/ |
90
|
12 |
|
public function installPackage($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::nullOrPackageName($name); |
96
|
|
|
|
97
|
11 |
|
$this->assertPackagesLoaded(); |
98
|
|
|
|
99
|
11 |
|
$installPath = Path::makeAbsolute($installPath, $this->rootDir); |
100
|
|
|
|
101
|
11 |
|
foreach ($this->packages as $package) { |
102
|
11 |
|
if ($installPath === $package->getInstallPath()) { |
103
|
1 |
|
return; |
104
|
|
|
} |
105
|
11 |
|
} |
106
|
|
|
|
107
|
10 |
|
if (null === $name && $packageFile = $this->loadPackageFile($installPath)) { |
108
|
|
|
// Read the name from the package file |
109
|
6 |
|
$name = $packageFile->getPackageName(); |
110
|
6 |
|
} |
111
|
|
|
|
112
|
8 |
|
if (null === $name) { |
113
|
1 |
|
throw new InvalidConfigException(sprintf( |
114
|
|
|
'Could not find a name for the package at %s. The name should '. |
115
|
1 |
|
'either be passed to the installer or be set in the "name" '. |
116
|
1 |
|
'property of %s.', |
117
|
1 |
|
$installPath, |
118
|
|
|
$installPath.'/puli.json' |
119
|
1 |
|
)); |
120
|
|
|
} |
121
|
|
|
|
122
|
7 |
|
if ($this->packages->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 |
|
$package = $this->loadPackage($installInfo); |
132
|
|
|
|
133
|
6 |
|
$this->assertNoLoadErrors($package); |
134
|
5 |
|
$this->rootPackageFile->addInstallInfo($installInfo); |
135
|
|
|
|
136
|
|
|
try { |
137
|
5 |
|
$this->packageFileStorage->saveRootPackageFile($this->rootPackageFile); |
138
|
5 |
|
} catch (Exception $e) { |
139
|
|
|
$this->rootPackageFile->removeInstallInfo($name); |
140
|
|
|
|
141
|
|
|
throw $e; |
142
|
|
|
} |
143
|
|
|
|
144
|
5 |
|
$this->packages->add($package); |
145
|
5 |
|
} |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* {@inheritdoc} |
149
|
|
|
*/ |
150
|
8 |
|
public function renamePackage($name, $newName) |
151
|
|
|
{ |
152
|
8 |
|
$package = $this->getPackage($name); |
153
|
|
|
|
154
|
8 |
|
if ($name === $newName) { |
155
|
2 |
|
return; |
156
|
|
|
} |
157
|
|
|
|
158
|
6 |
|
if ($this->packages->contains($newName)) { |
159
|
2 |
|
throw NameConflictException::forName($newName); |
160
|
|
|
} |
161
|
|
|
|
162
|
4 |
|
if ($package instanceof RootPackage) { |
163
|
2 |
|
$this->renameRootPackage($package, $newName); |
164
|
1 |
|
} else { |
165
|
2 |
|
$this->renameNonRootPackage($package, $newName); |
166
|
|
|
} |
167
|
2 |
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* {@inheritdoc} |
171
|
|
|
*/ |
172
|
4 |
|
public function removePackage($name) |
173
|
|
|
{ |
174
|
|
|
// Only check that this is a string. The error message "not found" is |
175
|
|
|
// more helpful than e.g. "package name must contain /". |
176
|
4 |
|
Assert::string($name, 'The package name must be a string. Got: %s'); |
177
|
|
|
|
178
|
4 |
|
$this->assertPackagesLoaded(); |
179
|
|
|
|
180
|
4 |
|
if ($this->rootPackageFile->hasInstallInfo($name)) { |
181
|
2 |
|
$installInfo = $this->rootPackageFile->getInstallInfo($name); |
182
|
2 |
|
$this->rootPackageFile->removeInstallInfo($name); |
183
|
|
|
|
184
|
|
|
try { |
185
|
2 |
|
$this->packageFileStorage->saveRootPackageFile($this->rootPackageFile); |
186
|
2 |
|
} catch (Exception $e) { |
187
|
1 |
|
$this->rootPackageFile->addInstallInfo($installInfo); |
188
|
|
|
|
189
|
1 |
|
throw $e; |
190
|
|
|
} |
191
|
1 |
|
} |
192
|
|
|
|
193
|
3 |
|
$this->packages->remove($name); |
194
|
3 |
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* {@inheritdoc} |
198
|
|
|
*/ |
199
|
3 |
|
public function removePackages(Expression $expr) |
200
|
|
|
{ |
201
|
3 |
|
$this->assertPackagesLoaded(); |
202
|
|
|
|
203
|
3 |
|
$installInfos = $this->rootPackageFile->getInstallInfos(); |
204
|
3 |
|
$packages = $this->packages->toArray(); |
205
|
|
|
|
206
|
3 |
|
foreach ($this->packages->getInstalledPackages() as $package) { |
207
|
3 |
|
if ($expr->evaluate($package)) { |
208
|
3 |
|
$this->rootPackageFile->removeInstallInfo($package->getName()); |
209
|
3 |
|
$this->packages->remove($package->getName()); |
210
|
3 |
|
} |
211
|
3 |
|
} |
212
|
|
|
|
213
|
3 |
|
if (!$installInfos) { |
|
|
|
|
214
|
|
|
return; |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
try { |
218
|
3 |
|
$this->packageFileStorage->saveRootPackageFile($this->rootPackageFile); |
219
|
3 |
|
} catch (Exception $e) { |
220
|
1 |
|
$this->rootPackageFile->setInstallInfos($installInfos); |
221
|
1 |
|
$this->packages->replace($packages); |
222
|
|
|
|
223
|
1 |
|
throw $e; |
224
|
|
|
} |
225
|
2 |
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* {@inheritdoc} |
229
|
|
|
*/ |
230
|
1 |
|
public function clearPackages() |
231
|
|
|
{ |
232
|
1 |
|
$this->removePackages(Expr::true()); |
233
|
1 |
|
} |
234
|
|
|
|
235
|
|
|
/** |
236
|
|
|
* {@inheritdoc} |
237
|
|
|
*/ |
238
|
10 |
|
public function getPackage($name) |
239
|
|
|
{ |
240
|
10 |
|
Assert::string($name, 'The package name must be a string. Got: %s'); |
241
|
|
|
|
242
|
10 |
|
$this->assertPackagesLoaded(); |
243
|
|
|
|
244
|
10 |
|
return $this->packages->get($name); |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
/** |
248
|
|
|
* {@inheritdoc} |
249
|
|
|
*/ |
250
|
1 |
|
public function getRootPackage() |
251
|
|
|
{ |
252
|
1 |
|
$this->assertPackagesLoaded(); |
253
|
|
|
|
254
|
1 |
|
return $this->packages->getRootPackage(); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
/** |
258
|
|
|
* {@inheritdoc} |
259
|
|
|
*/ |
260
|
24 |
|
public function getPackages() |
261
|
|
|
{ |
262
|
24 |
|
$this->assertPackagesLoaded(); |
263
|
|
|
|
264
|
|
|
// Never return he original collection |
265
|
24 |
|
return clone $this->packages; |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
/** |
269
|
|
|
* {@inheritdoc} |
270
|
|
|
*/ |
271
|
4 |
|
public function findPackages(Expression $expr) |
272
|
|
|
{ |
273
|
4 |
|
$this->assertPackagesLoaded(); |
274
|
|
|
|
275
|
4 |
|
$packages = new PackageCollection(); |
276
|
|
|
|
277
|
4 |
|
foreach ($this->packages as $package) { |
278
|
4 |
|
if ($expr->evaluate($package)) { |
279
|
4 |
|
$packages->add($package); |
280
|
4 |
|
} |
281
|
4 |
|
} |
282
|
|
|
|
283
|
4 |
|
return $packages; |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
/** |
287
|
|
|
* {@inheritdoc} |
288
|
|
|
*/ |
289
|
11 |
|
public function hasPackage($name) |
290
|
|
|
{ |
291
|
11 |
|
Assert::string($name, 'The package name must be a string. Got: %s'); |
292
|
|
|
|
293
|
11 |
|
$this->assertPackagesLoaded(); |
294
|
|
|
|
295
|
11 |
|
return $this->packages->contains($name); |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
/** |
299
|
|
|
* {@inheritdoc} |
300
|
|
|
*/ |
301
|
1 |
View Code Duplication |
public function hasPackages(Expression $expr = null) |
|
|
|
|
302
|
|
|
{ |
303
|
1 |
|
$this->assertPackagesLoaded(); |
304
|
|
|
|
305
|
1 |
|
if (!$expr) { |
306
|
1 |
|
return !$this->packages->isEmpty(); |
307
|
|
|
} |
308
|
|
|
|
309
|
1 |
|
foreach ($this->packages as $package) { |
310
|
1 |
|
if ($expr->evaluate($package)) { |
311
|
1 |
|
return true; |
312
|
|
|
} |
313
|
1 |
|
} |
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 packages referenced by the install file. |
328
|
|
|
* |
329
|
|
|
* @throws FileNotFoundException If the install path of a package not exist. |
330
|
|
|
* @throws NoDirectoryException If the install path of a package points to a |
331
|
|
|
* file. |
332
|
|
|
* @throws InvalidConfigException If a package is not configured correctly. |
333
|
|
|
* @throws NameConflictException If a package has the same name as another |
334
|
|
|
* loaded package. |
335
|
|
|
*/ |
336
|
52 |
|
private function loadPackages() |
337
|
|
|
{ |
338
|
52 |
|
$this->packages = new PackageCollection(); |
339
|
52 |
|
$this->packages->add(new RootPackage($this->rootPackageFile, $this->rootDir)); |
340
|
|
|
|
341
|
52 |
|
foreach ($this->rootPackageFile->getInstallInfos() as $installInfo) { |
342
|
|
|
// Catch and log exceptions so that single packages cannot break |
343
|
|
|
// the whole repository |
344
|
51 |
|
$this->packages->add($this->loadPackage($installInfo)); |
345
|
52 |
|
} |
346
|
52 |
|
} |
347
|
|
|
|
348
|
|
|
/** |
349
|
|
|
* Loads a package for the given install info. |
350
|
|
|
* |
351
|
|
|
* @param InstallInfo $installInfo The install info. |
352
|
|
|
* |
353
|
|
|
* @return Package The package. |
354
|
|
|
*/ |
355
|
52 |
|
private function loadPackage(InstallInfo $installInfo) |
356
|
|
|
{ |
357
|
52 |
|
$installPath = Path::makeAbsolute($installInfo->getInstallPath(), $this->rootDir); |
358
|
52 |
|
$packageFile = null; |
359
|
52 |
|
$loadError = null; |
360
|
|
|
|
361
|
|
|
try { |
362
|
52 |
|
$packageFile = $this->loadPackageFile($installPath); |
363
|
52 |
|
} catch (InvalidConfigException $loadError) { |
|
|
|
|
364
|
7 |
|
} catch (UnsupportedVersionException $loadError) { |
|
|
|
|
365
|
6 |
|
} catch (FileNotFoundException $loadError) { |
|
|
|
|
366
|
4 |
|
} catch (NoDirectoryException $loadError) { |
|
|
|
|
367
|
|
|
} |
368
|
|
|
|
369
|
52 |
|
$loadErrors = $loadError ? array($loadError) : array(); |
370
|
|
|
|
371
|
52 |
|
return new Package($packageFile, $installPath, $installInfo, $loadErrors); |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
/** |
375
|
|
|
* Loads the package file for the package at the given install path. |
376
|
|
|
* |
377
|
|
|
* @param string $installPath The absolute install path of the package |
378
|
|
|
* |
379
|
|
|
* @return PackageFile|null The loaded package file or `null` if none |
380
|
|
|
* could be found. |
381
|
|
|
*/ |
382
|
52 |
|
private function loadPackageFile($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
|
2 |
|
)); |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
try { |
396
|
50 |
|
return $this->packageFileStorage->loadPackageFile($installPath.'/puli.json'); |
397
|
4 |
|
} catch (FileNotFoundException $e) { |
398
|
|
|
// Packages without package files are ok |
399
|
1 |
|
return null; |
400
|
|
|
} |
401
|
|
|
} |
402
|
|
|
|
403
|
52 |
|
private function assertPackagesLoaded() |
404
|
|
|
{ |
405
|
52 |
|
if (!$this->packages) { |
406
|
52 |
|
$this->loadPackages(); |
407
|
52 |
|
} |
408
|
52 |
|
} |
409
|
|
|
|
410
|
6 |
|
private function assertNoLoadErrors(Package $package) |
411
|
|
|
{ |
412
|
6 |
|
$loadErrors = $package->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 renameRootPackage(RootPackage $package, $newName) |
421
|
|
|
{ |
422
|
2 |
|
$packageFile = $package->getPackageFile(); |
423
|
2 |
|
$previousName = $packageFile->getPackageName(); |
424
|
2 |
|
$packageFile->setPackageName($newName); |
425
|
|
|
|
426
|
|
|
try { |
427
|
2 |
|
$this->packageFileStorage->saveRootPackageFile($this->rootPackageFile); |
428
|
2 |
|
} catch (Exception $e) { |
429
|
1 |
|
$packageFile->setPackageName($previousName); |
430
|
|
|
|
431
|
1 |
|
throw $e; |
432
|
|
|
} |
433
|
|
|
|
434
|
1 |
|
$this->packages->remove($package->getName()); |
435
|
1 |
|
$this->packages->add(new RootPackage($packageFile, $package->getInstallPath())); |
436
|
1 |
|
} |
437
|
|
|
|
438
|
2 |
|
private function renameNonRootPackage(Package $package, $newName) |
439
|
|
|
{ |
440
|
2 |
|
$previousInstallInfo = $package->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
|
2 |
|
} |
448
|
|
|
|
449
|
2 |
|
$this->rootPackageFile->removeInstallInfo($package->getName()); |
450
|
2 |
|
$this->rootPackageFile->addInstallInfo($installInfo); |
451
|
|
|
|
452
|
|
|
try { |
453
|
2 |
|
$this->packageFileStorage->saveRootPackageFile($this->rootPackageFile); |
454
|
2 |
|
} catch (Exception $e) { |
455
|
1 |
|
$this->rootPackageFile->removeInstallInfo($newName); |
456
|
1 |
|
$this->rootPackageFile->addInstallInfo($previousInstallInfo); |
457
|
|
|
|
458
|
1 |
|
throw $e; |
459
|
|
|
} |
460
|
|
|
|
461
|
1 |
|
$this->packages->remove($package->getName()); |
462
|
1 |
|
$this->packages->add(new Package( |
463
|
1 |
|
$package->getPackageFile(), |
464
|
1 |
|
$package->getInstallPath(), |
465
|
1 |
|
$installInfo, |
466
|
1 |
|
$package->getLoadErrors() |
467
|
1 |
|
)); |
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.