25/03/2023

2. Recherche d'événements

Lu 409 fois Licence Creative Commons

Nous voulons créer un formulaire de recherche d'événements qui sera affiché dans le menu.

Route

Commencer par créer la route pour les résultats de recherche dans le EventController:

#[Route('/event', name: 'event_')]
class EventController extends AbstractController
{
    // ...

    #[Route('/search', name: 'search')]
    public function search(): Response
    {
        dd('TODO: page de résultats');
    }
}

Pour qu'elle ne rentre pas en conflit avec la route event_page, modifier le chemin de celle-ci pour indiquer que le paramètre id n'est composé que de chiffres:

#[Route('/{id<\d+>}', name: 'page')]
public function eventPage(Event $event): Response

Formulaire

Créer un nouveau formulaire qui ne sera lié à aucune entité:

php ./bin/console make:form
[output]
[output] The name of the form class (e.g. GentlePizzaType):
 > Event\SearchFormType
[output]
[output] The name of Entity or fully qualified model class name that the new form will be bound to (empty for none):
 > 
[output]
[output] created: src/Form/Event/SearchFormType.php

Avec les options de formulaire, définir la méthode HTTP à utiliser et désactiver la protection CSRF pour n'avoir que le terme de recherche dans l'URL:

use Symfony\Component\HttpFoundation\Request;

// ...
public function configureOptions(OptionsResolver $resolver): void
{
	$resolver->setDefaults([
		'csrf_protection' => false,
		'method' => Request::METHOD_GET,
	]);
}

L'unique champ searchValue devra simplement ne pas être vide:

use Symfony\Component\Form\Extension\Core\Type\SearchType;
use Symfony\Component\Validator\Constraints\NotBlank;

// ...
public function buildForm(FormBuilderInterface $builder, array $options): void
{
	$builder
		->add('searchValue', SearchType::class, [
			'constraints' => [
				new NotBlank(['message' => 'Votre recherche est vide.'])
			]
		])
	;
}

Puisque le formulaire sera affiché dans le menu et donc sur toutes les pages, il est important de définir l'action du formulaire (vers quelle page être redirigé après envoi). On va injecter un service de génération d'URL dans le constructeur pour définir l'action dans les options de formulaire:

use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

// ...

class SearchFormType extends AbstractType
{
    public function __construct(
        private readonly UrlGeneratorInterface $urlGenerator,
    ) {
    }

    // ...

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'csrf_protection' => false,
            'method' => Request::METHOD_GET,
            'action' => $this->urlGenerator->generate('event_search'),
        ]);
    }
}

Intégration dans le menu

Toujours dans le controller, créer une méthode pour gérer l'affichage du formulaire. La recherche se fera dans l'autre méthode mais le traitement de la requête en appelant handleRequest() permet de garder le formulaire rempli après envoi:

public function renderSearchForm(Request $request): Response
{
	$searchForm = $this->createForm(SearchFormType::class);
	$searchForm->handleRequest($request);

	return $this->render('_includes/event_search_form.html.twig', [
		'search_form' => $searchForm->createView(),
	]);
}

Créer le template associé:

{{ form_start(search_form) }}
    <div class="field has-addons my-0">
        {{ form_widget(search_form.searchValue) }}
        <div class="control">
            <button type="submit" class="button is-primary">
                <i class="fa-solid fa-magnifying-glass"></i>
            </button>
        </div>
    </div>
{{ form_end(search_form) }}

Puis appeler ce controller dans le template contenant le menu: /templates/_template.html.twig

<nav class="navbar is-dark" role="navigation" aria-label="main navigation">
	{# ... #}

	<div id="navbarBasicExample" class="navbar-menu">
		{# ... #}

		<div class="navbar-end">
			<div class="navbar-item">
				{{ render(controller('App\\Controller\\EventController::renderSearchForm', {request: app.request})) }}
			</div>
			{# ... #}
		</div>
	</div>
</nav>

Page de résultats

Créer une méthode de recherche dans le EventRepository:

/**
 * Recherche d'événements
 * @return Event[]
 */
public function search(string $search): array
{
	return $this->createQueryBuilder('e')
		->leftJoin('e.venue', 'v')
		->where('e.title LIKE :title')
		->orWhere('e.description LIKE :description')
		->orWhere('v.name LIKE :venue')
		->orWhere('v.address LIKE :address')
		->setParameters([
			'title' => '%' . $search . '%',
			'description' => '%' . $search . '%',
			'venue' => '%' . $search . '%',
			'address' => '%' . $search . '%',
		])
		->orderBy('e.startAt', 'ASC')
		->getQuery()
		->getResult()
		;
}

Puis l'utiliser dans la méthode de EventController créée au début:

#[Route('/search', name: 'search')]
public function search(Request $request, EventRepository $eventRepository): Response
{
	// On instancie le formulaire
	$searchForm = $this->createForm(SearchFormType::class);
	$searchForm->handleRequest($request);

	// S'il n'est pas envoyé ou invalide, on retourne sur l'agenda
	if (!$searchForm->isSubmitted() || !$searchForm->isValid()) {
		return $this->redirectToRoute('event_agenda');
	}

	// On récupère la valeur de recherche et les événements trouvés
	$searchValue = $searchForm->get('searchValue')->getData();
	$events = $eventRepository->search($searchValue);

	// On passe le tout au template
	return $this->render('event/search.html.twig', [
		'search' => $searchValue,
		'events' => $events,
	]);
}

Et enfin, le template des résultats de recherche:

{% extends "_template.html.twig" %}

{% block title 'Résultats pour "' ~ search ~ '"' %}

{% block content %}
    <div class="section">
        <h1 class="title">Résultats pour "{{ search }}": {{ events|length }}</h1>

        {% for event in events %}
            {% 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-sad-cry"></i></span>
                            <span>Oups !</span>
                        </p>
                        <p class="subtitle is-6">
                            Aucun événement trouvé pour votre recherche...
                        </p>
                    </div>
                </div>
            </div>
        {% endfor %}
    </div>
{% endblock %}

Essayez désormais de rechercher des événements par titre, description, nom ou adresse de lieu, depuis n'importe quelle page.