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.12
- 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.
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:
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:
- A unit test (TestCase)
- A kernel test (KernelTestCase)
- A functional test (WebTestCase)
- An API test (WebTestCase)
- An E2E test (PantherTestCase).
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.
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:
And for lints/cs/static analysis:
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 π.
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
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 )
- Report any error/typo.
- Report something that could be improved.
- Like and retweet!
- Follow me on Twitter Follow me on Twitter
- Subscribe to the RSS feed.
- Click on the More on Stackoverflow buttons to make me win "Announcer" badges π .
Thank you for reading! And see you soon on Strangebuzz! π
[π¬π§] 3rd blog post of the year: "Introducing the MicroSymfony application template" https://t.co/07kQXl2ciz Proofreading, comments, likes and retweets are welcome! π Annual goal: 3/7 #symfony #php #webapp #rad /cc @JoliCode
— COil #OnEstLaTech β πΊπ¦ (@C0il) August 23, 2023