How to: configure PHP & NPM on Circle CI

08 December 2017

Configuring your projects to use continuous integration (CI) and continuous distribution (CD) is a great way of ensuring their integrity from the moment you commit to the moment the projects make their ways to production.

There are free CI/CD services available for projects publically hosted on GitHub or Bitbucket. At the moment the most popular of these would likely be Travis. Travis is quick and easy to use and has been a good friend of open projects on GitHub for a long time now.

However, an alternative has been gaining ground recently in the form of Circle CI. With the release of the second version of their API, you may now build your projects on personalized Docker images using single configuration files that you add to your project’s repository.

In this article, we will be looking at how to fully configure the CI/CD process from start to finish of a PHP project which compiles it’s frontend assets using NPM. The PHP project type I will use will be a Laravel project and you will see custom commands for the framework in the end results. It should be relatively easy for you to replace or remove these lines if you use a different framework.

I write following the assumption you will be able to connect your project’s repository to Circle CI so that each time you push new code it correctly triggers a build. You may find additional information on building PHP projects in their official documentation through they do not mention how to implement NPM.

A complete circle.yml example can be found at the end of the article.

circle.yml

The first step is to create a file named circle.yml at the root of your project structure. In it, we will specify the Circle CI version so they can understand the format of the file on their end. You will also want to declare an image from the Docker Hub on which you will build the project.

version: 2
jobs:
build:
docker:
- image: misterio92/ci-php-node
steps:

This translates to “Using the 2nd version, your first job is to build a docker image using the following steps”. This version of the configuration API is really easy to read and understand as you can see.

I am suggesting you use misterio92/ci-php-node as Docker image because it is small and it comes will all we require already installed. As it is already fully bundled, Circle CI will pull and run it quickly and will likely cache bits of it to speed up the next run even further.

Know that there are many Docker images you can choose from to execute the build, however. You may even choose to use your own pre-built or dynamically add to a lightweight Alpine build of PHP and install NPM and other dependencies at run time using apt-get.

Setting up steps

Next up is to write the list of steps that need to be performed to build and deploy the project.

In this block, it is important to leverage Circle CI’s caching mechanism to ensure fast build execution. In a classic PHP project, the two big things that you should cache are the vendor packages from Composer as well as the node_modules from NPM. Having these two folders cached will prevent unnecessary file downloads if the dependencies are still valid from one build to the next. Hitting the cache instead of downloading them anew will grant massive performance gains.

In chronological order, the steps required to build a project using PHP and NPM could look like :

  1. Check out the project files
  2. Restore the file caches
  3. Install Composer dependencies
  4. Install NPM dependencies
  5. Save the updated file caches
  6. Build the assets
  7. Run unit tests

Transposed in Circle CI configuration, these steps would look like :

    steps:
- checkout

- restore_cache:
keys:
- composer-cache-{{ checksum "composer.json" }}
- composer-cache-
- dependency-cache-{{ checksum "package.json" }}

- run:
name: Installing PHP packages
command: composer install -n --prefer-dist --ignore-platform-reqs --optimize-autoloader

- run:
name: Installing NPM packages
command: npm install

- save_cache:
key: composer-cache-{{ checksum "composer.json" }}
paths:
- vendor

- save_cache:
key: dependency-cache-{{ checksum "package.json" }}
paths:
- ./node_modules

- run:
name: Building production package
command: npm run production

- run:
name: Unit tests
command: vendor/bin/phpunit

These new lines will allow the build to download all the project’s dependencies, to cache them at the current version using a unique checksum of the dependency manager’s JSON file, to build the assets and finally to check whether our unit tests are still passing.

If anything goes wrong or if something unexpected occurs during that process, the build will fail and stop at the point of the error. It’s important to see the step that runs PHPUnit as your last safeguard before the commit is deployed.

Deployment

Assuming the build has reached this point you may now allow Circle CI to deploy the build to a server. I guess there are multiple schools of thought on how to do that, but I will propose my preferred way of doing it.

You will need

You will need to create a user that will be dedicated to deploying files on the server which the build will be uploaded. This user will need to have similar rights as your web host (Apache, Ngnix, etc) to ensure files can be read and written correctly. The deployer user will also need to have Circle CI’s public key authorized on the server so that you do not leave any passwords in your circle.yml file. The details on how to configure all of these steps are out of the scope of the article.

In this example, the user used for deployment will be called circleci and we will be deploying to themusictank.com, or 138.197.148.166.

In chronological order, the detailed steps required to deploy the project could be :

  1. Tell the Docker image it should know the IP of the final server
  2. Sync the files between the two
  3. Execute command on the server to complete the deployment

Translated in Circle CI steps, this would be:

    steps:
- run:
name: Add server keys
command: ssh-keyscan 138.197.148.166 >> ~/.ssh/known_hosts

- run:
name: Deployment
command: rsync -avzp --delete --exclude-from '.rsyncignore' . circleci@138.197.148.166:/var/www/themusictank.com

- run:
name: Post deploy
command: ssh circleci@138.197.148.166 "cd /var/www/themusictank.com && php artisan route:cache"

In the previous example note how I chose to keep the list of ignored files in the sync as a file called .rsyncignore. Because it’s just another file in the project, you can easily modify it as you go. Among the files and folders ignored are node_modules, tests, and git files

Finally, the deployer will connect through SSH to cache Laravel routes to speed up the website.

End result

Here is the full version of a circle.yml file you can use on PHP projects which compiles its frontend through NPM. An updated real-world example can be seen in The Music Tank’s repository and that one features additional steps such as the integration of Codacy.

version: 2
jobs:
build:
docker:
- image: misterio92/ci-php-node
steps:
- checkout

- restore_cache:
keys:
- composer-cache-{{ checksum "composer.json" }}
- composer-cache-
- dependency-cache-{{ checksum "package.json" }}

- run:
name: Installing PHP packages
command: composer install -n --prefer-dist --ignore-platform-reqs --optimize-autoloader

- run:
name: Installing NPM packages
command: npm install

- save_cache:
key: composer-cache-{{ checksum "composer.json" }}
paths:
- vendor

- save_cache:
key: dependency-cache-{{ checksum "package.json" }}
paths:
- ./node_modules

- run:
name: Building production package
command: npm run production

- run:
name: Unit tests
command: vendor/bin/phpunit

- run:
name: Add server keys
command: ssh-keyscan 138.197.148.166 >> ~/.ssh/known_hosts

- run:
name: Deployment
command: rsync -avzp --delete --exclude-from '.rsyncignore' . circleci@138.197.148.166:/var/www/themusictank.com

- run:
name: Post deploy
command: ssh circleci@138.197.148.166 "cd /var/www/themusictank.com && php artisan route:cache"