Skip to content
  • About
    • What is Symfony?
    • Community
    • News
    • Contributing
    • Support
  • Documentation
    • Symfony Docs
    • Symfony Book
    • Screencasts
    • Symfony Bundles
    • Symfony Cloud
    • Training
  • Services
    • Platform.sh for Symfony Best platform to deploy Symfony apps
    • SymfonyInsight Automatic quality checks for your apps
    • Symfony Certification Prove your knowledge and boost your career
    • SensioLabs Professional services to help you with Symfony
    • Blackfire Profile and monitor performance of your apps
  • Other
  • Blog
  • Download
sponsored by
  1. Home
  2. Documentation
  3. Symfony: The Fast Track
  4. Dutch
  5. De lifecycle van Doctrine-objecten beheren

De lifecycle van Doctrine-objecten beheren

Bij het maken van een nieuwe reactie zou het geweldig zijn als de createdAt-datum automatisch op de huidige datum en tijd zou worden ingesteld.

Doctrine heeft verschillende manieren om objecten en hun properties te manipuleren tijdens hun lifecycle (voordat de rij in de database wordt aangemaakt, nadat de rij is bijgewerkt, ....).

Definiëren van lifecycle callbacks

Wanneer het gedrag geen service nodig heeft en slechts op één soort entity moet worden toegepast, definieer dan een callback in de entity class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
--- a/src/Entity/Comment.php
+++ b/src/Entity/Comment.php
@@ -6,6 +6,7 @@ use App\Repository\CommentRepository;
 use Doctrine\ORM\Mapping as ORM;

 #[ORM\Entity(repositoryClass: CommentRepository::class)]
+#[ORM\HasLifecycleCallbacks]
 class Comment
 {
     #[ORM\Id]
@@ -90,6 +91,12 @@ class Comment
         return $this;
     }

+    #[ORM\PrePersist]
+    public function setCreatedAtValue()
+    {
+        $this->createdAt = new \DateTimeImmutable();
+    }
+
     public function getConference(): ?Conference
     {
         return $this->conference;

Het ORM\PrePersist-event wordt geactiveerd wanneer het object voor het eerst in de database wordt opgeslagen. Als dat gebeurt, wordt de setCreatedAtValue()-methode aangeroepen en wordt de huidige datum en tijd gebruikt voor de waarde van het createdAt-property.

Slugs toevoegen aan conferenties

De URL's voor conferenties hebben momenteel geen betekenis: /conference/1. Belangrijker nog, ze zijn afhankelijk van een implementatiedetail (de primaire sleutel in de database is openbaar).

Misschien kunnen we in plaats daarvan beter gebruik maken van URL's zoals /conference/paris-2020? Dat zou er veel beter uitzien. paris-2020 is wat we noemen de slug van de conferentie.

Voeg een nieuw slug-property toe voor conferenties (een niet nullable string van 255 tekens):

1
$ symfony console make:entity Conference

Maak een migratiebestand aan om de nieuwe kolom toe te voegen:

1
$ symfony console make:migration

En voer die nieuwe migratie uit:

1
$ symfony console doctrine:migrations:migrate

Krijg je een foutmelding? Dat is zoals verwacht. Waarom? Omdat we gevraagd hebben om de slug niet null te laten zijn, maar bestaande gegevens in de conferentiedatabase zullen een waarde van null krijgen wanneer de migratie wordt uitgevoerd. Laten we dat oplossen door de migratie aan te passen:

1
2
3
4
5
6
7
8
9
10
11
12
13
--- a/migrations/Version00000000000000.php
+++ b/migrations/Version00000000000000.php
@@ -20,7 +20,9 @@ final class Version00000000000000 extends AbstractMigration
     public function up(Schema $schema): void
     {
         // this up() migration is auto-generated, please modify it to your needs
-        $this->addSql('ALTER TABLE conference ADD slug VARCHAR(255) NOT NULL');
+        $this->addSql('ALTER TABLE conference ADD slug VARCHAR(255)');
+        $this->addSql("UPDATE conference SET slug=CONCAT(LOWER(city), '-', year)");
+        $this->addSql('ALTER TABLE conference ALTER COLUMN slug SET NOT NULL');
     }

     public function down(Schema $schema): void

De truc hier is om de kolom toe te voegen en toe te laten dat deze null mag zijn. Vervolgens geef je de slug een waarde, om daarna weer toe te staan dat de kolom niet null mag zijn.

Note

Voor een echt project is het gebruik van CONCAT(LOWER(city), '-', year) misschien niet genoeg. In dat geval zouden we de "echte" Slugger moeten gebruiken.

De migratie zou nu goed moeten verlopen:

1
$ symfony console doctrine:migrations:migrate

Omdat de applicatie binnenkort gebruik zal maken van slugs om elke conferentie te vinden, moeten we de conferentie-entiteit aanpassen om ervoor te zorgen dat de slugs uniek zijn in de database:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
--- a/src/Entity/Conference.php
+++ b/src/Entity/Conference.php
@@ -6,8 +6,10 @@ use App\Repository\ConferenceRepository;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;
+use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

 #[ORM\Entity(repositoryClass: ConferenceRepository::class)]
+#[UniqueEntity('slug')]
 class Conference
 {
     #[ORM\Id]
@@ -27,7 +29,7 @@ class Conference
     #[ORM\OneToMany(mappedBy: 'conference', targetEntity: Comment::class, orphanRemoval: true)]
     private $comments;

-    #[ORM\Column(type: 'string', length: 255)]
+    #[ORM\Column(type: 'string', length: 255, unique: true)]
     private $slug;

     public function __construct()

Zoals je misschien al had geraden, moeten we de migratie-truc uitvoeren:

1
$ symfony console make:migration
1
$ symfony console doctrine:migrations:migrate

Slugs genereren

Het genereren van een slug die goed leesbaar is in een URL (waar alles behalve ASCII-tekens encoded moet worden), is een uitdagende taak. Vooral voor andere talen dan het Engels. Hoe converteer je é naar e bijvoorbeeld?

In plaats van het wiel opnieuw uit te vinden, gebruiken we de Symfony String component, die de manipulatie van strings makkelijker maakt en een slugger bevat.

Voeg een computeSlug() methode toe aan de Conference-class die de slug baseert op de gegevens van de conferentie:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
--- a/src/Entity/Conference.php
+++ b/src/Entity/Conference.php
@@ -7,6 +7,7 @@ use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;
 use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
+use Symfony\Component\String\Slugger\SluggerInterface;

 #[ORM\Entity(repositoryClass: ConferenceRepository::class)]
 #[UniqueEntity('slug')]
@@ -47,6 +48,13 @@ class Conference
         return $this->id;
     }

+    public function computeSlug(SluggerInterface $slugger)
+    {
+        if (!$this->slug || '-' === $this->slug) {
+            $this->slug = (string) $slugger->slug((string) $this)->lower();
+        }
+    }
+
     public function getCity(): ?string
     {
         return $this->city;

De computeSlug()-methode bouwt alleen een slug op wanneer de huidige slug leeg is of gelijk is aan de speciale waarde -. Waarom hebben we de speciale waarde - nodig? Omdat bij het toevoegen van een conferentie in de backend, de slug noodzakelijk is. We hebben dus een niet-lege waarde nodig die de applicatie vertelt dat we willen dat de slug automatisch gegenereerd wordt.

Een complexe lifecycle callback definiëren

Net als de createdAt-property, moet de slug automatisch gegenereerd worden wanneer de conferentie wordt bijgewerkt, door middel van het aanroepen van de computeSlug methode.

Maar omdat deze methode afhankelijk is van een implementatie van SluggerInterface, kunnen we geen prePersist-event toevoegen zoals voorheen (we hebben geen manier om de slugger te injecteren).

Maak in plaats daarvan een Doctrine entity listener:

src/EntityListener/ConferenceEntityListener.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
namespace App\EntityListener;

use App\Entity\Conference;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\String\Slugger\SluggerInterface;

class ConferenceEntityListener
{
    private $slugger;

    public function __construct(SluggerInterface $slugger)
    {
        $this->slugger = $slugger;
    }

    public function prePersist(Conference $conference, LifecycleEventArgs $event)
    {
        $conference->computeSlug($this->slugger);
    }

    public function preUpdate(Conference $conference, LifecycleEventArgs $event)
    {
        $conference->computeSlug($this->slugger);
    }
}

Merk op dat de slug wordt bijgewerkt wanneer er een nieuwe conferentie wordt aangemaakt ( prePersist() ) en wanneer deze wordt bijgewerkt ( preUpdate() ).

Een service in de container configureren

Tot nu toe hebben we het niet gehad over één belangrijk onderdeel van Symfony, de dependency injection container. De container is verantwoordelijk voor het beheer van de services: het creëren en injecteren van de services wanneer dat nodig is.

Een service is een "global" object dat functies biedt (bv. een mailer, een logger, een slugger, etc.) in tegenstelling tot data-objecten (bv. instanties van Doctrine-entity's).

Je hebt zelden direct interactie met de container, omdat deze automatisch service-objecten injecteert wanneer je ze nodig hebt: de container injecteert de objecten als argumenten van de controller wanneer je ze type-hint bijvoorbeeld.

Als je je afvroeg hoe de event listener in de vorige stap werd geregistreerd, dan heb je nu het antwoord: de container. Wanneer een class een aantal specifieke interfaces implementeert, dan weet de container dat de class op een bepaalde manier geregistreerd moet worden.

Helaas is niet alles geautomatiseerd, vooral niet voor packages van derden. De entity listener die we net schreven is zo'n voorbeeld; deze kan niet automatisch worden beheerd door de Symfony-servicecontainer, omdat het geen enkele interface implementeert en het breidt geen "well-known class" uit.

We moeten de listener in de container gedeeltelijk declareren. Het expliciet toevoegen van de dependencies kan weggelaten worden, omdat dit nog steeds geraden kan worden door de container, maar we moeten wel handmatig enkele tags toevoegen om de listener te registreren bij de Doctrine event dispatcher:

1
2
3
4
5
6
7
8
9
10
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -22,3 +22,7 @@ services:

     # add more service definitions when explicit configuration is needed
     # please note that last definitions always *replace* previous ones
+    App\EntityListener\ConferenceEntityListener:
+        tags:
+            - { name: 'doctrine.orm.entity_listener', event: 'prePersist', entity: 'App\Entity\Conference'}
+            - { name: 'doctrine.orm.entity_listener', event: 'preUpdate', entity: 'App\Entity\Conference'}

Note

Verwar Doctrine event listeners niet met Symfony listeners. Ook al lijken ze erg op elkaar, toch gebruiken ze niet dezelfde infrastructuur onder de motorkap.

Het gebruik van slugs in de applicatie

Probeer meer conferenties toe te voegen in de backend en verander de stad of het jaar van een bestaande conferentie; de slug zal niet worden bijgewerkt, behalve als je de speciale --waarde gebruikt.

De laatste wijziging is het bijwerken van de controllers en de templates om de slug van de conferentie te gebruiken voor routes, in plaats van het id van de conferentie:

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
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -28,7 +28,7 @@ class ConferenceController extends AbstractController
         ]));
     }

-    #[Route('/conference/{id}', name: 'conference')]
+    #[Route('/conference/{slug}', name: 'conference')]
     public function show(Request $request, Conference $conference, CommentRepository $commentRepository): Response
     {
         $offset = max(0, $request->query->getInt('offset', 0));
--- a/templates/base.html.twig
+++ b/templates/base.html.twig
@@ -18,7 +18,7 @@
             <h1><a href="{{ path('homepage') }}">Guestbook</a></h1>
             <ul>
             {% for conference in conferences %}
-                <li><a href="{{ path('conference', { id: conference.id }) }}">{{ conference }}</a></li>
+                <li><a href="{{ path('conference', { slug: conference.slug }) }}">{{ conference }}</a></li>
             {% endfor %}
             </ul>
             <hr />
--- a/templates/conference/index.html.twig
+++ b/templates/conference/index.html.twig
@@ -8,7 +8,7 @@
     {% for conference in conferences %}
         <h4>{{ conference }}</h4>
         <p>
-            <a href="{{ path('conference', { id: conference.id }) }}">View</a>
+            <a href="{{ path('conference', { slug: conference.slug }) }}">View</a>
         </p>
     {% endfor %}
 {% endblock %}
--- a/templates/conference/show.html.twig
+++ b/templates/conference/show.html.twig
@@ -22,10 +22,10 @@
         {% endfor %}

         {% if previous >= 0 %}
-            <a href="{{ path('conference', { id: conference.id, offset: previous }) }}">Previous</a>
+            <a href="{{ path('conference', { slug: conference.slug, offset: previous }) }}">Previous</a>
         {% endif %}
         {% if next < comments|length %}
-            <a href="{{ path('conference', { id: conference.id, offset: next }) }}">Next</a>
+            <a href="{{ path('conference', { slug: conference.slug, offset: next }) }}">Next</a>
         {% endif %}
     {% else %}
         <div>No comments have been posted yet for this conference.</div>

De conferentiepagina's moeten nu aangeroepen worden via de slug:

/conference/amsterdam-2019

Verder gaan

  • Het Doctrine event system (lifecycle callbacks en listeners, entity listeners en lifecycle subscribers);
  • De documentatie voor het String-component;
  • De Service container;
  • De Symfony Services Cheat Sheet ).
Previous page Luisteren naar events
Next page Feedback ontvangen via formulieren
This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.
TOC
    Version

    Symfony 5.4 is backed by

    Become certified from home

    Become certified from home

    Make sure your project is risk free

    Make sure your project is risk free

    Version:
    Locale:
    ebook

    This book is backed by:

    see all backers

    Symfony footer

    Avatar of HONORE HOUNWANOU, a Symfony contributor

    Thanks HONORE HOUNWANOU (@mercuryseries) for being a Symfony contributor

    4 commits • 8 lines changed

    View all contributors that help us make Symfony

    Become a Symfony contributor

    Be an active part of the community and contribute ideas, code and bug fixes. Both experts and newcomers are welcome.

    Learn how to contribute

    Symfony™ is a trademark of Symfony SAS. All rights reserved.

    • What is Symfony?

      • What is Symfony?
      • Symfony at a Glance
      • Symfony Components
      • Symfony Releases
      • Security Policy
      • Logo & Screenshots
      • Trademark & Licenses
      • symfony1 Legacy
    • Learn Symfony

      • Symfony Docs
      • Symfony Book
      • Reference
      • Bundles
      • Best Practices
      • Training
      • eLearning Platform
      • Certification
    • Screencasts

      • Learn Symfony
      • Learn PHP
      • Learn JavaScript
      • Learn Drupal
      • Learn RESTful APIs
    • Community

      • Symfony Community
      • SymfonyConnect
      • Events & Meetups
      • Projects using Symfony
      • Contributors
      • Symfony Jobs
      • Backers
      • Code of Conduct
      • Downloads Stats
      • Support
    • Blog

      • All Blog Posts
      • A Week of Symfony
      • Case Studies
      • Cloud
      • Community
      • Conferences
      • Diversity
      • Living on the edge
      • Releases
      • Security Advisories
      • Symfony Insight
      • Twig
      • SensioLabs Blog
    • Services

      • SensioLabs services
      • Train developers
      • Manage your project quality
      • Improve your project performance
      • Host Symfony projects

      Powered by

    Follow Symfony