no-framework-tutorial/04-development-helpers.md

14 KiB

<< previous | next >>

Development Helpers

I have added some more helpers to my composer.json that help me with development. As these are scripts and programms used only for development they should not be used in a production environment. Composer has a specific sections in its file called "dev-dependencies", everything that is required in this section does not get installen in production.

Let's install our dev-helpers and i will explain them one by one: composer require --dev phpstan/phpstan symfony/var-dumper slevomat/coding-standard symplify/easy-coding-standard

Static Code Analysis with phpstan

Phpstan is a great little tool, that tries to understand your code and checks if you are making any grave mistakes or create bad defined interfaces and structures. It also helps in finding logic-errors, dead code, access to array elements that are not (or not always) available, if-statements that always are true and a lot of other stuff.

A very simple example would be a small functions that takes a DateTime-Object and prints it in a human readable format.

/**
 * @param \DateTime $date
 * @return void
 */
function printDate($date) {
    $date->format('Y-m-d H:i:s');
}

printDate('now');

if we run phpstan with the command ./vendor/bin/phpstan analyse --level 9 ./src/

It firstly tells us that calling "format" on a DateTime-Object without outputting or returning the function result has no use, and secondly, that we are calling the function with a string instead of a datetime object.

1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

 ------ --------------------------------------------------------------------------------------------- 
Line   Bootstrap.php
 ------ --------------------------------------------------------------------------------------------- 
30     Call to method DateTime::format() on a separate line has no effect.                          
33     Parameter #1 $date of function Lubian\NoFramework\printDate expects DateTime, string given.
 ------ --------------------------------------------------------------------------------------------- 

The second error is something that "declare strict-types" already catches for us, but the first error is something that we usually would not discover easily without speccially looking for this errortype.

We can add a simple configfile called phpstan.neon to our project so that we do not have to specify the errorlevel and path everytime we want to check our code for errors:

parameters:
    level: max
    paths:
        - src

now we can just call ./vendor/bin/phpstan analyze and have the same setting for every developer working in our project

With this settings we have already a great setup to catch some errors before we execute the code, but it still allows us some silly things, therefore we want to add install some packages that enforce rules that are a little bit more strict.

composer require --dev phpstan/extension-installer
composer require --dev phpstan/phpstan-strict-rules thecodingmachine/phpstan-strict-rules

During the first install you need to allow the extension installer to actually install the extension. The second command installs some more strict rulesets and activates them in phpstan.

If we now rerun phpstan it already tells us about some errors we have made:

 ------ ----------------------------------------------------------------------------------------------- 
Line   Bootstrap.php
 ------ ----------------------------------------------------------------------------------------------- 
10     Short ternary operator is not allowed. Use null coalesce operator if applicable or consider    
       using long ternary.                                                                            
25     Do not throw the \Exception base class. Instead, extend the \Exception base class. More info:  
       http://bit.ly/subtypeexception                                                                 
26     Unreachable statement - code above always terminates.
 ------ ----------------------------------------------------------------------------------------------- 

The last two Errors are caused by the Exception we have used to test the ErrorHandler in the last chapter if we remove that we should be able to fix that. The first error is something we could fix, but I dont want to focus on that specific problem right now. Phpstan gives us the option to ignore some errors and handle them later. If for example we are working on an old legacy codebase and wanted to add static analysis to it but cant because we would get 1 Million error messages everytime we use phpstan, we could add all those errors to a list and tell phpstan to only bother us about new errors we are adding to our code.

In order to use that we have to add an empty file phpstan-baseline.neon to our project, include that in the phpstan.neon file and run phpstan with the --generate-baseline option:

includes:
    - phpstan-baseline.neon

parameters:
    level: max
    paths:
        - src
[vagrant@archlinux app]$ ./vendor/bin/phpstan analyze --generate-baseline
Note: Using configuration file /home/vagrant/app/phpstan.neon.
 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%


                                                                                                                        
 [OK] Baseline generated with 1 error.                                                                                  
                                                                                                                        

you can read more about the possible parameters and usage options in the documentation

Easy-Coding-Standard

There are two great tools that help us with applying a consistent coding style to our project as well as check and automatically fix some other errors and oversights that we might not bother with when writing our code.

The first one is PHP Coding Standards Fixer which can automatically detect violations of a defined coding standard and fix them. The second tool is PHP CodeSniffer which basically does the same has in my experience some more Rules available that we can apply to our code.

But we are going to use neither of those tools directly and instead choose the Easy Coding Standard which allows us to combine rules from both mentioned tools, and also claims to run much faster. You could check out the documentation and decide on your own coding standard. Or use the one provided by me, which is base on PSR-12 but adds some highly opiniated options. First create a file 'ecs.php' and either add your own configuration or copy the my prepared one:

<?php declare(strict_types=1);

use PhpCsFixer\Fixer\Import\OrderedImportsFixer;
use PhpCsFixer\Fixer\PhpTag\BlankLineAfterOpeningTagFixer;
use SlevomatCodingStandard\Sniffs\Classes\ClassConstantVisibilitySniff;
use SlevomatCodingStandard\Sniffs\Namespaces\AlphabeticallySortedUsesSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\DisallowGroupUseSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\MultipleUsesPerLineSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\NamespaceSpacingSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\ReferenceUsedNamesOnlySniff;
use SlevomatCodingStandard\Sniffs\Namespaces\UseSpacingSniff;
use SlevomatCodingStandard\Sniffs\TypeHints\DeclareStrictTypesSniff;
use SlevomatCodingStandard\Sniffs\TypeHints\UnionTypeHintFormatSniff;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symplify\EasyCodingStandard\ValueObject\Option;
use Symplify\EasyCodingStandard\ValueObject\Set\SetList;

return static function (ContainerConfigurator $containerConfigurator): void {
    $parameters = $containerConfigurator->parameters();
    $parameters->set(Option::PATHS, [__DIR__ . '/src', __DIR__ . '/ecs.php']);
    $parameters->set(Option::PARALLEL, true);
    $parameters->set(Option::SKIP, [BlankLineAfterOpeningTagFixer::class, OrderedImportsFixer::class]);

    $containerConfigurator->import(SetList::PSR_12);
    $containerConfigurator->import(SetList::STRICT);
    $containerConfigurator->import(SetList::ARRAY);
    $containerConfigurator->import(SetList::SPACES);
    $containerConfigurator->import(SetList::DOCBLOCK);
    $containerConfigurator->import(SetList::CLEAN_CODE);
    $containerConfigurator->import(SetList::COMMON);
    $containerConfigurator->import(SetList::COMMENTS);
    $containerConfigurator->import(SetList::NAMESPACES);
    $containerConfigurator->import(SetList::SYMPLIFY);
    $containerConfigurator->import(SetList::CONTROL_STRUCTURES);

    $services = $containerConfigurator->services();

    // force visibitily declaration on class constants
    $services->set(ClassConstantVisibilitySniff::class)
        ->property('fixable', true);

    // sort all use statements
    $services->set(AlphabeticallySortedUsesSniff::class);

    $services->set(DisallowGroupUseSniff::class);
    $services->set(MultipleUsesPerLineSniff::class);
    $services->set(NamespaceSpacingSniff::class);

    // import all namespaces, and event php core functions and classes
    $services->set(ReferenceUsedNamesOnlySniff::class)
        ->property('allowFallbackGlobalConstants', false)
        ->property('allowFallbackGlobalFunctions', false)
        ->property('allowFullyQualifiedGlobalClasses', false)
        ->property('allowFullyQualifiedGlobalConstants', false)
        ->property('allowFullyQualifiedGlobalFunctions', false)
        ->property('allowFullyQualifiedNameForCollidingClasses', true)
        ->property('allowFullyQualifiedNameForCollidingConstants', true)
        ->property('allowFullyQualifiedNameForCollidingFunctions', true)
        ->property('searchAnnotations', true)
        ->property('fixable', true);

    // define newlines between use statements
    $services->set(UseSpacingSniff::class)
        ->property('linesCountBeforeFirstUse', 1)
        ->property('linesCountBetweenUseTypes', 1)
        ->property('linesCountAfterLastUse', 1);

    // strict types declaration should be on same line as opening tag
    $services->set(DeclareStrictTypesSniff::class)
        ->property('declareOnFirstLine', true)
        ->property('spacesCountAroundEqualsSign', 0);

    // disallow ?Foo typehint in favor of Foo|null
    $services->set(UnionTypeHintFormatSniff::class)
        ->property('withSpaces', 'no')
        ->property('shortNullable', 'no')
        ->property('nullPosition', 'last');
};

You can now use ./vendor/bin/ecs to list all violations of the defined standard and ./vendor/bin/ecs --fix to automatically fix them.

PHP Codesniffer

The PHPCodesniffer is sort of a combination of the previous tools, it checks for a defined codingstyle and some extra rules that are not just stylechanges but instead enforces extra rules in if-statements, exception handling etc.

it provides the phpcs command to check for violations and the phpcbf command to actually fix most of the violations.

Without configuration the tool tries to apply the PSR12 standard just like the php-cs-fixer, but as you might have guessed we are adding some extra rules.

Lets install the ruleset with composer

composer require --dev mnapoli/hard-mode

and add a configuration file to actually use it .phpcs.xml.dist

<?xml version="1.0"?>
<ruleset>
    <arg name="basepath" value="."/>

    <file>src</file>

    <rule ref="HardMode"/>
</ruleset>

running ./vendor/bin/phpcs now checks our src directory for violations and gives us a detailed list about the findings.

[vagrant@archlinux app]$ ./vendor/bin/phpcs

FILE: src/Bootstrap.php
----------------------------------------------------------------------------------------------------
FOUND 4 ERRORS AFFECTING 4 LINES
----------------------------------------------------------------------------------------------------
  7 | ERROR | [x] Use statements should be sorted alphabetically. The first wrong one is Throwable.
  8 | ERROR | [x] Expected 1 lines between different types of use statement, found 0.
 11 | ERROR | [x] Expected 1 lines between different types of use statement, found 0.
 24 | ERROR | [x] String "ERROR: " does not require double quotes; use single quotes instead
----------------------------------------------------------------------------------------------------
PHPCBF CAN FIX THE 4 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------------------------------------

Time: 639ms; Memory: 10MB

You can then use ./vendor/bin/phpcbf to try to fix them

Symfony Var-Dumper

another great tool for some quick debugging without xdebug is the symfony var-dumper. This just gives us some small functions.

dump(); is basically like phps var_dump() but has a better looking output that helps when looking into bigger objects or arrays.

dd() on the other hand is a function that dumps its parameters and then exits the php-script.

you could just write dd($whoops) somewhere in your bootstrap.php to check how the output looks.

Composer scripts

now we have a few commands that are available on the command line. I personally do not like to type complex commands with lots of parameters by hand all the time, so I added a few lines to my composer.json:

"scripts": {
    "serve": [
      "Composer\\Config::disableProcessTimeout",
      "php -S 0.0.0.0:1235 -t public"       
    ],
    "phpstan": "./vendor/bin/phpstan analyze",
    "baseline": "./vendor/bin/phpstan analyze --generate-baseline",
    "check": "./vendor/bin/phpcs",
    "fix": "./vendor/bin/php-cs-fixer fix && ./vendor/bin/phpcbf"
},

that way I can just type "composer" followed by the command name in the root of my project. if i want to start the php devserver I can just type "composer serve" and dont have to type in the hostname, port and targetdirectory all the time.

You could also configure PhpStorm to automatically run these commands in the background and highlight the violations directly in the file you are currently editing. I personally am not a fan of this approach because it often disrupts my flow when programming and always forces me to be absolutely strict even if I am only trying out an idea for debugging.

My workflow is to just write my code the way i currently feel and that execute the phpstan and the fix scripts before commiting and pushing the code.

<< previous | next >>