Building and Deploying PHP apps with Phing
-
Upload
michiel-rook -
Category
Software
-
view
1.240 -
download
0
description
Transcript of Building and Deploying PHP apps with Phing
Building and deploying PHP apps with
>< ><
Michiel Rook
PHP Johannesburg April 2014
About me
• Freelance PHP & Java contractor / consultant
• PHP since '99
• Phing project lead
• Dutch Web Alliance
• http://www.linkedin.com/in/michieltcs
• @michieltcs
This talk
• Why a build tool
• What is Phing
• Usage
• Various examples
• Deployments
• Extending Phing
Why a build tool?
This is PHP!?
Repetitive tasks
• Version control
• Database changes
• Testing
• Minifying
• Packaging
• Uploading
• Deploying
• Configuring
Good programmers are lazy
Good programmers automate repeatablethings
Automate!
• Easier handover
• Improves quality
• Reduces errors
• Saves time
• Consolidate scripts, reduce technical debt
What is Phing?
Phing is AWESOME
What is Phing?
• “PHing Is Not GNU make; it's a PHP project build systemor build tool based on Apache Ant.”
• XML build files
• Mostly cross-platform
• Integrates various popular (PHP) tools
• Lots. Of. Tasks.
"Phing is good glue"
The basics
Installing Phing
• PEAR installation
$ pear channel-discover pear.phing.info$ pear install [--alldeps] phing/phing
• Optionally, install the documentation package
$ pear install phing/phingdocs• Composer
• Phar package
Build file
• XML• Contains standard elements
• Task: performs a specific function (copy, git commit, etc.)• Target: collection of tasks, can optionally depend on othertargets
• Project: root node, contains multiple targets
Example build file
<project name="Example" default="world"><echo>Hi!</echo>
<target name="hello"><echo>Hello</echo>
</target>
<target name="world" depends="hello"><echo>World!</echo>
</target></project>
Example build file
$ phing [-f build.xml]Buildfile: /home/michiel/phing/build.xml
[echo] Hi!
Example > hello:
[echo] Hello
Example > world:
[echo] World!
BUILD FINISHED
Properties
• Simple key-value pairs
• ${attribute}
• Replaced by actual value
Properties
<project name="Example" default="default"><target name="default">
<property file="build.properties" /><property name="foo" value="bar" />
<echo>${foo}</echo></target>
</project>
Properties
$ phingBuildfile: /home/michiel/phing/build.xml
Example > default:
[echo] bar
BUILD FINISHED
Fileset
• Denotes a group of files
• Include or exclude files based on patterns
• References: define once, use many
Fileset
<copy todir="build"><fileset dir="./application">
<include name="**/*.php" /><exclude name="**/*Test.php" />
</fileset></copy>
<fileset dir="./application" includes="**"/>
<fileset dir="./application" includes="**" id="files"/>
<fileset refid="files"/>
Fileset
• Selectors allow fine-grained matching on certain attributes
• contains, date, file name & size, ...
<fileset dir="${dist}"><and>
<filename name="**"/><date datetime="01/01/2011" when="before"/>
</and></fileset>
Conditions
• Nested elements that evaluate to booleans
• Used in "condition", "if" and "waitfor" tasks
Conditions
<if><equals arg1="${foo}" arg2="bar" /><then>
<echo message="The value of property foo is bar" /></then><else>
<echo message="The value of property foo is not bar" /></else>
</if>
Conditions
<if><available file="composer.json" /><then>
<exec checkreturn="true" command="composer install"passthru="true" logoutput="true" dir="build" />
</then></if>
Examples
Examples
• Version control
• Unit testing
• Packaging
• Deployment
• Database migration
• Continuous integration
Version control
• Git
• SVN
• CVS
Version control
<svnexportrepositoryurl="svn://localhost/project/trunk/"todir="/home/michiel/dev"/>
<svnlastrevisionrepositoryurl="svn://localhost/project/trunk/"propertyname="lastrev"/>
<echo>Last revision: ${lastrev}</echo>
Version control
<gitcommitrepository="/home/michiel/dev/phing"message="Update documentation" allFiles="true"/>
<gitpushrepository="/home/michiel/dev/phing"refspec="master" tags="true" />
PHPUnit
• Built-in support for most configuration options
• Gathers code coverage information
• Various output formats / reports
• PHPUnit 4.x support soon!
PHPUnit
• Stop the build when a test fails
<phpunit haltonfailure="true" haltonerror="true"bootstrap="my_bootstrap.php" printsummary="true"><batchtest>
<fileset dir="src"><include name="**/*Test.php"/>
</fileset></batchtest>
</phpunit>
Buildfile: /home/michiel/phpunit/build.xml
Demo > test:
[phpunit] Total tests run: 1, Failures: 1, Errors: 0,Incomplete: 0, Skipped: 0, Time elapsed: 0.00591 s
Execution of target "test" failed for the following reason:/home/michiel/phpunit/build.xml:3:44: Test FAILURE (testSayHello inclass HelloWorldTest): Failed asserting that two strings are equal.
PHPUnit example
• Determine which files to include in the coverage report
<coverage-setup database="reports/coverage.db"><fileset dir="src">
<include name="**/*.php"/><exclude name="**/*Test.php"/>
</fileset></coverage-setup>
• Gather code coverage and other data during the test run<phpunit codecoverage="true">
<formatter type="xml" todir="reports"/><batchtest>
<fileset dir="src"><include name="**/*Test.php"/>
</fileset></batchtest>
</phpunit>
PHPUnit example
• Generate some reports
<phpunitreport infile="reports/testsuites.xml"format="frames" todir="reports/tests"/>
<coverage-report outfile="reports/coverage.xml"><report todir="reports/coverage" title="Demo"/>
</coverage-report>
Documentation
• Phing currently integrates with popular documentationtools
• phpDocumentor (2)• ApiGen
• Also supports r(e)ST (reStructuredText)
<phpdoc2 title="Phing API Documentation" output="docs"><fileset dir="../../classes">
<include name="**/*.php"/></fileset>
</phpdoc2>
phpDocumentor
Packaging
• Create bundles or packages• tar• zip• phar• PEAR
Tar / zip
<tar compression="gzip" destFile="package.tgz"basedir="build"/>
<zip destfile="htmlfiles.zip"><fileset dir=".">
<include name="**/*.html"/></fileset>
</zip>
Phar packages
<pharpackagecompression="gzip"destfile="test.phar"stub="stub.php"basedir="."><fileset dir="hello">
<include name="**/**" /></fileset><metadata>
<element name="version" value="1.0" /><element name="authors">
<element name="John Doe"><element name="e-mail"
value="[email protected]" /></element>
</element></metadata>
</pharpackage>
SSH
<ssh username="john" password="smith"host="webserver" command="ls" />
<scp username="john" password="smith"host="webserver" todir="/www/htdocs/project/"><fileset dir="test">
<include name="*.html"/></fileset>
</scp>
Jenkins
Jenkins
Jenkins
Putting it all together
Build & deploy script
Objectives:
• Perform syntax check
• Run tests
• Create package
• Deploy via SSH
• To selectable target / environment
• Update database
• Roll back
Syntax checks & tests
<phplint haltonfailure="true"><fileset dir=".">
<include name="src/**" /></fileset>
</phplint>
<phpunit haltonfailure="true"><batchtest>
<fileset dir="."><include name="src/**/*Test.php" />
</fileset></batchtest>
</phpunit>
Packaging
<tstamp><format property="build.timestamp" pattern="%Y%m%d%H%M%S"/>
</tstamp>
<property name="build.release" value="${project.name}-${build.timestamp}" /><property name="package.name" value="${build.release}.tar.gz" />
<tar destfile="artifacts/${package.name}" basedir="${build.dir.project}" />
Multiple targets
• Several deployment targets: testing, staging, production, ...
• One property file per target
• Select based on input
ssh.host=127.0.0.1ssh.username=phingssh.key.private=development-sshssh.key.public=development-ssh.pubdeploy.location=/home/phing/apps
Multiple targets
<input propertyname="deploy.target"validArgs="testing,staging,production">
Enter target name</input>
<property file="${deploy.target}.properties"/>
Uploading
<ssh host="${ssh.host}"username="${ssh.username}"privkeyfile="${ssh.key.private}"pubkeyfile="${ssh.key.public}"command="mkdir -p ${deploy.location.project}/${build.release}"failonerror="true" />
<echo>Copying package</echo><scp host="${ssh.host}"
port="${ssh.port}"username="${ssh.username}"privkeyfile="${ssh.key.private}"pubkeyfile="${ssh.key.public}"todir="${deploy.location.project}/${build.release}"file="${package.name.full}" />
Uploading
<echo>Extracting package</echo><ssh ...
command="cd ${deploy.location.project}/${build.release};tar xzf ${package.name}"
failonerror="true" />
Symbolic links
• All releases stored in separate directories
• Symlink "current" to latest release
• Allows for easy (code) rollbacks
<echo>Creating symbolic link</echo><ssh ...
command="cd ${deploy.location.project};if [ -h "current" ]; thenrm -f previous; mv current previous; fi;ln -s ${build.release} current" />
Database migration
• Set of delta SQL files (1-create-post.sql)
• Tracks current version of your db in changelog table
• Generates do and undo SQL files
CREATE TABLE changelog (change_number BIGINT NOT NULL,delta_set VARCHAR(10) NOT NULL,start_dt TIMESTAMP NOT NULL,complete_dt TIMESTAMP NULL,applied_by VARCHAR(100) NOT NULL,description VARCHAR(500) NOT NULL
)
Database migration
• Delta scripts with do (up) & undo (down) parts
--//
CREATE TABLE `post` (`title` VARCHAR(255),`time_created` DATETIME,`content` MEDIUMTEXT
);
--//@UNDO
DROP TABLE `post`;
--//
Database migration
<dbdeployurl="sqlite:test.db"dir="deltas"outputfile="deploy.sql"undooutputfile="undo.sql"/>
<pdosqlexecsrc="deploy.sql"url="sqlite:test.db"/>
[dbdeploy] Getting applied changed numbers from DB:mysql:host=localhost;dbname=demo
[dbdeploy] Current db revision: 0[dbdeploy] Checkall:[pdosqlexec] Executing file: /home/michiel/dbdeploy/deploy.sql[pdosqlexec] 3 of 3 SQL statements executed successfully
Database migration
-- Fragment begins: 1 --INSERT INTO changelog
(change_number, delta_set, start_dt, applied_by, description)VALUES (1, 'Main', NOW(), 'dbdeploy','1-create_initial_schema.sql');
--//
CREATE TABLE `post` (`title` VARCHAR(255),`time_created` DATETIME,`content` MEDIUMTEXT
);
UPDATE changelogSET complete_dt = NOW()WHERE change_number = 1AND delta_set = 'Main';
-- Fragment ends: 1 --
Database migration
-- Fragment begins: 1 --
DROP TABLE `post`;
--//
DELETE FROM changelogWHERE change_number = 1AND delta_set = 'Main';
-- Fragment ends: 1 --
Restart services
<ssh ...command="sudo service apache2 reload"failonerror="true" />
Rolling back
<trycatch><try>
<ssh ...command="cd ${deploy.location.project};
rm -f current;mv -f previous current"
failonerror="true" /><echo>Rollback complete</echo>
</try><catch>
<echo>No previous version deployed!</echo></catch>
</trycatch>
Extending Phing
Writing your own task
• Extend from Task
• Contains main() method and optionally init()
• Setter method for each attribute in the build file
Our new task should
• Accept filesets
• Count number of lines in each file
• Fail the build if a file with zero lines is found
Our new task
<?php
require_once 'phing/Task.php';
class CountLinesTask extends Task{
public function main(){
$foundEmpty = false;
if ($foundEmpty) {throw new BuildException("One or more files have zero lines");
}}
}
Injecting file sets
private $_filesets = array();
/*** Creator for _filesets** @return FileSet*/public function createFileset(){
$num = array_push($this->_filesets, new FileSet());return $this->_filesets[$num-1];
}
Injecting file sets
foreach ($this->_filesets as $fileset) {$files = $fileset->getDirectoryScanner($this->project)
->getIncludedFiles();$dir = $fileset->getDir($this->project)->getAbsolutePath();
foreach ($files as $file) {$path = realpath($dir . DIRECTORY_SEPARATOR . $file);$lines = count(file($path));
$this->log($path . ": " . $lines . " line(s)");
if ($lines == 0) {$foundEmpty = true;
}}
}
Using the task
<project name="Count Lines" default="count"><target name="count">
<taskdef name="countlines"classpath="/home/michiel/tasks"classname="CountLinesTask" />
<countlines><fileset dir=".">
<include name="**/*.txt" /></fileset>
</countlines></target>
</project>
Using the task
Buildfile: /home/michiel/examples/count.xml
Count Lines > count:
[countlines] /home/michiel/examples/empty.txt: 0 line(s)[countlines] /home/michiel/examples/lines.txt: 3 line(s)Execution of target "count" failed for the following reason:/home/michiel/examples/count.xml:7:20: One or more files have zero lines
BUILD FAILED/home/michiel/examples/count.xml:7:20: One or more files have zero linesTotal time: 0.0454 seconds
Alternative: Ad Hoc
<target name="main"><adhoc-task name="foo"><![CDATA[class FooTask extends Task {
private $bar;
public function setBar($bar) {$this->bar = $bar;
}
public function main() {$this->log("In main(): " . $this->bar);
}}]]></adhoc-task><foo bar="TEST"/>
</target>
Where to go from here
• Tool versions
• Performance
• Documentation
• PHP 5.3/5.4/5.5
• IDE support
• CI integration
Questions?
Example code at https://github.com/mrook/phing-examplesPlease leave feedback at https://joind.in/10411
Contact us on:
http://www.phing.info#phing (freenode)@phingofficial
Thank you!