Projet OverKill
Projet réaliser dans le cadre d'une série pour la chaine Youtube YoanDev.
Ce projet est volontairement overkill et fait usage d'une débauche de techno, uniquement dans un objectif récréatif.
1 - Création du projet Symfony
symfony new OverKill --full
cd OverKill
symfony serve -d
2 - Création du Docker-compose
On ajoute Redis
redis:
image: redis:5-alpine
ports: [6379]
Puis n8n
n8n:
image: n8nio/n8n
ports:
- 5678:5678
Puis Imaginary
imaginary:
image: h2non/imaginary:latest
volumes:
- ./images:/mnt/data
environment:
PORT: 9000
command: -enable-url-source -mount /mnt/data
ports:
- "9000:9000"
N'oublions pas RabbitMQ
rabbitmq:
image: rabbitmq:3.7-management
ports: [5672, 15672]
Et enfin, MinIO
minio:
image: minio/minio
environment:
MINIO_ROOT_USER: access1234
MINIO_ROOT_PASSWORD: secret1234
volumes:
- ./data/minio:/data
command: server /data --console-address ":9001"
ports:
- 9090:9000
- 9001:9001
Et finalement, nous pouvons démarrer l'ensemble
docker-compose up -d
3 - Installation de Webpack Encore et Pico.css
- Installons Webpack Encore et Pico.css
composer require symfony/webpack-encore-bundle
npm install
npm install @picocss/pico
- Modification du
/assets/app.js
/*
* Welcome to your app's main JavaScript file!
*
* We recommend including the built version of this JavaScript file
* (and its CSS file) in your base layout (base.html.twig).
*/
// any CSS you import will output into a single css file (app.css in this case)
import './styles/app.scss';
// start the Stimulus application
import './bootstrap';
- Rennomer
/assets/styles/app.css
en/assets/styles/app.scss
- Installer
npm install sass-loader@^12.0.0 sass --save-dev
- Décommenter la ligne
.enableSassLoader()
dans le fichierwebpack.config.js
- Lancer la compilation en mode watch :
npm run watch
- Remplacer le contenu de
/assets/styles/app.sccs
par
@import "~@picocss/pico/scss/pico.scss";
- Modifier le fichier /templates/base.html.twig
<!DOCTYPE html>
<html id="theme" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Welcome!{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>">
{# Run `composer require symfony/webpack-encore-bundle` to start using Symfony UX #}
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
{% endblock %}
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
{% endblock %}
</head>
<body>
<main class="container">
<!-- Header -->
<header class="container">
<hgroup>
<h1>OverKill</h1>
<h2>Un truc complètement OverKill pour le fun !</h2>
</hgroup>
</header>
<!-- ./ Header -->
{% block body %}{% endblock %}
</main>
</body>
</html>
4 - Création d'un controller overkill
Commençon par créer un controlleur du nom de overkill
symfony console make:controller overkill
Et modifions le :
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class OverkillController extends AbstractController
{
/**
* @Route("/", name="overkill")
*/
public function index(): Response
{
return $this->render('overkill/index.html.twig', [
'controller_name' => 'OverkillController',
]);
}
}
5 - Entité, Vich Uploader et Stockage Objet
- Créons une entité Upload
symfony console make:entity Upload
> imageName
symfony console make:migration
symfony console d:m:m
- Installation Vich
composer require vich/uploader-bundle
- On paramètre Vich
# config/packages/vich_uploader.yaml or app/config/config.yml
vich_uploader:
db_driver: orm
mappings:
upload:
uri_prefix: /upload
upload_destination: '%kernel.project_dir%/public/upload'
- On adpate notre entitée pour utilise Vich
<?php
namespace App\Entity;
use App\Repository\UploadRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
* @ORM\Entity(repositoryClass=UploadRepository::class)
* @Vich\Uploadable
*/
class Upload
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $imageName;
public function getId(): ?int
{
return $this->id;
}
/**
* NOTE: This is not a mapped field of entity metadata, just a simple property.
*
* @Vich\UploadableField(mapping="upload", fileNameProperty="imageName")
*
* @var File|null
*/
private $imageFile;
public function getImageName(): ?string
{
return $this->imageName;
}
public function setImageName(string $imageName): self
{
$this->imageName = $imageName;
return $this;
}
/**
* If manually uploading a file (i.e. not using Symfony Form) ensure an instance
* of 'UploadedFile' is injected into this setter to trigger the update. If this
* bundle's configuration parameter 'inject_on_load' is set to 'true' this setter
* must be able to accept an instance of 'File' as the bundle will inject one here
* during Doctrine hydration.
*
* @param File|\Symfony\Component\HttpFoundation\File\UploadedFile|null $imageFile
*/
public function setImageFile(?File $imageFile = null): void
{
$this->imageFile = $imageFile;
if (null !== $imageFile) {
// It is required that at least one field changes if you are using doctrine
// otherwise the event listeners won't be called and the file is lost
$this->updatedAt = new \DateTimeImmutable();
}
}
public function getImageFile(): ?File
{
return $this->imageFile;
}
}
- Créons un formulaire d'upload
symfony console make:form UploadType
> Upload
Et modifions le :
<?php
namespace App\Form;
use App\Entity\Upload;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Vich\UploaderBundle\Form\Type\VichImageType;
class UploadType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('imageFile', VichImageType::class, [
'required' => false,
'allow_delete' => true,
'download_uri' => false,
'image_uri' => false,
'label' => false,
])
->add('submit', SubmitType::class, [
'label' => 'Upload',
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Upload::class,
]);
}
}
Et enfin, affichons le dans notre page, d'abord le controlleur
/**
* @Route("/", name="overkill")
*/
public function index(Request $request): Response
{
$upload = new Upload();
$form = $this->createForm(UploadType::class, $upload);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($upload);
$this->entityManager->flush();
return $this->redirectToRoute('overkill');
}
return $this->render('overkill/index.html.twig', [
'form' => $form->createView(),
]);
}
Puis le fichier Twig
{% extends 'base.html.twig' %}
{% block title %}Hello OverkillController!{% endblock %}
{% block body %}
{{ form(form) }}
{% endblock %}
Installons la surcouche pour utiliser MinIO
composer require league/flysystem-bundle
league/flysystem-aws-s3-v3
Et configurons Flysystem
# /config/packages/flysystem.yaml
flysystem:
storages:
default.storage:
adapter: 'local'
options:
directory: '%kernel.project_dir%/public/fichier'
aws.storage:
adapter: 'aws'
options:
client: Aws\S3\S3Client
bucket: 'fichier'
Puis, modifions la configuration de Vich pour qu'il utilise Flysystem :
vich_uploader:
db_driver: orm
storage: flysystem
mappings:
upload:
uri_prefix: /upload
upload_destination: aws.storage
Et enfin, déclarons un service dans service.yml
Aws\S3\S3Client:
arguments:
- version: 'latest'
region: 'eu-east-1'
endpoint: '127.0.0.1:9090'
credentials:
key: 'access1234'
secret: 'secret1234'
Créon un Bucket fichier dans MinIO :
- Ouvrir une session
- Menu Buckets
- Bucket name: fichier
Testons à nouveau d'upload un fichier : il est dans MiniO
6 - Création d'un Login/Register
- Création en premier lieu un user
symfony console make:user
The name of the security user class (e.g. User) [User]:
> User
Do you want to store user data in the database (via Doctrine)? (yes/no) [yes]:
> yes
Enter a property name that will be the unique "display" name for the user (e.g. email, username, uuid) [email]:
> email
Will this app need to hash/check user passwords? Choose No if passwords are not needed or will be checked/hashed by some other system (e.g. a single sign-on server).
Does this app need to hash/check user passwords? (yes/no) [yes]:
> yes
created: src/Entity/User.php
created: src/Repository/UserRepository.php
updated: src/Entity/User.php
updated: config/packages/security.yaml
Success!
- Puis gérons les migrations
symfony console make:migration
symfony console d:m:m
- Créons un système de création de compte
symfony console make:registration-form
Creating a registration form for App\Entity\User
Do you want to add a @UniqueEntity validation annotation on your User class to make sure duplicate accounts aren't created? (yes/no) [yes]:
> yes
Do you want to send an email to verify the user's email address after registration? (yes/no) [yes]:
> no
Do you want to automatically authenticate the user after registration? (yes/no) [yes]:
> yes
! [NOTE] No Guard authenticators found - so your user won't be automatically authenticated after registering.
What route should the user be redirected to after registration?:
[0 ] _wdt
[1 ] _profiler_home
[2 ] _profiler_search
[3 ] _profiler_search_bar
[4 ] _profiler_phpinfo
[5 ] _profiler_search_results
[6 ] _profiler_open_file
[7 ] _profiler
[8 ] _profiler_router
[9 ] _profiler_exception
[10] _profiler_exception_css
[11] overkill
[12] _preview_error
> 11
updated: src/Entity/User.php
created: src/Form/RegistrationFormType.php
created: src/Controller/RegistrationController.php
created: templates/registration/register.html.twig
Success!
- On supprime le bout de code suivant du fichier
src/Form/RegistrationFormType.php
/* On supprime cette portion de code */
->add('agreeTerms', CheckboxType::class, [
'mapped' => false,
'constraints' => [
new IsTrue([
'message' => 'You should agree to our terms.',
]),
],
])
- Et on supprime sont equivalent dans le fichier Twig
templates/registration/register.html.twig
{{ form_row(registrationForm.agreeTerms) }}
-
On peut désormais consulter l'url
/register
de son application, et tester la création d'un compte ! -
Et enfin, créons une page de login
symfony console make:auth
What style of authentication do you want? [Empty authenticator]:
[0] Empty authenticator
[1] Login form authenticator
> 1
The class name of the authenticator to create (e.g. AppCustomAuthenticator):
> AppAuthenticator
Choose a name for the controller class (e.g. SecurityController) [SecurityController]:
> SecurityController
Do you want to generate a '/logout' URL? (yes/no) [yes]:
> yes
created: src/Security/AppAuthenticator.php
updated: config/packages/security.yaml
created: src/Controller/SecurityController.php
created: templates/security/login.html.twig
Success!
- N'oublions pas de modifier le fichier
src/Security/AppAuthenticator.php
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class AppAuthenticator extends AbstractLoginFormAuthenticator
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'app_login';
private UrlGeneratorInterface $urlGenerator;
public function __construct(UrlGeneratorInterface $urlGenerator)
{
$this->urlGenerator = $urlGenerator;
}
public function authenticate(Request $request): Passport
{
$email = $request->request->get('email', '');
$request->getSession()->set(Security::LAST_USERNAME, $email);
return new Passport(
new UserBadge($email),
new PasswordCredentials($request->request->get('password', '')),
[
new CsrfTokenBadge('authenticate', $request->request->get('_csrf_token')),
]
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
return new RedirectResponse($targetPath);
}
// For example:
return new RedirectResponse($this->urlGenerator->generate('overkill'));
//throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
}
protected function getLoginUrl(Request $request): string
{
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
}
}
- Et pour tout cela est un sens ^^, protégons la page
overkill
, en ajoutons simplement cela à notre controller.
$this->denyAccessUnlessGranted('ROLE_USER');
7 - Stockons la session dans REDIS
- Modifions le fichier
config/services.yaml
services:
# ...
Redis:
class: Redis
calls:
- connect:
- '%env(REDIS_HOST)%'
- '%env(int:REDIS_PORT)%'
Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler:
arguments:
- '@Redis'
- Puis, le fichier
config/packages/framework.yaml
# ...
session:
handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler
cookie_secure: auto
cookie_samesite: lax
storage_factory_id: session.storage.factory.native
# ...
- Et... c'est tout, notre serveur Redis étant dèja lancé :)
8 - Créons une jointure entre User et Upload, et utilisons la lors d'un Upload
Comme notre objectif est d'envoyer par email le résultat de la transformation d'image à un utilisateur, créons une jointure entre les deux entités!
- Créons la relation entre les deux entités
symfony console make:entity Upload
New property name (press <return> to stop adding fields):
> uploadBy
Field type (enter ? to see all types) [string]:
> relation
What class should this entity be related to?:
> User
Relation type? [ManyToOne, OneToMany, ManyToMany, OneToOne]:
> ManyToOne
Is the Upload.uploadBy property allowed to be null (nullable)? (yes/no) [yes]:
> yes
Do you want to add a new property to User so that you can access/update Upload objects from it - e.g. $user->getUploads()? (yes/no) [yes]:
> yes
A new property will also be added to the User class so that you can access the related Upload objects from it.
New field name inside User [uploads]:
> uploads
updated: src/Entity/Upload.php
updated: src/Entity/User.php
Success!
- Et comme d'habitude, on pense aux migrations !
symfony console make:migration
symfony console d:m:m:
- Utilisons cette relation lors de l'upload, en ajoutant un
$upload->setUploadBy()
lors de la soumission d'un formulaire d'upload.
/**
* @Route("/", name="overkill")
*/
public function index(Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_USER');
$upload = new Upload();
$form = $this->createForm(UploadType::class, $upload);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$upload->setUploadBy($this->getUser());
$this->entityManager->persist($upload);
$this->entityManager->flush();
return $this->redirectToRoute('overkill');
}
return $this->render('overkill/index.html.twig', [
'form' => $form->createView(),
]);
}
9 - Envoyons notre Upload et l'adresse Email dans un RabbitMQ
- Débutons pas le début, et installons le composant Messenger
composer require symfony/messenger
-
Puis créons le Message et sont Handler
- D'abord le message dans un fichier
src/Message/UploadMessage.php
<?php namespace App\Message; class UploadMessage { private $upload; private $user; public function __construct(string $upload, string $user) { $this->upload = $upload; $this->user = $user; } public function getUpload(): string { return $this->upload; } public function getUser(): string { return $this->user; } }
- Puis le handler, dans le fichier
src/MessageHandler/UploadMessageHandler.php
<?php namespace App\MessageHandler; use App\Message\UploadMessage; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; class UploadMessageHandler implements MessageHandlerInterface { public function __invoke(UploadMessage $message) { dump($message); } }
- D'abord le message dans un fichier
-
Ensuite, dispatchons un message lors de la soumission du formulaire d'upload.
# ...
use Symfony\Component\Messenger\MessageBusInterface;
# ...
public function index(Request $request, MessageBusInterface $bus, UploaderHelper $helper): Response
{
# ...
if ($form->isSubmitted() && $form->isValid()) {
# ...
$bus->dispatch(new UploadMessage($upload->getImageFile(), $this->getUser()->getUserIdentifier()));
# ...
}
}
- Et pour finir, passons à l'utilisation de notre RabbitMQ et editant le fichier
config/packages/messenger.yaml
framework:
messenger:
# Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
# failure_transport: failed
transports:
# https://symfony.com/doc/current/messenger.html#transport-configuration
async: '%env(RABBITMQ_DSN)%'
# failed: 'doctrine://default?queue_name=failed'
# sync: 'sync://'
routing:
# Route your messages to the transports
'App\Message\UploadMessage': async
serializer:
default_serializer: messenger.transport.symfony_serializer
symfony_serializer:
format: json
context: { }
- Nous pouvons faire un test est constater que le message est bien émis dans le bus de message RabbitMQ, avec les deux informations qui nous interesse (Le nom de l'upload, et l'adresse Email).