The importance of coding standard.
8 minutes
Over the years there has been plenty of debates around coding standards. People have argued over things like tabs vs spaces, line lengths and other aspects when it comes to writing code. Groups have been setup to help define these standards in the PHP world we have the PHP-FIG who propose standards for us to use.
One thing we have noticed becoming more and more apparent is that developers are now starting to listen and think more about this, not just about standards like PSR-1, PSR-2, PSR-12 and now PER Coding Style 2.0, but also with the evolutions of the type system in PHP people are starting to develop more robust applications by using these types and writing tests (finally).
New tools are being developed that make applying standards to your code and fixing inconsistencies a lot easier, there are also tools that have been out there in the wild for a long time and are now being promoted and talked about again which is great to see.
How standards help us deliver better applications
Jump24 started out with a single developer, back then PHP standards weren’t top of the list when writing code. As the company expanded and more developers came on board the lack of standards in our PHP applications became more apparent. Each developer would write and push up code with method names either in camel case or snake case; variables declared much the same. Line lengths were sometimes very long and unreadable in Pull Requests and line endings where different as well.
With all these various issues spread across multiple projects, we found that Pull Requests were taking longer than expected to go through as each developer looking at it would leave varying comments based on what they thought the standards should be.
The solution
Once we identified the problem and how much of a bottleneck it was causing the team, we sat down and planned how to tackle the problem.
We talked about how we would go about it from both a frontend and backend perspective. The standards we should start adhering to and looked at the tools we could use that would help us going forward to minimise the amount of this type of feedback that would appear on PRs.
Each programming language has differing standards; PHP has a number of these PHP Standard Recommendations three of them outline how you should write code in particular. We decided that we should be adhering to these standards. We started with PSR1 and PSR2, then when PSR12 was approved, we added that to our code requirements.
We found it a lot easier to adhere to these standards when working on new projects as we were writing the code from the start, but when we were tackling old codebases that we’d taken on this wasn’t as straightforward. To this end, we decided that every time we had to work on one of these projects that the new code developed would be standard compliant and any code we’d change around it would be updated to be compliant. So slowly each project over time would evolve to match our requirements, we took this approach instead of merely updating the whole project in one go as the Pull Request would be too big and getting this signed off would take quite a bit of time.
The tools and packages we use to keep these standards at bay
Now that we’d decided on the standards we were going to use when working with PHP we had to figure out the best way to help us maintain these. We didn’t want to 100% rely on our developers having to remember every part of each standard. We were already using PHPStorm as our IDE our first thought was how we could set this up to let us know when we were breaking these rules hopefully stopping us even committing this kind of code in the first place.
Firstly we looked at what was available for the IDE that would help us achieve the goal. We found an excellent plugin called PHP Inspections (EA Extended). This plugin is an open-source Static Code Analyser, after installing this plugin, we were instantly made aware of several issues that we were able to solve quickly, so the plugin was already doing a great job.
We then looked at other tools we could use with composer and how we could implement them into our development lifecycle.
We finally settled on the following packages that we’ve started including in every build that we have be it a new project or working on an old project.
Due to always requiring the above packages in our projects we decided to create our own packages which encapsulates all of the required items so now we simply install the following two packages to gain access to ECS, Slevomat, PHPStan and Larastan.
Jump24 Laravel Coding Standards - only for Laravel based projects
Jump24 PHP Coding Standards - For non Laravel based projects
To get these tools up and running in our projects firstly we would install these as dependencies in the project then there are a couple of files we need to add to our codebase to handle the configuration for the tools doing our checks. The two main files we have are phpstan.neon
for PHPStan/Larastan to configure our static analysis and ecs.php
for Easy Coding Standard.
Why easy coding standard
We made the decision to adopt Easy Coding Standard (ECS) by Tomas Votruba as the foundation for all of our coding standard rules. We chose ECS because we prefer writing configuration rules in PHP that are easily understood by PHPStorm, as opposed to XML.
A typical ecs.php
that can be found in our projects
1<?php 2 3use JumpTwentyFour\PhpCodingStandards\Support\ConfigHelper; 4use PHP_CodeSniffer\Standards\Generic\Sniffs\NamingConventions\CamelCapsFunctionNameSniff; 5use SlevomatCodingStandard\Sniffs\Functions\UnusedParameterSniff; 6use Symplify\EasyCodingStandard\Config\ECSConfig; 7use Symplify\EasyCodingStandard\ValueObject\Option; 8 9return static function (ECSConfig $ecsConfig): void {10 $ecsConfig->import(__DIR__.'/vendor/jumptwentyfour/laravel-coding-standards/ecs.php');11 12 $parameters = $ecsConfig->parameters();13 14 $parameters->set(Option::PATHS, [15 __DIR__.'/app',16 __DIR__.'/config',17 __DIR__.'/database',18 __DIR__.'/routes',19 __DIR__.'/tests',20 ]);21 22 $parameters->set(Option::SKIP, array_merge(ConfigHelper::make($ecsConfig)->getParameter(Option::SKIP), [23 'Unused parameter $request.' => [24 __DIR__.'/app/Http/Middleware',25 __DIR__.'/app/Http/Resources',26 __DIR__.'/app/Domain/*/Resources/*.php',27 __DIR__.'/app/Http/Controllers/*.php',28 ],29 ]));30};
As you can see from the example above we import the base jump24 standards and then tell ECS which folders to look at when reviewing the code. You can then overwrite any particular changes that you might need on a per project basis. Sometimes we need to do this, particularly if we are bringing ECS into an older project where we will need to improve the standards over time. We did have to make a change to our package to build out a new ConfigHelper
to grab all the skips we defined in our package and array merge them with individual skips from the specific project we are in.
Using ECS also allows us to define our own company standards as an external open source package that can be brought into our projects and extended as needed. Furthermore, since ECS is built on top of PHPCS, we don’t lose any of the original capabilities, this includes the fixer which is now centralised under a single command passing an additional argument --fix
. With ECS, we have the added benefit of being able to write rules as PHP code and access numerous predefined sets, such as PSR12 and Clean Code, which can simplify the base configuration of the standards file. They also provide a quick guide away to migrate from PHPCS How to Migrate From PHP_CodeSniffer to ECS in 7 Steps
Tomas also actively maintains RectorPHP and has recently become more involved with the Laravel community as a whole.
Looking to start your next Laravel project?
We’re a passionate, happy team and we care about creating top-quality web applications that stand out from the crowd. Let our skilled development team work with you to bring your ideas to life! Get in touch today and let’s get started!
Running ECS
To remove the dependency of remembering the syntax of running coding standard commands, we alias them as composer commands in the composer.json
file. Running vendor/bin/ecs check
instead becomes composer ecs
and running vendor/bin/ecs check --fix
becomes composer ecs-fix
.
An example of a typical scripts section of a composer.json
file in our projects.
1"scripts": { 2 "stan": [ 3 "vendor/bin/phpstan analyse --memory-limit=4G" 4 ], 5 "test:parallel": [ 6 "Composer\\Config::disableProcessTimeout", 7 "php artisan cache:clear", 8 "php artisan test --parallel" 9 ],10 "ecs": [11 "vendor/bin/ecs check --debug"12 ],13 "ecs-fix": [14 "vendor/bin/ecs check --fix"15 ],16 },
Running the standards in a CI Pipeline
The majority of the projects we have run GitHub actions on Pull Requests, we have specific steps in our actions that run our standards as well as tests.
Github Action
1# GitHub Action for Laravel with MySQL and Redis 2name: Testing Laravel with MySQL 3on: 4 pull_request: 5 branches: [ main, release/**, feature/** ] 6 types: 7 - opened 8 - reopened 9 - synchronize 10 - ready_for_review 11jobs: 12 build: 13 name: Build 14 runs-on: ubuntu-latest 15 steps: 16 - name: Checkout 17 uses: actions/checkout@v3 18 19 - name: Setup PHP 20 uses: shivammathur/setup-php@v2 21 with: 22 php-version: '8.2' 23 extensions: bcmath, ctype, fileinfo, gd, json, intl, opcache, mbstring, openssl, pdo_mysql, tokenizer, xml, zip 24 coverage: xdebug 25 26 - name: Setup Node.js 27 uses: actions/setup-node@v1 28 with: 29 node-version: '15.x' 30 31 - uses: actions/cache@v3 32 id: npm-cache 33 with: 34 path: node_modules 35 key: node-modules-${{ hashFiles('package-lock.json') }} 36 37 - name: Install JavaScript Dependencies 38 if: steps.npm-cache.outputs.cache-hit != 'true' 39 run: npm ci 40 41 phpstan: 42 name: PHPStan 43 needs: build 44 runs-on: ubuntu-latest 45 steps: 46 - name: Setup PHP 47 uses: shivammathur/setup-php@v2 48 with: 49 php-version: '8.2' 50 extensions: bcmath, ctype, fileinfo, gd, json, intl, opcache, mbstring, openssl, pdo_mysql, tokenizer, xml, zip 51 coverage: xdebug 52 53 - uses: actions/checkout@v3 54 - uses: ramsey/composer-install@v2 55 56 - name: Copy .env.pipeline file to .env 57 run: php -r "file_exists('.env') || copy('.env.pipeline', '.env');" 58 59 - name: PHPStan (Static Analysis Tool) 60 run: composer run stan 61 62 ecs: 63 name: ECS 64 needs: build 65 runs-on: ubuntu-latest 66 steps: 67 - name: Setup PHP 68 uses: shivammathur/setup-php@v2 69 with: 70 php-version: '8.2' 71 extensions: bcmath, ctype, fileinfo, gd, json, intl, opcache, mbstring, openssl, pdo_mysql, tokenizer, xml, zip 72 coverage: xdebug 73 - uses: actions/checkout@v3 74 - uses: ramsey/composer-install@v2 75 76 - name: Copy .env.pipeline file to .env 77 run: php -r "file_exists('.env') || copy('.env.pipeline', '.env');" 78 79 - name: PHP ECS (Easy Coding Standard) 80 run: composer run ecs 81 82 test-php: 83 name: Test PHP 84 needs: build 85 runs-on: ubuntu-latest 86 services: 87 mysql: 88 image: mysql:8.0 89 env: 90 MYSQL_ALLOW_EMPTY_PASSWORD: false 91 MYSQL_ROOT_PASSWORD: root 92 MYSQL_DATABASE: testing_db 93 ports: 94 - 3306/tcp 95 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 96 redis: 97 image: redis 98 ports: 99 - 6379/tcp100 options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3101 strategy:102 fail-fast: false103 matrix:104 php-versions: ['8.2']105 env:106 DB_HOST: 127.0.0.1107 BROADCAST_DRIVER: log108 CACHE_DRIVER: file109 QUEUE_CONNECTION: sync110 SESSION_DRIVER: file111 steps:112 - name: Setup PHP113 uses: shivammathur/setup-php@v2114 with:115 php-version: '8.2'116 extensions: bcmath, ctype, fileinfo, gd, json, intl, opcache, mbstring, openssl, pdo_mysql, tokenizer, xml, zip117 coverage: xdebug118119 - uses: actions/checkout@v3120 - uses: ramsey/composer-install@v2121 - uses: actions/cache@v3122 with:123 path: node_modules124 key: node-modules-${{ hashFiles('package-lock.json') }}125126 - name: Start mysql service127 run: sudo /etc/init.d/mysql start128129 - name: Copy .env.pipeline file to .env130 run: php -r "file_exists('.env') || copy('.env.pipeline', '.env');"131132 - name: Build CSS/JavaScript Files133 run: npm run build134135 - name: Run tests with PHPUnit136 run: composer run test:parallel137 env:138 REDIS_HOST: redis139 DB_PORT: ${{ job.services.mysql.ports['3306'] }}140 REDIS_PORT: ${{ job.services.redis.ports['6379'] }}
Verdict
After all the work we put into getting this far what do we think? Firstly we feel it was the right decision to make as a team. The PR feedback is now just about the code itself and recommendations on improving the design, no more comments on indentation, old use statements hanging around or even variables being declared and not being used.
We think spending time doing this was beneficial as a company but also as individual developers as we now know a lot more about the PHP Standards in place and are writing documented code that is much cleaner to read, we’re now writing more consistent and readable code as a team