Initializing your Symfony project with solid foundations

Published on 2022-06-11 • Modified on 2022-06-12

This post shows how to initialize a Symfony project with solid foundations. I give several bits of advice about this crucial step that determines how your project will evolve in the long run; will it stay maintainable and fun to work with? Or will it become the legacy project everyone tries to avoid? Let's go! 😎

» Published in "A week of Symfony 806" (6-12 June 2022).

Prerequisite

I will assume you have at least a basic knowledge of Symfony and know how to initialize a project with the Symfony CLI.

Introduction

The more I discover and work on different projects, the more I think the initialization phase of a project is crucial. When you think about it, it's so logical. This is like the antagonism between writing "quick and dirty code" and "clean code". Perhaps you can save some time initially, but the time loss can be tremendous in the end. I hope you will find interesting ideas to apply to your future projects.

Goal

We will pass in review several points I find essential. For each one, I'll give a concrete example of what I applied to this project or other ones. I'll try to update this blog post on each Symfony major and minor version.

Choosing the Symfony version

So, let's start at the beginning: the Symfony version, we have two choices. Should we start with the last LTS (Long Term Support), or should we start with the last minor version? You should always start with the last minor version. The choice wasn't clear in the past, as even Fabien advised using the LTS, but not anymore. Beginning with the last minor version has the following pros:

  • The migration from a minor version to the next one is straightforward and fast to do
  • You can enjoy new features! 🚀

As I write this article, the last minor version is 6.1; it was released on May 27, 2022. This is a "special" minor version; why? Because it has quite a significant BC change, we'll see it in the next chapter.

“Conclusion: Use Symfony 6.1. ”

Choosing the PHP version

We talked about the Symfony version but didn't talk about PHP. Symfony specifies the required PHP version in its composer file, let's have a look at https://github.com/symfony/symfony/blob/6.1/composer.json:

"require": {
    "php": ">=8.1",
    "composer-runtime-api": ">=2.1",
    "ext-xml": "*",

As we can see, Symfony 6.1 requires at least PHP 8.1. It's not "normal" as Symfony 6.0 requires PHP 8.0.

If you follow the "A week of Symfony" blog post, you probably read this one: "Symfony 6.1 will require PHP 8.1". Check out the blog post to understand why it is needed. Usually, there is no PHP version change in the lifetime of a Symfony major version, but that's an exception.

“Conclusion: Use PHP 8.1. ”

Creating and running the application

We have several options:

Docker less or hybrid setup

What I call a "docker less" setup is that the application is not served by Docker but by the Symfony CLI. To create a new app, run:

symfony new --webapp symfony61

And we have a working application; we can serve it by running:

cd symfony61
symfony serve --daemon

Then the application is available at https://127.0.0.1:8000 with the welcome screen. What I call a "hybrid" setup, is having services served with Docker (PostgreSQL, Redis, Elasticsearch...), but the Symfony CLI serves the application. The Symfony demo application is an excellent example of a "docker-less" setup; it uses an SQLite database that only requires the ext-pdo_sqlite PHP extension. I can use the Docker setup or the Symfony CLI for this project, but I almost always use the Symfony CLI as it is much faster to serve the pages.

Docker setup

This time the whole application is served by Docker. We also have several options:

  • Create your own Docker files and Docker Compose setup
  • Use an existing Docker starter kit

I will not list all Docker kits; there are many, but here are two popular ones:

On both these repositories, you will notice a Use this template button. That means you are going to create a new repository based on this repository's files. It's different from a fork, as you lose all commits history and start a new repository. That's the goal. I haven't tested the JoliCode one yet; I use dunglas/symfony-docker on multiple projects; it's easy to use and works well. Check out the GitHub homepage of these two kits to have more information.

No conclusion here. It depends on multiple factors. A setup with the Symfony CLI can be enough for small/personal projects, but a full Docker setup is generally required for a complex stack.

Static analysis

This is one of the most critical points of this article for me. I just cannot develop without this nowadays. Tools like PHPStan or Psalm are so great and a time saver. They can prevent from critical bugs in production. I like PHPStan, but some prefer Psalm (or another). My advice here is to choose one, don't use both simultaneously. Let's see how to install PHPStan:

composer require phpstan/phpstan --dev
composer require phpstan/extension-installer --dev
composer require phpstan/phpstan-symfony --dev

We install the Symfony plugin that allows more checks and the analysis of the main dependency injection container. Here is the configuration I use on the Strangebuzz project:

# configuration/phpstan.neon
includes:
    # require phpstan/extension-installer to avoid including these lines                                            PHPStan 1.x compat
    #- vendor/ekino/phpstan-banned-code/extension.neon     # https://github.com/ekino/phpstan-banned-code           ✅
    #- vendor/phpstan/phpstan-symfony/extension.neon       # https://github.com/phpstan/phpstan-symfony             ✅
    #- vendor/phpstan/phpstan-deprecation-rules/rules.neon # https://github.com/phpstan/phpstan-deprecation-rules   ✅
    #- vendor/phpstan/phpstan-strict-rules/rules.neon      # https://github.com/phpstan/phpstan-strict-rules        ✅
    #- vendor/phpstan/phpstan/phpstan-doctrine             # https://github.com/phpstan/phpstan-doctrine            ✅

# These are custom rules, check-out: https://www.strangebuzz.com/en/blog/creating-custom-phpstan-rules-for-your-symfony-project
rules:
    - App\PHPStan\ControllerIsFinalRule
    - App\PHPStan\ControllerExtendsSymfonyRule
    #- App\PHPStan\NoNewinControllerRule

parameters:
    # https://phpstan.org/config-reference#rule-level
    level: max # Max is level 9 as of PHPStan 1.0

    # https://phpstan.org/config-reference#analysed-files
    # Note that I have put my configuraiton file in the "./configuration" directory
    # if you have yours at the root of your project remove the "../"
    paths:
        - ../config
        - ../src
        - ../tests
        - ../public

    # https://github.com/phpstan/phpstan-symfony#configuration
    # Specific configuration for the Symfony plugin
    symfony:
        # I use the prod env because I have false positive regarding the tests which
        # are executed in the test environment.
        container_xml_path: ../var/cache/prod/App_KernelProdDebugContainer.xml

    # https://phpstan.org/config-reference#vague-typehints
    checkMissingIterableValueType:           true # https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
    checkGenericClassInNonGenericObjectType: true # this parameter is activated at level 6

    # Nothing ignored! (almost!) 🎉
    ignoreErrors:
        - '#Dead catch - Error is never thrown in the try block.#'
        - '#Variable method call#'
        - '#to IntlDateFormatter constructor#' # @see Snippet197Trait

    # I don't use the Symfony PHPUnit bridge in this project, but if you do, you
    # probably will have to add the following bootstrap file:
    #bootstrapFiles:
        #- %rootDir%/../../../vendor/bin/.phpunit/phpunit/vendor/autoload.php

Some explanations. For a new project, we can set the maximum level! This is great, because it's impossible to do this on legacy or older projects (I hear you: don't use a baseline!!). Generally, you start at level 0; you fix the warnings then you can pass to the next level, step by step (the most critical bugs are at the lower levels). I use several plugins, adding more checks. We can see the path to the dependency injection container XML file (it allows the ContainerInterface::get() function to have the correct return type, for example). I also have several custom rules, as explained in a previous article: Creating custom PHPStan rules for your Symfony project. Don't hesitate to have a look at it. 🙂

As I said before, this is one of the most critical points, because the quality difference between a code base without and with static code analysis at the maximum level is just huge. Really huge. ✨

“Conclusion: Use a static analyser at the maximum level from the start. ”

Coding standards

As we are developing with Symfony, it seems logical that our application itself respects the Symfony coding standards. The most popular tool is probably PHP-CS-Fixer. We can install it with:

composer require friendsofphp/php-cs-fixer --dev

What is nice is that you have a Symfony rule set for the official Symfony coding standards. The Symfony CS itself applies the PSR-12 rule set. Here is my configuration file:

<?php

// php-cs-fixer.php

declare(strict_types=1);

// https://cs.symfony.com/

$finder = PhpCsFixer\Finder::create()
    ->in(__DIR__)
    ->exclude('var')
    ->exclude('code')
    ->exclude('snapshots')
    ->exclude('tmp')
;

return (new PhpCsFixer\Config())->setRules([
    '@Symfony' => true,
    'array_syntax' => ['syntax' => 'short'],
    'declare_strict_types' => true,
    'no_superfluous_phpdoc_tags' => true,
    'php_unit_fqcn_annotation' => false,
    'phpdoc_to_comment' => false,
    'yoda_style' => false,
    'native_function_invocation' => [         // https://cs.symfony.com/doc/rules/function_notation/native_function_invocation.html
        'include' => ['@compiler_optimized'],
        'scope' => 'namespaced',
        'strict' => true,
    ], ])
->setFinder($finder);

The Symfony rule set is activated. The declare_strict_types option is, for me, mandatory. It forces you to have declare(strict_types=1); in each of your PHP files. It's important, because not using the strict mode can lead to unexpected behaviours because of implicit type conversion. The other options are a matter of taste. Before committing your code, just run:

/vendor/bin/php-cs-fixer fix --allow-risky=yes

“Conclusion: Agree with a coding standards rule set with your team and forget this. ”

Tests

Tests are, of course, crucial for an application. Not only does it allows you to prove that your code works. But another essential point is often forgotten; they will enable you to refactor your code with confidence. There are four main categories of tests:

  • Unit tests
  • Integration tests
  • Functional tests
  • End-to-end tests

I have dedicated an entire article to the subject. I explain how you should organize them cleanly from the start, so they can stay efficient and easy to maintain: Organizing your Symfony project tests. The primary tool is PHPUnit. It is perfectly integrated into Symfony thanks to the PHPUnit bridge.

“Conclusion: Write tests, deploy and refactor with confidence. ”

Code coverage

Tests are great. But how can you check that they really run all the code of your application? It can be done with Xdebug or pcov. Let's try with xdebug. First, we must install it:

pecl install xdebug
echo "zend_extension=xdebug" > /usr/local/etc/php/conf.d/99-xdebug.ini
php -v

PHP version should show that xdebug is activated now:

PHP 8.1.6 (cli) (built: May 17 2022 16:48:09) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.6, Copyright (c) Zend Technologies
    with Zend OPcache v8.1.6, Copyright (c), by Zend Technologies
    with Xdebug v3.1.2, Copyright (c) 2002-2021, by Derick Rethans

Then, we can get the coverage with the following command. It generates a nice HTML report.

php -d xdebug.enable=1 -d xdebug.mode=coverage -d memory_limit=-1  vendor/bin/phpunit --coverage-html=var/coverage

If everything is OK, the report is available in var/coverage. Here is an example for my main controllers. Honestly, I didn't run the report for weeks and had to work several hours to reach this. What is excellent with this report is that you can find bugs in your tests! And indeed, I found at least two bugs in my functional tests when writing this article. Globally the project has "only" 65% of coverage; I must improve this. 😅


The code coverage of some of my controllers

You probably heard before the 100% code coverage myth. Does it ensure that you application doesn't have bugs? Of course not. But it's vital as a low code coverage is generally the consequence of a bad design of your application. Your tests are not well written, or the code you produced isn't decoupled enough to be tested correctly. So what threshold should we use? 100% can be hard to achieve. PHPUnit considers that 90% is already a good score. So it can be a good starting point. Once again, we are creating a new project, so if you want to have very high coverage, you should start now. In six months or one year, it will be too late and difficult to get back a high percentage. A simple rule can be: that a pull request should never decrease the global percentage.


The code coverage of some of my controllers

“Conclusion: Follow the code coverage, get a high percentage from the start then keep it. ”

Continuous integration

This chapter will be concise. I already made an entire article on the subject (I must update it 😫): Setting a CI/CD workflow for a Symfony project thanks to the GitHub actions. To sum up, the CI ensures that all your workflows work flawlessly. The environment must be identical to your production environment, so you don't have a lousy surprise when deploying. It can also help you with migrations as it's easy to test a specific PHP version or component.

“Conclusion: use a CI, make it stable and boring ”

Makefile

This chapter is particular for me because it is about the Developer Experience (DX). This is something I find important. This is precisely the goal of Makefile: it can help you documenting the most common tasks when developing. A typical example is to reset, create the database and load the fixtures of a project. Do you really want to type this everevery time?

bin/console doctrine:cache:clear-metadata --env=dev
bin/console doctrine:database:create --if-not-exists --env=dev
bin/console doctrine:schema:drop --force --env=dev
bin/console doctrine:schema:create --env=dev
bin/console doctrine:schema:validate --env=dev
bin/console hautelook:fixtures:load --no-interaction --env=dev

This is where the Makefile can help; you can create a target; it looks like this now:

load-fixtures: ## Build the DB, control the schema validity, load fixtures and check the migration status
	@$(SYMFONY) doctrine:cache:clear-metadata
	@$(SYMFONY) doctrine:database:create --if-not-exists
	@$(SYMFONY) doctrine:schema:drop --force
	@$(SYMFONY) doctrine:schema:create
	@$(SYMFONY) doctrine:schema:validate
	@$(SYMFONY) hautelook:fixtures:load --no-interaction

And now you just have to run make load-fixtures. What is nice is that we document everything simultaneously (thanks to a little trick). When running make at the root of your project, you get a summary of all available targets (actions):


output of the default make command

Of course, you can also use composer scripts (they can also be documented), but I find them far less pratical. There is also Taskfile, but I haven't tested it yet.

“Conclusion: Document all your development workflow in a Makefile and make developers' life easier. ”

Conclusion

That's it; these are the points I really find essential. There are other things to discuss, but I wanted to focus on "foundations". I may make a second part for this article or not. Of course, these points could be the subject of a dedicated article. Here is a summary of the ones I already wrote:

That's it! I hope you like it. Check out the links below to have additional information related to the post. As always, feedback, likes and retweets are welcome. (see the box below) See you! COil. 😊

  Read the doc

They gave feedback and helped me to fix errors and typos in this article, many thanks to rherault. 👍


Call to action

Did you like this post? You can help me back in several ways: (use the Tweet on the right to comment or to contact me )

Thank you for reading! And see you soon on Strangebuzz! 😉

COil