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
SymfonyCon Vienna 2024
December 5-6
Worldwide event in English
+35 talks and workshops
  1. Home
  2. Documentation
  3. Symfony: The Fast Track
  4. Japanese
  5. Doctrine オブジェクトのライフサイクルを管理する

Doctrine オブジェクトのライフサイクルを管理する

新しくコメントをした際には、自動的に現在の日時が createdAt としてセットされると良いですね。

Doctrine は、データベースに追加されるときや更新されるときといったライフサイクルにおいてオブジェクトやプロパティを操作するいろいろな方法があります。

ライフサイクルのコールバックを定義する

サービスの依存が必要なく、エンティティを1つしか操作しないときは、エンティティクラスにコールバックを定義すると良いでしょう:

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
--- a/src/Controller/Admin/CommentCrudController.php
+++ b/src/Controller/Admin/CommentCrudController.php
@@ -57,8 +57,6 @@ class CommentCrudController extends AbstractCrudController
         ]);
         if (Crud::PAGE_EDIT === $pageName) {
             yield $createdAt->setFormTypeOption('disabled', true);
-        } else {
-            yield $createdAt;
         }
     }
 }
--- a/src/Entity/Comment.php
+++ b/src/Entity/Comment.php
@@ -7,6 +7,7 @@ use Doctrine\DBAL\Types\Types;
 use Doctrine\ORM\Mapping as ORM;

 #[ORM\Entity(repositoryClass: CommentRepository::class)]
+#[ORM\HasLifecycleCallbacks]
 class Comment
 {
     #[ORM\Id]
@@ -86,6 +87,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 プロパティにセットされます。

カンファンレンスへスラッグを追加する

/conference/1 といったカンファレンスの URL は特に意味はありません。これはデータベースのプライマリーキーといった実装の詳細に依るものになっています。

代わりに /conference/paris-2020 といった URL はどうですか?こちらの方が良いですね。 paris-2020 はカンファレンスの スラッグ と呼んでいます。

カンファレンスに slug プロパティを追加しましょう ( 255文字の長さで nullable でない型です):

1
$ symfony console make:entity Conference

新しいカラムを追加するのでマイグレーションファイルを作成しましょう:

1
$ symfony console make:migration

新しいマイグレーションを実行します:

1
$ symfony console doctrine:migrations:migrate

エラーになりましたが、想定内のことです。先程スラッグは 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 でない値をセットします。最後に、スラッグのカラムを null 不可にしています。

Note

実際のプロジェクトでは、 CONCAT(LOWER(city), '-', year) ではなく、 "本当の" スラッグを使用する必要があります。

これでマイグレーションが正しく動くはずです:

1
$ symfony console doctrine:migrations:migrate

これで各カンファレンスを探すためにスラッグを使うようにしたので、カンファレンスエンティティを修正して、スラッグがデータベース上でユニークになるようにしましょう:

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]
@@ -30,7 +32,7 @@ class Conference
     #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'conference', orphanRemoval: true)]
     private Collection $comments;

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

     public function __construct()

既にわかっているとは思いますが、ここでマイグレーションをする必要があります:

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

スラッグを生成する

URL は、ASCII 文字以外を変換する必要があり、正しくスラッグを生成することは、英語圏以外の言語にとって難しいです。例えば、 é を e に変換する必要があります。

車輪の再発明をせずに Symfony の String コンポーネントを使いましょう。 文字列から スラッグを生成 する方法が実装されています。

Conference クラスに、カンファレンスの情報からスラッグを生成する computeSlug() メソッドを追加します:

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')]
@@ -50,6 +51,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() メソッドは、現在のスラッグが何も指定していないか - と値が渡ったときのみ動作します。- の値は、バックエンドでカンファレンスを追加するときにスラッグが必須となるので使用します。空ではないこの特別な値でアプリケーションにスラッグを自動生成させることができます。

複雑なライフサイクルのコールバックを定義する

createdAt プロパティのように slug も更新時に computeSlug() メソッドを呼べば自動的にセットされるようにした方が良いですね。

このメソッドは SluggerInterface の実装に依存していますので、以前のように prePersist イベントに追加することはできません。

代わりに 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
namespace App\EntityListener;

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

class ConferenceEntityListener
{
    public function __construct(
        private SluggerInterface $slugger,
    ) {
    }

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

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

新しくカンファレンスが追加されたとき(perPersist())と更新されたとき(preUpdated())に、スラッグは更新されます。

コンテナにサービスを設定する

まだ、Symfony の鍵となるコンポーネント DIコンテナ について話していませんでした。このコンテナは、 サービス を作成したり必要なときにインジェクトしたりといった管理を行います:

サービス は "グローバル" なオブジェクトで、メーラーやロガーやスラッグ作成などの機能を提供します。これらは Doctrine のエンティティのインスタンスのような データオブジェクト とは違います。

実際は、必要なときにサービスが自動的にインジェクトされるのでコンテナを直接使うことはあまりありません。コンテナは型宣言によってコントローラの引数のオブジェクトを注入します。

前のステップでイベントリスナーがどうやって登録されたか不思議に思いませんでしたか?コンテナがその役割を担っていました。クラスが特定のインターフェースを実装すると、コンテナは、そのクラスがどうやって登録されるか知ることになるのです。

しかしここでは、クラスはインターフェースの実装や基底クラスの拡張をしていないので、Symfonyは自動的に設定することができません。代わりに、アトリビュートを使って、Symfonyコンテナに登録します:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
--- a/src/EntityListener/ConferenceEntityListener.php
+++ b/src/EntityListener/ConferenceEntityListener.php
@@ -3,9 +3,13 @@
 namespace App\EntityListener;

 use App\Entity\Conference;
+use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
+use Doctrine\ORM\Events;
 use Doctrine\Persistence\Event\LifecycleEventArgs;
 use Symfony\Component\String\Slugger\SluggerInterface;

+#[AsEntityListener(event: Events::prePersist, entity: Conference::class)]
+#[AsEntityListener(event: Events::preUpdate, entity: Conference::class)]
 class ConferenceEntityListener
 {
     public function __construct(

Note

Doctrine のイベントリスナーとSymfony のイベントリスナーは同じように見えますが、内部では異なるインフラストラクチャーを使っており別物ですので注意してください。

アプリケーションでスラッグを使用する

バックエンドでさらにカンファレンスを追加したり、既に登録されているものの年や都市を変更してみましょう。 - を値として使用しなければ、スラッグは更新されません。

最後に行う変更として、コントローラーやテンプレートでカンファレンスの 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
@@ -20,7 +20,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
@@ -16,7 +16,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>

これでカンファレンスのページへスラッグから辿ることができるようになりました:

/conference/amsterdam-2019

より深く学ぶために

  • Doctrine イベントシステム (ライフサイクルコールバックとリスナーとエンティティリスナーとライフサイクルサブスクライバー);
  • String コンポーネントのドキュメント;
  • サービスコンテナ;
  • Symfony サービスの Cheat Sheet.
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 6.4 is backed by

    Peruse our complete Symfony & PHP solutions catalog for your web development needs.

    Peruse our complete Symfony & PHP solutions catalog for your web development needs.

    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 German Bortoli, a Symfony contributor

    Thanks German Bortoli (@germanaz0) for being a Symfony contributor

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