loading...
Cover image for πŸš₯ How to Write Clean Code in PHP 🐘

πŸš₯ How to Write Clean Code in PHP 🐘

biros profile image Boris Jamot ✊ / Originally published at mamyn0va.github.io ・Updated on ・9 min read

This is the first part of a 2-parts' post about code quality.
This is mainly about PHP, but wait, don't run way!, you may still be interested if you code in some Β«serious languageΒ».


good code vs bad code

Why should we care about code quality?

Why not just make our code work for what it is supposed to? In fact, it depends on the purpose of your app and on your target. If you're developing a throwaway app, for a demo, or just for you, you won't obviously spend your time on quality. But if it's something more serious, it will become mandatory.

Mandatory for the readibility of your code, mandatory for the maintainability, mandatory for your health and that of your teammates.

For the most impatient of you, I created a template project on gitlab.com to allow you to quickly bootstrap a PHP project with ready-to-use quality tools.


Oh wait! You're talking about quality? Really? In PHP?

PHP has a bad reputation, probably because it's a very permissive language used by unexperimented Β«developersΒ». Its dynamically type mechanism is often misused and before the version 5 and the introduction of the OOP paradigm, it was very messy. On top of that, most of the applications developed in PHP are just Web sites, blogs, forums or any other CMS like drupal, wordpress, joomla, prestashop...

Today, things are very different in PHP ecosystem. Its dependency manager, composer, has been massively adopted by most of the project maintainers and helped to make PHP a credible choice for serious backend applications.
Moreover, PHP7 has brought a breaking change in PHP's performance by enhancing the opcode cache and the Zend Engine. And now, there is the possibility to type functions' parameters and return value. Combined with the strict types mode, a lot of hidden runtime bugs are now detected in your IDE.


I won't cover here all the aspects of the code quality (tests, review, TDD, ...) but I'll focus on the first very basics things: coding standards/styles & static analysis.

Let it be clear that, of course, tooling has never made by itself the quality of any software, but they help to. Let's say they put us in the mood to continually seek to improve our code. But that does not prevent us from doing unit tests and code review!

βœ”οΈ Code static analysis tools

The PHP-FIG​ consortium publishes a set of recommendations that aims to standardize the use of PHP and to facilitate its interoperability. Three of them focus on the coding style and on stuff like the auto-loading, the encoding, and so on:

The tool listed below are widely adopted by the PHP community and are actively supported/maintained.

PHP_CodeSniffer (aka phpcs)

Fortunately, we don't need to know all the details of the PSR specifications (even if it would be better), because some tools can do (a part of) the job for us. I mean, pretty-printing the code to make it compliant with the PSRs and detect violations. And as time goes on and with experience, it will become natural for you.

PHP_CodeSniffer detects PSR1/PSR2 violations, and a lot more with the Squiz standard that check 130 rules (7 rules for PSR1 and 42 for PSR2) and that also works with CSS and JS. That being so, it is a good compromise to start with the PSR2 standard and then to create your own standard (aka ruleset) from the PSR2, and to enrich it with the Squiz rules that you need.

πŸ’‘ phpcs installation

This will add phpcs as a project dependency:

composer require "squizlabs/php_codesniffer"

To use it outside of a project, you should install it globally and add vendor/bin to your PATH:

composer global require "squizlabs/php_codesniffer"
export PATH=$PATH:~/path/to/composer/vendor/bin

πŸ†˜ phpcs usage

Now, consider the following PHP example that contains some violations...

<?php
class MaClasse{
    var $monAttribut;

    public function MaClasse ($monAttribut){
        $this->setMonAttribut( $monAttribut );
    }

    function setMonAttribut($monAttribut){
        $this->monAttribut=$monAttribut;
    }
}

...and see what happens if we use phpcs with the PSR2 standard:

phpcs psr2

12 errors are detected by the tool and 6 of them can be fixed automatically.

Let's see what happen if we re-run the tool with the Squiz standard:

phpcs squiz

28 errors are detected by the tool and 18 of them can be fixed automatically. My opinion about Squiz standard is that it is a little too verbose. Thus, what I did is that I created my own ruleset with all the PSR2 rules and added some of the Squiz rules that make sense to me.

To go further, you can have a look at the phpcs_psr2 and phpcs_squiz rulesets.

PHP Code Beautifier and Fixer (aka phpcbf)

Well, it's nice to detect violations, but it's even better if we can correct them automatically, right? Fortunately, PHP_CodeSniffer comes with another utility that fixes the main PSR1 and PSR2 violations: PHP Code Beautifier and Fixer (aka phpcbf). Just run it on a PHP file or on a code base, and it will scan all the files and try to automatically fix the violations:

phpcbf psr2

When using it on MaClasse.php with PSR2 standard, it fixes 6 out of 12 violations:

<?php

class MaClasse
{
    var $monAttribut;

    public function MaClasse($monAttribut)
    {
        $this->setMonAttribut($monAttribut);
    }

    function setMonAttribut($monAttribut)
    {
        $this->monAttribut=$monAttribut;
    }
}

Now it's your turn to play around with it and create your own ruleset that fits your needs, your teammates' need and your project needs. Of course, this MUST be shared with the other devs to have an homogenous codebase and to be able to work together.

PHP Mess Detector (aka phpmd)

In the same way as Java and its PMD, PHP also has its own mess detection tool. Through a rulesets mechanism (like PHP_CodeSniffer), this tool will detect many violations towards the clean code philosophy:

  • possible bugs
  • suboptimal code
  • overcomplicated expressions
  • unused parameters, methods, properties
  • ...

πŸ’‘ phpmd installation

composer require phpmd/phpmd

Then, you need to provide phpmd with a file or directory, and to specify an output format (text, XML or HTML) and one or more rulesets.

πŸ†˜ phpmd usage

Let's take the previously fixed example with phpcbf/PSR2, and add a unused variable in it:

<?php
class MaClasse
{
    var $monAttribut;

    public function MaClasse($monAttribut)
    {
        $this->setMonAttribut($monAttribut);
    }

    function setMonAttribut($monAttribut)
    {
        $test;
        $this->monAttribut=$monAttribut;
    }
}

Now, check it with phpmd and all available rulesets:

phpmd MaClasse.php text cleancode,codesize,controversial,design,naming,unusedcode

Output of phpmd:

MaClasse.php:6    The method MaClasse is not named in camelCase.
MaClasse.php:6    Classes should not have a constructor method with the same name as the class
MaClasse.php:12    Avoid unused local variables such as '$test'.

As you can see, the tool is complementary to PHP_CodeSniffer as it found three new interesting violations that weren't detected previously. Of course, if you run it through a full codebase, it will also raise a lot of more or less useful violations that may be noisy and prevent you to see meaningful information. Thus, as I said before for PHP_CodeSniffer, I suggest you to create your own ruleset based on existing ones. You need to create a file named phpmd.xml.dist at the root of your project and you'll be able to use it in the terminal, in your editor/IDE and in your pipeline.

Please find here the ruleset that uses all the existing rules.

But like for phpcs, I'd advise you to proceed iteratively by starting by the minimal ruleset unusedcode, and to gradually include new rules that make sense for you and your project.

Please find below a custom ruleset that you can use to start with phpmd:

<?xml version="1.0"?>
<ruleset name="Custom rule set used in pre-commit git hook"
         xmlns="http://pmd.sf.net/ruleset/1.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd"
         xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd">
    <description>
        Custom rule set used in pre-commit git hooks
    </description>

    <!-- Import the entire unused code rule set -->
    <rule ref="rulesets/unusedcode.xml" />
    <!-- Import a part of design code rule set -->
    <rule ref="rulesets/design.xml">
        <exclude name="NumberOfChildren" />
        <exclude name="DepthOfInheritance" />
        <exclude name="CouplingBetweenObjects" />
    </rule>
</ruleset>

PHP Coding Standards Fixer (aka php-cs-fixer)

What if we ended with another tool? If you had to choose only one tool, it would be this one.

It does nearly the same things as phpcbf, namely fix PSR rules, but it does it better and deeper. Like phpcbf, it also allow you to add some other rules from a list of 172 rules. The other cool thing about it is the creation of the ruleset file which is a PHP file and not a verbose XML.

πŸ’‘ php-cs-fixer installation

composer require friendsofphp/php-cs-fixer

πŸ†˜ php-cs-fixer usage

php-cs-fixer --verbose fix MaClasse.php
Loaded config default from "~/.php_cs.dist".
Using cache file ".php_cs.cache".
Paths from configuration file have been overridden by paths provided as command arguments.
F
Legend: ?-unknown, I-invalid file syntax, file ignored, S-Skipped, .-no changes, F-fixed, E-error
   1) MaClasse.php (class_attributes_separation, no_spaces_inside_parenthesis, declare_strict_types, blank_line_after_opening_tag, array_syntax, class_definition, 
function_declaration, visibility_required, declare_equal_normalize, binary_operator_spaces, braces)

Checked all files in 0.008 seconds, 8.000 MB memory used

Here is the output on MaClasse.php with a custom ruleset:

declare(strict_types = 1);
class MaClasse
{
    public $monAttribut = [];

    public function __construct($monAttribut)
    {
        $this->setMonAttribut($monAttribut);
    }

    public function setMonAttribut($monAttribut)
    {
        $this->monAttribut = $monAttribut;
        $test = 1;
    }
}

And here is my custom ruleset (.php_cs.dist) :

$finder = PhpCsFixer\Finder::create()
    ->exclude('vendor')
    ->exclude('output')
    ->exclude('releases')
    ->in(__DIR__)
;

return PhpCsFixer\Config::create()
    ->setRiskyAllowed(true)
    ->setRules([
        '@PSR2' => true,
        'declare_strict_types' => true,
        'align_multiline_comment' => ['comment_type' => 'phpdocs_only'],
        'binary_operator_spaces' => true,
        'blank_line_after_opening_tag' => true,
        'cast_spaces' => true,
        'class_attributes_separation' => true,
        'compact_nullable_typehint' => true,
        'concat_space' => ['spacing' => 'one'],
        'declare_equal_normalize' => ['space' => 'single'],
        'function_typehint_space' => true,
        'no_blank_lines_after_class_opening' => true,
        'no_blank_lines_after_phpdoc' => true,
        'no_extra_consecutive_blank_lines' => true,
        'no_mixed_echo_print' => true,
        'no_php4_constructor' => true,
        'no_unused_imports' => true,
        'no_useless_else' => true,
        'no_useless_return' => true,
        'no_whitespace_before_comma_in_array' => true,
        'no_whitespace_in_blank_line' => true,
        'object_operator_without_whitespace' => true,
        'ordered_class_elements' => true,
        'ordered_imports' => true,
        'phpdoc_align' => true,
        'phpdoc_indent' => true,
        'yoda_style' => true,
        'protected_to_private' => true,
        'return_type_declaration' => true,
        'self_accessor' => true,
        'single_blank_line_before_namespace' => true,
        'single_quote' => true,
        'ternary_operator_spaces' => true,
        'ternary_to_null_coalescing' => true,
        'whitespace_after_comma_in_array' => true,
        'array_syntax' => ['syntax' => 'short'],
    ])
    ->setFinder($finder)
;

Some of these rules are so called risky because they can impact the PHP's runtime, so use it carefully! On my project, I use two of them:

  • declare_strict_types (type mode is set to strict instead of weak)
  • no_php4_constructor (constructors MUST be named __construct)

You can disable this with the setter setRiskyAllowed(false).

The declare_strict_types rule is particularly interesting because it adds in every PHP files' header the following line: declare(strict_types = 1);

This PHP7 statement allows strictly typing function parameters and return type for scalar types (int, array, string, bool, ...). It means that if you provide a function with a string instead of an expected int, it will raise an error instead of casting the string to an int (e.g. "7 times" => 7).


Closing on the tooling, you must consider it as an integral part of your app, that is to say that one hand, they're listed in your project's dependencies, and on the other hand, their configuration files and rulesets are also part of your codebase:

php_repo

This is true for PHP, but also for any kind of language. This is the only way to have an homogeneous codebase over time within your team.

πŸ› οΈ To go further

  • phan : a powerful code static analyzer
  • phpstan : PHP Static Analyzis Tool
  • PHP metrics : (yet) another PHP static analyzer that outputs some user-friendly reports
  • PHP copy/paste detector (aka phpcpd) : a duplicate code detector
  • SonarPHP : a static PHP code analyzer for SonarQube and SonarLint
  • phploc : a tool that gives you statistics on your PHP project

🌟 Please feel free to ask me anything on this topic!

⏭️ In the next part, we'll be talking about integrating these tools in your project's lifecycle (git, editor/IDE, CI).


Useful links:

Discussion

pic
Editor guide
Collapse
mirakmalsulton profile image
akmal

thank you

Collapse
dunglv profile image
Dung

thanks so much

Collapse
biros profile image
Boris Jamot ✊ / Author

Thanks to you ☺️