Passed
Push — master ( e1f86a...4e1a3a )
by Siad
05:23
created

Project   F

Complexity

Total Complexity 134

Size/Duplication

Total Lines 1224
Duplicated Lines 0 %

Test Coverage

Coverage 81.1%

Importance

Changes 0
Metric Value
eloc 286
dl 0
loc 1224
ccs 296
cts 365
cp 0.811
rs 2
c 0
b 0
f 0
wmc 134

71 Methods

Rating   Name   Duplication   Size   Complexity  
A setName() 0 4 1
A getStrictmode() 0 3 1
A initSubProject() 0 6 1
A addDataTypeDefinition() 0 3 1
A _makeCircularException() 0 9 2
A getUserProperties() 0 3 1
A replaceProperties() 0 3 1
A getPhingVersion() 0 7 2
A createCondition() 0 3 1
A createTask() 0 3 1
A hasReference() 0 3 1
A getTaskDefinitions() 0 3 1
A createSubProject() 0 10 2
A getInheritedProperties() 0 3 1
A addTarget() 0 6 2
A executeTargets() 0 6 2
A setInputHandler() 0 3 1
A copyUserProperties() 0 3 1
A getDataTypeDefinitions() 0 3 1
B _tsort() 0 50 11
A addTaskDefinition() 0 3 1
A getReference() 0 3 1
A addReference() 0 12 6
A resolveFile() 0 6 2
A getTargets() 0 3 1
A isKeepGoingMode() 0 3 1
A fireMessageLoggedEvent() 0 5 2
A setBasedir() 0 22 4
A copyInheritedProperties() 0 3 1
A getProperties() 0 3 1
A init() 0 8 1
A setNewProperty() 0 3 1
A getUserProperty() 0 3 1
A fireTaskStarted() 0 8 2
A setStrictMode() 0 4 1
A fireBuildStarted() 0 8 2
A getName() 0 3 1
A fireBuildFinished() 0 9 2
A removeBuildListener() 0 9 3
A fireTaskFinished() 0 9 2
A setInheritedProperty() 0 3 1
A addOrReplaceTarget() 0 9 1
A getDefaultTarget() 0 3 1
A getProperty() 0 3 1
A getGlobalFilterSet() 0 3 1
A findSuggestion() 0 12 4
A getReferences() 0 3 1
A setPhingVersion() 0 4 1
A getExecutedTargetNames() 0 3 1
A createDataType() 0 3 1
A getBasedir() 0 11 3
A getBuildListeners() 0 3 1
A getInputHandler() 0 3 1
A log() 0 3 1
A fireTargetFinished() 0 9 2
B topoSort() 0 46 7
A setUserProperty() 0 3 1
A fireMessageLogged() 0 7 2
A setProperty() 0 3 1
A logObject() 0 8 3
A getDescription() 0 6 2
A setPropertyInternal() 0 3 1
A fireTargetStarted() 0 8 2
A setKeepGoingMode() 0 3 1
A setDescription() 0 3 1
A toBoolean() 0 8 4
A setDefaultTarget() 0 3 1
B executeTarget() 0 55 10
A addBuildListener() 0 3 1
A setSystemProperties() 0 16 4
A __construct() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Project often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Project, and based on these observations, apply Extract Interface, too.

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
namespace Phing;
21
22
use Phing\ComponentHelper;
23
use Phing\Tasks\System\Condition\Condition;
24
use Phing\Type\Description;
25
use Exception;
26
use Phing\Exception\BuildException;
27
use Phing\Input\InputHandler;
28
use Phing\Io\FileSystem;
29
use Phing\Io\FileUtils;
30
use Phing\Io\IOException;
31
use Phing\Io\File;
32
use Phing\Listener\BuildEvent;
33
use Phing\Listener\BuildListener;
34
use Phing\Parser\ProjectConfigurator;
35
use Phing\Phing;
36
use Phing\Target;
37
use Phing\UnknownElement;
38
use Phing\PropertyHelper;
39
use Phing\Type\PropertyValue;
40
use ReflectionException;
41
use ReflectionObject;
42
use Phing\Task;
43
44
/**
45
 *  The Phing project class. Represents a completely configured Phing project.
46
 *  The class defines the project and all tasks/targets. It also contains
47
 *  methods to start a build as well as some properties and FileSystem
48
 *  abstraction.
49
 *
50
 * @author Andreas Aderhold <[email protected]>
51
 * @author Hans Lellelid <[email protected]>
52
 *
53
 * @package phing
54
 */
55
class Project
56
{
57
58
    // Logging level constants.
59
    public const MSG_DEBUG = 4;
60
    public const MSG_VERBOSE = 3;
61
    public const MSG_INFO = 2;
62
    public const MSG_WARN = 1;
63
    public const MSG_ERR = 0;
64
65
    /**
66
     * contains the targets
67
     *
68
     * @var Target[]
69
     */
70
    private $targets = [];
71
    /**
72
     * global filterset (future use)
73
     */
74
    private $globalFilterSet = [];
75
    /**
76
     * all globals filters (future use)
77
     */
78
    private $globalFilters = [];
0 ignored issues
show
introduced by
The private property $globalFilters is not used, and could be removed.
Loading history...
79
80
    /**
81
     * holds ref names and a reference to the referred object
82
     */
83
    private $references = [];
84
85
    /**
86
     * The InputHandler being used by this project.
87
     *
88
     * @var InputHandler
89
     */
90
    private $inputHandler;
91
92
    /* -- properties that come in via xml attributes -- */
93
94
    /**
95
     * basedir (PhingFile object)
96
     */
97
    private $basedir;
98
99
    /**
100
     * the default target name
101
     */
102
    private $defaultTarget = 'all';
103
104
    /**
105
     * project name (required)
106
     */
107
    private $name;
108
109
    /**
110
     * project description
111
     */
112
    private $description;
113
114
    /**
115
     * require phing version
116
     */
117
    private $phingVersion;
118
119
    /**
120
     * project strict mode
121
     */
122
    private $strictMode = false;
123
124
    /**
125
     * a FileUtils object
126
     */
127
    private $fileUtils;
128
129
    /**
130
     * Build listeneers
131
     */
132
    private $listeners = [];
133
134
    /**
135
     * Keep going flag.
136
     */
137
    private $keepGoingMode = false;
138
139
    /**
140
     * @var string[]
141
     */
142
    private $executedTargetNames = [];
143
144
    /**
145
     *  Constructor, sets any default vars.
146
     */
147 872
    public function __construct()
148
    {
149 872
        $this->fileUtils = new FileUtils();
150 872
    }
151
152
    /**
153
     * Sets the input handler
154
     *
155
     * @param InputHandler $handler
156
     */
157 33
    public function setInputHandler($handler)
158
    {
159 33
        $this->inputHandler = $handler;
160 33
    }
161
162
    /**
163
     * Retrieves the current input handler.
164
     *
165
     * @return InputHandler
166
     */
167 33
    public function getInputHandler()
168
    {
169 33
        return $this->inputHandler;
170
    }
171
172
    /**
173
     * inits the project, called from main app
174
     */
175 837
    public function init()
176
    {
177
        // set builtin properties
178 837
        $this->setSystemProperties();
179
180 837
        $componentHelper = ComponentHelper::getComponentHelper($this);
181
182 837
        $componentHelper->initDefaultDefinitions();
183 837
    }
184
185
    /**
186
     * Create and initialize a subproject. By default the subproject will be of
187
     * the same type as its parent. If a no-arg constructor is unavailable, the
188
     * <code>Project</code> class will be used.
189
     * @return Project instance configured as a subproject of this Project.
190
     */
191 36
    public function createSubProject(): Project
192
    {
193
        try {
194 36
            $ref = new ReflectionObject($this);
195 36
            $subProject = $ref->newInstance();
196
        } catch (ReflectionException $e) {
197
            $subProject = new Project();
198
        }
199 36
        $this->initSubProject($subProject);
200 36
        return $subProject;
201
    }
202
203
    /**
204
     * Initialize a subproject.
205
     * @param Project $subProject the subproject to initialize.
206
     */
207 36
    public function initSubProject(Project $subProject): void
208
    {
209 36
        ComponentHelper::getComponentHelper($subProject)
210 36
            ->initSubProject(ComponentHelper::getComponentHelper($this));
211 36
        $subProject->setKeepGoingMode($this->isKeepGoingMode());
212 36
        $subProject->setStrictMode($this->strictMode);
213 36
    }
214
215
    /**
216
     * returns the global filterset (future use)
217
     */
218
    public function getGlobalFilterSet()
219
    {
220
        return $this->globalFilterSet;
221
    }
222
223
    // ---------------------------------------------------------
224
    // Property methods
225
    // ---------------------------------------------------------
226
227
    /**
228
     * Sets a property. Any existing property of the same name
229
     * is overwritten, unless it is a user property.
230
     *
231
     * @param string $name The name of property to set.
232
     *                       Must not be <code>null</code>.
233
     * @param string $value The new value of the property.
234
     *                       Must not be <code>null</code>.
235
     */
236 245
    public function setProperty($name, $value)
237
    {
238 245
        PropertyHelper::getPropertyHelper($this)->setProperty(null, $name, $value, true);
239 245
    }
240
241
    /**
242
     * Sets a property if no value currently exists. If the property
243
     * exists already, a message is logged and the method returns with
244
     * no other effect.
245
     *
246
     * @param string $name The name of property to set.
247
     *                      Must not be <code>null</code>.
248
     * @param string $value The new value of the property.
249
     *                      Must not be <code>null</code>.
250
     * @since 2.0
251
     */
252 93
    public function setNewProperty($name, $value)
253
    {
254 93
        PropertyHelper::getPropertyHelper($this)->setNewProperty(null, $name, $value);
255 93
    }
256
257
    /**
258
     * Sets a user property, which cannot be overwritten by
259
     * set/unset property calls. Any previous value is overwritten.
260
     *
261
     * @param string $name The name of property to set.
262
     *                      Must not be <code>null</code>.
263
     * @param string $value The new value of the property.
264
     *                      Must not be <code>null</code>.
265
     * @see   setProperty()
266
     */
267 837
    public function setUserProperty($name, $value)
268
    {
269 837
        PropertyHelper::getPropertyHelper($this)->setUserProperty(null, $name, $value);
270 837
    }
271
272
    /**
273
     * Sets a user property, which cannot be overwritten by set/unset
274
     * property calls. Any previous value is overwritten. Also marks
275
     * these properties as properties that have not come from the
276
     * command line.
277
     *
278
     * @param string $name The name of property to set.
279
     *                      Must not be <code>null</code>.
280
     * @param string $value The new value of the property.
281
     *                      Must not be <code>null</code>.
282
     * @see   setProperty()
283
     */
284 12
    public function setInheritedProperty($name, $value)
285
    {
286 12
        PropertyHelper::getPropertyHelper($this)->setInheritedProperty(null, $name, $value);
287 12
    }
288
289
    /**
290
     * Sets a property unless it is already defined as a user property
291
     * (in which case the method returns silently).
292
     *
293
     * @param string $name The name of the property.
294
     *                      Must not be
295
     *                      <code>null</code>.
296
     * @param string $value The property value. Must not be <code>null</code>.
297
     */
298 853
    private function setPropertyInternal($name, $value)
299
    {
300 853
        PropertyHelper::getPropertyHelper($this)->setProperty(null, $name, $value, false);
301 853
    }
302
303
    /**
304
     * Returns the value of a property, if it is set.
305
     *
306
     * @param string $name The name of the property.
307
     *                      May be <code>null</code>, in which case
308
     *                      the return value is also <code>null</code>.
309
     * @return string The property value, or <code>null</code> for no match
310
     *                     or if a <code>null</code> name is provided.
311
     */
312 837
    public function getProperty($name)
313
    {
314 837
        return PropertyHelper::getPropertyHelper($this)->getProperty(null, $name);
315
    }
316
317
    /**
318
     * Replaces ${} style constructions in the given value with the
319
     * string value of the corresponding data types.
320
     *
321
     * @param string $value The value string to be scanned for property references.
322
     *                      May be <code>null</code>.
323
     *
324
     * @return string the given string with embedded property names replaced
325
     *                by values, or <code>null</code> if the given string is
326
     *                <code>null</code>.
327
     *
328
     * @throws BuildException if the given value has an unclosed
329
     *                           property name, e.g. <code>${xxx</code>
330
     */
331 693
    public function replaceProperties($value)
332
    {
333 693
        return PropertyHelper::getPropertyHelper($this)->replaceProperties($value, $this->getProperties());
334
    }
335
336
    /**
337
     * Returns the value of a user property, if it is set.
338
     *
339
     * @param string $name The name of the property.
340
     *                      May be <code>null</code>, in which case
341
     *                      the return value is also <code>null</code>.
342
     * @return string The property value, or <code>null</code> for no match
343
     *                     or if a <code>null</code> name is provided.
344
     */
345 1
    public function getUserProperty($name)
346
    {
347 1
        return PropertyHelper::getPropertyHelper($this)->getUserProperty(null, $name);
348
    }
349
350
    /**
351
     * Returns a copy of the properties table.
352
     *
353
     * @return array A hashtable containing all properties
354
     *               (including user properties).
355
     */
356 838
    public function getProperties()
357
    {
358 838
        return PropertyHelper::getPropertyHelper($this)->getProperties();
359
    }
360
361
    /**
362
     * Returns a copy of the user property hashtable
363
     *
364
     * @return array a hashtable containing just the user properties
365
     */
366
    public function getUserProperties()
367
    {
368
        return PropertyHelper::getPropertyHelper($this)->getUserProperties();
369
    }
370
371
    public function getInheritedProperties()
372
    {
373
        return PropertyHelper::getPropertyHelper($this)->getInheritedProperties();
374
    }
375
376
    /**
377
     * Copies all user properties that have been set on the command
378
     * line or a GUI tool from this instance to the Project instance
379
     * given as the argument.
380
     *
381
     * <p>To copy all "user" properties, you will also have to call
382
     * {@link #copyInheritedProperties copyInheritedProperties}.</p>
383
     *
384
     * @param Project $other the project to copy the properties to.  Must not be null.
385
     * @return void
386
     * @since  phing 2.0
387
     */
388 33
    public function copyUserProperties(Project $other)
389
    {
390 33
        PropertyHelper::getPropertyHelper($this)->copyUserProperties($other);
391 33
    }
392
393
    /**
394
     * Copies all user properties that have not been set on the
395
     * command line or a GUI tool from this instance to the Project
396
     * instance given as the argument.
397
     *
398
     * <p>To copy all "user" properties, you will also have to call
399
     * {@link #copyUserProperties copyUserProperties}.</p>
400
     *
401
     * @param Project $other the project to copy the properties to.  Must not be null.
402
     *
403
     * @since phing 2.0
404
     */
405 33
    public function copyInheritedProperties(Project $other)
406
    {
407 33
        PropertyHelper::getPropertyHelper($this)->copyUserProperties($other);
408 33
    }
409
410
    // ---------------------------------------------------------
411
    //  END Properties methods
412
    // ---------------------------------------------------------
413
414
    /**
415
     * Sets default target
416
     *
417
     * @param string $targetName
418
     */
419 837
    public function setDefaultTarget($targetName)
420
    {
421 837
        $this->defaultTarget = (string) trim($targetName);
422 837
    }
423
424
    /**
425
     * Returns default target
426
     *
427
     * @return string
428
     */
429 33
    public function getDefaultTarget()
430
    {
431 33
        return (string) $this->defaultTarget;
432
    }
433
434
    /**
435
     * Sets the name of the current project
436
     *
437
     * @param string $name name of project
438
     * @return void
439
     * @author Andreas Aderhold, [email protected]
440
     */
441 837
    public function setName($name)
442
    {
443 837
        $this->name = (string) trim($name);
444 837
        $this->setUserProperty("phing.project.name", $this->name);
445 837
    }
446
447
    /**
448
     * Returns the name of this project
449
     *
450
     * @return string projectname
451
     * @author Andreas Aderhold, [email protected]
452
     */
453 2
    public function getName()
454
    {
455 2
        return (string) $this->name;
456
    }
457
458
    /**
459
     * Set the projects description
460
     *
461
     * @param string $description
462
     */
463 6
    public function setDescription($description)
464
    {
465 6
        $this->description = $description;
466 6
    }
467
468
    /**
469
     * return the description, null otherwise
470
     *
471
     * @return string|null
472
     */
473 6
    public function getDescription()
474
    {
475 6
        if ($this->description === null) {
476 5
            $this->description = Description::getAll($this);
477
        }
478 6
        return $this->description;
479
    }
480
481
    /**
482
     * Set the minimum required phing version
483
     *
484
     * @param string $version
485
     */
486 5
    public function setPhingVersion($version)
487
    {
488 5
        $version = str_replace('phing', '', strtolower($version));
489 5
        $this->phingVersion = (string) trim($version);
490 5
    }
491
492
    /**
493
     * Get the minimum required phing version
494
     *
495
     * @return string
496
     */
497 5
    public function getPhingVersion()
498
    {
499 5
        if ($this->phingVersion === null) {
500 5
            $this->setPhingVersion(Phing::getPhingVersion());
501
        }
502
503 5
        return $this->phingVersion;
504
    }
505
506
    /**
507
     * Sets the strict-mode (status) for the current project
508
     * (If strict mode is On, all the warnings would be converted to an error
509
     * (and the build will be stopped/aborted)
510
     *
511
     * @param bool $strictmode
512
     * @return void
513
     * @access public
514
     * @author Utsav Handa, [email protected]
515
     */
516 36
    public function setStrictMode(bool $strictmode)
517
    {
518 36
        $this->strictMode = $strictmode;
519 36
        $this->setProperty("phing.project.strictmode", $this->strictMode);
520 36
    }
521
522
    /**
523
     * Get the strict-mode status for the project
524
     *
525
     * @return boolean
526
     */
527
    public function getStrictmode()
528
    {
529
        return $this->strictMode;
530
    }
531
532
    /**
533
     * Set basedir object from xm
534
     *
535
     * @param File|string $dir
536
     * @throws BuildException
537
     */
538 853
    public function setBasedir($dir)
539
    {
540 853
        if ($dir instanceof File) {
541 96
            $dir = $dir->getAbsolutePath();
542
        }
543
544 853
        $dir = $this->fileUtils->normalize($dir);
545 853
        $dir = FileSystem::getFileSystem()->canonicalize($dir);
546
547 853
        $dir = new File((string) $dir);
548 853
        if (!$dir->exists()) {
549
            throw new BuildException("Basedir " . $dir->getAbsolutePath() . " does not exist");
550
        }
551 853
        if (!$dir->isDirectory()) {
552
            throw new BuildException("Basedir " . $dir->getAbsolutePath() . " is not a directory");
553
        }
554 853
        $this->basedir = $dir;
555 853
        $this->setPropertyInternal("project.basedir", $this->basedir->getPath());
556 853
        $this->log("Project base dir set to: " . $this->basedir->getPath(), Project::MSG_VERBOSE);
557
558
        // [HL] added this so that ./ files resolve correctly.  This may be a mistake ... or may be in wrong place.
559 853
        chdir($dir->getAbsolutePath());
560 853
    }
561
562
    /**
563
     * Returns the basedir of this project
564
     *
565
     * @return File      Basedir PhingFile object
566
     *
567
     * @throws BuildException
568
     *
569
     * @author Andreas Aderhold, [email protected]
570
     */
571 180
    public function getBasedir()
572
    {
573 180
        if ($this->basedir === null) {
574
            try { // try to set it
575
                $this->setBasedir(".");
576
            } catch (BuildException $exc) {
577
                throw new BuildException("Can not set default basedir. " . $exc->getMessage());
578
            }
579
        }
580
581 180
        return $this->basedir;
582
    }
583
584
    /**
585
     * Set &quot;keep-going&quot; mode. In this mode Ant will try to execute
586
     * as many targets as possible. All targets that do not depend
587
     * on failed target(s) will be executed.  If the keepGoing settor/getter
588
     * methods are used in conjunction with the <code>ant.executor.class</code>
589
     * property, they will have no effect.
590
     *
591
     * @param bool $keepGoingMode &quot;keep-going&quot; mode
592
     */
593 36
    public function setKeepGoingMode($keepGoingMode)
594
    {
595 36
        $this->keepGoingMode = $keepGoingMode;
596 36
    }
597
598
    /**
599
     * Return the keep-going mode.  If the keepGoing settor/getter
600
     * methods are used in conjunction with the <code>phing.executor.class</code>
601
     * property, they will have no effect.
602
     *
603
     * @return bool &quot;keep-going&quot; mode
604
     */
605 36
    public function isKeepGoingMode()
606
    {
607 36
        return $this->keepGoingMode;
608
    }
609
610
    /**
611
     * Sets system properties and the environment variables for this project.
612
     *
613
     * @return void
614
     */
615 837
    public function setSystemProperties()
616
    {
617
618
        // first get system properties
619 837
        $systemP = array_merge($this->getProperties(), Phing::getProperties());
620 837
        foreach ($systemP as $name => $value) {
621 837
            $this->setPropertyInternal($name, $value);
622
        }
623
624
        // and now the env vars
625 837
        foreach ($_SERVER as $name => $value) {
626
            // skip arrays
627 837
            if (is_array($value)) {
628 837
                continue;
629
            }
630 837
            $this->setPropertyInternal('env.' . $name, $value);
631
        }
632 837
    }
633
634
    /**
635
     * Adds a task definition.
636
     *
637
     * @param string $name Name of tag.
638
     * @param string $class The class path to use.
639
     * @param string $classpath The classpat to use.
640
     */
641 73
    public function addTaskDefinition($name, $class, $classpath = null)
642
    {
643 73
        ComponentHelper::getComponentHelper($this)->addTaskDefinition($name, $class, $classpath);
644 73
    }
645
646
    /**
647
     * Returns the task definitions
648
     *
649
     * @return array
650
     */
651 46
    public function getTaskDefinitions()
652
    {
653 46
        return ComponentHelper::getComponentHelper($this)->getTaskDefinitions();
654
    }
655
656
    /**
657
     * Adds a data type definition.
658
     *
659
     * @param string $typeName Name of the type.
660
     * @param string $typeClass The class to use.
661
     * @param string $classpath The classpath to use.
662
     */
663 40
    public function addDataTypeDefinition($typeName, $typeClass, $classpath = null)
664
    {
665 40
        ComponentHelper::getComponentHelper($this)->addDataTypeDefinition($typeName, $typeClass, $classpath);
666 40
    }
667
668
    /**
669
     * Returns the data type definitions
670
     *
671
     * @return array
672
     */
673 838
    public function getDataTypeDefinitions()
674
    {
675 838
        return ComponentHelper::getComponentHelper($this)->getDataTypeDefinitions();
676
    }
677
678
    /**
679
     * Add a new target to the project
680
     *
681
     * @param string $targetName
682
     * @param Target $target
683
     * @throws BuildException
684
     */
685 837
    public function addTarget($targetName, $target)
686
    {
687 837
        if (isset($this->targets[$targetName])) {
688
            throw new BuildException("Duplicate target: $targetName");
689
        }
690 837
        $this->addOrReplaceTarget($targetName, $target);
691 837
    }
692
693
    /**
694
     * Adds or replaces a target in the project
695
     *
696
     * @param string $targetName
697
     * @param Target $target
698
     */
699 837
    public function addOrReplaceTarget($targetName, &$target)
700
    {
701 837
        $this->log("  +Target: $targetName", Project::MSG_DEBUG);
702 837
        $target->setProject($this);
703 837
        $this->targets[$targetName] = $target;
704
705 837
        $ctx = $this->getReference(ProjectConfigurator::PARSING_CONTEXT_REFERENCE);
706 837
        $current = $ctx->getCurrentTargets();
707 837
        $current[$targetName] = $target;
708 837
    }
709
710
    /**
711
     * Returns the available targets
712
     *
713
     * @return Target[]
714
     */
715 837
    public function getTargets()
716
    {
717 837
        return $this->targets;
718
    }
719
720
    /**
721
     * @return string[]
722
     */
723
    public function getExecutedTargetNames()
724
    {
725
        return $this->executedTargetNames;
726
    }
727
728
    /**
729
     * Create a new task instance and return reference to it.
730
     *
731
     * @param string $taskType Task name
732
     * @return Task           A task object
733
     * @throws BuildException
734
     */
735 698
    public function createTask($taskType)
736
    {
737 698
        return ComponentHelper::getComponentHelper($this)->createTask($taskType);
738
    }
739
740
    /**
741
     * Creates a new condition and returns the reference to it
742
     *
743
     * @param string $conditionType
744
     * @return Condition
745
     * @throws BuildException
746
     */
747 1
    public function createCondition($conditionType)
748
    {
749 1
        return ComponentHelper::getComponentHelper($this)->createCondition($conditionType);
750
    }
751
752
    /**
753
     * Create a datatype instance and return reference to it
754
     * See createTask() for explanation how this works
755
     *
756
     * @param string $typeName Type name
757
     * @return object         A datatype object
758
     * @throws BuildException
759
     *                                 Exception
760
     */
761 83
    public function createDataType($typeName)
762
    {
763 83
        return ComponentHelper::getComponentHelper($this)->createDataType($typeName);
764
    }
765
766
    /**
767
     * Executes a list of targets
768
     *
769
     * @param array $targetNames List of target names to execute
770
     * @return void
771
     * @throws BuildException
772
     */
773
    public function executeTargets($targetNames)
774
    {
775
        $this->executedTargetNames = $targetNames;
776
777
        foreach ($targetNames as $tname) {
778
            $this->executeTarget($tname);
779
        }
780
    }
781
782
    /**
783
     * Executes a target
784
     *
785
     * @param string $targetName Name of Target to execute
786
     * @return void
787
     * @throws BuildException
788
     */
789 648
    public function executeTarget($targetName)
790
    {
791
792
        // complain about executing void
793 648
        if ($targetName === null) {
0 ignored issues
show
introduced by
The condition $targetName === null is always false.
Loading history...
794
            throw new BuildException("No target specified");
795
        }
796
797
        // invoke topological sort of the target tree and run all targets
798
        // until targetName occurs.
799 648
        $sortedTargets = $this->topoSort($targetName);
800
801 648
        $curIndex = (int) 0;
802 648
        $curTarget = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $curTarget is dead and can be removed.
Loading history...
803 648
        $thrownException = null;
804 648
        $buildException = null;
805
        do {
806
            try {
807 648
                $curTarget = $sortedTargets[$curIndex++];
808 648
                $curTarget->performTasks();
809 168
            } catch (BuildException $exc) {
810 168
                if (!($this->keepGoingMode)) {
811 168
                    throw $exc;
812
                }
813
                $thrownException = $exc;
814
            }
815 532
            if ($thrownException != null) {
816
                if ($thrownException instanceof BuildException) {
817
                    $this->log(
818
                        "Target '" . $curTarget->getName()
819
                        . "' failed with message '"
820
                        . $thrownException->getMessage() . "'.",
821
                        Project::MSG_ERR
822
                    );
823
                    // only the first build exception is reported
824
                    if ($buildException === null) {
825
                        $buildException = $thrownException;
826
                    }
827
                } else {
828
                    $this->log(
829
                        "Target '" . $curTarget->getName()
830
                        . "' failed with message '"
831
                        . $thrownException->getMessage() . "'." . PHP_EOL
832
                        . $thrownException->getTraceAsString(),
833
                        Project::MSG_ERR
834
                    );
835
                    if ($buildException === null) {
836
                        $buildException = new BuildException($thrownException);
837
                    }
838
                }
839
            }
840 532
        } while ($curTarget->getName() !== $targetName);
841
842 532
        if ($buildException !== null) {
843
            throw $buildException;
844
        }
845 532
    }
846
847
    /**
848
     * Helper function
849
     *
850
     * @param string $fileName
851
     * @param File $rootDir
852
     * @return \Phing\Io\File
853
     * @throws IOException
854
     */
855 455
    public function resolveFile(string $fileName, File $rootDir = null): File
856
    {
857 455
        if ($rootDir === null) {
858 449
            return $this->fileUtils->resolveFile($this->basedir, $fileName);
859
        }
860 88
        return $this->fileUtils->resolveFile($rootDir, $fileName);
861
    }
862
863
    /**
864
     * Return the boolean equivalent of a string, which is considered
865
     * <code>true</code> if either <code>"on"</code>, <code>"true"</code>,
866
     * or <code>"yes"</code> is found, ignoring case.
867
     *
868
     * @param string $s The string to convert to a boolean value.
869
     *
870
     * @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...
871
     *         <code>"true"</code> or <code>"yes"</code>, or
872
     *         <code>false</code> otherwise.
873
     */
874 7
    public static function toBoolean($s)
875
    {
876
        return (
877 7
            strcasecmp($s, 'on') === 0
878 7
            || strcasecmp($s, 'true') === 0
879 5
            || strcasecmp($s, 'yes') === 0
880
            // FIXME next condition should be removed if the boolean behavior for properties will be solved
881 7
            || strcasecmp($s, 1) === 0
882
        );
883
    }
884
885
    /**
886
     * Topologically sort a set of Targets.
887
     *
888
     * @param string $rootTarget is the (String) name of the root Target. The sort is
889
     *                         created in such a way that the sequence of Targets until the root
890
     *                         target is the minimum possible such sequence.
891
     * @return Target[] targets in sorted order
892
     * @throws Exception
893
     * @throws BuildException
894
     */
895 648
    public function topoSort($rootTarget)
896
    {
897 648
        $rootTarget = (string) $rootTarget;
898 648
        $ret = [];
899 648
        $state = [];
900 648
        $visiting = [];
901
902
        // We first run a DFS based sort using the root as the starting node.
903
        // This creates the minimum sequence of Targets to the root node.
904
        // We then do a sort on any remaining unVISITED targets.
905
        // This is unnecessary for doing our build, but it catches
906
        // circular dependencies or missing Targets on the entire
907
        // dependency tree, not just on the Targets that depend on the
908
        // build Target.
909
910 648
        $this->_tsort($rootTarget, $state, $visiting, $ret);
911
912 648
        $retHuman = "";
913 648
        for ($i = 0, $_i = count($ret); $i < $_i; $i++) {
914 648
            $retHuman .= (string) $ret[$i] . " ";
915
        }
916 648
        $this->log("Build sequence for target '$rootTarget' is: $retHuman", Project::MSG_VERBOSE);
917
918 648
        $keys = array_keys($this->targets);
919 648
        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...
920 648
            $curTargetName = (string) array_shift($keys);
921 648
            if (!isset($state[$curTargetName])) {
922 648
                $st = null;
923
            } else {
924 648
                $st = (string) $state[$curTargetName];
925
            }
926
927 648
            if ($st === null) {
928 648
                $this->_tsort($curTargetName, $state, $visiting, $ret);
929 648
            } elseif ($st === "VISITING") {
930
                throw new Exception("Unexpected node in visiting state: $curTargetName");
931
            }
932
        }
933
934 648
        $retHuman = "";
935 648
        for ($i = 0, $_i = count($ret); $i < $_i; $i++) {
936 648
            $retHuman .= (string) $ret[$i] . " ";
937
        }
938 648
        $this->log("Complete build sequence is: $retHuman", Project::MSG_VERBOSE);
939
940 648
        return $ret;
941
    }
942
943
    // one step in a recursive DFS traversal of the target dependency tree.
944
    // - The array "state" contains the state (VISITED or VISITING or null)
945
    //   of all the target names.
946
    // - The stack "visiting" contains a stack of target names that are
947
    //   currently on the DFS stack. (NB: the target names in "visiting" are
948
    //    exactly the target names in "state" that are in the VISITING state.)
949
    // 1. Set the current target to the VISITING state, and push it onto
950
    //    the "visiting" stack.
951
    // 2. Throw a BuildException if any child of the current node is
952
    //    in the VISITING state (implies there is a cycle.) It uses the
953
    //    "visiting" Stack to construct the cycle.
954
    // 3. If any children have not been VISITED, tsort() the child.
955
    // 4. Add the current target to the Vector "ret" after the children
956
    //    have been visited. Move the current target to the VISITED state.
957
    //    "ret" now contains the sorted sequence of Targets up to the current
958
    //    Target.
959
960
    /**
961
     * @param $root
962
     * @param $state
963
     * @param $visiting
964
     * @param $ret
965
     * @throws BuildException
966
     * @throws Exception
967
     */
968 648
    public function _tsort($root, &$state, &$visiting, &$ret)
969
    {
970 648
        $state[$root] = "VISITING";
971 648
        $visiting[] = $root;
972
973 648
        if (!isset($this->targets[$root]) || !($this->targets[$root] instanceof Target)) {
974 3
            $target = null;
975
        } else {
976 648
            $target = $this->targets[$root];
977
        }
978
979
        // make sure we exist
980 648
        if ($target === null) {
981 3
            $sb = "Target '$root' does not exist in this project.";
982 3
            array_pop($visiting);
983 3
            if (!empty($visiting)) {
984
                $parent = (string) $visiting[count($visiting) - 1];
985
                $sb .= " It is a dependency of target '$parent'.";
986
            }
987 3
            if ($suggestion = $this->findSuggestion($root)) {
988 2
                $sb .= sprintf(" Did you mean '%s'?", $suggestion);
989
            }
990 3
            throw new BuildException($sb);
991
        }
992
993 648
        $deps = $target->getDependencies();
994
995 648
        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...
996 142
            $cur = (string) array_shift($deps);
997 142
            if (!isset($state[$cur])) {
998 78
                $m = null;
999
            } else {
1000 124
                $m = (string) $state[$cur];
1001
            }
1002 142
            if ($m === null) {
1003
                // not been visited
1004 78
                $this->_tsort($cur, $state, $visiting, $ret);
1005 124
            } elseif ($m == "VISITING") {
1006
                // currently visiting this node, so have a cycle
1007
                throw $this->_makeCircularException($cur, $visiting);
1008
            }
1009
        }
1010
1011 648
        $p = (string) array_pop($visiting);
1012 648
        if ($root !== $p) {
1013
            throw new Exception("Unexpected internal error: expected to pop $root but got $p");
1014
        }
1015
1016 648
        $state[$root] = "VISITED";
1017 648
        $ret[] = $target;
1018 648
    }
1019
1020
    /**
1021
     * @param string $end
1022
     * @param array $stk
1023
     * @return BuildException
1024
     */
1025
    public function _makeCircularException($end, $stk)
1026
    {
1027
        $sb = "Circular dependency: $end";
1028
        do {
1029
            $c = (string) array_pop($stk);
1030
            $sb .= " <- " . $c;
1031
        } while ($c != $end);
1032
1033
        return new BuildException($sb);
1034
    }
1035
1036
    /**
1037
     * Adds a reference to an object. This method is called when the parser
1038
     * detects a id="foo" attribute. It passes the id as $name and a reference
1039
     * to the object assigned to this id as $value
1040
     *
1041
     * @param string $name
1042
     * @param object $object
1043
     */
1044 857
    public function addReference($name, $object)
1045
    {
1046 857
        $ref = $this->references[$name] ?? null;
1047 857
        if ($ref === $object) {
1048
            return;
1049
        }
1050 857
        if ($ref !== null && !$ref instanceof UnknownElement) {
1051 37
            $this->log("Overriding previous definition of reference to $name", Project::MSG_VERBOSE);
1052
        }
1053 857
        $refName = (is_scalar($object) || $object instanceof PropertyValue) ? (string) $object : get_class($object);
1054 857
        $this->log("Adding reference: $name -> " . $refName, Project::MSG_DEBUG);
1055 857
        $this->references[$name] = $object;
1056 857
    }
1057
1058
    /**
1059
     * Returns the references array.
1060
     *
1061
     * @return array
1062
     */
1063 33
    public function getReferences()
1064
    {
1065 33
        return $this->references;
1066
    }
1067
1068
    /**
1069
     * Returns a specific reference.
1070
     *
1071
     * @param string $key The reference id/key.
1072
     * @return object Reference or null if not defined
1073
     */
1074 858
    public function getReference($key)
1075
    {
1076 858
        return $this->references[$key] ?? null; // just to be explicit
1077
    }
1078
1079
    /**
1080
     * Does the project know this reference?
1081
     *
1082
     * @param string $key The reference id/key.
1083
     * @return bool
1084
     */
1085 5
    public function hasReference(string $key): bool
1086
    {
1087 5
        return isset($this->references[$key]);
1088
    }
1089
1090
    /**
1091
     * Abstracting and simplifyling Logger calls for project messages
1092
     *
1093
     * @param string $msg
1094
     * @param int $level
1095
     */
1096 857
    public function log($msg, $level = Project::MSG_INFO)
1097
    {
1098 857
        $this->logObject($this, $msg, $level);
1099 857
    }
1100
1101
    /**
1102
     * @param mixed $obj
1103
     * @param string $msg
1104
     * @param int $level
1105
     * @param Exception|null $t
1106
     */
1107 858
    public function logObject($obj, $msg, $level, Exception $t = null)
1108
    {
1109 858
        $this->fireMessageLogged($obj, $msg, $level, $t);
1110
1111
        // Checking whether the strict-mode is On, then consider all the warnings
1112
        // as errors.
1113 858
        if (($this->strictMode) && (Project::MSG_WARN == $level)) {
1114
            throw new BuildException('Build contains warnings, considered as errors in strict mode', null);
1115
        }
1116 858
    }
1117
1118
    /**
1119
     * @param BuildListener $listener
1120
     */
1121 837
    public function addBuildListener(BuildListener $listener)
1122
    {
1123 837
        $this->listeners[] = $listener;
1124 837
    }
1125
1126
    /**
1127
     * @param BuildListener $listener
1128
     */
1129 9
    public function removeBuildListener(BuildListener $listener)
1130
    {
1131 9
        $newarray = [];
1132 9
        for ($i = 0, $size = count($this->listeners); $i < $size; $i++) {
1133 9
            if ($this->listeners[$i] !== $listener) {
1134 9
                $newarray[] = $this->listeners[$i];
1135
            }
1136
        }
1137 9
        $this->listeners = $newarray;
1138 9
    }
1139
1140
    /**
1141
     * @return array
1142
     */
1143 33
    public function getBuildListeners()
1144
    {
1145 33
        return $this->listeners;
1146
    }
1147
1148
    public function fireBuildStarted()
1149
    {
1150
        $event = new BuildEvent($this);
1151
        foreach ($this->listeners as $listener) {
1152
            $listener->buildStarted($event);
1153
        }
1154
1155
        $this->log((string) $event, Project::MSG_DEBUG);
1156
    }
1157
1158
    /**
1159
     * @param Exception $exception
1160
     */
1161
    public function fireBuildFinished($exception)
1162
    {
1163
        $event = new BuildEvent($this);
1164
        $event->setException($exception);
1165
        foreach ($this->listeners as $listener) {
1166
            $listener->buildFinished($event);
1167
        }
1168
1169
        $this->log((string) $event, Project::MSG_DEBUG);
1170
    }
1171
1172
    /**
1173
     * @param $target
1174
     */
1175 648
    public function fireTargetStarted($target)
1176
    {
1177 648
        $event = new BuildEvent($target);
1178 648
        foreach ($this->listeners as $listener) {
1179 648
            $listener->targetStarted($event);
1180
        }
1181
1182 648
        $this->log((string) $event, Project::MSG_DEBUG);
1183 648
    }
1184
1185
    /**
1186
     * @param $target
1187
     * @param $exception
1188
     */
1189 648
    public function fireTargetFinished($target, $exception)
1190
    {
1191 648
        $event = new BuildEvent($target);
1192 648
        $event->setException($exception);
1193 648
        foreach ($this->listeners as $listener) {
1194 648
            $listener->targetFinished($event);
1195
        }
1196
1197 648
        $this->log((string) $event, Project::MSG_DEBUG);
1198 648
    }
1199
1200
    /**
1201
     * @param $task
1202
     */
1203 701
    public function fireTaskStarted($task)
1204
    {
1205 701
        $event = new BuildEvent($task);
1206 701
        foreach ($this->listeners as $listener) {
1207 701
            $listener->taskStarted($event);
1208
        }
1209
1210 701
        $this->log((string) $event, Project::MSG_DEBUG);
1211 701
    }
1212
1213
    /**
1214
     * @param $task
1215
     * @param $exception
1216
     */
1217 701
    public function fireTaskFinished($task, $exception)
1218
    {
1219 701
        $event = new BuildEvent($task);
1220 701
        $event->setException($exception);
1221 701
        foreach ($this->listeners as $listener) {
1222 701
            $listener->taskFinished($event);
1223
        }
1224
1225 701
        $this->log((string) $event, Project::MSG_DEBUG);
1226 701
    }
1227
1228
    /**
1229
     * @param $event
1230
     * @param $message
1231
     * @param $priority
1232
     */
1233 858
    public function fireMessageLoggedEvent(BuildEvent $event, $message, $priority)
1234
    {
1235 858
        $event->setMessage($message, $priority);
1236 858
        foreach ($this->listeners as $listener) {
1237 837
            $listener->messageLogged($event);
1238
        }
1239 858
    }
1240
1241
    /**
1242
     * @param mixed $object
1243
     * @param string $message
1244
     * @param int $priority
1245
     * @param Exception $t
1246
     * @throws \Exception
1247
     */
1248 858
    public function fireMessageLogged($object, $message, $priority, Exception $t = null)
1249
    {
1250 858
        $event = new BuildEvent($object);
1251 858
        if ($t !== null) {
1252
            $event->setException($t);
1253
        }
1254 858
        $this->fireMessageLoggedEvent($event, $message, $priority);
1255 858
    }
1256
1257
    /**
1258
     * Finds the Target with the most similar name to function's argument
1259
     *
1260
     * Will return null if buildfile has no targets.
1261
     *
1262
     * @see https://www.php.net/manual/en/function.levenshtein.php
1263
     * @param string $unknownTarget Target name
1264
     *
1265
     * @return Target
1266
     */
1267 3
    private function findSuggestion(string $unknownTarget): ?Target
1268
    {
1269 3
        return array_reduce($this->targets, function (?Target $carry, Target $current) use ($unknownTarget): ?Target {
1270
            // Omit target with empty name (there's always one)
1271 3
            if (empty(strval($current))) {
1272 3
                return $carry;
1273
            }
1274
            // $carry is null the first time
1275 2
            if (is_null($carry)) {
1276 2
                return $current;
1277
            }
1278 2
            return levenshtein($unknownTarget, $carry) < levenshtein($unknownTarget, $current) ? $carry : $current;
1279 3
        });
1280
    }
1281
}
1282