{% raw %} <-- keep this for Jekyll to fully bypass this documents, because of the Twig tags.
📑
Symfony Website Checklist Summary~~~~
- Elevator pitch
- Philosophy
- 1. Set up your environment
- 2. Set up a new Symfony project
- 3. Set up your Twig templates
- 4. Produce your models
- 5. Set up translations (even if you only use one language)
- 6. Set up your basic application logic
- 7. Secure your app
- 8. Use TailwindCSS for styles and RWI
- 9. Pre-flight checks
- 10. Dockerize your project
Elevator pitch
This project lists all the mandatory steps I recommend to build a website using:
- HTTPS + HTML output,
- A local, PHP-integrated server first, Docker as a "maybe" later on,
- Symfony,
- Twig,
- Doctrine ORM,
- Any RDBMS.
This project assumes you start from zero.
This project README will remain in one file to ease searching from a browser.
/files-you-will-need
directory of this repository. This directory reflects the exact, expected directory tree as you grow it.
All the files referenced in this document can be found in the Philosophy
The idea behind this is as follows: |
---|
The exhaustive version of a HowTo would be the official documentation. |
The slightly slower version of this would be to watch tutorials and use cases from SymfonyCasts. |
The faster way would be to read The Fast Track, precisely written by Fabien Potencier. |
The fastest way to me, trading possibilities for opinions, should be this repository. |
All contributions and suggestions are welcome.
1. Set up your environment
This section applies to any local, host OS or Docker project construction.
If you intend to use Docker the super fast way, you can bypass this section.
- Head up to point #10 in this document if you're building everything yourself.
- Head up to https://symfony.com/blog/introducing-docker-support and https://github.com/dunglas/symfony-docker otherwise.
- Set up PHP/latest.
On Linux, use your package manager (like Aptitude or Yum).sudo su && apt-get update && apt-get install php8
at least.
On MacOS, use Brew throughbrew install php
.
On Windows:- Download it from windows.php.net (take the latest version, VS16 x64 Thread Safe, with OpenSSL)
- Unzip it to
C:\php[VERSION DIGITS]
. - Then change your
PATH
system variable (Windows + R
, typePATH
, hitEnter
, click onEnvironment Variables
, then your user variables, edit thePATH
entry and append the previously unzipped directory path to it).
- Copy
php.ini-development
tophp.ini
in your - Configure your PHP locally for your
dev
environment.- Set
memory_limit
to8M
- Set
max_ececution_time
to200
- Set
upload_max_filesize
to200M
- Uncomment the
extension_dir
directive. Careful: it's different on Windows vs the rest of the world. - Uncomment to enable the following extensions:
bz2, curl, gd, intl, mbstring, mysqli, openssl, pdo_mysql, sodium
. - Define the
date.timezone
directive to your local timezone.- Mine is
"Europe/Paris"
, for instance. - The complete list is here.
- Mine is
- Set
- On Windows, download and install Composer Setup and Symfony Setup.
- Check that you got everything OK using
symfony check:requirements
in any environement. Ignore the "Enable or install a PHP accelerator" advice in development. - Start from an empty directory, use
symfony new [your_project_directory_name]
. - Create a
README.md
file inside the root directory and put everything you can document inside, at least those sections:- The title of the project.
- The purpose of the project.
- How to set it up.
- How to run basic commands it requires to work (including Docker).
- Contribution / modification instructions.
- Architectures / coding / technology choices.
- Add a
readme-sources
directory at the root of your project. Anything that is included in MarkDown documentation goes there. - Install a RDMBS (let's start with that, right?), like MySQL, MariaDB, or PostGreSQL. Same again:
- On Windows, just download the installers and set them up as services, don't use standalone Zip files.
- On MacOSX, use Brew through
brew install mysql
thenbrew services start mysql
.
- Set up your DotEnv files:
- Create a
.env.local
file by copying the.env
one. - Change the
APP_SECRET
value to anything. - Change the
DATABASE_URL
to the appropriate values to connect to your RDBMS.
- Create a
- If you use PHPStorm, use
File > Manage IDE settings > Import settings
and pick up thephpstorm-settings.zip
included in this repository.
2. Set up a new Symfony project
- Install PHP-Stan as a dev dependency (
composer require --dev phpstan/phpstan
).- You can do this in a separate directory outside your project as a better practice.
- Create your configuration file for PHP-Stan if you know what you're doing. If you don't, just use the one in this repository (
phpstan.neon
). - Let it at the root of your project for now.
- Also install additional Symfony and Doctrine plugins:
composer require --dev phpstan/extension-installer
,composer require --dev phpstan/phpstan-symfony
,composer require --dev phpstan/phpstan-doctrine
.
- Make sure PHP-Stan also checks the
config
directory, and goes to the maximum level. Mine looks like this:
parameters:
level: 9
paths:
- config
- src
- tests
checkGenericClassInNonGenericObjectType: false
ignoreErrors:
- '#Property .* is never written, only read\.#'
symfony:
container_xml_path: var/cache/dev/App_KernelDevDebugContainer.xml
- Install PHP-CS-Fixer as a dev dependency (
composer require --dev friendsofphp/php-cs-fixer
).- You can do this in a separate directory outside your project as a better practice.
- Create your configuration file for PHP-CS-Fixer if you know what you're doing. If you don't, just use the one in this repository (
.php-cs-fixer.dist.php
). - Let it at the root of your project for now.
- Install Psalm as a dev dependency (
composer require --dev vimeo/psalm
).- You can do this in a separate directory outside your project as a better practice.
- Create your configuration file for Psalm if you know what you're doing (
php vendor/bin/psalm --init
). If you don't, just use the one in this repository (psalm.xml
). - Let it at the root of your project for now.
- Also install additional Symfony and Doctrine plugins:
composer require --dev psalm/plugin-symfony
andcomposer require --dev weirdan/doctrine-psalm-plugin
.
- Create a shell command to start them, at the root of your directory (you can safely copy the ones in this repository).
- Give them short names so you don't lose time calling them manually.
- Don't start their names with "
php
", they all share this and slows down your CLI calls. - Call them
csfixer
,stan
andpsalm
, with appropriate shell extensions (.bat
or.sh
).
- Add a
.editorconfig
file at the root directory of your project to ensure your IDE settings don't get messed up.- At least set it up so you use UTF-8,
LF
characters for newlines and 4 spaces as tabulations. - If you don't, just use the one in this repository (
.editorconfig
).
- At least set it up so you use UTF-8,
- If you know what you're doing, use REDIS to store PHP sessions at least. Try it for custom cache pools (this goes beyond the purpose of this document).
- Make sure you provide the appropriate DSN for your databases and the right
serverVersion
parameter. Use dynamic parameters with$
to set them up.
3. Set up your Twig templates
- Identify your application domains. If you have no idea what this means, or which to use, simply go with
Admin
andFront
, plus one for each of your application "modules". - Install Twig and the extensions you're going to use.
composer require twig
composer require twig/extra-bundle
- Create global variables for Twig that you're going to need, simply edit
config/packages/twig.yaml
as follows:
twig:
# ...
globals:
# Used for the tab title
globals_website_title_suffix: ' | Your website suffix'
# Used for OpenGraph
globals_website_url: 'https://your.super.url'
# Used at many places
globals_website_name: 'Your website name'
# Used for schema.org data
globals_website_subtitle: 'Something like your slogan'
# Used for OpenGraph data
globals_website_description: 'The long description, mostly for <meta> tags and OpenGraph description.'
# You'll need to change this if you want to enable Facebook Sharer
globals_facebook_app_id: '1111111111111111'
# ...
- For the following steps, if you want to be lazy, just copy the templates from this repository for a start:
admin_layout.html.twig
goes totemplates/admin
front_layout.html.twig
goes totemplates/front
base.html.twig
goes totemplates
- If you didn't copy the files from the previous step, then do as follows:
- Create a layout file inside each domain subdirectory, name it as follows:
[domain]_layout.html.twig
. - Add a base class block to your
<html>
tag inbase.html.twig
template and override it in each[domain]_layout.html.twig
templates. - Define your metadata in your
base.html.twig
, at least:- The viewport strategy for CSS processing.
- OpenGraph metadata.
- HTML metadata.
- Schema.org metadata.
- Twitter / Facebook metadata.
- Webpack Encore styles and scripts tags.
- Dynamic <title> markup.
- UTF-8 charset.
- A canonical markup for SEO (
<link rel="canonical" href="{{ url(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) }}">
). - Overridable blocks for all of these.
- Create a layout file inside each domain subdirectory, name it as follows:
- Your
/templates
directory should at least contain:admin
common
front
registration
security
base.html.twig
- Make sure all the files and directories under
/templates
use snake_case only for their filenames. - Add file input previews to all image fields using a
LiipImagineBundle
preset through a custom form theme (add it in config/twig.yaml
). - Customize your error pages.
- The whole tutorial is in the documentation (https://symfony.com/doc/current/controller/error_pages.html).
- This implies that you change at least the basic messages: HTTP 500, 404 and 403 (if you don't use them, you app seems weird or overcomfident).
- Basically, this simply means you need to create templates in
templates/bundles/TwigBundle/Exception
, don't forget to start with a generic one:error.html.twig
. - Test them during development (there's a simple way to do that, you don't need to generate the errors, the framework will create artificial routes for you)
- Use a friendly message for the content, put something nice (and funny, to compensante the sadness of viewing error pages), with jokes linked to your website contents.
- Include a way for people to go on browsing. You have to have all main navigation, menus and incentives to redirect users to the most common pages.
4. Produce your models
- List all your entities, think about them until you can't find new fields/properties to add.
- Add Doctrine DBAL + ORM (
composer require orm
). - If you want to use migrations, add Doctrine Migrations (
composer require migrations
).- If you have no idea what to do and work alone on your project, don't use them (
composer remove doctrine/migrations
, same for dependencies). - If you end up using them, create a
/migrations
directory at the root directory of your project.
- If you have no idea what to do and work alone on your project, don't use them (
- Install the MakerBundle (
composer require --dev maker
). - Use the MakerBundle to generate their CRUDL. For each entity, run
php bin/console make:crud
. - Create a
Model
directory under/src
to store all models that are not entities. - Make sure all the files and directories under
/src
use CamelCase only for their filenames. - Check that your entities are valid using
php bin/console doctrine:schema:validate
. - If you're using migrations, check that you're good with
php bin/console doctrine:migrations:up-to-date
. - Slugify all your public content URLs in the form of
[domain]/[entity type]/{slug}-{id}
usingAsciiSlugger
. Remove the words shorter than 3 characters.
5. Set up translations (even if you only use one language)
- Install the Translation component:
composer require symfony/translation
. - Set up your default language in
config/packages/translation.yaml
. Mine looks like this (for non-geographic English as the default language):
framework:
# ...
default_locale: en
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
- en
# ...
- Set up your languages logic in
config/services.yaml
. Mine looks like this:
parameters:
# ...
app.supported_locales:
- en
- fr
# ...
- Create a
messages.[yourdefaultlanguage].yaml
in thetranslations
folder. Don't use ICU unless you know why. - Create a
validators.[yourdefaultlanguage].yaml
in thetranslations
folder. Don't use ICU unless you know why. - Repeat last two steps for each additional language you'll need.
- Whatever you'll do, make sure you keep alphabetical / ASCII order for translation keys inside YAML files.
- Whatever you'll do, make sure you'll ONLY use YAML-parse syntax for keys (e.g.
front.forms.users.create.name.help
). - Make sure all the files and directories under
/translations
use snake_case only for their filenames.
6. Set up your basic application logic
- Create a subdirectory for each of your domains, at least for
Admin
andFront
(that's already enough):- In
src/Controller
. - In
src/Form
. - In
src/Model
.
- In
- Do the same in the Twig
/templates
directory: create/admin
and/front
subdirectories in it. - Move your CRUDL controllers to
src/Controller/Admin
, and their templates totemplates/admin
. - Update the namespaces, templates name references in the controllers and templates according to last point.
- Inside each
messages.[language].yaml
translations file, start root keys with your domains, all snake case. At least they should look like this:
front:
admin:
- Add constraint validation to the maximum properties you can set to in your entity files.
- Run
composer require symfony/validator doctrine/annotations
. - This supposes, at least:
- That ALL your fields have a
@Assert
- That ALL your fields have a
- Run
- Make sure all your entities are historizable, which means they should have creation and last modification dates attached:
- To achieve that, use a PHP trait in all your entities.
- If you have no idea what this means, simply use the file
HistoryTrackableEntity.php
in this repository. - Put the trait in your
src/Entities
directory. - Add an
@ORM\HasLifecycleCallbacks
annotation to all your entities. - Add
use HistoryTrackableEntity;
after each Entity class opening bracket (first line, before constants and properties). - Update the database (
php bin/console doctrine:schema:update
or use migrations if you chose to use them).
- Review ALL your forms, they need to have:
empty_data
provided, especially for non-nullable fields.help
for all fields with filling guidelines.- A clear
label
for all fields. - A placeholder for all fields (
'attr' => ['placeholder' => 'your.placeholder.translation.key']
).
- Define a custom, static menu configuration. If you want a dynamic one or use a premade bundle like KnpMenuBundle, it's up to you.
- Create
model/Admin/AdminMenus.php
andmodel/Front/FrontMenus.php
files. - Just make them use a multidimensional
public static array $adminMenuTree = [];
array structure that returns all menus. See includedFrontMenus.php
class. - Create a Twig extension to output them, either use
make:twig-extension
or just copy the one included in this repository (MenuExtension.php
). - Then make a common template for menus (
templates/common/_menu.html.twig
) or just copy the one included in this repository.
- Create
- Remove the dead code in your entity repositories (
src/Repository
). - Unify templates as much as you can:
- Move any MakerBundle-generated
_delete_form.html.twig
and_form.html.twig
totemplates/admin/common
. - Update all the CRUDL templates so they use those.
- Delete the remaining, unused templates.
- Move any MakerBundle-generated
- Make sure your app doesn't send emails, apart from user account management (account creation, password reset). Most of them are not necessary.
- As much as you can, try to offer an eco-friendly version of your app. To do so:
- Use cookies (since they're functional, they're GPDR-compliant) to remember usage choice.
- Create eco-friendly and regular Twig layouts.
- Pick up the top layout from your cookies as defined above.
- Load only one compressed, optimized, alternate CSS stylesheet.
- Include all accessibility HTML, removed the decorative HTML by using the same Twig conditions on cookies.
- When cookies are not set, redirect the user to a minimal choice page.
- If your application logic requires an update because of that, you need to rethink how it works.
- Don't forget responsive web integration as well.
7. Secure your app
- Make a User class using the MakerBundle:
php bin/console make:user
. - Make a authentication plugin using the MakerBundle:
php bin/console make:auth
. Pick a custom authenticator name. - Don't forget to make the appropriate CRUDL using the MakerBundle:
php bin/console make:crud
. - Modify the
new
andedit
actions of the generated controller so that they encode passwords usingUserPasswordHasherInterface
. - Define your roles in
config/packages/security.yaml
. Mine looks like this (read the comments):
security:
enable_authenticator_manager: true
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
App\Entity\User:
algorithm: auto
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email # Can be anything you want
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
custom_authenticator: App\Security\AppAuthenticator # Use your custom authenticator name here
logout:
path: app_logout
target: app_home # Set this to your public homepage, that defaults to "/" at least
access_control:
# Uncomment this after step 6. in this chapter
# - { path: ^/admin, roles: ROLE_ADMIN }
role_hierarchy:
# Administers the whole website
ROLE_ADMIN: ROLE_USER
# Simple role added by Symfony
ROLE_USER:
- Then create your admin user while the CRUDL are public, and set yourself to the maximum administrator role level.
- Once your admin user is created:
- Uncomment the
access_control
directive in the file above - Prefix your generated
UserController
with:#[Route('admin/user')]
. - Do the same for ALL the controllers you set:
- behind the
src/Controller/Admin
directory, - the admin ones wherever they are,~~~~
- and all the generated CRUDL ones.
- behind the
- Uncomment the
- Use the MakerBundle to make a registration process (
php bin/console make:registration-form
). - The
SecurityController.php
andRegistrationController.php
stay at the root ofsrc/Controller
directory. - Run
composer require tgalopin/html-sanitizer-bundle
and sanitize all user-generated fields that are displayed without escaping. - Configure the
framework.trusted_hosts
andframework.trusted_headers
parameters in yourconfig/packages/prod/framework.yaml
file (or any environment file needed). - Check your app through
https://github.com/fabpot/local-php-security-checker
on a regular basis (add it to your CI and your local habits). - Every time you add an action, before you even start writing it, check the security first. Do the same when you're done.
- Make sure you use absolutely generic and unique error messages for login and password reset actions. Don't reveal what was wrong on user side.
8. Use TailwindCSS for styles and RWI~~~~
- Update
config/twig.yaml
and set this:
twig:
# ...
form_themes: [ 'tailwind_2_layout.html.twig' ]
# ...
- Add Webpack Encore, with PurgeCSS and PostCSS:
- Install Node (pick the latest LTS version):
- On Windows and MacOS, just use the installer and restart your shell.
- On Linux, use your package manager (like Aptitude or Yum).
sudo su && apt-get update && apt-get install nodejs npm
at least.
- Run
composer require encore
andnpm install
. - Run
npm install -D tailwindcss postcss-loader purgecss-webpack-plugin glob-all path autoprefixer
. - Setup Webpack, PostCSS and Tailwind.
- If you don't know what this means, simply copy/overwrite the following files from this repository to your project root directory:
postcss.config.js
tailwind.config.js
webpack.config.js
- If you don't know what this means, simply copy/overwrite the following files from this repository to your project root directory:
- Run
npm run build
.
- Install Node (pick the latest LTS version):
- Make sure you have the following structure at least in
assets
:controllers
favicon
fonts
images
styles
app.js
bootstrap.js
controllers.json
- Create a favicon and add its configuration to your
base.html.twig
andassets/favicon/browserconfig.xml
. Use a favicon generator for that and a manifest file. - Create a default OpenGraph image for your site and put it in
assets/images
(name itogimage.jpg
if you copied the included files of this project). - Prepare an external shell script to start your project from your user home directory. See an example with
start-project
included scripts.
9. Pre-flight checks
- Run
symfony check:security
to validate that your project has no known vulnerabilities from its dependencies. - Check that you got everything OK using
symfony check:requirements
while on the production server (see above). This time, pay attention to OPCache (see below). - Create a deployment script for your non-dev environments.
- If you don't know what you're doing, use the one provided in this repository (
production-deployment.sh.dist
) for a start. - On your non-dev environments, copy the
production-deployment.sh.dist
to[environment]-deployment.sh
. - Check that they're in the
.gitignore
and only on destination servers filesystems. Don't version the final ones. - Use those scripts to clear OpCache and realpath caches.
- If you don't know what you're doing, use the one provided in this repository (
- Make sure your application only uses HTTPS. Your
config/services.yaml
should contain this:
parameters:
# ...
router.request_context.scheme: 'https'
asset.request_context.secure: true
router:
request_context:
scheme: 'https'
asset:
request_context:
secure: true
# ...
- Validate your project with PHP-Stan (using the shell scripts created in #1.).
- Validate your project with Psalm (using the shell scripts created in #1.).
- Validate your project with PHP-CS-Fixer (using the shell scripts created in #1.).
- If needed, configure your CI. There's an included sample file for GitLab CI inside this project, see
.gitlab-ci.yml
(checks that you didn't forget PHP-CS-Fixer). - Add a
robots.txt
file inside thepublic
directory. Use the one provided in this repository for a start. - Add a
site.webmanifest
file inside thepublic
directory. Use the one provided in this repository for a start. - Enable it by adding this to your
base.html.twig
file:<link rel="manifest" href="{{ asset('site.webmanifest') }}">
. - Finally, you can start your project (locally) using:
symfony local:server:ca:install
symfony server:start -d
npm run watch
- And you can prepare your assets for deployment using:
npm run build
. - Configure your non-dev environments (this goes way beyond this project boundaries ^^). If your non-dev servers are Apache, you can use
composer require symfony/apache-pack
. - Start writing PHPUnit tests under the
tests/
directory. You will need WAY less of them as long as your code passes PHP-Stan and Psalm maximum level scans. - On production servers, make sure:
- PHP
memory_limit
is set just around8M
for CLI and1M
for web SAPIs. More if PHP processes files. - PHP
max_execution_time
is set to1200
for CLI and30
for wep SAPIs. More for CLI if your app has heavy CLI processing. - PHP
realpath_cache_size
is set to512K
for CLI and16M
for wep SAPIs. - PHP
realpath_cache_ttl
is set to600
for CLI and2000
for wep SAPIs. - PHP OpCache is configured:
opcache.preload=[your project path]/config/preload.php
and setparameters.container.dumper.inline_factories: true
inconfig/services.yaml
opcache.preload_user=www-data
opcache.memory_consumption=1M
opcache.max_accelerated_files=100000
opcache.validate_timestamps=0
(WARNING: this implies that your deployment scripts empty OpCache pools)
- You dump the autoloader classmap statically in your deployment script (
composer dump-autoload --no-dev --classmap-authoritative
). - You have chmod'ed (
755
) your upload, cache and logs directories. Use umask if necessary. - You deploy via Git exclusively:
git fetch --all
git rev-parse refs/remotes/origin/master
(or any deployment branch)git checkout -f master
(or any deployment branch)
- You have entities updated through deployment. Migrations if you're not alone, straight schema update if you are (see above).
- PHP
- Start profiling your app:
- Use a free BlackFire environment (limited, if you're not an individual, consider buying a license) and profile your app.
- Use the Symfony profiler (
composer require --dev symfony/profiler-pack
) and take a look at:- Your queries. You can reduce them to the minimum (between 0 and 1 per page) easily.
- Use Twig
{% cache 'your_key' ttl(600) %}{% endcache %}
for anything locally cacheable especially the results of Twig functions that use DB. - Otherwise, add caching to your controllers and models:
- Create a cache pool.
- Use it by memoizing your actions.
- Add expiration headers to anything that simply returns entities for a given HTTP request, give them at least 10 minutes, at best a day or more.
- Add validation headers to any complex action that has non-linear behaviour.
- Make sure you have resized all user-generated images with LiipImagineBundle (
composer require liip/imagine-bundle
) and usesrcset
HTML5 attribute. - Make sure you have optimized all your theme images using TinyPng.
- Make sure you have no remaining missing translations (
php bin/console debug:translation [your locale]
). - Make sure you browser console is absolutely empty, no:
- CORS alerts
- JavaScript errors
- Bad cookie API usages
- 404 on files
- Other browser warnings
- Check that your services definitions are OK using
php bin/console lint:container
. - Unless your website ecosystem doesn't like it, configure your web server to use
SameSite / strict
cookies. - Define a custom (random) string for the
APP_SECRET
variable in your DotEnv file, one for each different environement.
10. Dockerize your project
- Create a
docker-sources
directory inside the project root directory. You'll put your Docker Compose files inside. - Create a
docker-sources/containers-config
directory inside. You'll put your Dockerfiles inside, named according to the container name. - Create a
environment-files
directory inside the project root directory, and move all your DotEnv files inside. - Make Symfony aware that they moved, modify your
composer.json
file as follows:
{
"...": "...",
"extra": {
"...": "...",
"runtime": {
"dotenv_path": "environment-files/.env"
}
}
}
- Create at least a
global-docker-compose.yml
insidedocker-sources
. - Create at least a
[environment-name]-docker-compose.yml
insidedocker-sources
(likedev-docker-compose.yml
). - Notify your Docker Compose files that the environment files they should use are inside this directory.
- Add a shell script inside the project root to start the project Docker containers fast (a sample one is included in this repository:
build-and-run-dev-docker-containers.bat
).
The rest will be part of your project choices. ;)
Image taken from free image stock UnSplash / Guillaume Jaillet.
{% endraw %} <-- keep this for Jekyll to fully bypass this documents, because of the Twig tags.