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. Polish
  5. Zarządzanie cyklem życia obiektów Doctrine

Zarządzanie cyklem życia obiektów Doctrine

Byłoby wspaniale, gdyby przy tworzeniu nowego komentarza atrybut createdAt przyjął automatycznie wartość bieżącej daty i godziny.

Doctrine oferuje wiele możliwości manipulowania obiektami i ich atrybutami podczas cyklu ich życia (przed utworzeniem rekordu w bazie danych, po aktualizacji rekordu, ...).

Definiowanie wywołań zwrotnych cyklu życia (ang. lifecycle callbacks)

Jeśli schemat działania nie wymaga dostępu do żadnej usługi i ma zastosowanie tylko do jednego rodzaju encji, należy zdefiniować wywołanie zwrotne (ang. callback) w klasie encji:

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;

Zdarzenie ORM\PrePersist jest emitowane, gdy obiekt jest po raz pierwszy zapisany w bazie danych. Gdy tak się stanie, wywołana zostanie metoda setCreatedAtValue(), a jako wartość atrybutu createdAt zostanie użyta bieżąca data i czas.

Dodawanie slugów do konferencji

Adresy URL konferencji nie mówią nam zbyt wiele: /conference/1. Co więcej, są one zależne od szczegółów implementacji (klucz podstawowy w bazie danych jest widoczny dla użytkownika).

A gdyby tak użyć adresów URL następującej formie: /conference/paris-2020? Wyglądałoby to o wiele lepiej. Fragment paris-2020 jest tym, co określamy mianem slug.

Dodaj nowy atrybut o nazwie slug dla konferencji (pole przechowujące ciąg znaków o długości do 255 znaków, nieprzyjmujące wartości null):

1
$ symfony console make:entity Conference

Utwórz plik migracji, aby dodać nową kolumnę:

1
$ symfony console make:migration

I wykonaj nowo utworzoną migrację:

1
$ symfony console doctrine:migrations:migrate

Widzisz komunikat błędu? To oczekiwany efekt. Dlaczego? Ponieważ zdefiniowaliśmy kolumnę slug tak, aby nie przyjmowała wartości null. Jeśli migracja zostałaby wykonana, to rekordy konferencji istniejące w bazie danych otrzymałyby wartość null w kolumnie slug. Naprawmy to, modyfikując naszą migrację:

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

Sztuczka polega na tym, aby najpierw dodać kolumnę i pozwolić na dodanie w niej wartości null, następnie zmodyfikować istniejące rekordy ustawiając wartość slug na wartość inną niż null, a na koniec zmodyfikować kolumnę slug, tak aby wartość null nie była dozwolona.

Note

W realnym projekcie używanie CONCAT(LOWER(city), '-', year) może okazać się niewystarczające. W takim przypadku będziemy musieli użyć "prawdziwego" Sluggera.

Tym razem migracja powinna wykonać się jak należy:

1
$ symfony console doctrine:migrations:migrate

Ponieważ aplikacja wkrótce użyje slugów do odnajdowania wymaganej konferencji, dostosujmy encję Conference aby zagwarantować, że wartości slugów będą unikalne w bazie danych:

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()

Jak można się było domyślać, musimy wykonać kolejne migracje:

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

Generowanie slugów

Generowanie slugów, które zachowają czytelność będąc częścią adresu URL (gdzie wszystko oprócz znaków ASCII powinno być zakodowane) nie jest łatwym zadaniem, szczególnie w przypadku języków innych niż angielski. Jak przekonwertujesz na przykład é do e?

Zamiast wymyślać koło na nowo, użyjmy komponentu Symfony o nazwie String, który ułatwia manipulację ciągami znaków, oraz dostarcza między innymi mechanizm sluggera.

Dodaj w klasie Conference metodę computeSlug(), której zadaniem będzie utworzenie sluga na podstawie danych konferencji:

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;

Metoda computeSlug() tworzy slug tylko wtedy, gdy aktualny slug jest pusty lub ustawiony na specjalną wartość -. Po co nam ta specjalna wartość -? Ponieważ przy dodawaniu konferencji w panelu administracyjnym slug jest wartością wymaganą. Potrzebujemy zatem niepustej wartości która przekaże aplikacji, że chcemy, aby slug został wygenerowany automatycznie.

Definiowanie złożonych wywołań zwrotnych cyklu życia (ang. lifecycle callback)

Podobnie jak w przypadku atrybutu createdAt, gdy konferencja zostaje zmodyfikowana, wartość atrybutu slug powinna zostać automatycznie uaktualniona poprzez wywołanie metody computeSlug().

Ponieważ metoda zależy od implementacji SluggerInterface, nie możemy w prosty sposób dodać obsługi zdarzenia prePersist, jak miało to miejsce w poprzednim przypadku (nie mamy możliwości wstrzyknięcia sluggera).

Zamiast tego utwórz nasłuchiwacz zdarzeń Doctrine (ang. 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);
    }
}

Zauważ, że slug jest aktualizowany w momencie, gdy tworzona jest nowa konferencja (prePersist()) i gdy jest ona modyfikowana (preUpdate()).

Konfigurowanie usługi w kontenerze

Do tej pory nie mówiliśmy o jednym z kluczowych komponentów Symfony, czyli o kontenerze wstrzykiwania zależności (ang. dependency injection container). Kontener jest odpowiedzialny za zarządzanie usługami (ang. services): ich tworzenie i wstrzykiwanie kiedy są wymagane.

Usługa (ang. service) jest obiektem "globalnym", który oferuje różnego rodzaju funkcje (np. mailer, logger, slugger itp.) w odróżnieniu od obiektów danych (np. instancje encji Doctrine).

Rzadko wchodzi się w bezpośrednią interakcję z kontenerem, ponieważ automatycznie wstrzykuje on obiekty usług kiedy są one wymagane: na przykład, sprawdzając typ argumentu kontrolera zdefiniowany przy użyciu mechanizmu podpowiadania typów (ang. type-hinting), kontener wstrzykuje odpowiedni typ usługi.

Jeśli zastanawiało Cię, jak w poprzednim kroku został zarejestrowany nasłuchiwacz zdarzeń (ang. event listener), teraz poznasz odpowiedź: poprzez kontener. Kiedy klasa implementuje określone interfejsy, kontener wie, że klasa musi być zarejestrowana w określony sposób.

Niestety, ten rodzaj automatyzacji nie zawsze jest możliwy, zwłaszcza w przypadku zewnętrznych zależności. Nasłuchiwacz zdarzeń, który właśnie utworzyliśmy, jest jednym z takich przykładów – nie może być automatycznie zarządzany przez kontener usług Symfony, ponieważ nie implementuje żadnego interfejsu i nie rozszerza „dobrze znanej klasy”.

Musimy częściowo zadeklarować nasz nasłuchiwacz w kontenerze. Możemy pominąć definiowanie usług, które powinny zostać wstrzyknięte, ponieważ kontener będzie w stanie je określić automatycznie, ale musimy ręcznie dodać kilka tagów, aby zarejestrować nasz nasłuchiwacz w dyspozytorze zdarzeń (ang. event dispatcher) Doctrine:

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

Nie należy mylić nasłuchiwaczy zdarzeń Doctrine i Symfony. Mimo że wyglądają bardzo podobnie, pod spodem korzystają z osobnych mechanizmów.

Stosowanie slugów w aplikacji

Spróbuj dodać kilka konferencji w panelu administracyjnym i zmienić miasto lub rok jednej z nich. Slug nie zostanie w tym przypadku zaktualizowany, chyba że użyjesz specjalnej wartości -.

Ostatnią zmianą jest aktualizacja kontrolerów i szablonów, tak aby mechanizm routingu wykorzystywał slug zamiast id konferencji.

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>

Dostęp do stron konferencji powinien być teraz możliwy z wykorzystaniem sluga:

/conference/amsterdam-2019

Idąc dalej

  • System zdarzeń Doctrine (lifecycle callbacks and listeners, entity listeners and lifecycle subscribers);
  • Dokumentacja komponentu String;
  • Kontener usług;
  • Ściągawka Symfony Services.
Previous page Nasłuchiwanie zdarzeń
Next page Przyjmowanie informacji zwrotnych za pomocą formularzy
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

    Online Symfony certification, take it now!

    Online Symfony certification, take it now!

    Be trained by SensioLabs experts (2 to 6 day sessions -- French or English).

    Be trained by SensioLabs experts (2 to 6 day sessions -- French or English).

    Version:
    Locale:
    ebook

    This book is backed by:

    see all backers

    Symfony footer

    Avatar of Markus Baumer, a Symfony contributor

    Thanks Markus Baumer for being a Symfony contributor

    1 commit • 22 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