· 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).

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.

Back to Blog