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\Conflict; |
13
|
|
|
|
14
|
|
|
use Puli\Manager\Api\Module\ModuleList; |
15
|
|
|
use RuntimeException; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* A directed, acyclic graph of module names. |
19
|
|
|
* |
20
|
|
|
* Modules can be added with {@link addModuleName()}. Edges between these modules |
21
|
|
|
* can then be added using {@link addEdge()}. Both ends of an edge must have |
22
|
|
|
* been defined before the edge is added. |
23
|
|
|
* |
24
|
|
|
* ```php |
25
|
|
|
* $graph = new DependencyGraph(); |
26
|
|
|
* $graph->addModuleName('acme/core'); |
27
|
|
|
* $graph->addModuleName('acme/blog'); |
28
|
|
|
* $graph->addModuleName('acme/blog-extension1'); |
29
|
|
|
* $graph->addModuleName('acme/blog-extension2'); |
30
|
|
|
* $graph->addDependency('acme/blog', 'acme/core'); |
31
|
|
|
* $graph->addDependency('acme/blog-extension1', 'acme/blog'); |
32
|
|
|
* $graph->addDependency('acme/blog-extension2', 'acme/blog'); |
33
|
|
|
* $graph->addDependency('acme/blog-extension2', 'acme/blog-extension1'); |
34
|
|
|
* ``` |
35
|
|
|
* |
36
|
|
|
* You can use {@link getPath()} and {@link hasPath()} to check whether a path |
37
|
|
|
* exists from one module to the other: |
38
|
|
|
* |
39
|
|
|
* ```php |
40
|
|
|
* // ... |
41
|
|
|
* |
42
|
|
|
* $graph->hasPath('acme/blog-extension1', 'acme/blog'); |
43
|
|
|
* // => true |
44
|
|
|
* |
45
|
|
|
* $graph->hasPath('acme/blog-extension2', 'acme/blog-extension1'); |
46
|
|
|
* // => false |
47
|
|
|
* |
48
|
|
|
* $graph->getPath('acme/blog-extension2', 'acme/core'); |
49
|
|
|
* // => array('acme/core', 'acme/blog', 'acme/blog-extension2') |
50
|
|
|
* ``` |
51
|
|
|
* |
52
|
|
|
* With {@link getSortedModuleNames()}, you can sort the modules such that the |
53
|
|
|
* dependencies defined via the edges are respected: |
54
|
|
|
* |
55
|
|
|
* ```php |
56
|
|
|
* // ... |
57
|
|
|
* |
58
|
|
|
* $graph->getSortedModuleNames(); |
59
|
|
|
* // => array('acme/core', 'acme/blog', 'acme/blog-extension1', 'acme/blog-extension2') |
60
|
|
|
* ``` |
61
|
|
|
* |
62
|
|
|
* @since 1.0 |
63
|
|
|
* |
64
|
|
|
* @author Bernhard Schussek <[email protected]> |
65
|
|
|
*/ |
66
|
|
|
class DependencyGraph |
67
|
|
|
{ |
68
|
|
|
/** |
69
|
|
|
* Stores the names of all modules (vertices) as keys. |
70
|
|
|
* |
71
|
|
|
* @var array |
72
|
|
|
*/ |
73
|
|
|
private $moduleNames = array(); |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* Stores the edges in the keys of a multi-dimensional array. |
77
|
|
|
* |
78
|
|
|
* The first dimension stores the module names, the second dimension the |
79
|
|
|
* names of the dependencies. |
80
|
|
|
* |
81
|
|
|
* @var array |
82
|
|
|
*/ |
83
|
|
|
private $dependencies = array(); |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* Creates an override graph for the given modules. |
87
|
|
|
* |
88
|
|
|
* @param ModuleList $modules The modules to load. |
89
|
|
|
* |
90
|
|
|
* @return static The created override graph. |
91
|
|
|
*/ |
92
|
93 |
|
public static function forModules(ModuleList $modules) |
93
|
|
|
{ |
94
|
93 |
|
$graph = new static($modules->getModuleNames()); |
95
|
|
|
|
96
|
93 |
|
foreach ($modules as $module) { |
97
|
93 |
|
if (null === $module->getModuleFile()) { |
98
|
2 |
|
continue; |
99
|
|
|
} |
100
|
|
|
|
101
|
93 |
|
foreach ($module->getModuleFile()->getDependencies() as $dependency) { |
102
|
35 |
|
if ($graph->hasModuleName($dependency)) { |
103
|
93 |
|
$graph->addDependency($module->getName(), $dependency); |
104
|
|
|
} |
105
|
|
|
} |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
// Do we have a root module? |
109
|
93 |
|
if (null === $modules->getRootModule()) { |
110
|
20 |
|
return $graph; |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
// Make sure we have numeric, ascending keys here |
114
|
73 |
|
$moduleOrder = array_values($modules->getRootModule()->getModuleFile()->getModuleOrder()); |
115
|
|
|
|
116
|
|
|
// Each module overrides the previous one in the list |
117
|
73 |
|
for ($i = 1, $l = count($moduleOrder); $i < $l; ++$i) { |
118
|
1 |
|
$dependency = $moduleOrder[$i - 1]; |
119
|
1 |
|
$moduleName = $moduleOrder[$i]; |
120
|
|
|
|
121
|
1 |
|
if ($graph->hasModuleName($dependency)) { |
122
|
1 |
|
$graph->addDependency($moduleName, $dependency); |
123
|
|
|
} |
124
|
|
|
} |
125
|
|
|
|
126
|
73 |
|
return $graph; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* Creates a new graph. |
131
|
|
|
* |
132
|
|
|
* @param string[] $moduleNames The module names stored in the nodes of |
133
|
|
|
* the graph. |
134
|
|
|
*/ |
135
|
132 |
|
public function __construct(array $moduleNames = array()) |
136
|
|
|
{ |
137
|
132 |
|
$this->addModuleNames($moduleNames); |
138
|
132 |
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Adds a module name to the graph. |
142
|
|
|
* |
143
|
|
|
* @param string $moduleName The module name. |
144
|
|
|
* |
145
|
|
|
* @throws RuntimeException If the module name already exists. |
146
|
|
|
*/ |
147
|
131 |
|
public function addModuleName($moduleName) |
148
|
|
|
{ |
149
|
131 |
|
if (isset($this->moduleNames[$moduleName])) { |
150
|
1 |
|
throw new RuntimeException(sprintf( |
151
|
1 |
|
'The module "%s" was added to the graph twice.', |
152
|
|
|
$moduleName |
153
|
|
|
)); |
154
|
|
|
} |
155
|
|
|
|
156
|
131 |
|
$this->moduleNames[$moduleName] = true; |
157
|
131 |
|
$this->dependencies[$moduleName] = array(); |
158
|
131 |
|
} |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* Adds a list of module names to the graph. |
162
|
|
|
* |
163
|
|
|
* @param string[] $moduleNames The module names. |
164
|
|
|
* |
165
|
|
|
* @throws RuntimeException If a module name already exists. |
166
|
|
|
*/ |
167
|
132 |
|
public function addModuleNames(array $moduleNames) |
168
|
|
|
{ |
169
|
132 |
|
foreach ($moduleNames as $moduleName) { |
170
|
105 |
|
$this->addModuleName($moduleName); |
171
|
|
|
} |
172
|
132 |
|
} |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* Returns whether a module name exists in the graph. |
176
|
|
|
* |
177
|
|
|
* @param string $moduleName The module name. |
178
|
|
|
* |
179
|
|
|
* @return bool Whether the module name exists. |
180
|
|
|
*/ |
181
|
40 |
|
public function hasModuleName($moduleName) |
182
|
|
|
{ |
183
|
40 |
|
return isset($this->moduleNames[$moduleName]); |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* Returns all module names in the graph. |
188
|
|
|
* |
189
|
|
|
* @return array All module names in the graph. |
190
|
|
|
*/ |
191
|
|
|
public function getModuleNames() |
192
|
|
|
{ |
193
|
|
|
return $this->moduleNames; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Returns the sorted module names. |
198
|
|
|
* |
199
|
|
|
* The names are sorted such that if a module m1 depends on a module m2, |
200
|
|
|
* then m2 comes before m1 in the sorted set. |
201
|
|
|
* |
202
|
|
|
* If module names are passed, only those module names are sorted. Otherwise |
203
|
|
|
* all module names are sorted. |
204
|
|
|
* |
205
|
|
|
* @param string[] $namesToSort The module names which should be sorted. |
206
|
|
|
* |
207
|
|
|
* @return string[] The sorted module names. |
208
|
|
|
* |
209
|
|
|
* @throws RuntimeException If any of the passed module names does not |
210
|
|
|
* exist in the graph. |
211
|
|
|
*/ |
212
|
83 |
|
public function getSortedModuleNames(array $namesToSort = array()) |
213
|
|
|
{ |
214
|
83 |
|
if (count($namesToSort) > 0) { |
215
|
54 |
|
$namesToSort = array_flip($namesToSort); |
216
|
|
|
|
217
|
54 |
|
foreach ($namesToSort as $module => $_) { |
218
|
54 |
|
if (!isset($this->moduleNames[$module])) { |
219
|
1 |
|
throw new RuntimeException(sprintf( |
220
|
54 |
|
'The module "%s" does not exist in the graph.', |
221
|
|
|
$module |
222
|
|
|
)); |
223
|
|
|
} |
224
|
|
|
} |
225
|
|
|
} else { |
226
|
29 |
|
$namesToSort = $this->moduleNames; |
227
|
|
|
} |
228
|
|
|
|
229
|
82 |
|
$sorted = array(); |
230
|
|
|
|
231
|
|
|
// Do a topologic sort |
232
|
|
|
// Start with any module and process until no more are left |
233
|
82 |
|
while (false !== reset($namesToSort)) { |
234
|
82 |
|
$this->sortModulesDFS(key($namesToSort), $namesToSort, $sorted); |
235
|
|
|
} |
236
|
|
|
|
237
|
82 |
|
return $sorted; |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
/** |
241
|
|
|
* Adds a dependency from one to another module. |
242
|
|
|
* |
243
|
|
|
* @param string $moduleName The module name. |
244
|
|
|
* @param string $dependency The name of the dependency. |
245
|
|
|
*/ |
246
|
67 |
|
public function addDependency($moduleName, $dependency) |
247
|
|
|
{ |
248
|
67 |
|
if (!isset($this->moduleNames[$dependency])) { |
249
|
1 |
|
throw new RuntimeException(sprintf( |
250
|
1 |
|
'The module "%s" does not exist in the graph.', |
251
|
|
|
$dependency |
252
|
|
|
)); |
253
|
|
|
} |
254
|
|
|
|
255
|
66 |
|
if (!isset($this->moduleNames[$moduleName])) { |
256
|
1 |
|
throw new RuntimeException(sprintf( |
257
|
1 |
|
'The module "%s" does not exist in the graph.', |
258
|
|
|
$moduleName |
259
|
|
|
)); |
260
|
|
|
} |
261
|
|
|
|
262
|
65 |
|
if (null !== ($path = $this->getPath($dependency, $moduleName))) { |
263
|
1 |
|
$last = array_pop($path); |
264
|
|
|
|
265
|
1 |
|
throw new CyclicDependencyException(sprintf( |
266
|
|
|
'A cyclic dependency was discovered between the modules "%s" '. |
267
|
|
|
'and "%s". Please check the "override" keys defined in these '. |
268
|
1 |
|
'modules.', |
269
|
1 |
|
implode('", "', $path), |
270
|
|
|
$last |
271
|
|
|
)); |
272
|
|
|
} |
273
|
|
|
|
274
|
65 |
|
$this->dependencies[$moduleName][$dependency] = true; |
275
|
65 |
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Removes a dependency from one to another module. |
279
|
|
|
* |
280
|
|
|
* @param string $moduleName The module name. |
281
|
|
|
* @param string $dependency The name of the dependency. |
282
|
|
|
*/ |
283
|
2 |
|
public function removeDependency($moduleName, $dependency) |
284
|
|
|
{ |
285
|
2 |
|
unset($this->dependencies[$moduleName][$dependency]); |
286
|
2 |
|
} |
287
|
|
|
|
288
|
|
|
/** |
289
|
|
|
* Returns whether a module directly depends on another module. |
290
|
|
|
* |
291
|
|
|
* @param string $moduleName The module name. |
292
|
|
|
* @param string $dependency The name of the dependency. |
293
|
|
|
* @param bool $recursive Whether to take recursive dependencies into |
294
|
|
|
* account. |
295
|
|
|
* |
296
|
|
|
* @return bool Whether an edge exists from the origin to the target module. |
297
|
|
|
*/ |
298
|
40 |
|
public function hasDependency($moduleName, $dependency, $recursive = true) |
299
|
|
|
{ |
300
|
40 |
|
if ($recursive) { |
301
|
9 |
|
return $this->hasPath($moduleName, $dependency); |
302
|
|
|
} |
303
|
|
|
|
304
|
39 |
|
return isset($this->dependencies[$moduleName][$dependency]); |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
/** |
308
|
|
|
* Returns whether a path exists from a module to a dependency. |
309
|
|
|
* |
310
|
|
|
* @param string $moduleName The module name. |
311
|
|
|
* @param string $dependency The name of the dependency. |
312
|
|
|
* |
313
|
|
|
* @return bool Whether a path exists from the origin to the target module. |
314
|
|
|
*/ |
315
|
18 |
|
public function hasPath($moduleName, $dependency) |
316
|
|
|
{ |
317
|
|
|
// does not exist in the graph |
318
|
18 |
|
if (!isset($this->dependencies[$moduleName])) { |
319
|
2 |
|
return false; |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
// adjacent node |
323
|
17 |
|
if (isset($this->dependencies[$moduleName][$dependency])) { |
324
|
4 |
|
return true; |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
// DFS |
328
|
15 |
|
foreach ($this->dependencies[$moduleName] as $predecessor => $_) { |
329
|
4 |
|
if ($this->hasPath($predecessor, $dependency)) { |
330
|
4 |
|
return true; |
331
|
|
|
} |
332
|
|
|
} |
333
|
|
|
|
334
|
14 |
|
return false; |
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
/** |
338
|
|
|
* Returns the path from a module name to a dependency. |
339
|
|
|
* |
340
|
|
|
* @param string $moduleName The module name. |
341
|
|
|
* @param string $dependency The name of the dependency. |
342
|
|
|
* |
343
|
|
|
* @return null|string[] The sorted module names on the path or `null` if no |
|
|
|
|
344
|
|
|
* path was found. |
345
|
|
|
*/ |
346
|
65 |
|
public function getPath($moduleName, $dependency) |
347
|
|
|
{ |
348
|
65 |
|
if ($this->getPathDFS($moduleName, $dependency, $reversePath)) { |
349
|
3 |
|
return array_reverse($reversePath); |
350
|
|
|
} |
351
|
|
|
|
352
|
65 |
|
return null; |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
/** |
356
|
|
|
* Finds a path between modules using Depth-First Search. |
357
|
|
|
* |
358
|
|
|
* @param string $moduleName The end module name. |
359
|
|
|
* @param string $dependency The start module name. |
360
|
|
|
* @param array $reversePath The path in reverse order. |
361
|
|
|
* |
362
|
|
|
* @return bool Whether a path was found. |
363
|
|
|
*/ |
364
|
65 |
|
private function getPathDFS($moduleName, $dependency, &$reversePath = array()) |
365
|
|
|
{ |
366
|
|
|
// does not exist in the graph |
367
|
65 |
|
if (!isset($this->dependencies[$moduleName])) { |
368
|
1 |
|
return false; |
369
|
|
|
} |
370
|
|
|
|
371
|
65 |
|
$reversePath[] = $moduleName; |
372
|
|
|
|
373
|
|
|
// adjacent node |
374
|
65 |
|
if (isset($this->dependencies[$moduleName][$dependency])) { |
375
|
3 |
|
$reversePath[] = $dependency; |
376
|
|
|
|
377
|
3 |
|
return true; |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
// DFS |
381
|
65 |
|
foreach ($this->dependencies[$moduleName] as $predecessor => $_) { |
382
|
45 |
|
if ($this->getPathDFS($predecessor, $dependency, $reversePath)) { |
383
|
45 |
|
return true; |
384
|
|
|
} |
385
|
|
|
} |
386
|
|
|
|
387
|
65 |
|
return false; |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
/** |
391
|
|
|
* Topologically sorts the given module name into the output array. |
392
|
|
|
* |
393
|
|
|
* The resulting array is sorted such that all predecessors of the module |
394
|
|
|
* come before the module (and their predecessors before them, and so on). |
395
|
|
|
* |
396
|
|
|
* @param string $currentName The current module name to sort. |
397
|
|
|
* @param array $namesToSort The module names yet to be sorted. |
398
|
|
|
* @param array $output The output array. |
399
|
|
|
*/ |
400
|
82 |
|
private function sortModulesDFS($currentName, array &$namesToSort, array &$output) |
401
|
|
|
{ |
402
|
82 |
|
unset($namesToSort[$currentName]); |
403
|
|
|
|
404
|
|
|
// Before adding the module itself to the path, add all predecessors. |
405
|
|
|
// Do so recursively, then we make sure that each module is visited |
406
|
|
|
// in the path before any of its successors. |
407
|
82 |
|
foreach ($this->dependencies[$currentName] as $predecessor => $_) { |
408
|
|
|
// The module was already processed. Either the module is on the |
409
|
|
|
// path already, then we're good. Otherwise, we have a cycle. |
410
|
|
|
// However, addEdge() guarantees that the graph is cycle-free. |
411
|
44 |
|
if (isset($namesToSort[$predecessor])) { |
412
|
44 |
|
$this->sortModulesDFS($predecessor, $namesToSort, $output); |
413
|
|
|
} |
414
|
|
|
} |
415
|
|
|
|
416
|
82 |
|
$output[] = $currentName; |
417
|
82 |
|
} |
418
|
|
|
} |
419
|
|
|
|
This check compares the return type specified in the
@return
annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.If the return type contains the type array, this check recommends the use of a more specific type like
String[]
orarray<String>
.