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. Ukrainian
  5. Управління життєвим циклом об'єктів Doctrine

Управління життєвим циклом об'єктів Doctrine

Створюючи новий коментар, було б чудово, якби дата createdAt була встановлена автоматично, з використанням значень поточної дати й часу.

В Doctrine є різні способи маніпулювання об'єктами та їх властивостями протягом їх життєвого циклу (до створення запису в базі даних, після оновлення, ...).

Визначення зворотних викликів життєвого циклу

Якщо поведінка не потребує доступу до сервісу й застосовується тільки до одного типу сутності, визначте зворотний виклик в класі сутності:

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;

Подія ORM\PrePersist оголошується тоді, коли об'єкт вперше зберігається у базі даних. Коли це трапляється, викликається метод setCreatedAtValue() який використовує поточні дату й час як значення для властивості createdAt.

Додавання "Slugs" для конференцій

URL-адреси для конференцій не несуть в собі смислового навантаження: /conference/1. Що ще важливіше, вони розкривають деталі реалізації (витік значення первинного ключа в базі даних).

А як щодо використання URL-адрес на кшталт /conference/paris-2020? Це виглядало б набагато краще. paris-2020 — це те, що ми називаємо slug конференції.

Додайте нову властивість slug для конференцій (рядок довжиною 255 символів, що не може містити значення null):

1
$ symfony console make:entity Conference

Створіть файл міграції, щоб додати новий стовпчик:

1
$ symfony console make:migration

А потім виконайте нову міграцію:

1
$ symfony console doctrine:migrations:migrate

Помилка? Це очікувано. Чому? Хоча ми вказали, що значення "slug" не може містити значення null, під час виконання міграції наявні записи в базі даних конференцій отримають значення null. Виправімо це, змінивши міграцію:

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

Хитрість полягає в тому, щоб додати стовпчик і дозволити йому мати значення null, потім встановити значення, що відмінні від null, і, нарешті, змінити стовпчик "slug" так, щоб не допустити можливості встановлення значення null.

Note

Для реального проекту, використання CONCAT(LOWER(city), '-', year) може виявитися недостатнім. У цьому випадку нам потрібно було б використовувати "справжній" Slugger.

Тепер міграція має виконатися нормально:

1
$ symfony console doctrine:migrations:migrate

Оскільки застосунок незабаром використовуватиме "slugs" для пошуку кожної конференції, налаштуймо сутність конференції, щоб мати впевненість, що значення у базі даних будуть унікальними:

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

Як ви могли здогадатися, нам потрібно виконати ще один танок з міграціями:

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

Генерування "Slugs"

Генерування "slugs", що добре читаються в URL-адресах (де все, крім символів ASCII, має бути закодовано), є складним завданням, особливо для мов, відмінних від англійської. Наприклад, як конвертувати é у e?

Замість того щоб винаходити колесо, використовуймо компонент Symfony String, який полегшує маніпуляції з рядками та забезпечує slugger.

Додайте метод computeSlug() до класу Conference, який обчислюватиме значення на основі даних конференції:

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;

Метод computeSlug() обчислює "slug" лише тоді, коли поточне значення порожнє або встановлено спеціальне значення -. Навіщо нам потрібне спеціальне значення -? Тому що при додаванні конференції у панелі керування потрібен "slug". Отже, нам потрібне непорожнє значення, яке повідомляє застосунку про те, що ми хочемо, щоб значення було згенеровано автоматично.

Визначення складних зворотних викликів життєвого циклу

Так само як і для властивості createdAt, значення slug слід встановлювати автоматично кожного разу, коли конференція оновлюється, викликаючи метод computeSlug().

Але оскільки цей метод залежить від реалізації SluggerInterface, ми не можемо додати подію prePersist, як і раніше (у нас немає способу для впровадження сервісу slugger).

Натомість створіть слухача сутності Doctrine:

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);
    }
}

Зверніть увагу, що значення оновлюється при створенні нової конференції (prePersist()) і кожного разу, коли вона оновлюється (preUpdate()).

Налаштування сервісу в контейнері

Досі ми не говорили про один із головних компонентів Symfony — контейнер впровадження залежностей. Він керує сервісами: створює їх і впроваджує, коли це необхідно.

Сервіс — це "глобальний" об'єкт, який надає певні функції (як-от mailer, logger, slugger і т.д.) на відміну від об'єктів даних (як-от екземплярів сутностей Doctrine).

Ви рідко будете працювати з контейнером безпосередньо, оскільки процес впровадження сервісів проходить автоматично всякий раз, коли це необхідно: контейнер впроваджує об'єкти, коли ви вказуєте тип сервісів у якості аргументів контролера.

Якщо ви були здивовані тим, як був зареєстрований слухач подій, на попередньому кроці, тепер у вас є відповідь — завдяки контейнеру. Коли клас реалізує певні інтерфейси, контейнер знає, що клас має бути зареєстровано певним чином.

На жаль, автоматизація передбачена не для усього, особливо це стосується сторонніх пакетів. Слухач сутності, про який ми щойно писали, є одним із таких прикладів; він не може бути налаштований автоматично, за допомогою контейнеру сервісів Symfony, оскільки він не реалізує жодного інтерфейсу й не розширює "добре відомий клас".

Нам потрібно частково оголосити слухача в контейнері. Зв'язування залежностей можна упустити, оскільки зв'язки все ще можуть бути вгадані контейнером, але нам потрібно вручну додати кілька тегів, щоб зареєструвати слухача в диспетчері подій 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

Не плутайте слухачів Doctrine і слухачів Symfony. Навіть якщо вони виглядають дуже схожими, вони використовують різну інфраструктуру.

Використання "Slugs" у застосунку

Спробуйте додати більше конференцій у панелі керування і змінити місто чи рік наявної; синонім не буде оновлюватися, за винятком тих випадків, коли ви використовуєте спеціальне значення -.

Остання зміна полягає в оновленні контролерів і шаблонів для використання slug замість id конференції для маршрутів:

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>

Доступ до сторінок конференції тепер має здійснюватися через її "slug":

/conference/amsterdam-2019

Йдемо далі

  • Система подій Doctrine (зворотні виклики і слухачі життєвого циклу, слухачі сутностей і підписники життєвого циклу);
  • Документація по компоненту String;
  • Контейнер сервісів;
  • Шпаргалка по сервісах Symfony.
Previous page Прослуховування подій
Next page Отримання відгуків за допомогою форм
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

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

    Put the code quality back at the heart of your project

    Put the code quality back at the heart of your project

    Version:
    Locale:
    ebook

    This book is backed by:

    see all backers

    Symfony footer

    Avatar of Neil Peyssard, a Symfony contributor

    Thanks Neil Peyssard (@nepey) for being a Symfony contributor

    4 commits • 498 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