Issues (5)

src/Tree.php (2 issues)

1
<?php
2
3
namespace bultonFr\DependencyTree;
4
5
use \Exception;
6
7
class Tree
8
{
9
    /**
10
     * @var array $dependenciesInfos : List of dependency with there infos;
11
     */
12
    protected $dependenciesInfos = [];
13
    
14
    /**
15
     * @var array $listDepends : List the depends of each dependancy
16
     */
17
    protected $listDepends = [];
18
    
19
    /**
20
     * @var array $dependenciesPositions : List of position in the tree for
21
     *                                      each dependancy
22
     */
23
    protected $dependenciesPositions = [];
24
    
25
    /**
26
     * @var array $tree : The generate tree
27
     */
28
    protected $tree = [];
29
    
30
    /**
31
     * @var \stdClass $genOrderSecurityLoop : Informations about dependency
32
     *  during the generateOrder.
33
     *  It's a security against infinite loop.
34
     */
35
    protected $genOrderSecurityLoop;
36
37
    /**
38
     * Add a dependency to system
39
     * 
40
     * @param string $name         : Dependency's name
41
     * @param int    $order        : Order to load dependency
42
     * @param array  $dependencies : All dependencies for this dependency
43
     * 
44
     * @throws Exception : If dependency already declared
45
     * 
46
     * @return Tree : Current instance
47
     */
48
    public function addDependency($name, $order = 0, $dependencies = [])
49
    {
50
        //Check if dependency is already declared.
51
        if (isset($this->dependenciesInfos[$name])) {
52
            throw new Exception('Dependency '.$name.' already declared.');
53
        }
54
        
55
        if (!is_array($dependencies)) {
0 ignored issues
show
The condition is_array($dependencies) is always true.
Loading history...
56
            throw new Exception('Dependencies must be passed in a array.');
57
        }
58
59
        $dependencyInfos        = new \stdClass;
60
        $dependencyInfos->order = $order;
61
        $dependencyInfos->dependencies = $dependencies;
62
63
        $this->dependenciesInfos[$name] = $dependencyInfos;
64
        
65
        //Create the key for list of depends if she doesn't exist
66
        if (!isset($this->listDepends[$name])) {
67
            $this->listDepends[$name] = [];
68
        }
69
        
70
        //Generate the list of depends
71
        if ($dependencies !== []) {
72
            foreach ($dependencies as $dependencyName) {
73
                if(!isset($this->listDepends[$dependencyName])) {
74
                    $this->listDepends[$dependencyName] = [];
75
                }
76
                
77
                $this->listDepends[$dependencyName][] = $name;
78
            }
79
        }
80
        
81
        return $this;
82
    }
83
84
    /**
85
     * Generate the tree
86
     * 
87
     * @return array
88
     */
89
    public function generateTree()
90
    {
91
        //Read all depencies declared and positioned each dependency on
92
        //the tree with they order value
93
        foreach ($this->dependenciesInfos as $name => $dependency) {
94
            $order = $dependency->order;
95
            
96
            //If the line for this order not exist
97
            if (!isset($this->tree[$order])) {
98
                $this->tree[$order] = [];
99
            }
100
            
101
            //Add to the tree and save the current position
102
            $this->tree[$order][]               = $name;
103
            $this->dependenciesPositions[$name] = [$order];
104
        }
105
        
106
        //Read the tree and check depends of each package.
107
        //Move some package in the tree if need for depends.
108
        foreach ($this->tree as $dependencies) {
109
            foreach ($dependencies as $dependencyName) {
110
                $this->checkDepend($dependencyName);
111
            }
112
        }
113
        
114
        //Sort by key. Some keys should be unorganized
115
        ksort($this->tree);
116
        
117
        return $this->tree;
118
    }
119
120
    /**
121
     * Check if all depends is correctly spoted in the tree
122
     * 
123
     * @param string $dependencyName : The name of the dependency for which
124
     *                                  check depends
125
     * 
126
     * @return void
127
     */
128
    protected function checkDepend($dependencyName)
129
    {
130
        $listDepends     = $this->listDepends[$dependencyName];
131
        $dependencyInfos = $this->dependenciesInfos[$dependencyName];
132
        $order           = $dependencyInfos->order;
133
134
        //No depends :)
135
        if ($listDepends === []) {
136
            return;
137
        }
138
139
        //Read all depends and check if they correctly spoted.
140
        //If not, call the method to move the depend read.
141
        foreach ($listDepends as $dependencyName) {
0 ignored issues
show
$dependencyName is overwriting one of the parameters of this function.
Loading history...
142
            $dependencyPos   = $this->dependenciesPositions[$dependencyName];
143
            $dependencyOrder = $dependencyPos[0];
144
145
            if ($dependencyOrder < $order) {
146
                $this->moveDepend($dependencyName, $order);
147
            }
148
        }
149
    }
150
151
    /**
152
     * Move a depend to a new position in the tree
153
     * 
154
     * @param string $dependencyName : The dependency name
155
     * @param int    $newOrder       : The new position in the tree
156
     * 
157
     * @return void
158
     */
159
    protected function moveDepend($dependencyName, $newOrder)
160
    {
161
        //Get old position in the tree for this dependency
162
        $dependencyInfos = $this->dependenciesInfos[$dependencyName];
163
        $oldOrder        = $dependencyInfos->order;
164
165
        //If the new position not already exist in the tree
166
        if (!isset($this->tree[$newOrder])) {
167
            $this->tree[$newOrder] = [];
168
        }
169
170
        //Add dependency to this new position
171
        $this->tree[$newOrder][] = $dependencyName;
172
173
        //Search the key corresponding to the old position in the array tree
174
        $oldKey = array_search($dependencyName, $this->tree[$oldOrder]);
175
        //Remove dependency from this old position
176
        unset($this->tree[$oldOrder][$oldKey]);
177
178
        //Call checkDepend for check all depends of this dependency
179
        $this->checkDepend($dependencyName);
180
    }
181
    
182
    /**
183
     * Generate the order position from the depends of each dependencies
184
     * 
185
     * First we generate a reversed tree from depends
186
     * Reverse the tree for have a tree in the correct depends order
187
     * And update the order value of each dependency with the tree of depends
188
     * 
189
     * @return void
190
     */
191
    public function generateOrderFromDependencies()
192
    {
193
        $this->initGenOrderSecurityLoop(0);
194
        $this->tree = [[]]; //generate a empty tree
195
        
196
        //Read all depends of each dependencies
197
        foreach ($this->listDepends as $dependencyName => $depends) {
198
        
199
            //If the dependency in the depend's list is declared on this tree.
200
            if (!isset($this->dependenciesInfos[$dependencyName])) {
201
                continue;
202
            }
203
            
204
            //If the package have depends, we continue
205
            if ($depends !== []) {
206
                continue;
207
            }
208
            
209
            //Add the dependency to the first line of tree
210
            $this->tree[0][$dependencyName] = $dependencyName;
211
            
212
            //And generate the order for this depends
213
            $this->genOrderSecurityLoopAddDepend($dependencyName, 0);
214
            $this->generateOrderForADependency($dependencyName, 0);
215
        }
216
        
217
        //Here, $this->tree have a reversed array of depends
218
        //So we reverse the array to have the array of depends
219
        //in the correct order
220
        $this->tree = array_reverse($this->tree);
221
        
222
        //Some line could be empty because the dependency is in another tree
223
        //So we define the order manually.
224
        $treeOrder = 0;
225
        
226
        //Read the tree for update the order of each dependency
227
        foreach ($this->tree as $dependencies) {
228
            if ($dependencies === []) {
229
                continue;
230
            }
231
            
232
            foreach ($dependencies as $dependencyName) {
233
                $dependencyInfos = &$this->dependenciesInfos[$dependencyName];
234
                
235
                //If the order has not be already updated
236
                if ($dependencyInfos->order > -1) {
237
                    continue;
238
                }
239
                
240
                $dependencyInfos->order = $treeOrder;
241
            }
242
            
243
            $treeOrder++;
244
        }
245
        
246
        //Reinit the tree for use the main system of tree generator.
247
        $this->tree = [];
248
    }
249
    
250
    /**
251
     * Define the order of all the dependencies of dependency
252
     * 
253
     * @param string $dependencyName : The name of dependency for which we
254
     *                                  read the dependencies
255
     * @param int    $currentOrder   : The current order of the dependency
256
     * 
257
     * @return void
258
     */
259
    protected function generateOrderForADependency($dependencyName, $currentOrder)
260
    {
261
        $depends = $this->dependenciesInfos[$dependencyName]->dependencies;
262
        if ($depends === []) {
263
            return;
264
        }
265
        
266
        $order = $currentOrder+1;
267
        if (!isset($this->tree[$order])) {
268
            $this->tree[$order] = [];
269
        }
270
        
271
        foreach ($depends as $dependName) {
272
            //If the dependency of the dependency is in a other tree
273
            if (!isset($this->dependenciesInfos[$dependName])) {
274
                continue;
275
            }
276
            
277
            //Infinite dependency loop security
278
            $this->checkReinitGenOrderSecurityLoop($order);
279
            $this->genOrderCheckInfiniteLoop($dependName);
280
            $this->tree[$order][$dependName] = $dependName;
281
            
282
            $this->genOrderSecurityLoopAddDepend($dependName, $order);
283
            $this->generateOrderForADependency(
284
                $dependName,
285
                $order
286
            );
287
        }
288
    }
289
    
290
    /**
291
     * Add a dependency into list used by security infinite depend loop.
292
     * 
293
     * @param string $dependencyName Dependency name
294
     * @param int    $order          The current order of the dependency
295
     * 
296
     * @return void
297
     */
298
    protected function genOrderSecurityLoopAddDepend($dependencyName, $order)
299
    {
300
        $this->checkReinitGenOrderSecurityLoop($order);
301
        
302
        $this->genOrderSecurityLoop->order  = $order;
303
        $this->genOrderSecurityLoop->list[] = (object) [
304
            'dependName' => $dependencyName,
305
            'dependList' => []
306
        ];
307
    }
308
    
309
    /**
310
     * Check if we reinitialize the list used against dependency infinite loop
311
     * 
312
     * @param int $order The current order of the dependency
313
     * 
314
     * @return void
315
     */
316
    protected function checkReinitGenOrderSecurityLoop($order)
317
    {
318
        if ($order <= $this->genOrderSecurityLoop->order) {
319
            $this->initGenOrderSecurityLoop($order);
320
        }
321
    }
322
    
323
    /**
324
     * (re)Initialize the list used against dependency infinite loop
325
     * 
326
     * @param int $order The current order of the dependency
327
     * 
328
     * @return void
329
     */
330
    protected function initGenOrderSecurityLoop($order)
331
    {
332
        $this->genOrderSecurityLoop = (object) [
333
            'order' => $order,
334
            'list'  => []
335
        ];
336
    }
337
    
338
    /**
339
     * Check if we allow to moved a dependency in the tree to protect
340
     * against infine loop.
341
     * It's for the case where a dependency is moved to be loaded before an
342
     * another, but this another dependency depend on the first package
343
     * who asked to be moved.
344
     * So the system try to moved packages at the infine. We protect this.
345
     * 
346
     * @see Issue #4 on the github repo.
347
     * 
348
     * @param string $checkDependName : The name of dependency
349
     * 
350
     * @return void
351
     * 
352
     * @throws Exception The infinite loop security.
353
     */
354
    protected function genOrderCheckInfiniteLoop($checkDependName)
355
    {
356
        $runException = false;
357
        foreach ($this->genOrderSecurityLoop->list as &$checkInfos) {
358
            if ($checkInfos->dependList === []) {
359
                $checkInfos->dependList[] = $checkDependName;
360
                continue;
361
            }
362
            
363
            if (
364
                $checkInfos->dependList !== []
365
                && $checkInfos->dependName !== $checkDependName
366
            ) {
367
                $checkInfos->dependList[] = $checkDependName;
368
                continue;
369
            }
370
            
371
            $runException = true;
372
            break;
373
        }
374
        
375
        if ($runException === false) {
376
            unset($checkInfos); //Kill ref
377
            return;
378
        }
379
        
380
        //Package is already moved for the original dependency : Loop error
381
        $loopInfos = '';
382
        foreach ($checkInfos->dependList as $packageName) {
383
            if ($loopInfos !== '') {
384
                $loopInfos .= ', ';
385
            }
386
            
387
            $loopInfos .= $packageName;
388
        }
389
        
390
        throw new Exception(
391
            'Infinite depends loop find for package '.$checkInfos->dependName
392
            .' - Loop info : '.$loopInfos
393
        );
394
    }
395
}
396