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.
<– keep this for Jekyll to fully bypass this documents, because of the Twig tags.
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.
/files-you-will-need
directory of this repository. This directory reflects the exact, expected directory tree as you grow it.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. |
A complementary faster way would be to read the framework best practices (mostly focused on Symfony). |
The fastest way to me, trading possibilities for opinions, should be this repository. |
All contributions and suggestions are welcome. 😇
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.
sudo su && apt-get update && apt-get install php8
at least.brew install php
.C:\php[VERSION DIGITS]
.PATH
system variable (Windows + R
, type PATH
, hit Enter
, click on Environment Variables
, then your user variables, edit the PATH
entry and append the previously unzipped directory path to it).php.ini-development
to php.ini
in yourdev
environment.
memory_limit
to 8M
max_ececution_time
to 200
upload_max_filesize
to 200M
extension_dir
directive. Careful: it’s different on Windows vs the rest of the world.bz2, curl, gd, intl, mbstring, mysqli, openssl, pdo_mysql, sodium
.date.timezone
directive to your local timezone.
"Europe/Paris"
, for instance.symfony check:requirements
in any environement. Ignore the “Enable or install a PHP accelerator” advice in development.symfony new [your_project_directory_name]
.README.md
file inside the root directory and put everything you can document inside, at least those sections:
readme-sources
directory at the root of your project. Anything that is included in MarkDown documentation goes there.brew install mysql
then brew services start mysql
..env.local
file by copying the .env
one.APP_SECRET
value to anything.DATABASE_URL
to the appropriate values to connect to your RDBMS.File > Manage IDE settings > Import settings
and pick up the phpstorm-settings.zip
included in this repository.git config --global rebase.autostash true
git config --global rebase.autosquash true
git config --global rebase.abbreviateCommands true
git config --global rebase.instructionFormat "[%an @ %ar] %s"
git config --global core.excludesfile ~/.gitignore
git config --global core.autocrlf false
echo ".idea" >> ~/.gitignore
echo ".DS_Store" >> ~/.gitignore
echo ".vscode" >> ~/.gitignore
composer require --dev phpstan/phpstan
).
phpstan.neon
).composer require --dev phpstan/extension-installer
,composer require --dev phpstan/phpstan-symfony
,composer require --dev phpstan/phpstan-doctrine
.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
composer require --dev friendsofphp/php-cs-fixer
).
.php-cs-fixer.dist.php
).composer require --dev vimeo/psalm
).
php vendor/bin/psalm --init
). If you don’t, just use the one in this repository (psalm.xml
).composer require --dev psalm/plugin-symfony
and composer require --dev weirdan/doctrine-psalm-plugin
.php
”, they all share this and slows down your CLI calls.csfixer
, stan
and psalm
, with appropriate shell extensions (.bat
or .sh
)..editorconfig
file at the root directory of your project to ensure your IDE settings don’t get messed up.
LF
characters for newlines and 4 spaces as tabulations..editorconfig
).serverVersion
parameter. Use dynamic parameters with $
to set them up.composer.json
file, group requirements, to allow better upgrades, like:
ext-ctype
, etc.)require-dev
: group by Symfony-related, PHP-CS-Fixer, static analyzers, etc.Admin
and Front
, plus one for each of your application “modules”.composer require twig
composer require twig/extra-bundle
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'
# ...
admin_layout.html.twig
goes to templates/admin
front_layout.html.twig
goes to templates/front
base.html.twig
goes to templates
[domain]_layout.html.twig
.<html>
tag in base.html.twig
template and override it in each [domain]_layout.html.twig
templates.base.html.twig
, at least (including default strategy):
<html>
defaults: language and direction.<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
.<link rel="canonical" href="{{ url(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) }}">
).<link>
if applicable, including the default one.browserconfig.xml
file)./templates
directory should at least contain:
admin
common
front
registration
security
base.html.twig
/templates
use snake_case only for their filenames.LiipImagineBundle
preset through a custom form theme (add it in config/twig.yaml
).templates/bundles/TwigBundle/Exception
, don’t forget to start with a generic one: error.html.twig
.info notice warning error
) and add “success
”, too.|raw
to use links and HTML, but NEVER output anything from the user or not sanitized in their contents (could be a security flaw).files-you-will-need/templates/common/_breadcrumb.html.twig
).<title>
markup doesn’t contain any HTML markup nor non-escaped special characters. Do the same for all the <head>
contents.async
/defer
) > images > audio > video.<noscript>
alternates, this is getting almost deprecated, but it’s not yet totally the case.dns-prefetch
for anything located on other domains, preconnect
for potential follow-ups, preload
for probable further needs and preload
for certain calls.composer require orm
).composer require migrations
).
composer remove doctrine/migrations
, same for dependencies)./migrations
directory at the root directory of your project.composer require --dev maker
).php bin/console make:crud
.Model
directory under /src
to store all models that are not entities./src
use CamelCase only for their filenames.php bin/console doctrine:schema:validate
.php bin/console doctrine:migrations:up-to-date
.[domain]/[entity type]/{slug}-{id}
using AsciiSlugger
. Remove the words shorter than 3 characters.fetch="EAGER"
on your
ManyToOne` relations.composer require symfony/translation
.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
# ...
config/services.yaml
. Mine looks like this:parameters:
# ...
app.supported_locales:
- en
- fr
# ...
messages.[yourdefaultlanguage].yaml
in the translations
folder. Don’t use ICU unless you know why.validators.[yourdefaultlanguage].yaml
in the translations
folder. Don’t use ICU unless you know why.front.forms.users.create.name.help
)./translations
use snake_case only for their filenames.Admin
and Front
(that’s already enough):
src/Controller
.src/Form
.src/Model
./templates
directory: create /admin
and /front
subdirectories in it.src/Controller/Admin
, and their templates to templates/admin
.messages.[language].yaml
translations file, start root keys with your domains, all snake case. At least they should look like this:front:
admin:
composer require symfony/validator doctrine/annotations
.@Assert
HistoryTrackableEntity.php
in this repository.src/Entities
directory.@ORM\HasLifecycleCallbacks
annotation to all your entities.use HistoryTrackableEntity;
after each Entity class opening bracket (first line, before constants and properties).php bin/console doctrine:schema:update
or use migrations if you chose to use them).empty_data
provided, especially for non-nullable fields.help
for all fields with filling guidelines.label
for all fields.'attr' => ['placeholder' => 'your.placeholder.translation.key']
).model/Admin/AdminMenus.php
and model/Front/FrontMenus.php
files.public static array $adminMenuTree = [];
array structure that returns all menus. See included FrontMenus.php
class.make:twig-extension
or just copy the one included in this repository (MenuExtension.php
).templates/common/_menu.html.twig
) or just copy the one included in this repository.src/Repository
)._delete_form.html.twig
and _form.html.twig
to templates/admin/common
.{cache}
markup).php bin/console make:user
.php bin/console make:auth
. Pick a custom authenticator name.php bin/console make:crud
.new
and edit
actions of the generated controller so that they encode passwords using UserPasswordHasherInterface
.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:
access_control
directive in the file aboveUserController
with: #[Route('admin/user')]
.src/Controller/Admin
directory,php bin/console make:registration-form
).SecurityController.php
and RegistrationController.php
stay at the root of src/Controller
directory.composer require tgalopin/html-sanitizer-bundle
and sanitize all user-generated fields that are displayed without escaping.framework.trusted_hosts
and framework.trusted_headers
parameters in your config/packages/prod/framework.yaml
file (or any environment file needed).https://github.com/fabpot/local-php-security-checker
on a regular basis (add it to your CI and your local habits).config/twig.yaml
and set this:twig:
# ...
form_themes: [ 'tailwind_2_layout.html.twig' ]
# ...
sudo su && apt-get update && apt-get install nodejs npm
at least.composer require encore
and npm install
.npm install -D tailwindcss postcss-loader purgecss-webpack-plugin glob-all path autoprefixer
.postcss.config.js
tailwind.config.js
webpack.config.js
npm run build
.assets
:
controllers
favicon
fonts
images
styles
app.js
bootstrap.js
controllers.json
base.html.twig
and assets/favicon/browserconfig.xml
. Use a favicon generator for that and a manifest file.assets/images
(name it ogimage.jpg
if you copied the included files of this project).start-project
included scripts.srcset
with multiple resolution sources, to adapt to screen resolution.height
and width
dimensions on all image sources to allow X/Y space reservation on browsers.alt
attribute which explains what they contain.<body>
markup. At worst put it all just before the </body>
closing tag.symfony check:security
to validate that your project has no known vulnerabilities from its dependencies.symfony check:requirements
while on the production server (see above). This time, pay attention to OPCache (see below).production-deployment.sh.dist
) for a start.production-deployment.sh.dist
to [environment]-deployment.sh
..gitignore
and only on destination servers filesystems. Don’t version the final ones.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
# ...
.gitlab-ci.yml
(checks that you didn’t forget PHP-CS-Fixer).robots.txt
file inside the public
directory. Use the one provided in this repository for a start.site.webmanifest
file inside the public
directory. Use the one provided in this repository for a start.base.html.twig
file: <link rel="manifest" href="{{ asset('site.webmanifest') }}">
.symfony local:server:ca:install
symfony server:start -d
npm run watch
npm run build
.composer require symfony/apache-pack
.tests/
directory. You will need WAY less of them as long as your code passes PHP-Stan and Psalm maximum level scans.memory_limit
is set just around 8M
for CLI and 1M
for web SAPIs. More if PHP processes files.max_execution_time
is set to 1200
for CLI and 30
for wep SAPIs. More for CLI if your app has heavy CLI processing.realpath_cache_size
is set to 512K
for CLI and 16M
for wep SAPIs.realpath_cache_ttl
is set to 600
for CLI and 2000
for wep SAPIs.opcache.preload=[your project path]/config/preload.php
and set parameters.container.dumper.inline_factories: true
in config/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)composer dump-autoload --no-dev --classmap-authoritative
).755
) your upload, cache and logs directories. Use umask if necessary.git fetch --all
git rev-parse refs/remotes/origin/master
(or any deployment branch)git checkout -f master
(or any deployment branch)composer require --dev symfony/profiler-pack
) and take a look at:
{% cache 'your_key' ttl(600) %}{% endcache %}
for anything locally cacheable especially the results of Twig functions that use DB.composer require liip/imagine-bundle
) and use srcset
HTML5 attribute.php bin/console debug:translation [your locale]
).php bin/console lint:container
.SameSite / strict
cookies.APP_SECRET
variable in your DotEnv file, one for each different environement.composer.json
file. Make them use only patch upgrades within Semantic versioning.docker-sources
directory inside the project root directory. You’ll put your Docker Compose files inside.docker-sources/containers-config
directory inside. You’ll put your Dockerfiles inside, named according to the container name.environment-files
directory inside the project root directory, and move all your DotEnv files inside.composer.json
file as follows:{
"...": "...",
"extra": {
"...": "...",
"runtime": {
"dotenv_path": "environment-files/.env"
}
}
}
global-docker-compose.yml
inside docker-sources
.[environment-name]-docker-compose.yml
inside docker-sources
(like dev-docker-compose.yml
).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.
<– keep this for Jekyll to fully bypass this documents, because of the Twig tags.