PHP Build Script 2022

Submitted by Jeff on

I have a lot of respect for Mr. Bergmann, the writer of many of these tools that I am about to list. He has written some great tools, and wrote the original build script I am using currently. Due to changes or lack of changes in many of these tools I have had to make some updates to my build script.

Now there will be a few programs that will need to be installed beside PHP and a web server. You need a few other things installed. I have since migrated to using Docker for my development environment, but you can do the same on any machine. I will use apt from Debian to so my installs. What we need to install are PHP CLI, Composer, and Ant. To install those it is relatively simple in Debian.

apt install ant composer php-cli

PHP-CLI is pretty straight forward, these are CLI tools so you kind need a CLI version of PHP. The FPM version will not work. With PHP-CLI you need a package manager to install the PHP tools we are about to setup. That package manager is Composer. While I would hope you already have this installed to manage your project, but I will put in the current way to install it. The most current install can be found at https://getcomposer.org/download/, but this will get you started.

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === '906a84df04cea2aa72f40b5f787e49f22d4c2f19492ac310e8cba5b96ac8b64115ac402c8cd292b8a03482574915d1a8') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"

mv composer.phar /usr/local/bin/composer

Now that we have Composer installed, we need to add to the composer.json file to install the tools. We will also want to change where the executables should go. In the "config" you will want to add "bin-dir" and set it to the current directories /bin directory, so "./bin". The rest you need to put the packages under the "require-dev" since you will most likely not need them in the production environment. I have some version numbers in here, but you will want to update those as needed. I had some issues with phpdocumentor install before, you might want to check out the phive version, as it is more isolated and doesn't matter what other libraries need to be installed. Many of the other tools have similar versions, it is a pretty new manager so I will not cover that here. I do use it some in my Docker images.

"config": {
	"bin-dir": "./bin"
},
"require-dev": {
	"phploc/phploc" : "^7.0",
	"pdepend/pdepend" : "dev-master",
	"phpmd/phpmd" : "@stable",
	"squizlabs/php_codesniffer": "^3",
	"sebastian/phpcpd": "^6.0",
	"phpunit/phpunit": "^9.4",
	"phpunit/php-invoker": "^3",
	"laminas/laminas-test": "^3.4",
	"friendsofphp/php-cs-fixer": "^2",
	"phpdocumentor/phpdocumentor" : "*"
},

The other system installed app you want to install is Ant. While there is a more PHP type build script called PHING, Ant seems to be better supported and popular tool. So I have kept with it and it works, but might need to look at this more later.

Any who, since we have Ant installed you might want to know what to use it for. Like most languages that need to pre-compile the application, which many times need a build script to the compiling. PHP is different in that it really doesn't need to be pre-complied to make an application. As PHP and our project grow we need to a way to download libraries and make sure things are put in the right places. Composer helps us with keeping out libraries up to date and keep our project organized at least in a namespace manner. Ant gives another layer to help us run tests and tools on our code to make sure things work and the quality of our code is up to snuff.

Here is the complete file I currently use to build my projects. My code is laid out with a library directory and an external test directory. Which is more of my library layout. My projects usually a bit different, but more excludes.

<?xml version="1.0" encoding="UTF-8"?>

<project name="project" default="build">
    <target name="build" depends="install,lint,phploc,phploc-xml,pdepend,phpmd-ci,phpcs-ci,phpcpd,phpstan-ci,phpunit,phpmetrics,phpdocumentor"/>

    <target name="cli" depends="lint,phploc,pdepend,phpmd,phpcs,phpcpd,phpstan,phpunit,phpmetrics,phpdocumentor"/>
    <target name="prod"
            depends="install-prod"/>

    <target name="clean" description="Cleanup build artifacts">
        <delete dir="${basedir}/build"/>
        <delete dir="${basedir}/bin"/>
        <delete dir="${basedir}/vendor"/>
        <delete file="${basedir}/composer.lock"/>
    </target>

    <target name="prepare" depends="clean" description="Prepare for build">
        <mkdir dir="${basedir}/build"/>
        <mkdir dir="${basedir}/build/api"/>
        <mkdir dir="${basedir}/build/coverage"/>
        <mkdir dir="${basedir}/build/logs"/>
        <mkdir dir="${basedir}/build/pdepend"/>
    </target>

    <target name="install" depends="clean" description="Runs composer">
        <exec executable="composer">
            <arg value="install"/>
        </exec>
    </target>

    <target name="install-prod" depends="clean" description="Runs composer">
        <exec executable="composer">
            <arg value="install"/>
            <arg value="--no-dev"/>
        </exec>
    </target>

    <target name="lint" description="Perform syntax check of sourcecode files">
        <apply executable="php" failonerror="true">
            <arg value="-l" />

            <fileset dir="${basedir}/library">
                <include name="**/*.php" />
                <modified />
            </fileset>

            <fileset dir="${basedir}/test">
                <include name="**/*.php" />
                <modified />
            </fileset>
        </apply>
    </target>

    <target name="phploc" description="Measure project size using PHPLOC">
        <exec executable="${basedir}/bin/phploc">
            <arg value="--log-csv" />
            <arg value="${basedir}/build/logs/phploc.csv" />
            <arg path="${basedir}/library" />
        </exec>
    </target>

    <target name="phploc-xml" description="Measure project size using PHPLOC">
        <exec executable="${basedir}/bin/phploc">
            <arg value="--log-xml" />
            <arg value="${basedir}/build/logs/phploc.xml" />
            <arg path="${basedir}/library" />
        </exec>
    </target>

    <target name="pdepend" description="Calculate software metrics using PHP_Depend">
        <exec executable="${basedir}/bin/pdepend">
            <arg value="--jdepend-xml=${basedir}/build/logs/jdepend.xml" />
            <arg value="--jdepend-chart=${basedir}/build/pdepend/dependencies.svg" />
            <arg value="--overview-pyramid=${basedir}/build/pdepend/overview-pyramid.svg" />
            <arg path="${basedir}/library" />
        </exec>
    </target>

    <target name="phpmd"
            description="Perform project mess detection using PHPMD and print human readable output. Intended for usage on the command line before committing.">
        <exec executable="${basedir}/bin/phpmd" failonerror="true">
            <arg path="${basedir}/library" />
            <arg value="text" />
            <arg value="codesize,unusedcode,naming,design" />
        </exec>
    </target>
    <target name="phpmd-ci" description="Perform project mess detection using PHPMD creating a log file for the continuous integration server">
        <exec executable="${basedir}/bin/phpmd">
            <arg path="${basedir}/library" />
            <arg value="xml" />
            <arg value="codesize,unusedcode,naming,design" />
            <arg value="--reportfile" />
            <arg value="${basedir}/build/logs/pmd.xml" />
        </exec>
    </target>

    <target name="phpcs"
            description="Find coding standard violations using PHP_CodeSniffer and print human readable output. Intended for usage on the command line before committing.">
        <exec executable="${basedir}/bin/phpcs" failonerror="true">
            <arg value="--standard=PSR2" />
            <arg path="${basedir}/library" />
        </exec>
    </target>

    <target name="phpcs-ci" description="Find coding standard violations using PHP_CodeSniffer creating a log file for the continuous integration server">
        <exec executable="${basedir}/bin/phpcs" output="/dev/null">
            <arg value="--report=checkstyle" />
            <arg value="--report-file=${basedir}/build/logs/checkstyle.xml" />
            <arg value="--standard=PSR2" />
            <arg path="${basedir}/library" />
        </exec>
    </target>

    <target name="phpcpd" description="Find duplicate code using PHPCPD">
        <exec executable="${basedir}/bin/phpcpd" failonerror="true">
            <arg value="--log-pmd" />
            <arg value="${basedir}/build/logs/pmd-cpd.xml" />
            <arg path="${basedir}/library" />
        </exec>
    </target>

    <target name="phpstan-ci" description="Error detection using PHPStan">
        <exec executable="${basedir}/bin/phpstan" output="build/logs/phpstan.xml">
            <arg value="analyse" />
            <arg value="--no-progress" />
            <arg value="--error-format" />
            <arg value="checkstyle" />
            <arg value="library" />
        </exec>
    </target>

    <target name="phpstan" description="Error detection using PHPStan">
        <exec executable="${basedir}/bin/phpstan" failonerror="true">
            <arg value="analyse" />
            <arg value="library" />
        </exec>
    </target>

    <target name="phpunit" description="Run unit tests with PHPUnit">
        <exec executable="${basedir}/bin/phpunit" failonerror="true"/>
    </target>
 
    <target name="phpmetrics" description="Calculates software metrices use PHPMetric">
        <exec executable="${basedir}/bin/phpmetrics">
            <arg value="--config=${basedir}/phpmetrics.json"/>
            <arg value="--junit=${basedir}/build/logs/junit.xml"/>
        </exec>
    </target>
    
    <target name="phpdocumentor" description="Generate API documentation using phpDocumentor">
        <exec executable="phpDocumentor">
            <arg value="--config" />
            <arg value="${basedir}/phpdoc.xml" />
            <arg value="--setting=graphs.enabled=true" />
        </exec>
    </target>
   
    <target name="phpdox" description="Generate API documentation using phpDox">
        <exec executable="phpdox"/>
    </target>
</project>

To start everything is grouped into targets. The first couple targets are commands to run everything in different ways. The first "build" target is the default target and runs everything for a Jenkins job. The second target "cli" is something I added so I didn't have to build everything if I didn't need to and runs faster. I also have versions of the other targets that will fail if there is an error and display their output to the command line. It makes it easier to find errors and correct any style or other violations the tools might find. The third target is "prod" for use when I install it on the server. The "prod" target avoid installing the tools and only installs what your need for your application. You can also setup dependencies using the "depends" attribute. There is a fourth that I usually have that holds the scripts to publish the project.

Next is some clean up and download of the libraries. The ./build directory is where the reports end up, ./vendor is where composer puts it's libraries, ./bin is where we are putting the executables for the tools and last removes the composer lock. We then turn around and recreate most of the directories as many of the tools don't seem to create multiple level directories above their report very well. Then we build back the composer libraries. This is also a good place to put npm installs, also adding a target to run a grunt script is pretty straight forward.

The rest are tools and scripts to check code and build reports that typically help clean up my code. The first is a basic lint check. It uses a feature of PHP CLI that will check for syntax errors in the code. As you can see I lint both my code directory and my test directories. That way when you run your tests you know your syntax is good.

    <target name="lint" description="Perform syntax check of sourcecode files">
        <apply executable="php" failonerror="true">
            <arg value="-l" />

            <fileset dir="${basedir}/library">
                <include name="**/*.php" />
                <modified />
            </fileset>

            <fileset dir="${basedir}/test">
                <include name="**/*.php" />
                <modified />
            </fileset>
        </apply>
    </target>

Next is PHPLoc which is used mainly to measure your code. It is good to know if your code is increasing or decreasing in size. Like when you refactor something you hopefully will end up with less code. It checks more than just number of line or file counts, it also checks for comments, cyclomatic complexity and number of object types as well. I have it output both in CSV and XML and Jenkins might use one or another for the job.

    <target name="phploc" description="Measure project size using PHPLOC">
        <exec executable="${basedir}/bin/phploc">
            <arg value="--log-csv" />
            <arg value="${basedir}/build/logs/phploc.csv" />
            <arg path="${basedir}/library" />
        </exec>
    </target>

    <target name="phploc-xml" description="Measure project size using PHPLOC">
        <exec executable="${basedir}/bin/phploc">
            <arg value="--log-xml" />
            <arg value="${basedir}/build/logs/phploc.xml" />
            <arg path="${basedir}/library" />
        </exec>
    </target>

The next tool is PDepend. This one the community/open-source version on GitHub is not being updated at all. In fact I have been working on trying to find an alternative to some of the metrics and graphs that it puts out. This is similar to PHPLoc in that it analyzes the code for a number of things like number of lines, number of comments, number of objects and other number of stats. It also takes things a bit farther and analyzes the relationships between the class and packages to get a number of other metrics. They do still have a nice document on many of the metrics and what they mean. I do like the pyramid and dependencies graphs, they make it easy to see where some work needs to be done. They also make the Jenkins interface look interesting.

    <target name="pdepend" description="Calculate software metrics using PHP_Depend">
        <exec executable="${basedir}/bin/pdepend">
            <arg value="--jdepend-xml=${basedir}/build/logs/jdepend.xml" />
            <arg value="--jdepend-chart=${basedir}/build/pdepend/dependencies.svg" />
            <arg value="--overview-pyramid=${basedir}/build/pdepend/overview-pyramid.svg" />
            <arg path="${basedir}/library" />
        </exec>
    </target>

PHPMD or PHP Mess Detector is a handy for code styling. It will check function line sizes, variable naming sizes and a number of style based checks. I also does some other analyzes like complexity and some of the same metrics from PDepend without the graphs. They have a number of other rules you can add to have it do more checks.


    <target name="phpmd"
            description="Perform project mess detection using PHPMD and print human readable output. Intended for usage on the command line before committing.">
        <exec executable="${basedir}/bin/phpmd" failonerror="true">
            <arg path="${basedir}/library" />
            <arg value="text" />
            <arg value="codesize,unusedcode,naming,design" />
        </exec>
    </target>
    <target name="phpmd-ci" description="Perform project mess detection using PHPMD creating a log file for the continuous integration server">
        <exec executable="${basedir}/bin/phpmd">
            <arg path="${basedir}/library" />
            <arg value="xml" />
            <arg value="codesize,unusedcode,naming,design" />
            <arg value="--reportfile" />
            <arg value="${basedir}/build/logs/pmd.xml" />
        </exec>
    </target>

PHPCS or PHP Code Sniffer is more of a style checker. You can select your preferred standard or build your own standard and will tell you where in your code where there are violations. This is a nice program to attach to your IDE to make sure your code looks pretty. They have a version that will auto-fix most styling issues.

   <target name="phpcs"
            description="Find coding standard violations using PHP_CodeSniffer and print human readable output. Intended for usage on the command line before committing.">
        <exec executable="${basedir}/bin/phpcs" failonerror="true">
            <arg value="--standard=PSR2" />
            <arg path="${basedir}/library" />
        </exec>
    </target>

    <target name="phpcs-ci" description="Find coding standard violations using PHP_CodeSniffer creating a log file for the continuous integration server">
        <exec executable="${basedir}/bin/phpcs" output="/dev/null">
            <arg value="--report=checkstyle" />
            <arg value="--report-file=${basedir}/build/logs/checkstyle.xml" />
            <arg value="--standard=PSR2" />
            <arg path="${basedir}/library" />
        </exec>
    </target>

PHPCP or PHP Copy Paste report sees where you copied and pasted in your code. Or where you have code that looks identical. Both are bad practices that this tool helps you locate.

    <target name="phpcs-ci" description="Find coding standard violations using PHP_CodeSniffer creating a log file for the continuous integration server">
        <exec executable="${basedir}/bin/phpcs" output="/dev/null">
            <arg value="--report=checkstyle" />
            <arg value="--report-file=${basedir}/build/logs/checkstyle.xml" />
            <arg value="--standard=PSR2" />
            <arg path="${basedir}/library" />
        </exec>
    </target>

    <target name="phpcpd" description="Find duplicate code using PHPCPD">
        <exec executable="${basedir}/bin/phpcpd" failonerror="true">
            <arg value="--log-pmd" />
            <arg value="${basedir}/build/logs/pmd-cpd.xml" />
            <arg path="${basedir}/library" />
        </exec>
    </target>

PHPStan is one I don't know a whole lot about. It does some similar checks to PHPMD, but goes a bit farther I think. It does it all with out running your code, which is kind of nice. It does catch some errors that PHPMD doesn't, like wrong variable types. It is one of my newer additions to my build checks.

    <target name="phpstan-ci" description="Error detection using PHPStan">
        <exec executable="${basedir}/bin/phpstan" output="build/logs/phpstan.xml">
            <arg value="analyse" />
            <arg value="--no-progress" />
            <arg value="--error-format" />
            <arg value="checkstyle" />
            <arg value="library" />
        </exec>
    </target>

    <target name="phpstan" description="Error detection using PHPStan">
        <exec executable="${basedir}/bin/phpstan" failonerror="true">
            <arg value="analyse" />
            <arg value="library" />
        </exec>
    </target>

 

PHPUnit is probably what Mr. Bergmann is most known for. It allows for unit and even integration tests as well. The task is pretty simple as most of the configuration information in the configuration file.  I usually have it output the coverage report that lets you know where your tests haven't touched. It has a few nice report to build graphs and HTML reports.

    <target name="phpunit" description="Run unit tests with PHPUnit">
        <exec executable="${basedir}/bin/phpunit" failonerror="true"/>
    </target>

PHP Metrics is another new tool that I have been using. It has been a good tool, as I points out some of the issues the PDepend usually points out. It will output a nice report that tell you why a package is not following the SOLID practice. It gives you some tips and links to documentation on what the violation is. It has a similar graph to what PDepend has, but has more information. There are some other reports that are useful. Be warned the setup and features documentation on the tool is a little incomplete.

    <target name="phpmetrics" description="Calculates software metrices use PHPMetric">
        <exec executable="${basedir}/bin/phpmetrics">
            <arg value="--config=${basedir}/phpmetrics.json"/>
            <arg value="--junit=${basedir}/build/logs/junit.xml"/>
        </exec>
    </target>

PHPDocumentor and PHP Dox are tools that read DocBlock comments and outputs an HTML report. PHP Dox has had some library issues which is why I have PHPDocumentor installed. After going with PHPDocumentor I have been pretty impressed with it as it has some nice UML using PlantUML class view. It makes it nice to reference if you need to make a quick look at your comments.

    <target name="phpdocumentor" description="Generate API documentation using phpDocumentor">
        <exec executable="phpDocumentor">
            <arg value="--config" />
            <arg value="${basedir}/phpdoc.xml" />
            <arg value="--setting=graphs.enabled=true" />
        </exec>
    </target>
   
    <target name="phpdox" description="Generate API documentation using phpDox">
        <exec executable="phpdox"/>
    </target>

While this is a good way to build your application and adding it to a CI/CD system would make more of this worth while. The "cli" target makes things nice while trying to check your code from the command line. You can access many of the HTML reports just by loading the directory with your browser. Some of these tools I just recently added to my build script. They have been beneficial and making me take another look at my code. In fact I have had to redesign some of it based on the output of the PHPMetric tool and is already looking better.

Taxonomy