On veut que des utilisateurs puissent indiquer leur participation aux événements
Relation
Ajouter une relation ManyToMany entre les User
et Event
:
php bin/console make:entity
[output]
[output] Class name of the entity to create or update (e.g. AgreeablePizza):
> User
[output]
[output] Your entity already exists! So let's add some new fields!
[output]
[output] New property name (press <return> to stop adding fields):
> events
[output]
[output] Field type (enter ? to see all types) [string]:
> relation
[output]
[output] What class should this entity be related to?:
> Event
[output]
[output]What type of relationship is this?
[output] ------------ ------------------------------------------------------------------
[output] Type Description
[output] ------------ ------------------------------------------------------------------
[output] ManyToOne Each User relates to (has) one Event.
[output] Each Event can relate to (can have) many User objects.
[output]
[output] OneToMany Each User can relate to (can have) many Event objects.
[output] Each Event relates to (has) one User.
[output]
[output] ManyToMany Each User can relate to (can have) many Event objects.
[output] Each Event can also relate to (can also have) many User objects.
[output]
[output] OneToOne Each User relates to (has) exactly one Event.
[output] Each Event also relates to (has) exactly one User.
[output] ------------ ------------------------------------------------------------------
[output]
[output] Relation type? [ManyToOne, OneToMany, ManyToMany, OneToOne]:
> ManyToMany
[output]
[output] Do you want to add a new property to Event so that you can access/update User objects from it - e.g. $event->getUsers()? (yes/no) [yes]:
>
[output]
[output] A new property will also be added to the Event class so that you can access the related User objects from it.
[output]
[output] New field name inside Event [users]:
> participants
Générer et exécuter une migration:
php bin/console make:migration
php bin/console doctrine:migrations:migrate
Mettre à jour les fixtures pour les utilisateurs afin qu'ils participent à des événements:
App\Entity\User:
user_{1..100}:
# ...
events: '<numberBetween(0, 10)>x @event_*'
Note: on utilise la notation
@reference_*
pour récupérer une référence aléatoire aux entitésEvent
, et le préfixeNx
oùN
est dynamique et permet d'associer entre 0 et 10 références à des entitésEvent
.
Puis on recharge les fixtures:
php bin/console hautelook:fixtures:load
Liste d'événements
Pour pouvoir afficher les événements passés et futurs de l'utilisateur sur sa page de profil, ajouter deux méthodes dans l'entité User
comme nous l'avons fait pour les lieux:
/**
* Liste des prochains événements
* @return Collection<int, Event>
*/
public function getUpcomingEvents(): Collection
{
return $this->events
->filter(fn (Event $event) => $event->getStartAt() > new \DateTime())
->matching(Criteria::create()->orderBy(['startAt' => 'ASC']))
;
}
/**
* Liste des événements passés
* @return Collection<int, Event>
*/
public function getPastEvents(): Collection
{
return $this->events
->filter(fn (Event $event) => $event->getStartAt() < new \DateTime())
->matching(Criteria::create()->orderBy(['startAt' => 'DESC']))
;
}
Sur la page de profil, ajouter la liste des événements auxquels participe l'utilisateur avec le même système d'onglet que pour la page des lieux:
{% block content %}
<div class="section">
<h1 class="title">Profil de {{ app.user.pseudo }}:</h1>
<div class="content">
<p>
<strong>Email</strong>: {{ app.user.email }}<br>
<strong>Pseudo</strong>: {{ app.user.pseudo }}<br>
</p>
<h3>Événements:</h3>
</div>
<div class="tabs is-fullwidth">
<ul>
<li class="{{ app.request.query.get('tab', 'upcoming') == 'upcoming' ? 'is-active' : '' }}">
<a href="{{ path('user_profile', {tab: 'upcoming'}) }}">
<span class="icon is-small"><i class="fa-regular fa-star"></i></span>
<span>À venir ({{ app.user.upcomingEvents|length }})</span>
</a>
</li>
<li class="{{ app.request.query.get('tab') == 'past' ? 'is-active' : '' }}">
<a href="{{ path('user_profile', {tab: 'past'}) }}">
<span class="icon is-small"><i class="fa-solid fa-clock-rotate-left"></i></span>
<span>Passés ({{ app.user.pastEvents|length }})</span>
</a>
</li>
</ul>
</div>
<div class="{{ app.request.query.get('tab', 'upcoming') == 'upcoming' ? '' : 'is-hidden' }}">
{% for event in app.user.upcomingEvents %}
{% include '_includes/event_card.html.twig' with {event} %}
{% else %}
<div class="card">
<div class="card-content">
<div class="content">
<p class="title is-4">
<span class="icon"><i class="fa-regular fa-face-frown"></i></span>
<span>Oups !</span>
</p>
<p class="subtitle is-6">
Vous n'avez aucun événement prévu prochainement.
</p>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="{{ app.request.query.get('tab') == 'past' ? '' : 'is-hidden' }}">
{% for event in app.user.pastEvents %}
{% include '_includes/event_card.html.twig' with {event} %}
{% else %}
<div class="card">
<div class="card-content">
<div class="content">
<p class="title is-4">
<span class="icon"><i class="fa-regular fa-face-frown"></i></span>
<span>Oups !</span>
</p>
<p class="subtitle is-6">
Vous n'avez aucun événement passé pour le moment.
</p>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
Participation aux événements
Voter
Pour pouvoir participer à un événement ou annuler sa participation, il faudra que l'utilisateur courant soit connecté. Mais il faudra également vérifier que l'événement n'est pas déjà passé !
Dans ce cas, une simple vérification de rôle n'est pas suffisant, il faut vérifier au cas par cas. Plutôt que de contourner le système de sécurité de Symfony avec une "solution maison", on peut utiliser des voters.
Un voter est un service chargé de vérifier un droit d'accès. Ces droits sont représentés sous la forme d'attributs en chaîne de caractères, avec le nom de notre choix. Nous pourrons ensuite utiliser les fonctions de vérification de droits d'accès comme pour les rôles. En réalité, la vérification des rôles est effectuée par un voter !
Créer un voter pour la gestion de participation:
php bin/console make:voter
[output]
[output] The name of the security voter class (e.g. BlogPostVoter):
> EventParticipationVoter
[output]
[output] created: src/Security/Voter/EventParticipationVoter.php
La méthode supports()
doit indiquer si elle est en mesure de gérer le droit d'accès, en fonction de l'attribut testé et du sujet du droit d'accès.
Le voter servira a vérifier la participation à un événement et vérifier si on peut changer sa participation:
class EventParticipationVoter extends Voter
{
public const EDIT = 'EVENT_PARTICIPATION_EDIT';
public const IS_ACTIVE = 'EVENT_PARTICIPATION_IS_ACTIVE';
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, [self::EDIT, self::IS_ACTIVE]) && $subject instanceof Event;
}
// ...
}
Si supports()
retourne true
, la méthode voteOnAttribute()
est exécutée pour vérifier le droit d'accès:
/**
* @param Event $subject
*/
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
// Utilisateur non connecté
if (!$user instanceof User) {
return false;
}
return match ($attribute) {
// L'événement ne doit pas avoir commencé pour modifier sa participation
self::EDIT => null !== $subject->getStartAt() && $subject->getStartAt() > new \DateTime(),
self::IS_ACTIVE => $user->getEvents()->contains($subject),
default => false,
};
}
Route
Nous allons créer une route dans EventController
pour indiquer ou annuler une participation à un événement. Le chemin comporte des paramètres pour identifier l'événement concerné ainsi qu'un jeton CSRF pour protéger l'action:
use Symfony\Component\Security\Core\Security;
// ...
#[Route('/event/{id}/participate/{token}', name: 'participation_edit')]
#[IsGranted('EVENT_PARTICIPATION_EDIT', subject: 'event')]
public function eventParticipation(
Event $event,
string $token,
Request $request,
EntityManagerInterface $entityManager,
Security $security,
): Response {
// Récupération de l'utilisateur courant
$user = $security->getUser();
// Vérification du jeton CSRF
if ($user instanceof User && $this->isCsrfTokenValid('event-participation-edit', $token)) {
// Ajoute/supprime la participation selon la présence du paramètre "cancel" dans l'URL
if ($request->query->has('cancel')) {
$user->removeEvent($event);
} else {
$user->addEvent($event);
}
// Sauvegarde en base de données
$entityManager->flush();
}
// Redirection vers la page précédente
$previousPage = $request->headers->get('referer');
if (null !== $previousPage) {
return $this->redirect($previousPage);
}
// ... ou sur la page de l'événement par défaut
return $this->redirectToRoute('event_page', [
'id' => $event->getId(),
]);
}
Puisqu'un voter gère le cas par cas sur un sujet, il est important de ne pas oublier l'argument subject
dans l'attribut.
Template
Créer un template /templates/_includes/event_participation.html.twig pour afficher les liens de participation. Encore une fois, la fonction is_granted()
nécessite un second argument sur lequel accorder ou refuser le droit d'accès. On génère le jeton CSRF avec la fonction Twig dédiée:
{% if is_granted('ROLE_USER') %}
{% if is_granted('EVENT_PARTICIPATION_EDIT', event) %}
{% if is_granted('EVENT_PARTICIPATION_IS_ACTIVE', event) %}
<a href="{{ path('event_participation_edit', {id: event.id, token: csrf_token('event-participation-edit'), cancel: true}) }}" class="button is-danger is-outlined my-4">
Je ne participe plus
</a>
{% else %}
<a href="{{ path('event_participation_edit', {id: event.id, token: csrf_token('event-participation-edit')}) }}" class="button is-success is-outlined my-4">
Je participe !
</a>
{% endif %}
{% elseif is_granted('EVENT_PARTICIPATION_IS_ACTIVE', event) %}
<div class="notification is-info is-light">
Vous avez participé à l'événement.
</div>
{% endif %}
{% endif %}
Dans /templates/_includes/event_card.html.twig, ajouter une icone pour afficher le nombre de participants, puis inclure les liens de participation avant la description:
<div class="box columns my-6 p-0">
{# ... #}
<div class="column">
<div class="content">
{# ... #}
<p class="is-size-6">
{# ... #}
<i class="fa-solid fa-clock"></i> {{ event.startAt|date('H\\hi') }} - {{ event.endAt|date('H\\hi') }}<br>
<i class="fa-solid fa-users-line"></i> {{ event.participants|length }} participants
</p>
{% include '_includes/event_participation.html.twig' with {event} %}
<p>{{ event.description }}</p>
</div>
</div>
</div>
On ajoute la même chose dans /templates/event/page.html.twig:
{% block content %}
<div class="section">
<div class="columns">
{# ... #}
<div class="column">
{# ... #}
<div class="columns">
<div class="column">
<div class="notification is-info is-light">
{# ... #}
<i class="fa-solid fa-clock"></i> de {{ event.startAt|date('H\\hi') }} à {{ event.endAt|date('H\\hi') }}<br>
<i class="fa-solid fa-users-line"></i> {{ event.participants|length }} participants
</div>
</div>
{# ... #}
</div>
{% include '_includes/event_participation.html.twig' with {event} %}
<p>{{ event.description }}</p>
</div>
</div>
</div>
{% endblock %}
À vous de jouer !
Connectez-vous avec un compte utilisateur et modifiez votre participations aux événements depuis différentes pages.