Project   F
last analyzed

Complexity

Total Complexity 131

Size/Duplication

Total Lines 1232
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 131
eloc 282
c 0
b 0
f 0
dl 0
loc 1232
rs 2

71 Methods

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