Allgemein PHP Tutorials

Pagination in Symfony umsetzen

Heute gibt es mal wieder ein Tutorial von mir. Christian von Modius-Techblog hat mich gebeten eine Anleitung zu schreiben, wie man in Symfony eine Pagination umsetzen kann.

Was ist eine Pagination und welchen Vorteil hat diese?

Bei einer Pagination sprechen wir von einer Seitennummerierung. Der Vorteil ist ganz klar, es werden nicht direkt alle 100 Einträge in einer Liste angezeigt sondern jeweils 10 Einträge auf 10 Seiten verteilt wodurch Lade- und Renderzeit gespart wird und es muss nicht ewig gescrollt werden.

Los gehts

Ich gehe davon aus dass Du bereits ein Twig Template, eine Entity mit Repository sowie einen Controller hast. Aktuell sieht dein Template etwa so aus (eben wie eine Liste ohne Pagination 🙂 , Die Daten wurden mit dem FakeNameGenerator generiert):
Liste ohne Pagination

Beziehungsweise die Source-Files:
index.html.twig:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{% extends '::base.html.twig' %}
 
{% block body %}
    <h1>Pagination Demo</h1>
    {% if persons|length %}
        <table class="table table-striped table-bordered">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>Vorname</th>
                    <th>Nachname</th>
                    <th>Adresse</th>
                    <th>Ort</th>
                    <th>PLZ</th>
                    <th>Land</th>
                    <th>Benutzername</th>
                    <th>E-Mail</th>
                    <th>Alter</th>
                </tr>
            </thead>
            <tbody>
                {% for person in persons %}
                    <tr>
                        <td>{{ person.id }}</td>
                        <td>{{ person.name }}</td>
                        <td>{{ person.surname }}</td>
                        <td>{{ person.address }}</td>
                        <td>{{ person.city }}</td>
                        <td>{{ person.zipcode }}</td>
                        <td>{{ person.country }}</td>
                        <td>{{ person.username }}</td>
                        <td>{{ person.email }}</td>
                        <td>{{ person.age }}</td>
                    </tr>
                {% endfor %}
            </tbody>
        </table>
    {% else %}
        <div class="text-center">Es sind noch keine Personen vorhanden</div>
    {% endif %}
{% endblock %}

DefaultController.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
 
namespace AppBundle\Controller;
 
use AppBundle\Entity\Person;
use AppBundle\Repository\PersonRepository;
use Doctrine\ORM\EntityManager;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
 
class DefaultController extends Controller
{
    /**
     * @Route("/", name="list")
     * @Template()
     */
    public function indexAction(Request $request)
    {
        /** @var EntityManager $em */
        $em = $this->getDoctrine()->getManager();
 
        /** @var PersonRepository $repo */
        $repo = $em->getRepository('AppBundle:Person');
 
        /** @var Person[] $persons */
        $persons = $repo->findAll();
 
        return [
            'persons' => $persons,
        ];
    }
}

Informationen sammeln

Welche Informationen brauchen wir für eine Pagination?
Richtig:

  • die Seite auf der ich mich gerade befinde
  • die Anzahl der Elemente die pro Seite
  • die Gesamtzahl der Seiten

Pagination implementieren

Perfekt, weiter geht es mit unserer Helfer-Klasse mit welcher wir die Anzahl der Gesamtseiten sowie das Ergebnis für eine Seite auswerten werden.
Erstelle dazu in deinem Bundle den Ordner Doctrine und darin die Datei PaginationHelper.php.
Doctrine bietet zu unserem Glück schon eine passende Klasse (Doctrine\ORM\Tools\Pagination\Paginator) für Paginations, diese werden wir natürlich auch benutzen.

Fülle die PaginationHelper.php mit folgendem Code (Namespace ggf. Anpassen):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php
 
namespace AppBundle\Doctrine;
 
use Doctrine\ORM\Query;
use Doctrine\ORM\Tools\Pagination\Paginator;
 
class PaginationHelper
{
    /**
     * Gibt die Anzahl der Seiten für eine Query zurück
     *
     * @param Query $query Die Query
     * @param int $pageSize Die Anzahl der Elemente pro Seite
     * @return int Die Anzahl der Seiten
     */
    public static function getPagesCount(Query $query, $pageSize = 20)
    {
        $paginator = new Paginator($query);
 
        // Aus Performance den OutputWalker nicht nutzen ;)
        $paginator->setUseOutputWalkers(false);
 
        // Anzahl Seiten = aufrunden(Gesamtzahl in der DB / Anzahl pro Seite)
        return ceil($paginator->count() / $pageSize);
    }
 
    /**
     * Gibt das Seitenergebnis für eine Query zurück
     *
     * Quelle:
     * http://stackoverflow.com/questions/24598567/doctrine-orm-pagination-and-use-with-twig
     *
     * @param Query $query Die Abfrage
     * @param int $pageSize Die Anzahl der Elemente pro Seite
     * @param int $currentPage Die aktuelle Seite
     * @return array Das Ergebnis
     */
    public static function paginate(Query $query, $pageSize = 10, $currentPage = 1)
    {
        $pageSize = (int)$pageSize;
        $currentPage = (int)$currentPage;
 
        if ($pageSize < 1) {
            $pageSize = 10;
        }
 
        if ($currentPage < 1) {
            $currentPage = 1;
        }
 
        $paginator = new Paginator($query);
 
        $results = $paginator
            ->getQuery()
            ->setFirstResult($pageSize * ($currentPage - 1))
            ->setMaxResults($pageSize)
            ->getResult();
 
        return $results;
    }
}

Ok, jetzt können wir schon mal die Anzahl der Seiten auslesen und die Ergebnisse für eine Seite anzeigen.

Wie du in den beiden Methoden sehen kannst, erwarten diese als erstes Argument ein Objekt vom Typen Query, eine passende Methode zum generieren werden wir hierfür jetzt erstellen.
Gehe hierzu in dein Repository und füge folgende Methode hinzu (Muss ich, denke ich, nicht erklären 😉 ):

1
2
3
4
5
6
7
8
9
10
11
12
13
    /**
     * Gibt die Query zum laden aller Entities zurück
     * 
     * @return \Doctrine\ORM\Query Die Query
     */
    public function getFindAllQuery()
    {
        $query = $this->_em->createQueryBuilder()
            ->select('e')
            ->from($this->getEntityName(), 'e')
            ->getQuery();
        return $query;
    }

Nun wechsle in deinen Controller.

Damit wir in unserer Action wissen, auf welcher Seite wir uns befinden müssen wir diese an die Action übergeben, hierzu legen wir eine zweite Route an und fügen den optionalen Parameter page mit dem Default-Wert 1 hinzu. Ersetze nun den Bereich in der Action, in dem du die Entities lädst, wie folgt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    // Alt aus Beispiel:
    /** @var Person[] $persons */
    $persons = $repo->findAll();
    // ...
    return [
        'persons' => $persons,
    ];
 
    // Neu:
    /** @var Query $query */
    $query = $repo->getFindAllQuery();
 
    /** @var int $pages */
    $pages = PaginationHelper::getPagesCount($query);
 
    /** @var Person[] $persons */
    $persons = PaginationHelper::paginate($query, 10, $page);
    // ...
    return [
        'persons' => $persons,
        'page' => $page,
        'pages' => $pages
    ];

Nun sollte deine Action etwa so aussehen (Änderungen vorgehoben):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
 * @Route("/", name="list")
 * @Route("/page/{page}", name="list_page") * @Template()
 */
public function indexAction(Request $request, $page = 1){
    /** @var EntityManager $em */
    $em = $this->getDoctrine()->getManager();
 
    /** @var PersonRepository $repo */
    $repo = $em->getRepository('AppBundle:Person');
 
    /** @var Query $query */    $query = $repo->getFindAllQuery(); 
    /** @var int $pages */    $pages = PaginationHelper::getPagesCount($query); 
    /** @var Person[] $persons */    $persons = PaginationHelper::paginate($query, 10, $page); 
    return [        'persons' => $persons,        'page' => $page,        'pages' => $pages    ];}

Wenn du jetzt die Seite aktualisierst sollte nun schon mal die Anzahl auf 10 begrenzt sein und wenn du an die URL /page/2 hängst sollten nun die nächsten 20 Einträge erscheinen:

Pagination Seite 1
Pagination Seite 1

Pagination Seite 2
Pagination Seite 2

Das Template für unsere Pagination

Was wäre eine Pagination ohne Pagination? 😀
Da eine Pagination ein „allgemeines Bauteil“ ist welches man an mehreren Stellen verwenden kann, sollte man dieses in ein eigenständiges Template auslagern.
Ich habe mir dazu angewöhnt einen Fragment-Ordner in meinem views-Ordner anzulegen in welchem ich solche „Bauteile“ speichere.
Also, leg dir den Ordner views/Fragment in deinem Bundle an und erstelle darin die Datei pagination.html.twig mit folgendem Inhalt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
{% set parameters = parameters is defined ? parameters : {} %}
{% set first_parameters = parameters|merge({'page': 1 }) %}
{% set prev_parameters = parameters|merge({'page': page - 1}) %}
{% set next_parameters = parameters|merge({'page': page + 1}) %}
{% set last_parameters = parameters|merge({'page': pages }) %}
 
{% set additional = 5 %}
 
{% set start_page = max(1, page - additional) %}
{% set end_page = min(pages, page + additional) %}
 
{% set delta = end_page - start_page - additional %}
{% set end_page = min(pages, end_page + ( delta <= additional ? additional - delta : 0)) %}
{% set start_page = max(1, start_page - ( delta <= additional ? additional - delta : 0)) %}
 
<nav aria-label="Page navigation">
    <ul class="pagination">
        {%  set class = page <= 1 ? 'disabled' : '' %}
        <li class="{{ class }}">
            <a href="{{ path(route, first_parameters) }}" aria-label="Previous">
                <span aria-hidden="true">&laquo;</span>
            </a>
        </li>
        {%  set class = page <= 1 ? 'disabled' : '' %}
        <li class="{{ class }}">
            <a href="{{ path(route, prev_parameters) }}" aria-label="Previous">
                <span aria-hidden="true">&lsaquo;</span>
            </a>
        </li>
 
        {% for x in start_page..end_page %}
            {% set current_parameters = parameters|merge({'page': x }) %}
            {% set class = page == x ? 'active' : '' %}
            <li class="{{ class }}"><a href="{{ path(route, current_parameters) }}">{{ x }}</a></li>
        {% endfor %}
 
        {%  set class = page >= pages ? 'disabled' : '' %}
        <li class="{{ class }}">
            <a href="{{ path(route, next_parameters) }}" aria-label="Next">
                <span aria-hidden="true">&rsaquo;</span>
            </a>
        </li>
        <li class="{{ class }}">
            <a href="{{ path(route, last_parameters) }}" aria-label="Next">
                <span aria-hidden="true">&raquo;</span>
            </a>
        </li>
    </ul>
</nav>

Sieht komplizierter aus als es ist 😛

Zeile 1Parameter für Routen definieren falls keine übergeben wurden
Zeile 2 – 5Parameter für erste Seite, vorherige Seite, nächste Seite und letzte Seite definieren
Zeile 7Anzahl der anzuzeigenden Seiten vor und nach der aktuellen Seite (Angezeigte Seiten = additional * 2 + 1)
Zeile 9 – 14Berechnung der Start und End-Seite für die Loop
Zeile 31 – 35Durchlaufen der Seiten

Und zuletzt noch die Pagination einbauen. Gehe hierzu einfach in das Template mit der Liste und füge an die entsprechende Stelle folgende Zeile hinzu:

{% include 'AppBundle:Fragment:pagination.html.twig' with {
    'route': 'list_page',
    'parameters': {},
    'page': page,
    'pages': pages,
} %}

Falls die Route noch weitere Parameter erwarten sollte, kannst du diese entsprechend in parameters hinterlegen.

Dein Template sollte nun wie folgt aussehen:
Liste mit Pagination

Zusammenfassung

Du hast heute

  • einen Pagination mit dem Doctrine Paginator erstellt
  • in deinem Repository eine Methode angelegt, welche die Query zum auslesen aller Einträge in einer Tabelle liefert
  • ein Twig-Template angelegt und per include in ein anderes Template geladen

Danke fürs lesen, Fragen, Lob und Kritik gerne in die Kommentare 🙂


0x gelesen

Print Friendly, PDF & Email

Kommentar hinterlassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.