Comment concilier PHP et Front-end ?

Qui je suis ?

  • Jonathan Boyer
  • Freelance full-stack
  • Formateur

C'est quoi le front et le backend ?

  • Back-end : Code côté serveur (PHP)
  • Front : Code côté navigateur (JavaScript)

C'était mieux avant !

Le front-end était géré par le backend

Le JavaScript est arrivé

Un peu de JS par ci, par là


      <fieldset>
        <label for="age">
          Votre age
          <input id="age" />
        </label>
        <label for="beers" hidden>
          Combien de bières avez-vous bu aujourd'hui ?
          <input id="beers" />
        </label>
        <button type="submit">Envoyer</button>
      </fieldset>
        
        

// Afficher le champs "beers" si l'utilisateur a moins de 18 ans
const age = document.querySelector('#age');
const beers = document.querySelector('label[for="beers"]');

age.addEventListener('input', (e) => {
  if (e.currentTarget.valueAsNumber && e.currentTarget.valueAsNumber >= 18) {
    beers.removeAttribute('hidden');
  } else {
    beers.setAttribute('hidden', true);
  }
});

                    $('#age').on('input', function () {
                      const age = parseInt($(this).val(), 10);
                      $('label[for="beers"]').attr('hidden', isNaN(age) || age < 18);
                    });
        

Solution 1 : Single page application

PHP ne s'occupe que de la partie API


    #[Route('', name: 'post_index', methods: ['GET'])]
    public function index(Request $request, PostRepository $postRepository): Response
    {
        $page = $request->query->getInt('page', 1);
        $paginator = $postRepository->findPaginated($page);
        $totalPosts = count($paginator);
        $pagesCount = ceil($totalPosts / PostRepository::POSTS_PER_PAGE);

        return $this->render('post/index.html.twig', [
            'posts' => $paginator,
            'currentPage' => $page,
            'pagesCount' => $pagesCount,
            'totalPosts' => $totalPosts, // Pass total count if needed in template
            'limit' => PostRepository::POSTS_PER_PAGE // Pass limit if needed
        ]);
    }

    #[Route('', name: 'api_post_index', methods: ['GET'])]
    public function index(PostRepository $postRepository, Request $request): JsonResponse
    {
        $page = $request->query->getInt('page', 1);
        $limit = $request->query->getInt('limit', 10);

        $paginator = $postRepository->findPaginated($page, $limit);
        $totalItems = count($paginator);
        $pagesCount = ceil($totalItems / $limit);

        $responseData = [
            'data' => $paginator,
            'pagination' => [
                'currentPage' => $page,
                'totalPages' => $pagesCount,
                'totalItems' => $totalItems,
                'itemsPerPage' => $limit,
            ]
        ];

        // Return the structured data as JSON
        return $this->json($responseData, Response::HTTP_OK, [], ['groups' => 'post:list']);
    }

Demo

Avantages

  • ✅ Front et Back bien séparé (pratique en équipe)
  • ✅ Interface plus fluide (pas de rechargement complet)

Inconvénients

  • 👎 Authentification (cross-domain, cookies...)
  • 👎 Logique à dupliquer (permissions, validation...)
  • 👎 Référencement (rendu côté serveur)
  • 👎 Mise à jour

Solution 2 : Booster l'HTML

Donner des super pouvoirs à l'HTML


    <fieldset x-data="{age: ''}">
        <label for="age">
            Votre age
            <input
                    x-model="age"
                    id="age"
                    name="age"
                    type="number"
                    placeholder="Age"
                    autocomplete="given-name"
            />
        </label>
        <label for="beers" x-show="age && age >= 18">
            Combien de bières avez-vous bu aujourd'hui ?
            <input
                    id="beers"
                    type="number"
                    name="beers"
                    placeholder="Nombre de bières"
            />
        </label>
        <button type="submit">Envoyer</button>
    </fieldset>
        

Avantages

  • ✅ Pas / Peu de JavaScript
  • ✅ Librairies pour des composants classiques (Pines UI)

Inconvénients

  • 👎 CSP (Content-Security Policy)
  • 👎 L'HTML peut devenir complexe
  • 👎 Pas de communication serveur par défaut

Solution 2.5 : Booster encore plus l'HTML

Piloter la mutation du DOM depuis le serveur


<table hx-indicator=".htmx-indicator">
  <thead><tr><th>Name</th><th>Email</th><th>ID</th></tr></thead>
  <tbody>
    <tr></tr>
    <tr></tr>
    <tr></tr>
    <tr hx-get="/users/?page=2" hx-trigger="revealed" hx-swap="afterend"></tr>
  </tbody>
</table>
  <img class="htmx-indicator" width="60" src="/img/bars.svg">
        

Demo

Avantages

  • ✅ Pas / Peu de JavaScript
  • ✅ Le serveur pilote le rendu

Inconvénients

  • 👎 L'HTML peut devenir complexe
  • 👎 Retour serveur plus complexe à gérer

Solution 3 : Les composants réactifs

Live components (Symfony) & Livewire (laravel)


#[AsLiveComponent]
class ProductSearch
{
    use DefaultActionTrait;

    #[LiveProp(writable: true)]
    public string $query = '';

    public function __construct(private ProductRepository $productRepository)
    {
    }

    public function getProducts(): array
    {
        // example method that returns an array of Products
        return $this->productRepository->search($this->query);
    }
}
        

<div {{ attributes }}>
    <input
            type="search"
            data-model="query"
    >

    <ul>
        {% for product in this.products %}
        <li>{{ product.name }}</li>
        {% endfor %}
    </ul>
</div>
        

Demo

Avantages

  • ✅ La logique reste en PHP
  • ✅ Composant réutilisable

Inconvénients

  • 👎 Aller-retour avec le serveur
  • 👎 Sécurité

Solution 4 : Les îlots

Comme avant mais en plus moderne


<article>
  <h1><?= $post->title ?></h1>
  <p><?= $post->content ?></p>
  <comment-list post="<?= $post->id ?>"><comment-list>
</article>
        

Plein de solutions

Avantages

  • ✅ Logique réutilisable
  • ✅ Liberté dans le choix de la solution

Inconvénients

  • 👎 Il faut faire du JavaScript

Aucune solution n'est parfaite

Il faut souvent combiner

Question ?