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