· 9 min read
Mettre en production une application Symfony 5 avec Heroku
Je reçois très souvent des messages me demandant comment mettre en production une application Symfony. Il existe de nombreuses possibilités pour réaliser cela, aujourd’hui je vais vous présenter une solution simple (et gratuite dans un cadre d'un apprentissage).
Introduction
Je reçois très souvent des messages me demandant comment mettre en production une application Symfony. Il existe de nombreuses possibilités pour réaliser cela, aujourd’hui je vais vous présenter une solution simple (et gratuite dans un cadre d’un apprentissage).
En effet, aujourd’hui nous allons voir ensemble comment déployer une application Symfony (avec une base de donnée) sur les services de Heroku ! Heroku c’est ce que l’on appelle un PaaS (Platform as a Service), et il vous propose de “gérer” pour vous toute la partie infrastructure !
En gros, vous n’avez qu’à vous concentrer sur votre code, Heroku va faire le reste ;-)
Le projet
Histoire de rendre cette découverte plus proche d’un cas réel, je vous propose le cas d’usage suivant :
- On vous demande de développer une petite application permettant aux visiteurs de lister et partager leurs vidéos YouTube favorite
- Pour chacune des vidéos nous voulons stocker l’url et un nom
- Une page doit lister toutes les vidéos (pour la démo nous ne mettrons pas en place de pagination) avec la miniature de la vidéo
- Une page doit permettre de consulter une vidéo, avec le nom et un player YouTube
- Pour faciliter le déploiement de l’application sur Heroku, la base de données devra être une base Postgres (et non pas MySQL ou MariaDB)
Initialiser le projet Symfony
Commençons par initier un nouveau projet Symfony 5.
symfony new youtube-heroku --full
cd youtube-heroku
Mettre en place une base de donnée Postgres avec Docker
Profitons de l’intégration de docker et docker-compose par le serveur Symfony pour mettre en place notre base de données de développement. Créons un fichier docker-compose.yml à la racine du projet.
version: '3'
services:
database:
image: postgres:13-alpine
environment:
POSTGRES_USER: main
POSTGRES_PASSWORD: main
POSTGRES_DB: main
ports: [5432]
La prise en charge de ce fichier par le serveur Symfony nous permet de ne pas avoir besoin de déclarer notre base de données dans le fichier .env, cela va être fait pour nous.
Démarrons le conteneur et le serveur Symfony.
docker-compose up -d
symfony serve -d
Créons la base de données.
symfony console doctrine:database:create
Créons l’entité Youtube
Créons l’entité qui va stocker les URL et les noms des vidéos.
symfony console make:entity Youtube
created: src/Entity/Youtube.php
created: src/Repository/YoutubeRepository.php
Entity generated! Now let's add some fields!
You can always add more fields later manually or by re-running this command.
New property name (press <return> to stop adding fields):
> url
Field type (enter ? to see all types) [string]:
> string
Field length [255]:
> 255
Can this field be null in the database (nullable) (yes/no) [no]:
> no
updated: src/Entity/Youtube.php
Add another property? Enter the property name (or press <return> to stop adding fields):
> name
Field type (enter ? to see all types) [string]:
> string
Field length [255]:
> 255
Can this field be null in the database (nullable) (yes/no) [no]:
> no
updated: src/Entity/Youtube.php
Add another property? Enter the property name (or press <return> to stop adding fields):
>
Success!
N’oublions pas de générer la migration et de l’appliquée !
symfony console make:migration
symfony console d:m:m
Créons le formulaire YoutubeType
Histoire de pouvoir alimenter notre entité, créons-nous un formulaire.
symfony console make:form YoutubeType
The name of Entity or fully qualified model class name that the new form will be bound to (empty for none):
> Youtube
created: src/Form/YoutubeType.php
Success!
Éditons le fichier src/Form/YoutubeType.php pour personnaliser notre formulaire.
<?php
namespace App\Form;
use App\Entity\Youtube;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class YoutubeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('url', UrlType::class)
->add('name', TextType::class)
->add('Submit', SubmitType::class)
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Youtube::class,
]);
}
}
Créons un contrôleur pour gérer nos pages
Il est temps de mettre en place le contrôleur qui gérera nos deux pages.
symfony console make:controller Youtube
Éditons le fichier src/Controller/YoutubeController.php, pour dans un premier temps, afficher le formulaire d’ajout.
<?php
/**
* @Route("/", name="app_home")
*/
public function index(Request $request, EntityManagerInterface $em): Response
{
$youtube = new Youtube();
$form = $this->createForm(YoutubeType::class, $youtube);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$youtube = $form->getData();
$em->persist($youtube);
$em->flush();
return $this->redirectToRoute('app_home');
}
return $this->render('youtube/index.html.twig', [
'form' => $form->createView(),
]);
}
Modifions le fichier /templates/youtube.index.html.twig pour y ajouter le formulaire.
{% extends 'base.html.twig' %}
{% block title %}Best Youtube Video !{% endblock %}
{% block body %}
{{ form(form) }}
{% endblock %}
Ajoutons le code nécessaire pour récupérer toutes les entrées de notre entités YouTube, et envoyons les vers la vue.
<?php
public function index(Request $request, EntityManagerInterface $em, YoutubeRepository $youtubeRepository): Response
{
(...)
return $this->render('youtube/index.html.twig', [
'form' => $form->createView(),
'youtubes' => $youtubeRepository->findAll(),
]);
}
Modifions notre fichier /templates/youtube/index.html.twig pour afficher les liens de vidéos.
{% extends 'base.html.twig' %}
{% block title %}Best Youtube Video !{% endblock %}
{% block body %}
{{ form(form) }}
{% for youtube in youtubes %}
<p>{{ youtube.url }} - {{ youtube.name }}</p>
{% endfor %}
{% endblock %}
Vérifions que tout fonctionne dans un navigateur internet.
Ajoutons un peu de style avec Bootstrap !
Histoire que notre petite application soit un peu plus sympathique, nous allons mettre en place Bootstrap via un CDN (pour un vrai projet une technique plus complète et personnalisable est décrite ici). Modifions donc notre fichier /templates/base.html.twig.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>
{% block title %}Welcome!
{% endblock %}
</title>
{% block stylesheets %}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary mb-5">
<div class="container-fluid">
<a class="navbar-brand" href="#">YoutubeHeroku</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="{{ path('app_home') }}">Toutes les vidéos</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="{{ path('app_home') }}#addvideo">Ajouter une vidéo</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container">
{% block body %}{% endblock %}
</div>
{% block javascripts %}
<!-- JavaScript Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script>
{% endblock %}
</body>
</html>
Indiquons à twig de styliser nos formulaires avec Bootstrap, pour cela modifions le fichier /config/packages/twig.yaml.
twig:
default_path: '%kernel.project_dir%/templates'
form_themes: ['bootstrap_4_layout.html.twig']
Affichons la liste de nos vidéos avec leurs miniatures
Rappelez-vous la demande initiale d’afficher toutes nos vidéos avec leurs vignettes. Occupons-nous de cela.
Pour éviter d’écrire les bouts de code permettant de récupérer les vignettes (et plus tard les players), j’ai trouvé cette petite librairie qui fera le job pour cette démo.
composer require copadia/php-video-url-parser
Nous allons utiliser la bibliothèque dans un filtre Twig custom.
symfony console make:twig-extension
The name of the Twig extension class (e.g. AppExtension):
> YoutubeExtension
created: src/Twig/YoutubeExtension.php
Success!
Éditons le fichier src/Twig/YoutubeExtension.php pour écrire un filtre Twig qui renverra la miniature d’une vidéo YouTube à partir de son URL.
<?php
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
use RicardoFiorani\Matcher\VideoServiceMatcher;
class YoutubeExtension extends AbstractExtension
{
private $youtubeParser;
public function __construct()
{
$this->youtubeParser = new VideoServiceMatcher();
}
public function getFilters(): array
{
return [
// If your filter generates SAFE HTML, you should add a third
// parameter: ['is_safe' => ['html']]
// Reference: https://twig.symfony.com/doc/2.x/advanced.html#automatic-escaping
new TwigFilter('youtube_thumbnail', [$this, 'youtubeThumbnail']),
];
}
public function getFunctions(): array
{
return [
new TwigFunction('function_name', [$this, 'doSomething']),
];
}
public function youtubeThumbnail($value)
{
$video = $this->youtubeParser->parse($value);
return $video->getLargestThumbnail();
}
}
Profitons de ce nouveau filtre Twig pour améliorer notre mise en page dans le fichier /template/youtube/index.html.twig.
{% extends 'base.html.twig' %}
{% block title %}Best Youtube Video !
{% endblock %}
{% block body %}
<h2>Toutes les vidéos</h2>
<div class="row row-cols-1 row-cols-md-2 g-4">
{% for youtube in youtubes %}
<div class="col">
<div class="card shadow p-3 mb-5 bg-white rounded">
<img src="{{ youtube.url|youtube_thumbnail }}" class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title">{{ youtube.name }}</h5>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="mb-5 mt-5" id="addvideo">
<h2>Ajouter une vidéo</h2>
{{ form(form) }}
</div>
{% endblock %}
Affichons notre page, elle devrait avoir un peu plus de style ;-)
Créons la page avec le player Youtube
Éditons notre fichier /src/Controller/YoutubeController.php pour y ajouter une nouvelle route qui affichera une seule vidéo YouTube.
Nous réutiliserons la librairie “copadia/php-video-url-parser” installée plus tôt pour récupérer le code du player YouTube.
<?php
/**
* @Route("/{id}", name="app_video")
*/
public function video(Youtube $youtube): Response
{
return $this->render('youtube/video.html.twig', [
'name' => $youtube->getName(),
'url' => $youtube->getUrl(),
]);
}
Créons le filtre Twig qui récupère-le code du player en éditant notre fichier /src/Twig/ToutubeExtension.php.
<?php
public function youtubePlayer($value)
{
$video = $this->youtubeParser->parse($value);
return $video->getEmbedCode('100%', 500, true, true);
}
Il ne nous reste plus qu’a créer le fichier twig /templates/youtube/video.html.twig.
{% extends 'base.html.twig' %}
{% block title %}Best Youtube Video !
{% endblock %}
{% block body %}
<h2>{{ name }}</h2>
<div>{{ url|youtube_player|raw }}</div>
{% endblock %}
Pour que la boucle soit boucle, créons un lien de notre liste de vidéos vers la page avec le player. Modifions-le fichier /templates/youtube/index.php.
{% extends 'base.html.twig' %}
{% block title %}Best Youtube Video !
{% endblock %}
{% block body %}
<h2>Toutes les vidéos</h2>
<div class="row row-cols-1 row-cols-md-2 g-4">
{% for youtube in youtubes %}
<div class="col">
<div class="card shadow p-3 mb-5 bg-white rounded">
<a href="{{ path('app_video', {'id': youtube.id }) }}"><img src="{{ youtube.url|youtube_thumbnail }}" class="card-img-top"></a>
<div class="card-body">
<h5 class="card-title">{{ youtube.name }}</h5>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="mb-5 mt-5" id="addvideo">
<h2>Ajouter une vidéo</h2>
{{ form(form) }}
</div>
{% endblock %}
Et vérifions le résultat !
Versionnons notre code !
Il est pus que temps de versionner notre code (et de le pousser sur un dépôt distant, GitLab dans mon cas !).
git init
git add .
git commit -m "initial commit"
git remote add origin git@gitlab.com:yoandev.co/youtube-heroku.git
git push -u origin --all
Mise en production avec Heroku !
Bon maintenant que nous avons une application qui fonctionne, il est temps de la mettre en production sur Heroku !
Pour réaliser la suite des opérations vous devez disposer d’un compte (gratuit) sur Heroku et vous devez également installer la CLI de Heroku sur votre poste.
Nous devons initialiser un nouveau projet Heroku dans votre projet.
heroku create
Vous devez ensuite créer un fichier Procfile qui décrit la configuration de votre serveur chez Heroku. Ici nous allons lui demander d’utiliser un serveur Apache, avec php, et de servir le répertoire public/.
echo 'web: heroku-php-apache2 public/' > Procfile
Ajoutons une base de données Postgres à notre configuration Heroku.
heroku addons:create heroku-postgresql:hobby-dev
Passons la variable d’environnement “APP_ENV” sur “PROD” pour indiquer que notre application s’exécutera en production.
heroku config:set APP_ENV=prod
Ajoutons une étape dans notre fichier composer.json, pour indiquer que lors du déploiement, les migrations devrons être appliquées.
(...),
"scripts": {
(...),
"compile": [
"php bin/console doctrine:migrations:migrate"
]
},
(...)
Installons le pack Apache pour Symfony (nécessaire pour faire fonctionner les ré écriture d’url). Grosso-modo cela va mettre en place un .htaccess dans votre répertoire /public.
composer require symfony/apache-pack
Commitons ces changements dans notre dépôt Git.
git add .
git commit -m "add config heroku"
git push
Mettons en production !!! Et OUI c’est aussi simple que cela !
git push heroku master
Et ouvrons notre application dans un navigateur web. Oui, votre application est en production et vous pouvez partager l’URL !
heroku open
Conclusions et dépôt GitLab
Dans cet article nous avons développé très rapidement une mini application, qui interagie avec une base de données et nous avons pu expérimenter un déploiement avec Heroku. Vous avez pu vous rendre compte de l’immense simplicité que vous offre Heroku et surtout de la rapidité de mise en œuvre !
Les sources du projet sont disponibles sur ce dépôt GitLab.