Passed
Push — master ( 2d0497...67acf7 )
by Siad
05:26
created

Project::_tsort()   B

Complexity

Conditions 11
Paths 32

Size

Total Lines 50
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 11.2865

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 31
c 1
b 0
f 0
nc 32
nop 4
dl 0
loc 50
ccs 26
cts 30
cp 0.8667
crap 11.2865
rs 7.3166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the LGPL. For more information please see
17
 * <http://phing.info>.
18
 */
19
20
/**
21
 *  The Phing project class. Represents a completely configured Phing project.
22
 *  The class defines the project and all tasks/targets. It also contains
23
 *  methods to start a build as well as some properties and FileSystem
24
 *  abstraction.
25
 *
26
 * @author Andreas Aderhold <[email protected]>
27
 * @author Hans Lellelid <[email protected]>
28
 *
29
 * @package phing
30
 */
31
class Project
32
{
33
34
    // Logging level constants.
35
    public const MSG_DEBUG = 4;
36
    public const MSG_VERBOSE = 3;
37
    public const MSG_INFO = 2;
38
    public const MSG_WARN = 1;
39
    public const MSG_ERR = 0;
40
41
    /**
42
     * contains the targets
43
     *
44
     * @var Target[]
45
     */
46
    private $targets = [];
47
    /**
48
     * global filterset (future use)
49
     */
50
    private $globalFilterSet = [];
51
    /**
52
     * all globals filters (future use)
53
     */
54
    private $globalFilters = [];
0 ignored issues
show
introduced by
The private property $globalFilters is not used, and could be removed.
Loading history...
55
56
    /**
57
     * holds ref names and a reference to the referred object
58
     */
59
    private $references = [];
60
61
    /**
62
     * The InputHandler being used by this project.
63
     *
64
     * @var InputHandler
65
     */
66
    private $inputHandler;
67
68
    /* -- properties that come in via xml attributes -- */
69
70
    /**
71
     * basedir (PhingFile object)
72
     */
73
    private $basedir;
74
75
    /**
76
     * the default target name
77
     */
78
    private $defaultTarget = 'all';
79
80
    /**
81
     * project name (required)
82
     */
83
    private $name;
84
85
    /**
86
     * project description
87
     */
88
    private $description;
89
90
    /**
91
     * require phing version
92
     */
93
    private $phingVersion;
94
95
    /**
96
     * project strict mode
97
     */
98
    private $strictMode = false;
99
100
    /**
101
     * a FileUtils object
102
     */
103
    private $fileUtils;
104
105
    /**
106
     * Build listeneers
107
     */
108
    private $listeners = [];
109
110
    /**
111
     * Keep going flag.
112
     */
113
    private $keepGoingMode = false;
114
115
    /**
116
     * @var string[]
117
     */
118
    private $executedTargetNames = [];
119
120
    /**
121
     *  Constructor, sets any default vars.
122
     */
123 868
    public function __construct()
124
    {
125 868
        $this->fileUtils = new FileUtils();
126 868
    }
127
128
    /**
129
     * Sets the input handler
130
     *
131
     * @param InputHandler $handler
132
     */
133 33
    public function setInputHandler($handler)
134
    {
135 33
        $this->inputHandler = $handler;
136 33
    }
137
138
    /**
139
     * Retrieves the current input handler.
140
     *
141
     * @return InputHandler
142
     */
143 33
    public function getInputHandler()
144
    {
145 33
        return $this->inputHandler;
146
    }
147
148
    /**
149
     * inits the project, called from main app
150
     */
151 833
    public function init()
152
    {
153
        // set builtin properties
154 833
        $this->setSystemProperties();
155
156 833
        $componentHelper = ComponentHelper::getComponentHelper($this);
157
158 833
        $componentHelper->initDefaultDefinitions();
159 833
    }
160
161
    /**
162
     * Create and initialize a subproject. By default the subproject will be of
163
     * the same type as its parent. If a no-arg constructor is unavailable, the
164
     * <code>Project</code> class will be used.
165
     * @return Project instance configured as a subproject of this Project.
166
     */
167 36
    public function createSubProject(): \Project
168
    {
169
        try {
170 36
            $ref = new ReflectionObject($this);
171 36
            $subProject = $ref->newInstance();
172
        } catch (ReflectionException $e) {
173
            $subProject = new Project();
174
        }
175 36
        $this->initSubProject($subProject);
176 36
        return $subProject;
177
    }
178
179
    /**
180
     * Initialize a subproject.
181
     * @param Project $subProject the subproject to initialize.
182
     */
183 36
    public function initSubProject(Project $subProject): void
184
    {
185 36
        ComponentHelper::getComponentHelper($subProject)
186 36
            ->initSubProject(ComponentHelper::getComponentHelper($this));
187 36
        $subProject->setKeepGoingMode($this->isKeepGoingMode());
188 36
        $subProject->setStrictMode($this->strictMode);
189 36
    }
190
191
    /**
192
     * returns the global filterset (future use)
193
     */
194
    public function getGlobalFilterSet()
195
    {
196
        return $this->globalFilterSet;
197
    }
198
199
    // ---------------------------------------------------------
200
    // Property methods
201
    // ---------------------------------------------------------
202
203
    /**
204
     * Sets a property. Any existing property of the same name
205
     * is overwritten, unless it is a user property.
206
     *
207
     * @param string $name The name of property to set.
208
     *                       Must not be <code>null</code>.
209
     * @param string $value The new value of the property.
210
     *                       Must not be <code>null</code>.
211
     */
212 241
    public function setProperty($name, $value)
213
    {
214 241
        PropertyHelper::getPropertyHelper($this)->setProperty(null, $name, $value, true);
215 241
    }
216
217
    /**
218
     * Sets a property if no value currently exists. If the property
219
     * exists already, a message is logged and the method returns with
220
     * no other effect.
221
     *
222
     * @param string $name The name of property to set.
223
     *                      Must not be <code>null</code>.
224
     * @param string $value The new value of the property.
225
     *                      Must not be <code>null</code>.
226
     * @since 2.0
227
     */
228 93
    public function setNewProperty($name, $value)
229
    {
230 93
        PropertyHelper::getPropertyHelper($this)->setNewProperty(null, $name, $value);
231 93
    }
232
233
    /**
234
     * Sets a user property, which cannot be overwritten by
235
     * set/unset property calls. Any previous value is overwritten.
236
     *
237
     * @param string $name The name of property to set.
238
     *                      Must not be <code>null</code>.
239
     * @param string $value The new value of the property.
240
     *                      Must not be <code>null</code>.
241
     * @see   setProperty()
242
     */
243 833
    public function setUserProperty($name, $value)
244
    {
245 833
        PropertyHelper::getPropertyHelper($this)->setUserProperty(null, $name, $value);
246 833
    }
247
248
    /**
249
     * Sets a user property, which cannot be overwritten by set/unset
250
     * property calls. Any previous value is overwritten. Also marks
251
     * these properties as properties that have not come from the
252
     * command line.
253
     *
254
     * @param string $name The name of property to set.
255
     *                      Must not be <code>null</code>.
256
     * @param string $value The new value of the property.
257
     *                      Must not be <code>null</code>.
258
     * @see   setProperty()
259
     */
260 12
    public function setInheritedProperty($name, $value)
261
    {
262 12
        PropertyHelper::getPropertyHelper($this)->setInheritedProperty(null, $name, $value);
263 12
    }
264
265
    /**
266
     * Sets a property unless it is already defined as a user property
267
     * (in which case the method returns silently).
268
     *
269
     * @param string $name The name of the property.
270
     *                      Must not be
271
     *                      <code>null</code>.
272
     * @param string $value The property value. Must not be <code>null</code>.
273
     */
274 849
    private function setPropertyInternal($name, $value)
275
    {
276 849
        PropertyHelper::getPropertyHelper($this)->setProperty(null, $name, $value, false);
277 849
    }
278
279
    /**
280
     * Returns the value of a property, if it is set.
281
     *
282
     * @param  string $name The name of the property.
283
     *                      May be <code>null</code>, in which case
284
     *                      the return value is also <code>null</code>.
285
     * @return string The property value, or <code>null</code> for no match
286
     *                     or if a <code>null</code> name is provided.
287
     */
288 833
    public function getProperty($name)
289
    {
290 833
        return PropertyHelper::getPropertyHelper($this)->getProperty(null, $name);
291
    }
292
293
    /**
294
     * Replaces ${} style constructions in the given value with the
295
     * string value of the corresponding data types.
296
     *
297
     * @param string $value The value string to be scanned for property references.
298
     *                      May be <code>null</code>.
299
     *
300
     * @return string the given string with embedded property names replaced
301
     *                by values, or <code>null</code> if the given string is
302
     *                <code>null</code>.
303
     *
304
     * @throws BuildException if the given value has an unclosed
305
     *                           property name, e.g. <code>${xxx</code>
306
     */
307 689
    public function replaceProperties($value)
308
    {
309 689
        return PropertyHelper::getPropertyHelper($this)->replaceProperties($value, $this->getProperties());
310
    }
311
312
    /**
313
     * Returns the value of a user property, if it is set.
314
     *
315
     * @param  string $name The name of the property.
316
     *                      May be <code>null</code>, in which case
317
     *                      the return value is also <code>null</code>.
318
     * @return string The property value, or <code>null</code> for no match
319
     *                     or if a <code>null</code> name is provided.
320
     */
321 1
    public function getUserProperty($name)
322
    {
323 1
        return PropertyHelper::getPropertyHelper($this)->getUserProperty(null, $name);
324
    }
325
326
    /**
327
     * Returns a copy of the properties table.
328
     *
329
     * @return array A hashtable containing all properties
330
     *               (including user properties).
331
     */
332 834
    public function getProperties()
333
    {
334 834
        return PropertyHelper::getPropertyHelper($this)->getProperties();
335
    }
336
337
    /**
338
     * Returns a copy of the user property hashtable
339
     *
340
     * @return array a hashtable containing just the user properties
341
     */
342
    public function getUserProperties()
343
    {
344
        return PropertyHelper::getPropertyHelper($this)->getUserProperties();
345
    }
346
347
    public function getInheritedProperties()
348
    {
349
        return PropertyHelper::getPropertyHelper($this)->getInheritedProperties();
350
    }
351
352
    /**
353
     * Copies all user properties that have been set on the command
354
     * line or a GUI tool from this instance to the Project instance
355
     * given as the argument.
356
     *
357
     * <p>To copy all "user" properties, you will also have to call
358
     * {@link #copyInheritedProperties copyInheritedProperties}.</p>
359
     *
360
     * @param  Project $other the project to copy the properties to.  Must not be null.
361
     * @return void
362
     * @since  phing 2.0
363
     */
364 33
    public function copyUserProperties(Project $other)
365
    {
366 33
        PropertyHelper::getPropertyHelper($this)->copyUserProperties($other);
367 33
    }
368
369
    /**
370
     * Copies all user properties that have not been set on the
371
     * command line or a GUI tool from this instance to the Project
372
     * instance given as the argument.
373
     *
374
     * <p>To copy all "user" properties, you will also have to call
375
     * {@link #copyUserProperties copyUserProperties}.</p>
376
     *
377
     * @param Project $other the project to copy the properties to.  Must not be null.
378
     *
379
     * @since phing 2.0
380
     */
381 33
    public function copyInheritedProperties(Project $other)
382
    {
383 33
        PropertyHelper::getPropertyHelper($this)->copyUserProperties($other);
384 33
    }
385
386
    // ---------------------------------------------------------
387
    //  END Properties methods
388
    // ---------------------------------------------------------
389
390
    /**
391
     * Sets default target
392
     *
393
     * @param string $targetName
394
     */
395 833
    public function setDefaultTarget($targetName)
396
    {
397 833
        $this->defaultTarget = (string) trim($targetName);
398 833
    }
399
400
    /**
401
     * Returns default target
402
     *
403
     * @return string
404
     */
405 33
    public function getDefaultTarget()
406
    {
407 33
        return (string) $this->defaultTarget;
408
    }
409
410
    /**
411
     * Sets the name of the current project
412
     *
413
     * @param  string $name name of project
414
     * @return void
415
     * @author Andreas Aderhold, [email protected]
416
     */
417 833
    public function setName($name)
418
    {
419 833
        $this->name = (string) trim($name);
420 833
        $this->setUserProperty("phing.project.name", $this->name);
421 833
    }
422
423
    /**
424
     * Returns the name of this project
425
     *
426
     * @return string projectname
427
     * @author Andreas Aderhold, [email protected]
428
     */
429 2
    public function getName()
430
    {
431 2
        return (string) $this->name;
432
    }
433
434
    /**
435
     * Set the projects description
436
     *
437
     * @param string $description
438
     */
439 6
    public function setDescription($description)
440
    {
441 6
        $this->description = $description;
442 6
    }
443
444
    /**
445
     * return the description, null otherwise
446
     *
447
     * @return string|null
448
     */
449 6
    public function getDescription()
450
    {
451 6
        if ($this->description === null) {
452 5
            $this->description = Description::getAll($this);
453
        }
454 6
        return $this->description;
455
    }
456
457
    /**
458
     * Set the minimum required phing version
459
     *
460
     * @param string $version
461
     */
462 5
    public function setPhingVersion($version)
463
    {
464 5
        $version = str_replace('phing', '', strtolower($version));
465 5
        $this->phingVersion = (string) trim($version);
466 5
    }
467
468
    /**
469
     * Get the minimum required phing version
470
     *
471
     * @return string
472
     */
473 5
    public function getPhingVersion()
474
    {
475 5
        if ($this->phingVersion === null) {
476 5
            $this->setPhingVersion(Phing::getPhingVersion());
477
        }
478
479 5
        return $this->phingVersion;
480
    }
481
482
    /**
483
     * Sets the strict-mode (status) for the current project
484
     * (If strict mode is On, all the warnings would be converted to an error
485
     * (and the build will be stopped/aborted)
486
     *
487
     * @param bool $strictmode
488
     * @return void
489
     * @access public
490
     * @author Utsav Handa, [email protected]
491
     */
492 36
    public function setStrictMode(bool $strictmode)
493
    {
494 36
        $this->strictMode = $strictmode;
495 36
        $this->setProperty("phing.project.strictmode", $this->strictMode);
496 36
    }
497
498
    /**
499
     * Get the strict-mode status for the project
500
     *
501
     * @return boolean
502
     */
503
    public function getStrictmode()
504
    {
505
        return $this->strictMode;
506
    }
507
508
    /**
509
     * Set basedir object from xm
510
     *
511
     * @param  PhingFile|string $dir
512
     * @throws BuildException
513
     */
514 849
    public function setBasedir($dir)
515
    {
516 849
        if ($dir instanceof PhingFile) {
517 92
            $dir = $dir->getAbsolutePath();
518
        }
519
520 849
        $dir = $this->fileUtils->normalize($dir);
521 849
        $dir = FileSystem::getFileSystem()->canonicalize($dir);
522
523 849
        $dir = new PhingFile((string) $dir);
524 849
        if (!$dir->exists()) {
525
            throw new BuildException("Basedir " . $dir->getAbsolutePath() . " does not exist");
526
        }
527 849
        if (!$dir->isDirectory()) {
528
            throw new BuildException("Basedir " . $dir->getAbsolutePath() . " is not a directory");
529
        }
530 849
        $this->basedir = $dir;
531 849
        $this->setPropertyInternal("project.basedir", $this->basedir->getPath());
532 849
        $this->log("Project base dir set to: " . $this->basedir->getPath(), Project::MSG_VERBOSE);
533
534
        // [HL] added this so that ./ files resolve correctly.  This may be a mistake ... or may be in wrong place.
535 849
        chdir($dir->getAbsolutePath());
536 849
    }
537
538
    /**
539
     * Returns the basedir of this project
540
     *
541
     * @return PhingFile      Basedir PhingFile object
542
     *
543
     * @throws BuildException
544
     *
545
     * @author Andreas Aderhold, [email protected]
546
     */
547 177
    public function getBasedir()
548
    {
549 177
        if ($this->basedir === null) {
550
            try { // try to set it
551
                $this->setBasedir(".");
552
            } catch (BuildException $exc) {
553
                throw new BuildException("Can not set default basedir. " . $exc->getMessage());
554
            }
555
        }
556
557 177
        return $this->basedir;
558
    }
559
560
    /**
561
     * Set &quot;keep-going&quot; mode. In this mode Ant will try to execute
562
     * as many targets as possible. All targets that do not depend
563
     * on failed target(s) will be executed.  If the keepGoing settor/getter
564
     * methods are used in conjunction with the <code>ant.executor.class</code>
565
     * property, they will have no effect.
566
     *
567
     * @param bool $keepGoingMode &quot;keep-going&quot; mode
568
     */
569 36
    public function setKeepGoingMode($keepGoingMode)
570
    {
571 36
        $this->keepGoingMode = $keepGoingMode;
572 36
    }
573
574
    /**
575
     * Return the keep-going mode.  If the keepGoing settor/getter
576
     * methods are used in conjunction with the <code>phing.executor.class</code>
577
     * property, they will have no effect.
578
     *
579
     * @return bool &quot;keep-going&quot; mode
580
     */
581 36
    public function isKeepGoingMode()
582
    {
583 36
        return $this->keepGoingMode;
584
    }
585
586
    /**
587
     * Sets system properties and the environment variables for this project.
588
     *
589
     * @return void
590
     */
591 833
    public function setSystemProperties()
592
    {
593
594
        // first get system properties
595 833
        $systemP = array_merge($this->getProperties(), Phing::getProperties());
596 833
        foreach ($systemP as $name => $value) {
597 833
            $this->setPropertyInternal($name, $value);
598
        }
599
600
        // and now the env vars
601 833
        foreach ($_SERVER as $name => $value) {
602
            // skip arrays
603 833
            if (is_array($value)) {
604 833
                continue;
605
            }
606 833
            $this->setPropertyInternal('env.' . $name, $value);
607
        }
608 833
    }
609
610
    /**
611
     * Adds a task definition.
612
     *
613
     * @param string $name Name of tag.
614
     * @param string $class The class path to use.
615
     * @param string $classpath The classpat to use.
616
     */
617 73
    public function addTaskDefinition($name, $class, $classpath = null)
618
    {
619 73
        ComponentHelper::getComponentHelper($this)->addTaskDefinition($name, $class, $classpath);
620 73
    }
621
622
    /**
623
     * Returns the task definitions
624
     *
625
     * @return array
626
     */
627 45
    public function getTaskDefinitions()
628
    {
629 45
        return ComponentHelper::getComponentHelper($this)->getTaskDefinitions();
630
    }
631
632
    /**
633
     * Adds a data type definition.
634
     *
635
     * @param string $typeName Name of the type.
636
     * @param string $typeClass The class to use.
637
     * @param string $classpath The classpath to use.
638
     */
639 40
    public function addDataTypeDefinition($typeName, $typeClass, $classpath = null)
640
    {
641 40
        ComponentHelper::getComponentHelper($this)->addDataTypeDefinition($typeName, $typeClass, $classpath);
642 40
    }
643
644
    /**
645
     * Returns the data type definitions
646
     *
647
     * @return array
648
     */
649 834
    public function getDataTypeDefinitions()
650
    {
651 834
        return ComponentHelper::getComponentHelper($this)->getDataTypeDefinitions();
652
    }
653
654
    /**
655
     * Add a new target to the project
656
     *
657
     * @param  string $targetName
658
     * @param  Target $target
659
     * @throws BuildException
660
     */
661 833
    public function addTarget($targetName, $target)
662
    {
663 833
        if (isset($this->targets[$targetName])) {
664
            throw new BuildException("Duplicate target: $targetName");
665
        }
666 833
        $this->addOrReplaceTarget($targetName, $target);
667 833
    }
668
669
    /**
670
     * Adds or replaces a target in the project
671
     *
672
     * @param string $targetName
673
     * @param Target $target
674
     */
675 833
    public function addOrReplaceTarget($targetName, &$target)
676
    {
677 833
        $this->log("  +Target: $targetName", Project::MSG_DEBUG);
678 833
        $target->setProject($this);
679 833
        $this->targets[$targetName] = $target;
680
681 833
        $ctx = $this->getReference(ProjectConfigurator::PARSING_CONTEXT_REFERENCE);
682 833
        $current = $ctx->getCurrentTargets();
683 833
        $current[$targetName] = $target;
684 833
    }
685
686
    /**
687
     * Returns the available targets
688
     *
689
     * @return Target[]
690
     */
691 833
    public function getTargets()
692
    {
693 833
        return $this->targets;
694
    }
695
696
    /**
697
     * @return string[]
698
     */
699
    public function getExecutedTargetNames()
700
    {
701
        return $this->executedTargetNames;
702
    }
703
704
    /**
705
     * Create a new task instance and return reference to it.
706
     *
707
     * @param string $taskType Task name
708
     * @return Task           A task object
709
     * @throws BuildException
710
     */
711 694
    public function createTask($taskType)
712
    {
713 694
        return ComponentHelper::getComponentHelper($this)->createTask($taskType);
714
    }
715
716
    /**
717
     * Creates a new condition and returns the reference to it
718
     *
719
     * @param  string $conditionType
720
     * @return Condition
721
     * @throws BuildException
722
     */
723 1
    public function createCondition($conditionType)
724
    {
725 1
        return ComponentHelper::getComponentHelper($this)->createCondition($conditionType);
726
    }
727
728
    /**
729
     * Create a datatype instance and return reference to it
730
     * See createTask() for explanation how this works
731
     *
732
     * @param  string $typeName Type name
733
     * @return object         A datatype object
734
     * @throws BuildException
735
     *                                 Exception
736
     */
737 83
    public function createDataType($typeName)
738
    {
739 83
        return ComponentHelper::getComponentHelper($this)->createDataType($typeName);
740
    }
741
742
    /**
743
     * Executes a list of targets
744
     *
745
     * @param  array $targetNames List of target names to execute
746
     * @return void
747
     * @throws BuildException
748
     */
749
    public function executeTargets($targetNames)
750
    {
751
        $this->executedTargetNames = $targetNames;
752
753
        foreach ($targetNames as $tname) {
754
            $this->executeTarget($tname);
755
        }
756
    }
757
758
    /**
759
     * Executes a target
760
     *
761
     * @param  string $targetName Name of Target to execute
762
     * @return void
763
     * @throws BuildException
764
     */
765 644
    public function executeTarget($targetName)
766
    {
767
768
        // complain about executing void
769 644
        if ($targetName === null) {
0 ignored issues
show
introduced by
The condition $targetName === null is always false.
Loading history...
770
            throw new BuildException("No target specified");
771
        }
772
773
        // invoke topological sort of the target tree and run all targets
774
        // until targetName occurs.
775 644
        $sortedTargets = $this->topoSort($targetName);
776
777 644
        $curIndex = (int) 0;
778 644
        $curTarget = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $curTarget is dead and can be removed.
Loading history...
779 644
        $thrownException = null;
780 644
        $buildException = null;
781
        do {
782
            try {
783 644
                $curTarget = $sortedTargets[$curIndex++];
784 644
                $curTarget->performTasks();
785 168
            } catch (BuildException $exc) {
786 168
                if (!($this->keepGoingMode)) {
787 168
                    throw $exc;
788
                }
789
                $thrownException = $exc;
790
            }
791 528
            if ($thrownException != null) {
792
                if ($thrownException instanceof BuildException) {
793
                    $this->log(
794
                        "Target '" . $curTarget->getName()
795
                        . "' failed with message '"
796
                        . $thrownException->getMessage() . "'.",
797
                        Project::MSG_ERR
798
                    );
799
                    // only the first build exception is reported
800
                    if ($buildException === null) {
801
                        $buildException = $thrownException;
802
                    }
803
                } else {
804
                    $this->log(
805
                        "Target '" . $curTarget->getName()
806
                        . "' failed with message '"
807
                        . $thrownException->getMessage() . "'." . PHP_EOL
808
                        . $thrownException->getTraceAsString(),
809
                        Project::MSG_ERR
810
                    );
811
                    if ($buildException === null) {
812
                        $buildException = new BuildException($thrownException);
813
                    }
814
                }
815
            }
816 528
        } while ($curTarget->getName() !== $targetName);
817
818 528
        if ($buildException !== null) {
819
            throw $buildException;
820
        }
821 528
    }
822
823
    /**
824
     * Helper function
825
     *
826
     * @param  string $fileName
827
     * @param  PhingFile $rootDir
828
     * @return \PhingFile
829
     * @throws IOException
830
     */
831 452
    public function resolveFile(string $fileName, PhingFile $rootDir = null): PhingFile
832
    {
833 452
        if ($rootDir === null) {
834 446
            return $this->fileUtils->resolveFile($this->basedir, $fileName);
835
        }
836 84
        return $this->fileUtils->resolveFile($rootDir, $fileName);
837
    }
838
839
    /**
840
     * Return the boolean equivalent of a string, which is considered
841
     * <code>true</code> if either <code>"on"</code>, <code>"true"</code>,
842
     * or <code>"yes"</code> is found, ignoring case.
843
     *
844
     * @param string $s The string to convert to a boolean value.
845
     *
846
     * @return <code>true</code> if the given string is <code>"on"</code>,
0 ignored issues
show
Documentation Bug introduced by
The doc comment <code>true</code> at position 0 could not be parsed: Unknown type name '<' at position 0 in <code>true</code>.
Loading history...
847
     *         <code>"true"</code> or <code>"yes"</code>, or
848
     *         <code>false</code> otherwise.
849
     */
850 7
    public static function toBoolean($s)
851
    {
852
        return (
853 7
            strcasecmp($s, 'on') === 0
854 7
            || strcasecmp($s, 'true') === 0
855 5
            || strcasecmp($s, 'yes') === 0
856
            // FIXME next condition should be removed if the boolean behavior for properties will be solved
857 7
            || strcasecmp($s, 1) === 0
858
        );
859
    }
860
861
    /**
862
     * Topologically sort a set of Targets.
863
     *
864
     * @param  string $rootTarget is the (String) name of the root Target. The sort is
865
     *                         created in such a way that the sequence of Targets until the root
866
     *                         target is the minimum possible such sequence.
867
     * @return Target[] targets in sorted order
868
     * @throws Exception
869
     * @throws BuildException
870
     */
871 644
    public function topoSort($rootTarget)
872
    {
873 644
        $rootTarget = (string) $rootTarget;
874 644
        $ret = [];
875 644
        $state = [];
876 644
        $visiting = [];
877
878
        // We first run a DFS based sort using the root as the starting node.
879
        // This creates the minimum sequence of Targets to the root node.
880
        // We then do a sort on any remaining unVISITED targets.
881
        // This is unnecessary for doing our build, but it catches
882
        // circular dependencies or missing Targets on the entire
883
        // dependency tree, not just on the Targets that depend on the
884
        // build Target.
885
886 644
        $this->_tsort($rootTarget, $state, $visiting, $ret);
887
888 644
        $retHuman = "";
889 644
        for ($i = 0, $_i = count($ret); $i < $_i; $i++) {
890 644
            $retHuman .= (string) $ret[$i] . " ";
891
        }
892 644
        $this->log("Build sequence for target '$rootTarget' is: $retHuman", Project::MSG_VERBOSE);
893
894 644
        $keys = array_keys($this->targets);
895 644
        while ($keys) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $keys of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
896 644
            $curTargetName = (string) array_shift($keys);
897 644
            if (!isset($state[$curTargetName])) {
898 644
                $st = null;
899
            } else {
900 644
                $st = (string) $state[$curTargetName];
901
            }
902
903 644
            if ($st === null) {
904 644
                $this->_tsort($curTargetName, $state, $visiting, $ret);
905 644
            } elseif ($st === "VISITING") {
906
                throw new Exception("Unexpected node in visiting state: $curTargetName");
907
            }
908
        }
909
910 644
        $retHuman = "";
911 644
        for ($i = 0, $_i = count($ret); $i < $_i; $i++) {
912 644
            $retHuman .= (string) $ret[$i] . " ";
913
        }
914 644
        $this->log("Complete build sequence is: $retHuman", Project::MSG_VERBOSE);
915
916 644
        return $ret;
917
    }
918
919
    // one step in a recursive DFS traversal of the target dependency tree.
920
    // - The array "state" contains the state (VISITED or VISITING or null)
921
    //   of all the target names.
922
    // - The stack "visiting" contains a stack of target names that are
923
    //   currently on the DFS stack. (NB: the target names in "visiting" are
924
    //    exactly the target names in "state" that are in the VISITING state.)
925
    // 1. Set the current target to the VISITING state, and push it onto
926
    //    the "visiting" stack.
927
    // 2. Throw a BuildException if any child of the current node is
928
    //    in the VISITING state (implies there is a cycle.) It uses the
929
    //    "visiting" Stack to construct the cycle.
930
    // 3. If any children have not been VISITED, tsort() the child.
931
    // 4. Add the current target to the Vector "ret" after the children
932
    //    have been visited. Move the current target to the VISITED state.
933
    //    "ret" now contains the sorted sequence of Targets up to the current
934
    //    Target.
935
936
    /**
937
     * @param $root
938
     * @param $state
939
     * @param $visiting
940
     * @param $ret
941
     * @throws BuildException
942
     * @throws Exception
943
     */
944 644
    public function _tsort($root, &$state, &$visiting, &$ret)
945
    {
946 644
        $state[$root] = "VISITING";
947 644
        $visiting[] = $root;
948
949 644
        if (!isset($this->targets[$root]) || !($this->targets[$root] instanceof Target)) {
950 3
            $target = null;
951
        } else {
952 644
            $target = $this->targets[$root];
953
        }
954
955
        // make sure we exist
956 644
        if ($target === null) {
957 3
            $sb = "Target '$root' does not exist in this project.";
958 3
            array_pop($visiting);
959 3
            if (!empty($visiting)) {
960
                $parent = (string) $visiting[count($visiting) - 1];
961
                $sb .= " It is a dependency of target '$parent'.";
962
            }
963 3
            if ($suggestion = $this->findSuggestion($root)) {
964 2
                $sb .= sprintf(" Did you mean '%s'?", $suggestion);
965
            }
966 3
            throw new BuildException($sb);
967
        }
968
969 644
        $deps = $target->getDependencies();
970
971 644
        while ($deps) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $deps of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
972 142
            $cur = (string) array_shift($deps);
973 142
            if (!isset($state[$cur])) {
974 78
                $m = null;
975
            } else {
976 124
                $m = (string) $state[$cur];
977
            }
978 142
            if ($m === null) {
979
                // not been visited
980 78
                $this->_tsort($cur, $state, $visiting, $ret);
981 124
            } elseif ($m == "VISITING") {
982
                // currently visiting this node, so have a cycle
983
                throw $this->_makeCircularException($cur, $visiting);
984
            }
985
        }
986
987 644
        $p = (string) array_pop($visiting);
988 644
        if ($root !== $p) {
989
            throw new Exception("Unexpected internal error: expected to pop $root but got $p");
990
        }
991
992 644
        $state[$root] = "VISITED";
993 644
        $ret[] = $target;
994 644
    }
995
996
    /**
997
     * @param string $end
998
     * @param array $stk
999
     * @return BuildException
1000
     */
1001
    public function _makeCircularException($end, $stk)
1002
    {
1003
        $sb = "Circular dependency: $end";
1004
        do {
1005
            $c = (string) array_pop($stk);
1006
            $sb .= " <- " . $c;
1007
        } while ($c != $end);
1008
1009
        return new BuildException($sb);
1010
    }
1011
1012
    /**
1013
     * Adds a reference to an object. This method is called when the parser
1014
     * detects a id="foo" attribute. It passes the id as $name and a reference
1015
     * to the object assigned to this id as $value
1016
     *
1017
     * @param string $name
1018
     * @param object $object
1019
     */
1020 853
    public function addReference($name, $object)
1021
    {
1022 853
        $ref = $this->references[$name] ?? null;
1023 853
        if ($ref === $object) {
1024
            return;
1025
        }
1026 853
        if ($ref !== null && !$ref instanceof UnknownElement) {
1027 37
            $this->log("Overriding previous definition of reference to $name", Project::MSG_VERBOSE);
1028
        }
1029 853
        $refName = (is_scalar($object) || $object instanceof PropertyValue) ? (string) $object : get_class($object);
1030 853
        $this->log("Adding reference: $name -> " . $refName, Project::MSG_DEBUG);
1031 853
        $this->references[$name] = $object;
1032 853
    }
1033
1034
    /**
1035
     * Returns the references array.
1036
     *
1037
     * @return array
1038
     */
1039 33
    public function getReferences()
1040
    {
1041 33
        return $this->references;
1042
    }
1043
1044
    /**
1045
     * Returns a specific reference.
1046
     *
1047
     * @param  string $key The reference id/key.
1048
     * @return object Reference or null if not defined
1049
     */
1050 854
    public function getReference($key)
1051
    {
1052 854
        return $this->references[$key] ?? null; // just to be explicit
1053
    }
1054
1055
    /**
1056
     * Does the project know this reference?
1057
     *
1058
     * @param  string $key The reference id/key.
1059
     * @return bool
1060
     */
1061 5
    public function hasReference(string $key): bool
1062
    {
1063 5
        return isset($this->references[$key]);
1064
    }
1065
1066
    /**
1067
     * Abstracting and simplifyling Logger calls for project messages
1068
     *
1069
     * @param string $msg
1070
     * @param int $level
1071
     */
1072 853
    public function log($msg, $level = Project::MSG_INFO)
1073
    {
1074 853
        $this->logObject($this, $msg, $level);
1075 853
    }
1076
1077
    /**
1078
     * @param mixed $obj
1079
     * @param string $msg
1080
     * @param int $level
1081
     * @param Exception|null $t
1082
     */
1083 854
    public function logObject($obj, $msg, $level, Exception $t = null)
1084
    {
1085 854
        $this->fireMessageLogged($obj, $msg, $level, $t);
1086
1087
        // Checking whether the strict-mode is On, then consider all the warnings
1088
        // as errors.
1089 854
        if (($this->strictMode) && (Project::MSG_WARN == $level)) {
1090
            throw new BuildException('Build contains warnings, considered as errors in strict mode', null);
1091
        }
1092 854
    }
1093
1094
    /**
1095
     * @param BuildListener $listener
1096
     */
1097 833
    public function addBuildListener(BuildListener $listener)
1098
    {
1099 833
        $this->listeners[] = $listener;
1100 833
    }
1101
1102
    /**
1103
     * @param BuildListener $listener
1104
     */
1105 9
    public function removeBuildListener(BuildListener $listener)
1106
    {
1107 9
        $newarray = [];
1108 9
        for ($i = 0, $size = count($this->listeners); $i < $size; $i++) {
1109 9
            if ($this->listeners[$i] !== $listener) {
1110 9
                $newarray[] = $this->listeners[$i];
1111
            }
1112
        }
1113 9
        $this->listeners = $newarray;
1114 9
    }
1115
1116
    /**
1117
     * @return array
1118
     */
1119 33
    public function getBuildListeners()
1120
    {
1121 33
        return $this->listeners;
1122
    }
1123
1124
    public function fireBuildStarted()
1125
    {
1126
        $event = new BuildEvent($this);
1127
        foreach ($this->listeners as $listener) {
1128
            $listener->buildStarted($event);
1129
        }
1130
1131
        $this->log((string) $event, Project::MSG_DEBUG);
1132
    }
1133
1134
    /**
1135
     * @param Exception $exception
1136
     */
1137
    public function fireBuildFinished($exception)
1138
    {
1139
        $event = new BuildEvent($this);
1140
        $event->setException($exception);
1141
        foreach ($this->listeners as $listener) {
1142
            $listener->buildFinished($event);
1143
        }
1144
1145
        $this->log((string) $event, Project::MSG_DEBUG);
1146
    }
1147
1148
    /**
1149
     * @param $target
1150
     */
1151 644
    public function fireTargetStarted($target)
1152
    {
1153 644
        $event = new BuildEvent($target);
1154 644
        foreach ($this->listeners as $listener) {
1155 644
            $listener->targetStarted($event);
1156
        }
1157
1158 644
        $this->log((string) $event, Project::MSG_DEBUG);
1159 644
    }
1160
1161
    /**
1162
     * @param $target
1163
     * @param $exception
1164
     */
1165 644
    public function fireTargetFinished($target, $exception)
1166
    {
1167 644
        $event = new BuildEvent($target);
1168 644
        $event->setException($exception);
1169 644
        foreach ($this->listeners as $listener) {
1170 644
            $listener->targetFinished($event);
1171
        }
1172
1173 644
        $this->log((string) $event, Project::MSG_DEBUG);
1174 644
    }
1175
1176
    /**
1177
     * @param $task
1178
     */
1179 697
    public function fireTaskStarted($task)
1180
    {
1181 697
        $event = new BuildEvent($task);
1182 697
        foreach ($this->listeners as $listener) {
1183 697
            $listener->taskStarted($event);
1184
        }
1185
1186 697
        $this->log((string) $event, Project::MSG_DEBUG);
1187 697
    }
1188
1189
    /**
1190
     * @param $task
1191
     * @param $exception
1192
     */
1193 697
    public function fireTaskFinished($task, $exception)
1194
    {
1195 697
        $event = new BuildEvent($task);
1196 697
        $event->setException($exception);
1197 697
        foreach ($this->listeners as $listener) {
1198 697
            $listener->taskFinished($event);
1199
        }
1200
1201 697
        $this->log((string) $event, Project::MSG_DEBUG);
1202 697
    }
1203
1204
    /**
1205
     * @param $event
1206
     * @param $message
1207
     * @param $priority
1208
     */
1209 854
    public function fireMessageLoggedEvent(BuildEvent $event, $message, $priority)
1210
    {
1211 854
        $event->setMessage($message, $priority);
1212 854
        foreach ($this->listeners as $listener) {
1213 833
            $listener->messageLogged($event);
1214
        }
1215 854
    }
1216
1217
    /**
1218
     * @param mixed $object
1219
     * @param string $message
1220
     * @param int $priority
1221
     * @param Exception $t
1222
     * @throws \Exception
1223
     */
1224 854
    public function fireMessageLogged($object, $message, $priority, Exception $t = null)
1225
    {
1226 854
        $event = new BuildEvent($object);
1227 854
        if ($t !== null) {
1228
            $event->setException($t);
1229
        }
1230 854
        $this->fireMessageLoggedEvent($event, $message, $priority);
1231 854
    }
1232
1233
    /**
1234
     * Finds the Target with the most similar name to function's argument
1235
     *
1236
     * Will return null if buildfile has no targets.
1237
     *
1238
     * @see https://www.php.net/manual/en/function.levenshtein.php
1239
     * @param string $unknownTarget Target name
1240
     *
1241
     * @return Target
1242
     */
1243 3
    private function findSuggestion(string $unknownTarget): ?Target
1244
    {
1245 3
        return array_reduce($this->targets, function (?Target $carry, Target $current) use ($unknownTarget): ?Target {
1246
            // Omit target with empty name (there's always one)
1247 3
            if (empty(strval($current))) {
1248 3
                return $carry;
1249
            }
1250
            // $carry is null the first time
1251 2
            if (is_null($carry)) {
1252 2
                return $current;
1253
            }
1254 2
            return levenshtein($unknownTarget, $carry) < levenshtein($unknownTarget, $current) ? $carry : $current;
1255 3
        });
1256
    }
1257
}
1258