Among the interesting features of Phing is its extensibility, and of the hallmarks of that exensibility is the ability to define new Task
types as PHP classes, which are by default located in the default namespace only. Can we do better ?
Step 0: "Ad-hoc" (inline) tasks
At the simplest, Task
classes are created by embedding ad-hoc tasks like:
<
<!-- somewhere in build.xml ->
<target name="defineadhoc" hidden="true">
<adhoc-task name="ah1"><![CDATA[
class SomeAdHocTaskClass extends Task {
private $arg1;
function setAh1Arg1($arg1) {
$this->arg1 = $arg1;
}
function main() {
$this->log("In " . __METHOD__ . ", arg1 = " . $this->arg1);
}
}
]]></adhoc-task>
</target>
<target name="useadhoc" depends="defineadhoc" description="Demo: use an ad-hoc class">
<ah1 ah1Arg1="foo" />
</target>
Step 1: Rip code out of build.xml
Embedding such logic in a build file is not acceptable in most cases, so let's jump to the <taskdef />
element and define our task in its own file:
<?phpclass DemoTask extends Task { protected $arg; public function setArg($arg) { $this->arg = strrev($arg); } public function main() { echo "In " . __METHOD__ . ", arg = " . $this->arg . "\n"; }}?>
By default, Phing will try to load the DemoTask
from the current directory, but it offers a mechanism to fetch the file either from the PHP class path, or from an explicit path, like this:
<taskdef name="demo" classname="src.Acme.DemoTask" />
In this example, it will try to load a class called DemoTask
from a DemoTask.php
file in the src/Acme/
directory. Assuming a typical PSR-0/PSR-4 deployment for a Silex of Symfony2 project, we are likely to write our code in a specific directory under src/
or maybe within a Bundle
directory. Supposing we place it in src/Acme/Build/DemoTask.php
, we can tweak the Phing build file to load from there:
<taskdef name="demo" classname="src.Acme.Build.DemoTask" />
Step 2: Enter PSR-0 autoloading
The namespace vs path problem
The problem with the previous approach is that it breaks PSR-0 conventions. Assuming a typicaly autoloading root of src/
, if DemoTask
is located in src/Acme/Build/DemoTask.php
, then the fully qualified class name should be Acme\Build\DemoTask
. But if we modify the class accordingly, Phing will not accept that class: in a Composer-deployed Phing we will get something like this:
<?phpnamespace Acme\Build;class DemoTask extends \Task { /* ... */ }?>
<!-- in build.xml -->
<taskdef name="demo" classname="src.Acme.Build.DemoTask" />
<target name="usetask">
<demo arg="some value" />
</target>
bin/phing usetask
Buildfile: <...>/build.xml
> usetask:
Execution of target "usetask" failed for the following reason: <...>/build.xml:53:14: Could not create task of type: demo
#0 <...>/vendor/phing/phing/classes/phing/UnknownElement.php(191): Project->createTask('demo')
#1 <...>/vendor/phing/phing/classes/phing/UnknownElement.php(171): UnknownElement->makeTask(Object(UnknownElement), Object(RuntimeConfigurable), true)
#2 <...>/vendor/phing/phing/classes/phing/UnknownElement.php(70): UnknownElement->makeObject(Object(UnknownElement), Object(RuntimeConfigurable))
#3 <...>/vendor/phing/phing/classes/phing/Task.php(259): UnknownElement->maybeConfigure()
#4 <...>/vendor/phing/phing/classes/phing/Target.php(297): Task->perform()
#5 <...>/vendor/phing/phing/classes/phing/Target.php(320): Target->main()
#6 <...>/vendor/phing/phing/classes/phing/Project.php(824): Target->performTasks()
#7 <...>/vendor/phing/phing/classes/phing/Project.php(797): Project->executeTarget('usetask')
#8 <...>/vendor/phing/phing/classes/phing/Phing.php(586): Project->executeTargets(Array)
#9 <...>/vendor/phing/phing/classes/phing/Phing.php(170): Phing->runBuild()
#10 <...>/vendor/phing/phing/classes/phing/Phing.php(278): Phing::start(Array, NULL)
#11 <...>/vendor/phing/phing/bin/phing.php(43): Phing::fire(Array)
#12 <...>/vendor/phing/phing/bin/phing(20): require_once('<...>...')
#13 {main}
Previous exception 'BuildException' with message 'Could not instantiate class DemoTask, even though a class was specified. (Make sure that the specified class file contains a class with the correct name.)' in <...>/vendor/phing/phing/classes/phing/Project.php:679
[...]
The solution: autoloading
Looking more closely, Phing does not object to loading such a namespaced class, the only problem is that it cannot locate it in the file it expects. What if it could rely on Composer autoloading ? Let us be sure src/
is a default autoloading directory by defining it in composer.json
in case it is not already there:
"autoload": {
"psr-0": {
"": "src/"
}
},
And now let us remove the explicit path in build.xml
:
<!-- in build.xml -->
<taskdef name="demo" classname="Acme\Build\DemoTask" />
<target name="usetask">
<demo arg="some value" />
</target>
bin/phing usetask
Buildfile: <...>/build.xml
> usetask:
In Acme\Build\DemoTask::main, arg = eulav emos
BUILD FINISHED
Total time: 0.0699 seconds
Thanks to Composer PSR-0 autoloading, we have been able at the same time to gain two benefits:
- remove explicit path configuration in
build.xml
- support namespaced
Task
classes.
I'd say you forgot to mention
I'd say you forgot to mention the addition of the following line of code in build.xml:
This is according to http://stackoverflow.com/a/19464200/37706