How to automatically Build and Deploy your Symfony Application.

When working on any software development project, automation of repeating tasks boosts your productivity and decreases human errors. Running tests, validating coding styles, and deploying to your test server are good examples of tasks that can be automated.

In this post, you will learn how to set-up an automated pipeline in CircleCI that will check each commit on code style, errors, and failing tests. If the builds succeed and you merge your branch in the develop branch, CircleCI will automatically deploy your application to the configured test server.

What do you need?

To follow along you need the following things:

  • A (sandbox) Symfony application to build and deploy.
  • A (Free) CircleCI account, connected to your Git repository.
  • Deployer, locally to test your deployment script.

The final project can be found on github: https://github.com/jeroendk/symfony-build-and-deploy

What will this post cover?

  • User PHP-CS-Fixer for code style checking.
  • Add PHPStan for static code analysis.
  • Set Up CircleCI for an automated build after each commit.
  • Configure Deployer for a smooth and automated deployment process.

PHP-CS-Fixer

PHP-CS-fixer is a tool to check and fix coding style issues in your project. To test it locally you can follow the installation instructions here. Then you can specify the preferred coding style rules in a config file named “.php_cs.dist”. For this project our config looks as follows:

<?php
//.php_cs.dist
$finder = PhpCsFixer\Finder::create()
    ->exclude('vendor')
    ->exclude('var')
    ->exclude('config')
    ->exclude('build')
    ->notPath('src/Kernel.php')
    ->notPath('public/index.php')
    ->in(__DIR__)
    ->name('*.php')   
    ->ignoreDotFiles(true);

return PhpCsFixer\Config::create()
    ->setRules([
        '@PSR2' => true,
        '@PhpCsFixer' => true,
        '@Symfony' => true,
        '@PHP70Migration:risky' => true,
        '@PHP71Migration:risky' => true,
        '@DoctrineAnnotation' => true,
        '@PhpCsFixer:risky' => true
    ])
    ->setFinder($finder);

With this config you will apply some risky rules, you can modify it to your needs with the docs as a reference.

If you installed in your development environment or in your docker container, you can execute the following command in the appropriate terminal:

php-cs-fixer --diff --dry-run -v --allow-risky=yes fix

To actually fix any errors remove the dry-run parameter.

PHPStan

PHPStan is a Static Analysis tool for PHP. It will scan your codebase for possible bugs without running your application. Static Analysis is a handy tool in your CI/CD workflow.

Start by installing it as a dev dependency:

composer require --dev phpstan/phpstan

Then create a config file phpstan.neon.dist in the root of your project.

parameters:
	excludes_analyse:
		- %rootDir%/../../../src/DataFixtures/*

One of the things you can do with the config file is to specify which folders you want to exclude, like the DataFixture folder in the example above. Take a look at the documentation for all the other things you can configure.

Execute PHPStan with the following command in your PHP environment:

vendor/bin/phpstan analyse src --level max

CircleCI

Now that we have our checks in place, it is time to tell CircleCI what we want to run each build. For this we have to create a config file called “config.yml” put this file in the folder “.circle”.

Our initial config looks as follows:

version: 2

jobs:
  build:
    docker:
      - image: circleci/php:7.4-node-browsers
        environment:
          MYSQL_HOST: 127.0.0.1
          MYSQL_DB: symfony
          MYSQL_USER: root
          MYSQL_ALLOW_EMPTY_PASSWORD: true
          MYSQL_PASSWORD:
      - image: mysql:5.7
        command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --innodb-large-prefix=true --innodb-file-format=Barracuda
        environment:
          MYSQL_USER: root
          MYSQL_ALLOW_EMPTY_PASSWORD: true
    working_directory: ~/symfony # directory where steps will run
    steps: # a set of executable commands
      - checkout # special step to check out source code to working directory
      - run: sudo apt update
      - run: sudo apt install -y libsqlite3-dev zlib1g-dev mariadb-client zlib1g-dev
      - run: sudo docker-php-ext-install zip pdo_mysql
      - run: sudo docker-php-ext-enable zip pdo_mysql
      - run: sudo composer self-update
      - restore_cache: # special step to restore the dependency cache if `composer.lock` does not change
          keys:
            - composer-v1-{{ checksum "composer.lock" }}
            # fallback to using the latest cache if no exact match is found (See https://circleci.com/docs/2.0/caching/)
            - composer-v1-
      - run: composer install -n --prefer-dist --no-scripts
      - save_cache: # special step to save the dependency cache with the `composer.lock` cache key template
          key: composer-v1-{{ checksum "composer.lock" }}
          paths:
            - vendor
      - restore_cache: # special step to restore the dependency cache if `package.json` does not change
          keys:
            - node-v1-{{ checksum "package.json" }}
            # fallback to using the latest cache if no exact match is found (See https://circleci.com/docs/2.0/caching/)
            - node-v1-
      - run: cp .env .env.local
      - run: yarn install
      - save_cache: # special step to save the dependency cache with the `package.json` cache key template
          key: node-v1-{{ checksum "package.json" }}
          paths:
            - node_modules
      - run: yarn run encore production
      - run: php bin/console security:check
      - run: curl -L https://cs.symfony.com/download/php-cs-fixer-v2.phar -o php-cs-fixer
      - run: chmod a+x php-cs-fixer
      - run: ./php-cs-fixer --diff --dry-run -v --allow-risky=yes fix
      - run: php -d memory_limit=-1 vendor/bin/phpstan analyse src --level max
      - run: php -d memory_limit=-1 vendor/bin/simple-phpunit

workflows:
  version: 2
  notify_deploy:
    jobs:
      - build

The config

A big part of the config is taken from the CircleCI sample config for a PHP project found here. Let’s go through it step by step:

  • In the docker step, you can specify which containers you need for your build. For this example, we need a PHP + MySQL container. If you need, for example, a Redis container you can specify it here.
  • After the docker step, we will pull in a few packages and enable some PHP extension that we need, if you need more modules, this is where you add them.
  • The next step is composer, this will recover the composer cache and optionally install composer packages and write it to the cache
  • After Composer, recover the npm cache and optionally install and write it to the cache. This config assumes you use Yarn & Webpack encore, you can adjust this to your needs and change it to npm or remove it altogether.

The next few steps make up our “test pipeline”, this is where we perform our tests and determine if our build succeeds. Try to run your scripts from fastest to slowest to avoid wasting minutes of failing builds.

  • First, we will check for known vulnerabilities with sensiolabs/security-checker (installed with Composer as dev dependency). This command checks for known vulnerabilities in your dependencies.
  • Second, we will download PHP-CS-Fixer and run it to detect code style issues.
  • Third, run PHPStan for static code analysis.
  • And at last, run your test suite using PHPUnit. Make sure to include the symfony/phpunit-bridge as a dev dependency.

If one of the steps fail, the build in CircleCI will fail and we will be notified.

Project Set Up

Make sure you pushed your project to your git repository and head over to the CircleCI website to set-up your project. You should already have an account linked to your git host. Go to “Projects” and press “Set Up Project”. Then choose “Use Existing Config” and acknowledge with “Start Building”.

Your first build should be running wait until it succeeds or fails. If it failed fix your errors and try again!

Great, you now have an automated build pipeline makes reviewing those PRs a lot easier. Next, we will add the deployment script using Deployer.

Deployment with Deployer

For deployment to our (test) server, we will use Deployer, this is an easy to use tool to automate your deployment process. Install it with one of the methods specified in the documentation.

Then add the following deploy script to the root of your project. This config is based at the standard Symfony4 recipe (at the time of writing the Symfony 5 recipe is not yet available).

<?php
// deploy.php
declare(strict_types=1);

namespace Deployer;

set('default_stage', 'test');

require 'recipe/symfony4.php';

// Project name
set('application', 'Sample app');

// Project repository
set('repository', '[email protected]:jeroendk/symfony-build-and-deploy.git');

// Set composer options
set('composer_options', '{{composer_action}} --verbose --prefer-dist --no-progress --no-interaction --optimize-autoloader --no-scripts');

// shared files & folders
add('shared_files', ['.env.local']);
add('shared_dirs', ['public/upload']);

// Hosts
host('test')
    ->hostname('localhost')
    ->user('jeroen')
    ->port(22)
    ->stage('test')
    ->set('branch', 'develop')
    ->set('deploy_path', '~/deploy-folder')
;

// Tasks
task('pwd', function (): void {
    $result = run('pwd');
    writeln("Current dir: {$result}");
});

// [Optional]  Migrate database before symlink new release.
// before('deploy:symlink', 'database:migrate');

// Build yarn locally
task('deploy:build:assets', function (): void {
    run('yarn install');
    run('yarn encore production');
})->local()->desc('Install front-end assets');

before('deploy:symlink', 'deploy:build:assets');

// Upload assets
task('upload:assets', function (): void {
    upload(__DIR__.'/public/build/', '{{release_path}}/public/build');
});

after('deploy:build:assets', 'upload:assets');

// [Optional] if deploy fails automatically unlock.
after('deploy:failed', 'deploy:unlock');

With the above, we defined one host to deploy to, the test environment in this case my Localhost. You can test your connection with the pwd task, run dep pwd and it should output the server directory.

Start deploying with dep deploy. If you configured ssh keys for you remote server these will be used. If not you will be prompted for the password. After which the result should look as follows:

When you know your deployment script works, we can leverage it in CircleCI to automatically deploy to your test server.

Continues Deployment to your (test) server

To make this happen you should be able to sign in using ssh keys, you can’t fill in your password in the CircleCI builds. Make sure you are able to login to your server with ssh keys and then upload your private key to CircleCI. Check these docs for more information on adding an ssh key to CircleCI.

The next step is to add another entry under the “jobs” key in the deploy config. And also add this deploy step to the workflow at the bottom of the file.

# .circleci/config.yml

...
deploy:
    docker:
      - image: circleci/php:7.4-browsers
    working_directory: ~/symfony
    steps:
      - checkout
      - add_ssh_keys:
          fingerprints:
            - "f1:xx:xx" # SSH Key Fingerprint found in CircleCI
      - run:
          name: Install Deployer
          command: |
            curl -LO https://deployer.org/deployer.phar
            sudo mv deployer.phar /usr/local/bin/dep
            sudo chmod +x /usr/local/bin/dep
      - run:
          name: Deploy
          command: | # Add our test server as "known host"
            echo '|1|eqoArfioX6VlupKTFQ6vdavJgZk=|0iGC0C/E1U5oPZGuO1Xd0Gux8oU= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==' >> ~/.ssh/known_hosts
            dep deploy

workflows:
  version: 2
  notify_deploy:
    jobs:
      - build
      - deploy:
          requires:
            - build
          filters:
            branches:
              only: develop # Only deploy builds on th develop branch

Few things to notice here:

  • Make sure to add your SSH Key fingerprint, you can grab this from CircleCI in your project settings.
  • Add your target deployment server as a known host, you can run the command ssh-keyscan -H yourserver.com locally and use the output in your deployment script. If the output contains multiple records, you can add more inserts above.
  • We will only deploy changes on our develop branch and only after the build job succeeds.

Conclusion

With the configuration above each of your commits will automatically get checked which lets you focus on the right stuff. All of your commits and merges in the develop branch will be automatically deployed to your test server ready to be reviewed by your product owner or your QA team.

A full example can be found here: https://github.com/jeroendk/symfony-build-and-deploy