Introducing the MicroSymfony application template

Published on 2023-08-21 • Modified on 2023-08-21

In this post, I introduce the MicroSymfony application template, which results from several ideas I already discussed in a previous post: "Initializing your Symfony project with solid foundations". Let's go! 😎

» Published in "A week of Symfony 971" (4-10 September 2023).

Prerequisite

I assume you have at least a basic knowledge of composer and Symfony.

Configuration

  • PHP 8.3
  • Symfony 6.4.6
  • Make 3.8
  • Castor 0.8

Introduction

Aren't you bored of installing the same packages, creating the same files and configuration? Over and over? That's why I created this little Symfony application template.

What is an application template?

For example, let's take dunglas/symfony-docker template; you can find it here: https://github.com/dunglas/symfony-docker.


The dunglas Symfony docker template

As you can see, the green button Use this template shows that this repository is an application template. There are two ways of using it:

With GitHub

You can click on this button; this creates a new repository with a single commit (it does not retrieve all the Git history of the original template). Then you can check it out and use it.

With composer

If the template is registered on packagist.org, you can install it by running the composer create command:

$ composer create-project vendor/template

What is MicroSymfony?

When you want to start a new Symfony application, you have several choices; you can use composer:

$ composer create-project symfony/skeleton:"6.3.*" my_new_project

Or the Symfony binary:

$ symfony new my_project_directory --version="6.3.*"

It installs the very minimal stuff to bootstrap a Symfony application. If you want to install a typical setup for an entire web application, you can pass the --webapp option to the Symfony CLI command.

Even if the --webapp option installs extra dependencies, it won't do much more. That's where MicroSymfony can be helpful. Consider it like a Symfony sandbox on "steroids". In a previous blog post, I already talked about what I consider good foundations to develop a Symfony application: Initializing your Symfony project with solid foundations. I applied all these pieces of advice/good practices in this template, so here are the features:

Features

MicroSymfony ships these features, ready to use:

  • A task runner (Make or Castor)
  • Static analysis with PHPStan
  • Coding standards with php-cs-fixer
  • Tests (all kinds)
  • Code coverage at 100% with a check script
  • GitHub CI (tests+lints)
  • Asset mapper+Stimulus
  • A custom error template

The purpose of MicroSymfony is to provide a sandbox with some sensible defaults and ready to use. It can be a solution if you want to quickly set up something, create a POC, test things, and even make a small "one-page" application.

I used it once to create a demo for the blog post we wrote with Slim Amamou on the Tilleuls blog: EasyAdmin & Mercure: a concrete use case. You can find the public repository here.

I have another use case in mind: if you pass a technical hiring test, and the exercise is to create a small Symfony application to check your skills. Then you can use MicroSymfony to save time and focus on the test, not bootstrapping the application.

The recruiter will surely be impressed by the quality of the code you produce. If they see you used a template and "cheated", you can retort that it isn't cheating: it is having a good knowledge of the Symfony ecosystem. πŸ™ƒ

Let's see what it contains exactly.

Initializing an application with MicroSymfony

As the application template is registered on Packagist, you can use composer to install it with the following command:

$ composer create-project strangebuzz/microsymfony

It creates a microsymfony directory with the new project. In this case, you must set up Git and a repository yourself. But that's the fastest way to test it. Note that the composer install command downloads all the required dependencies and builds the assets.

Or use the GitHub template:

Use this template

To serve the application with the Symfony binary, run:

$ make start

or

$ castor symfony:start

The application is now available at https://127.0.0.1:8000 (considering your 8000 port is available). But what is make, and what is castor? πŸ€”

Task runners

Microsymfony includes two task runners: a Makefile script (using the make executable) and a Castor script (using the Castor tool). I particularly like this; it's an example of Developer Experience (DX) enhancement. A task runner can help you provide shortcuts for your application so you don't have to type the same commands repeatedly. It also enables you to document the development workflow of your application. A task runner script is to your application, what is the table of contents for a book.

Makefile

Let's check what the Makefile contains; just run make. If using Windows, you have to install chocolatey.org or use Cygwin to use the make command. Check out this StackOverflow question for more explanations. So let's run:

$ make

We have the following output:

β€”β€” 🎢 The MicroSymfony Makefile 🎢 β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
help                           Outputs this help screen
 β€”β€” Symfony binary πŸ’» β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
start                          Serve the application with the Symfony binary
stop                           Stop the web server
 β€”β€” Tests βœ… β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
test                           Run all PHPUnit tests
coverage                       Generate the HTML PHPUnit code coverage report (stored in var/coverage)
cov-report                     Open the PHPUnit code coverage report (var/coverage/index.html)
 β€”β€” Coding standards/lints ✨ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
stan                           Run PHPStan
fix-php                        Fix PHP files with php-cs-fixer (ignore PHP 8.2 warning)
lint-container                 Lint the Symfony DI container
lint-twig                      Lint Twig files
lint-yaml                      Lint YAML files
cs                             Run all CS checks
lint                           Run all lints
ci                             Run CI locally
 β€”β€” Deploy & Prod πŸš€ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
deploy                         Simple manual deploy on VPS (this is to update the demo site https://microsymfony.ovh/)

When you run $ make without argument, the default task is run; in this case, the help task is displayed. We have all the available tasks divided by section. The colour syntax highlighting is done with a regexp trick. Now you can run all tasks in green by running make + task-name like we did to serve the application with make start. You can add the -n option to check what a task does.

$ make help -n
$ grep -E '(^[a-zA-Z0-9_-]+:.*?##.*$)|(^##)'  Makefile | awk 'BEGIN {FS = ":.*?## "}{printf "\033[32m%-30s\033[0m %s\n", $1, $2}' | sed -e 's/\[32m##/[33m/'

Some people don't like Makefile. I don't understand what they don't understand. What is shorter, typing:

$ make coverage

or

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

The answer is quite obvious. Some people have excellent memory, but I don't, so I want to avoid polluting my brain with this kind of thing: I want to focus on the product I build, not on this kind of detail.

Others also argue that the command line history is there, and it's easy to call a previous "complex" line thanks to the bash history. Yes, it's true, but, do your workmates have access to your bash history?

Therefore, a makefile (or other task runner script) can document your project and allows you to share your knowledge and tricks with other people.

A last point that people against a Makefile don't understand is that it allows hiding implementation. Consider the make start command; in this case, it uses the Symfony binary, but it could also use Docker or pop a cloud-based environment on the fly while still using the same command. That means you could standardize all commands in the company so people coming on a new project know how to start a project and list all main tasks for the current project. It is also explained in the JoliCode blog post about Castor.
Again, that's precisely what the developer experience is, to make things easier to use and your daily work more comfortable.

Still not convinced by make and its Makefile? Then use Castor!

Castor 🦫

Castor is a tool created by JoliCode in 2023; you can find its presentation in this blog post. It's a program like the Symfony CLI that you have to install first. Check out the installation instructions on the GitHub repository.

Once installed, like make, you can run castor to list all available commands. Let's try:

$ castor
castor v0.8.0

Usage:
  command [options] [arguments]

Options:
  -h, --help            Display help for the given command. When no command is given display help for the list command
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi|--no-ansi  Force (or disable --no-ansi) ANSI output
  -n, --no-interaction  Do not ask any interactive question
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Available commands:
  completion       Dump the shell completion script
  help             Display help for a command
  list             List commands
 ci
  ci:all           Run CI locally
 cs
  cs:all           Run all CS checks
  cs:fix-php       Fix PHP files with php-cs-fixer (ignore PHP 8.2 warning)
  cs:stan          Run PHPStan
 lint
  lint:all         Run all lints
  lint:container   Lint the Symfony DI container
  lint:twig        Lint Twig files
  lint:yaml        Lint Yaml files
 symfony
  symfony:start    Serve the application with the Symfony binary
  symfony:stop     Stop the web server
 test
  test:all         Run all PHPUnit tests
  test:cov-report  Open the PHPUnit code coverage report (var/coverage/index.html)
  test:coverage    Generate the HTML PHPUnit code coverage report (stored in var/coverage)

This looks familiar, right? Yes, it's the exact same output you have with the Symfony console or CLI. The commands are listed and can have a given namespace to regroup them by theme. For example, we have a test namespace. If you run castor test, only the commands of this namespace are listed:

$ castor test
castor v0.8.0

Usage:
  command [options] [arguments]

Options:
  -h, --help            Display help for the given command. When no command is given display help for the list command
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi|--no-ansi  Force (or disable --no-ansi) ANSI output
  -n, --no-interaction  Do not ask any interactive question
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Available commands for the "test" namespace:
  test:all         Run all PHPUnit tests
  test:cov-report  Open the PHPUnit code coverage report (var/coverage/index.html)
  test:coverage    Generate the HTML PHPUnit code coverage report (stored in var/coverage)

This output is generated from the castor.php script at the project's root. Let's have a look at the test:coverage task:

#[AsTask(namespace: 'test', description: 'Generate the HTML PHPUnit code coverage report (stored in var/coverage)')]
function coverage(): void
{
    io()->title(get_command()->getName().' > '.get_command()->getDescription());
    run('php -d xdebug.enable=1 -d memory_limit=-1 vendor/bin/simple-phpunit --coverage-html=var/coverage',
        environment: [
          'XDEBUG_MODE' => 'coverage',
        ],
        quiet: false
    );
    run('php bin/coverage-checker.php var/coverage/clover.xml 100', quiet: false);
    success();
}

We first notice that we have access to the SymfonyStyle object thanks to the io() helper. It is the same one you have in a classic Symfony command. And you can enjoy the power of all you can usually do in a Symfony command. Please read the documentation and the blog post to have all information.

I really like it. Writing more complex stuff requiring shell commands is much more accessible and convenient with Castor: we can write PHP and use the power of the Symfony command. For sure, I will use it instead of a Makefile for my future projects.

Composer

Composer can also be used as a task runner; I made an example with the test command. You can run the tests with the following command:

$ composer app:test

A small trick is to use a ":" to introduce a namespace for your application so the tasks are not listed with the other standards composer commands:

   ______
  / ____/___  ____ ___  ____  ____  ________  _____
 / /   / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ (__  )  __/ /
\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
                    /_/
Composer version 2.5.8 2023-06-09 17:13:21

Usage:
  command [options] [arguments]

Options:
  -h, --help                     Display help for the given command. When no command is given display help for the list command
  -q, --quiet                    Do not output any message
  -V, --version                  Display this application version
      --ansi|--no-ansi           Force (or disable --no-ansi) ANSI output
  -n, --no-interaction           Do not ask any interactive question
      --profile                  Display timing and memory usage information
      --no-plugins               Whether to disable plugins.
      --no-scripts               Skips the execution of all scripts defined in composer.json file.
  -d, --working-dir=WORKING-DIR  If specified, use the given directory as working directory.
      --no-cache                 Prevent use of the cache
  -v|vv|vvv, --verbose           Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Available commands:
  about                    Shows a short information about Composer
  ...
  validate                 Validates a composer.json and composer.lock
 app
  app:test                 Run all PHPUnit tests
 symfony
  symfony:dump-env         [dump-env] Compiles .env files to .env.local.php.
  ...

If you look at the composer.json file, the command is declared in the script section but documented in the script-description section. That's not very convenient. Composer is a fantastic tool, but it isn't a task runner, so I advise using it as it is supposed to be used, as a dependency manager, not as a task runner.
Of course, you should always use the post-*-cmd commands to prepare your application to run after a composer install or update. The only pro I see with Composer is that you don't have to install an extra tool, as you already use it to handle your dependencies.

Static analysis

PHPStan is configured at the maximum level with the Symfony plugin. The rules are simple:

  • don't use the ignoreErrors section (the least possible!)
  • don't use a baseline (never!)

When you start from the maximum level on a new project, it is easier to keep this level. Here is the basic config used, ready to be tuned:

# https://phpstan.org/config-reference
parameters:
    # https://phpstan.org/config-reference#rule-level
    level: max
    # https://phpstan.org/config-reference#multiple-files
    paths:
        - bin
        - config
        - public
        - src
        - tests
    # https://github.com/phpstan/phpstan-symfony#configuration
    symfony:
        container_xml_path: var/cache/dev/App_KernelDevDebugContainer.xml
    # https://phpstan.org/user-guide/ignoring-errors
    ignoreErrors:
        #- '#my_ignored_error_regexp_pattern#'

I have added links to the documentation. Note that the Symfony plugin is automatically loaded thanks to the phpstan/extension-installer one. And, yes, of course, you should also analyse the PHP files in the /tests directory.

Coding Standards

A simple php-cs-fixer ruleset is used with the most important rules:

  • The Symfony rule set
  • Activation of strict types

The phpdoc_to_comment setting is also deactivated because it sometimes messes with the @var annotations needed for the static analysis. I have also deactivated the yoda_style rule, but it's just a matter of taste. It's a reasonable basis; now it's up to you and your team to agree on the other rules to apply. Here is the configuration:

<?php

declare(strict_types=1);

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

return (new PhpCsFixer\Config())->setRules([
    '@Symfony' => true,             // https://cs.symfony.com/doc/ruleSets/Symfony.html
    'declare_strict_types' => true, // https://cs.symfony.com/doc/rules/strict/declare_strict_types.html
    'yoda_style' => false,          // https://cs.symfony.com/doc/rules/control_structure/yoda_style.html
    // # Needed to avoid messing with @var annotations for PHPStan
    'phpdoc_to_comment' => false,   // https://cs.symfony.com/doc/rules/phpdoc/phpdoc_to_comment.html
])->setFinder($finder);

I have also added the links to the documentation.

Tests

In the blog post: "Organizing your Symfony project tests", I explained how I organize my tests in a Symfony application. So what I did here is to create an elementary test of each kind, so you can copy/paste instead of using the maker bundle; you have:

If you use API Platform, I made an example of using the ApiTestCase instead of the WebTestCase. The Panther test is also an example; you must install it first to make it work.

Run the tests with make, Castor or composer. Here is the output with Castor:

$ castor test:all
test: Run all PHPUnit tests
===========================

PHPUnit 9.5.28 by Sebastian Bergmann and contributors.

Testing
............                                                      12 / 12 (100%)

Time: 00:00.341, Memory: 32.00 MB

OK (12 tests, 19 assertions)

Remaining indirect deprecation notices (2)


                                                                                β€Ž
 [OK] Done!                                                                     β€Ž
                                                                                β€Ž

Code coverage

It is a part that developers very often neglect. Good code coverage does not guarantee that you don't have bugs, but it gives even more confidence to your code and your tests. We can consider the code coverage as "testing your tests".
I use a small trick here, it's, in fact, a script that Ocramius created, and even if it is ten years old, it still works with PHPUnit 9.5! Let's read the blog post, but to sum up, it calculates the global code coverage by parsing and analysing the output of the Clover report in the clover.xml file. Here is the output with Castor:

coverage: Generate the HTML PHPUnit code coverage report (stored in var/coverage)
=================================================================================

PHPUnit 9.5.28 by Sebastian Bergmann and contributors.

Testing
............                                                      12 / 12 (100%)

Time: 00:00.854, Memory: 40.00 MB

OK (12 tests, 19 assertions)

Generating code coverage report in Clover XML format ... done [00:00.003]

Generating code coverage report in HTML format ... done [00:00.023]

Remaining indirect deprecation notices (2)

 > Code coverage: 100% - OK! βœ…


                                                                                β€Ž
 [OK] Done!                                                                     β€Ž
                                                                                β€Ž

And here is an example of the HTML output.


100% code coverage

Now it's up to you to keep the coverage at 100%. You won't have any excuses. Modify the threshold in your PR if you need to lower the percentage for a given reason, such as delivering an urgent bug fix.

Continuous Integration (CI)

All we put in place should be automatically tested to avoid the "works on my machine" problem. So there is a working GitHub actions script that runs the tests, code coverage and all lints. There is a job for tests:


GitHub actions tests logs

And for lints/cs/static analysis:


GitHub actions lint/cs/statis analysis logs

Note that the Castor tool will soon be available in the shivammathur/setup-php@v2 GitHub action. Once it is available, we can replace all manual commands with the Castor calls, which will also help us to avoid duplicate code. I'll update the blog post once it is OK.

Symfony Asset mapper+Stimulus

MicroSymfony is a perfect use case for the new Symfony asset mapper component and the use of hotwired/stimulus because we want the dependencies to stay as minimal as possible. We don't have to use node or webpack to build the assets.

The asset mapper component allows to include the Stimulus dependencies, thanks to the Stimulus bundle (asset/app.js):

import { startStimulusApp } from '@symfony/stimulus-bundle';

const app = startStimulusApp();

Then we can create our Stimulus controllers (assets/controllers/api_controller.js):

import {Controller} from '@hotwired/stimulus';

export default class extends Controller {

There are two controllers as examples, one with simple vanilla JavaScript and another asking for data on a dummy JSON endpoint of the Symfony application. You can access the demo here.

The new importmap() helper handles all the dependency stuff of the asset mapper component (templates/base.html.twig):

{% block javascripts %}
  {{ importmap() }}
{% endblock %}

And the importmap.php file:

<?php

declare(strict_types=1);

/**
 * Returns the import map for this application.
 *
 * - "path" is a path inside the asset mapper system. Use the
 *     "debug:asset-map" command to see the full list of paths.
 *
 * - "preload" set to true for any modules that are loaded on the initial
 *     page load to help the browser download them earlier.
 *
 * The "importmap:require" command can be used to add new entries to this file.
 *
 * This file has been auto-generated by the importmap commands.
 */
return [
    'app' => [
        'path' => 'app.js',
        'preload' => true,
    ],
    '@hotwired/stimulus' => [
        'url' => 'https://ga.jspm.io/npm:@hotwired/stimulus@3.2.1/dist/stimulus.js',
        'preload' => true,
    ],
        '@symfony/stimulus-bundle' => [
        'path' => '@symfony/stimulus-bundle/loader.js',
    ],
];

A custom error template

What is more annoying than having an error in your application? It's to see the default error template that doesn't even extend your layout. So, "cherry on the cake", like FabPot would say, MicroSymfony ships a custom error page that extends the layout and displays the returned HTTP status code and its human version thanks to the Response::$statusTexts variable. The FrankenPHP elePHPant is here to calm you down so you can debug your application without stress πŸ™.


The frankenPHP ElePHPant

Conclusion

While the Symfony demo is an example of a complete Symfony application with quite a lot of code and features, think of MicroSymfony as a Symfony skeleton on steroids, ready to use.
It was the first time I created an open-source "application template". It was fun to do, and as always, I learned a lot of things. If you use it or find something useful in the code or the blog post, don't hesitate to add a star on the GitHub repository πŸ™.
I'll update the code and will create a new tag at least at each Symfony minor version. I'll use the same version as Symfony, so that the 6.3.x tags will ship Symfony 6.3 and the same thing for the following versions.
Contributions are welcome if you spot errors or something that could be improved or simplified. πŸ™‚

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. 😊

  GitHub  More on the web 🦫  More on the web

  Work with me!


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