6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Symfonyで簡単なCRUDを作る

Last updated at Posted at 2024-12-30

この記事は カオナビ Advent Calendar 2024 に書くのが間に合わなかった記事になります。ちゃんと埋まってよかったね。

はじめに

はいさい。しまぶ@shimabox だよ。

というわけで本を書きました。こちらです。以降、カオナビ本と呼びますね。

本を書いた経緯などは、こちらが詳しいです。

ありがたいことに、レビューを書いてくれている人もちらほら見かけています。ほんまにありがてぇ。

さて、このカオナビ本では、フレームワークの紹介としてSymfonyのサンプルコードを載せています。(Chapter05 モダンなPHPフレームワーク)
ただ、紙面の都合で一部を省略しているんですよね。つまり、そのまま写経しても動かないんです。なんだか少し残念ですよね。

そこで今回はカオナビ本を片手に、Symfonyのサンプルコードを動かすところまでを、チュートリアル形式で一緒に進めていきたいと思います。

つまり、この記事のタイトルを正確に書くのなら

カオナビ本を片手にSymfonyで簡単なCRUDを作る

となります!

それでは、いってみましょう!レッツゴー!

前提条件

つくるもの

カオナビ本でいうところの、

  • Chapter05 モダンなPHPフレームワーク
    • 05-04 Symfony

が、該当します。以下のタスク一覧画面を作成します。

タスク一覧画面

  • タスクのタイトルと内容が書き込める
  • タスクのタイトルは必須、内容は任意
  • 登録したタスクは編集ができる
  • 登録したタスクの一覧を確認できる
  • タスクの削除ができる

今回の作業ログ

最初に貼っときます。今回、作業したリポジトリのログは以下になります。

環境を用意(Forkする)

まず環境を用意しましょう。こちらをForkします。

Forkする

  1. https://github.com/php-tech-master24/devenv にアクセスしたら右上のForkをクリックします。
    1.png

  2. 次に、Create forkをクリックします。
    2.png

  3. このように、forked from php-tech-master24/devenv となっていればOKです。
    3.png

  4. 以降、こちらのリポジトリをつかいます

作業ディレクトリを作る

ローカルに作業ディレクトリを作ります。

mkdir sandbox && cd sandbox

こちらで作ったローカルのsandboxディレクトリを、作業ディレクトリとします。

作業ディレクトリの名前はなんでもいいです

Cloneする

作業ディレクトリ内でclone

git clone https://github.com/shimabox/devenv.git # shimaboxのところは自身のアカウント名に変えてください

移動

cd devenv

環境を起動

make up

こんな感じになっていればOK。

docker ps

CONTAINER ID   IMAGE        COMMAND                  CREATED         STATUS         PORTS                               NAMES
be0409416857   devenv-web   "docker-php-entrypoi…"   9 seconds ago   Up 8 seconds   0.0.0.0:8080->80/tcp                web
a41cbfc9c9da   mysql:8.0    "docker-entrypoint.s…"   10 days ago     Up 10 days     0.0.0.0:3306->3306/tcp, 33060/tcp   db

以降、作業ディレクトリやコンテナー(App, DB)の中で作業をしていきます。
ターミナルは、別タブや別windowなどで複数開いておきましょう。

Appコンテナーの中に入る

make php-cli

ここにくる

root@be0409416857:/var/www/html#

ディレクトリはこうなっている

ls

chapter02  chapter09  chapter12  composer.json  composer.lock  phpunit.xml

composerが入っているか確認

which composer

/usr/bin/composer

ここまで確認できればGoooood。

Symfonyをインストール

コンテナーの中で以下の作業を行っていきます。

composer install

composer create-project symfony/skeleton symfony-sample

移動

cd symfony-sample

ディレクトリ構成を確認

ls

bin  composer.json  composer.lock  config  public  src  symfony.lock  var  vendor

各種Symfonyコンポーネントやバンドルをまとめてインストール

composer require webapp

webappパッケージをインストールすることで、以下のパッケージがまとめてインストールされます。

  • Twig
  • Doctrine ORM
  • Symfony MakerBundle
  • その他の必要なパッケージ

このコマンドについては、こちらが詳しいです。
「Symfonyをインストール」するとき、具体的に何が行われているのか

Dockerの設定をするか?という質問をされるので、n(No)と答えておきます。

Do you want to include Docker configuration from recipes?
[y] Yes
[n] No
[p] Yes permanently, never ask again for this project
[x] No permanently, never ask again for this project
(defaults to y): n

後は、がーっといろいろ行われると思います。最後まで実行されればOKです。

ディレクトリ構成を確認

ls

assets  bin  composer.json  composer.lock  config  importmap.php  migrations  phpunit.xml.dist  public  src  symfony.lock  templates  tests  translations  var  vendor

起動してみる

ここまでで、とりあえず起動してみます。
作業ディレクトリに移動します。Appコンテナーの中から抜けてください。

root@be0409416857:/var/www/html/symfony-sample# exit # exitで抜ける
exit
~/shimabox/sandbox/devenv (main)
%

Webサーバーを起動

Symfony CLI をインストールする

Symfony CLI はSymfonyをローカルで開発する際に利用できるコマンドラインツールです。いろいろと便利になるので、実際に開発する際は入れておいた方がいいです。

今回はこちらを利用します。

  • Mac
    brew install symfony-cli/tap/symfony-cli
    
  • Windows(試していません)
    scoop install symfony-cli
    

Webサーバーを起動

devenvにいると仮定します。

symfony server:start --dir=app/symfony-sample/public を実行します。

symfony server:start --dir=app/symfony-sample/public

〜

 [OK] Web server listening
      The Web server is using PHP CGI 8.3.9
      http://127.0.0.1:8000

デフォルトのportは8000です。

アクセス

http://localhost:8000 (http://127.0.0.1:8000) にアクセスしてみます。

CleanShot 2024-12-30 at 09.50.43.png

Goooood!!!

Symfonyの起動までをコミット

Symfonyの起動まで確認できたので、ここでいったんコミットしましょう。

git add .
git commit -m "feat: Symfonyの起動まで"

作業ログ

DBを用意する

CRUDを作っていきたいので、まずDBを用意します。
DBはMySQL8.0を使います。

DBの確認

DBコンテナーの中に入ります。
別タブや、別のwindowでターミナルなどを起動しましょう。

作業ディレクトリの中にいると仮定(いなかったら移動してください)

cd sandbox/devenv

DBコンテナーの中に入る

docker compose exec db /bin/bash

MySQLのバージョンを確認

mysql --version

mysql  Ver 8.0.37 for Linux on aarch64 (MySQL Community Server - GPL)

MySQLへ接続

DBの接続情報は以下になります。
compose.yml を参照

  • host
    • 127.0.0.1
  • port
    • 3306
  • database
    • sample
  • user
    • myuser
  • passward
    • mypassword
mysql -h 127.0.0.1 -u myuser -p sample
Enter password: 

中身を見る(まだ空です)

mysql> show tables;
Empty set (0.01 sec)

DBの確認が済んだらGoooood.

.env.local の作成

続いて.env.localファイルを用意します。

.envファイルは環境変数を定義しておくものになります。
ここへ環境ごとに読み込む値をセットしていきます。

今回はローカル環境なので、.env.localを用意します。
.env.localにセットされた値は、.envよりも優先されます。

参考

Appコンテナーの中に入る

make php-cli
cd symfony-sample/

.env.local の作成

touch .env.local

DB接続情報を書き込む

vim .env.local

DATABASE_HOSTDATABASE_URLを用意します。

DATABASE_HOST="127.0.0.1"
DATABASE_URL="mysql://myuser:mypassword@${DATABASE_HOST}:3306/sample?serverVersion=8.0.37"

環境変数の確認

php bin/console debug:container --env-vars

Symfony Container Environment Variables
=======================================

 ------------------------- ------------------ -----------------------------------------------------------------
  Name                      Default value      Real value
 ------------------------- ------------------ -----------------------------------------------------------------
  APP_SECRET                n/a                "5abb6e6abc1a0ac00fe0b74d3a6767fe"
  DATABASE_URL              n/a                "mysql://myuser:[email protected]:3306/sample?serverVersion=8.0.37"
  MAILER_DSN                n/a                "null://null"
  MESSENGER_TRANSPORT_DSN   n/a                "doctrine://default?auto_setup=0"
  VAR_DUMPER_SERVER         "127.0.0.1:9912"   n/a
 ------------------------- ------------------ -----------------------------------------------------------------

 // Note real values might be different between web and CLI.

DATABASE_URLが設定された値になっているか確認しましょう。
APP_SECRETは、.env.dev の値が読み込まれていてランダムな値が表示されているはずです。

この後は、

  1. エンティティ(リポジトリ)の作成
  2. マイグレーションファイルの生成
  3. マイグレーションファイルの適用

を行って、DBにテーブルを作成していきます。

なぜ、DATABASE_HOSTをわけているのか?

今回ホストOS側でPHPを実行(symfony server:start)するので、デフォルトはlocalhost(127.0.0.1)にしています。ですが、DBのマイグレーションはコンテナーの中で行うのでlocalhost(127.0.0.1)だと解決できません。

それを解消するためにDATABASE_HOSTをわけています。

DBのマイグレーションを行う際は、DATABASE_HOST="db"として実行します。
db:3306 のdbはDBコンテナーの名前です

環境変数の確認(コンテナ内での実行用)

DATABASE_HOST="db" php Bin/console debug:container --env-varsのように、先頭にDATABASE_HOST="db"をつけて、このコマンド実行時のみ環境変数を書き換えています。

DATABASE_HOST="db" php Bin/console debug:container --env-vars

Symfony Container Environment Variables
=======================================

 ------------------------- ------------------ -----------------------------------------------------------------
  Name                      Default value      Real value
 ------------------------- ------------------ -----------------------------------------------------------------
  APP_SECRET                n/a                "5abb6e6abc1a0ac00fe0b74d3a6767fe"
  DATABASE_URL              n/a                "mysql://myuser:mypassword@db:3306/sample?serverVersion=8.0.37"
  MAILER_DSN                n/a                "null://null"
  MESSENGER_TRANSPORT_DSN   n/a                "doctrine://default?auto_setup=0"
  VAR_DUMPER_SERVER         "127.0.0.1:9912"   n/a
 ------------------------- ------------------ -----------------------------------------------------------------

 // Note real values might be different between web and CLI.

エンティティ(リポジトリ)の作成

そのままAppコンテナーの中で作業します。

Taskエンティティを作成

php bin/console make:entity Taskを実行します。

php bin/console make:entity Task

created: src/Entity/Task.php
created: src/Repository/TaskRepository.php

Entity generated! Now let's add some fields!
You can always add more fields later manually or by re-running this command.

プロンプトに従って、以下のフィールドを追加します。

  • id (integer)
  • title (string)
    • フィールドの長さ: 255
    • Not Null
  • description (text)
    • Default Null
プロンプト
New property name (press <return> to stop adding fields):
 > title

 Field type (enter ? to see all types) [string]:
 > string

 Field length [255]:
 > 255

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

 updated: src/Entity/Task.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > description

 Field type (enter ? to see all types) [string]:
 > text

 Can this field be null in the database (nullable) (yes/no) [no]:
 > yes

 updated: src/Entity/Task.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 >

  Success!

 Next: When you're ready, create a migration with php bin/console make:migration

id はデフォルトで作成されます

Taskエンティティの確認

app/symfony-sample/src/Entity/Task.phpが作成されています。

<?php

namespace App\Entity;

use App\Repository\TaskRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: TaskRepository::class)]
class Task
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $title = null;

    #[ORM\Column(type: Types::TEXT, nullable: true)]
    private ?string $description = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getTitle(): ?string
    {
        return $this->title;
    }

    public function setTitle(string $title): static
    {
        $this->title = $title;

        return $this;
    }

    public function getDescription(): ?string
    {
        return $this->description;
    }

    public function setDescription(?string $description): static
    {
        $this->description = $description;

        return $this;
    }
}

app/symfony-sample/src/Repository/TaskRepository.php は以下になります。

TaskRepository
<?php

namespace App\Repository;

use App\Entity\Task;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/**
 * @extends ServiceEntityRepository<Task>
 */
class TaskRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Task::class);
    }

    //    /**
    //     * @return Task[] Returns an array of Task objects
    //     */
    //    public function findByExampleField($value): array
    //    {
    //        return $this->createQueryBuilder('t')
    //            ->andWhere('t.exampleField = :val')
    //            ->setParameter('val', $value)
    //            ->orderBy('t.id', 'ASC')
    //            ->setMaxResults(10)
    //            ->getQuery()
    //            ->getResult()
    //        ;
    //    }

    //    public function findOneBySomeField($value): ?Task
    //    {
    //        return $this->createQueryBuilder('t')
    //            ->andWhere('t.exampleField = :val')
    //            ->setParameter('val', $value)
    //            ->getQuery()
    //            ->getOneOrNullResult()
    //        ;
    //    }
}

エンティティ(リポジトリ)の作成までをコミット

エンティティ(リポジトリ)の作成まで確認できたので、ここでいったんコミットしましょう。
作業ディレクトリで行います。

git add src/Entity/Task.php src/Repository/TaskRepository.php
git commit -m "feat: エンティティ(リポジトリ)の作成"

作業ログ

マイグレーションファイルの生成

DATABASE_HOST="db" php bin/console make:migrationを実行します。

DATABASE_HOST="db" php bin/console make:migration

created: migrations/Version20241229070020.php

  Success!

Review the new migration then run it with php bin/console doctrine:migrations:migrate
See https://symfony.com/doc/current/bundles/DoctrineMigrationsBundle/index.html

マイグレーションファイルの適用

DATABASE_HOST="db" php bin/console doctrine:migrations:migrateを実行します。

DATABASE_HOST="db" php bin/console doctrine:migrations:migrate

 WARNING! You are about to execute a migration in database "sample" that could result in schema changes and data loss. Are you sure you wish to continue? (yes/no) [yes]:
 > yes

[notice] Migrating up to DoctrineMigrations\Version20241229070020
[notice] finished in 25.3ms, used 24M memory, 1 migrations executed, 2 sql queries

 [OK] Successfully migrated to version: DoctrineMigrations\Version20241229070020
WARNING! You are about to execute a migration in database "sample" that could result in schema changes and data loss. Are you sure you wish to continue? (yes/no) [yes]:
 > yes

ほんまにこれでいいんでっか?と、聞かれますが yesと答えましょう。
気になる方は、

php bin/console doctrine:migrations:migrate --no-interaction

のように、--no-interactionをつければ聞かれることはありません。

テーブルの確認

DBコンテナーの中で、DBにつないでテーブルを確認してみましょう。

docker compose exec db /bin/bash

mysql -h 127.0.0.1 -u myuser -p sample

mysql> desc task;
+-------------+--------------+------+-----+---------+----------------+
| Field       | Type         | Null | Key | Default | Extra          |
+-------------+--------------+------+-----+---------+----------------+
| id          | int          | NO   | PRI | NULL    | auto_increment |
| title       | varchar(255) | NO   |     | NULL    |                |
| description | longtext     | YES  |     | NULL    |                |
+-------------+--------------+------+-----+---------+----------------+
3 rows in set (0.01 sec)

Goooood.

マイグレーションファイルの適用までをコミット

マイグレーションファイルの適用まで確認できたので、ここでいったんコミットしましょう。
作業ディレクトリで行います。

git add migrations/.
git commit -m "feat: マイグレーションファイルの生成" 

作業ログ

エンティティの修正

追記 (2024/12/31) にあるとおり、こちらの作業はやらなくても大丈夫です!すっ飛ばしてこ!

Taskエンティティのidとtitleのアクセサは、Not Nullでいきたいのでsrc/Entity/Task.phpを修正します。

git diff

diff --git a/app/symfony-sample/src/Entity/Task.php b/app/symfony-sample/src/Entity/Task.php
index 4c802d1..e330414 100644
--- a/app/symfony-sample/src/Entity/Task.php
+++ b/app/symfony-sample/src/Entity/Task.php
@@ -12,20 +12,20 @@ class Task
     #[ORM\Id]
     #[ORM\GeneratedValue]
     #[ORM\Column]
-    private ?int $id = null;
+    private int $id;

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

     #[ORM\Column(type: Types::TEXT, nullable: true)]
     private ?string $description = null;

-    public function getId(): ?int
+    public function getId(): int
     {
         return $this->id;
     }

-    public function getTitle(): ?string
+    public function getTitle(): string
     {
         return $this->title;
     }
修正版Taskエンティティ
<?php

namespace App\Entity;

use App\Repository\TaskRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: TaskRepository::class)]
class Task
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $title = null;

    #[ORM\Column(type: Types::TEXT, nullable: true)]
    private ?string $description = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getTitle(): ?string
    {
        return $this->title;
    }

    public function setTitle(string $title): static
    {
        $this->title = $title;

        return $this;
    }

    public function getDescription(): ?string
    {
        return $this->description;
    }

    public function setDescription(?string $description): static
    {
        $this->description = $description;

        return $this;
    }
}

Taskエンティティの修正をコミット

修正したのでコミットしましょう。
作業ディレクトリで行います。

git add src/Entity/Task.php
git commit -m "feat: nullableをやめる"

作業ログ

CRUD

CRUDをひとつひとつ作っていきます。

  1. Read
  2. Create
  3. Update
  4. Delete

の順に作成していきます。Appコンテナーの中で作業します。

php bin/console make:crud Task

で自動生成できますが、今回はひとつひとつ作成していきます。
なるべくカオナビ本をコピペしてくのです!

Controller

まずコントローラを作ります。

app/symfony-sample/src/Controller/TaskController.php

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class TaskController extends AbstractController
{
}

Read

では、CRUDのRを作っていきます。タスク一覧です。

task_indexのルートを作成します。

Controllerの修正

<?php

namespace App\Controller;

use App\Entity\Task;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class TaskController extends AbstractController
{
    #[Route('/task', name: 'task_index', methods: ['GET'])]
    public function index(EntityManagerInterface $em): Response
    {
        $tasks = $em->getRepository(Task::class)->findAll();
        return $this->render('task/index.html.twig', ['tasks' => $tasks]);
    }
}

Attributeを利用したルーティングを行っています。config/routes.yamlで確認できます。

controllers:
    resource:
        path: ../src/Controller/
        namespace: App\Controller
    type: attribute

Viewの作成

app/symfony-sample/templates/task/index.html.twig を作成します。

{# templates/task/index.html.twig #}

{% extends 'base.html.twig' %}

{% block title %}タスク一覧{% endblock %}

{% block body %}
    <h1>タスク一覧</h1>
    <ul>
        {% for task in tasks %}
            <li>
                {{ task.title }}
            </li>
        {% else %}
            <li>タスクがありません。</li>
        {% endfor %}
    </ul>
{% endblock %}

タスクがあれば、タスクのタイトルを表示して、なければタスクがありません。と表示します。

確認

http://localhost:8000/task にアクセスして確認します。

タスクがないとき

CleanShot 2024-12-30 at 10.39.41.png

タスクがあるとき

まだタスクが無いので作ります。
DBコンテナーからDBに繋いで、以下クエリを流します。

devenvにいると仮定します

docker compose exec db /bin/bash

DBに接続

mysql -h 127.0.0.1 -u myuser -p sample
Enter password: 

以下のクエリを流す
INSERT INTO task (title, description) VALUES ('Task 1', 'This is task1.'), ('Task 2', 'This is task2.'), ('Task 3', 'This is task3.');

mysql> INSERT INTO task (title, description) VALUES ('Task 1', 'This is task1.'), ('Task 2', 'This is task2.'), ('Task 3', 'This is task3.');
Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> select * from task;
+----+--------+----------------+
| id | title  | description    |
+----+--------+----------------+
|  1 | Task 1 | This is task1. |
|  2 | Task 2 | This is task2. |
|  3 | Task 3 | This is task3. |
+----+--------+----------------+
3 rows in set (0.00 sec)

http://localhost:8000/task に再度アクセス。

CleanShot 2024-12-30 at 10.59.15.png

Goooood.

Readの作成までをコミット

Readの作成まで確認できたので、ここでいったんコミットしましょう。

git add app/symfony-sample/src/Controller/TaskController.php app/symfony-sample/templates/task/
git commit -m "feat: Readの作成まで"

作業ログ

Create

続いて、CRUDのCを作っていきます。タスクの登録です。

Controllerの修正

task_newのルートを作成します。

<?php

namespace App\Controller;

use App\Entity\Task;
use App\Form\TaskType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class TaskController extends AbstractController
{
    // 〜

    #[Route('/task/new', name: 'task_new', methods: ['GET', 'POST'])]
    public function new(Request $request, EntityManagerInterface $em): Response
    {
        $task = new Task();
        $form = $this->createForm(TaskType::class, $task);

        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $em->persist($task);
            $em->flush();

            return $this->redirectToRoute('task_index');
        }

        return $this->render('task/new.html.twig', ['form' => $form->createView()]);
    }
}

Formの作成

app/symfony-sample/src/Form/TaskType.php

<?php

namespace App\Form;

use App\Entity\Task;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('title', TextType::class, [
                'label' => 'Title',
            ])
            ->add('description', TextareaType::class, [
                'label' => 'Description',
                'required' => false,
            ]);
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Task::class,
        ]);
    }
}

descriptionは、'required' => false, にして任意としています。

Viewの作成と修正

app/symfony-sample/templates/task/new.html.twig を作成します。

{# templates/task/new.html.twig #}

{% extends 'base.html.twig' %}

{% block title %}新しいタスクを作成{% endblock %}

{% block body %}
    <h1>新しいタスクを作成</h1>

    {{ form_start(form) }}
    {{ form_widget(form) }}
    <button type="submit">作成</button>
    {{ form_end(form) }}

    <a href="{{ path('task_index') }}">戻る</a>
{% endblock %}

app/symfony-sample/templates/task/index.html.twig を修正します。

{# templates/task/index.html.twig #}

{% extends 'base.html.twig' %}

{% block title %}タスク一覧{% endblock %}

{% block body %}
    <h1>タスク一覧</h1>

    <a href="{{ path('task_new') }}">新しいタスクを作成</a>

    <ul>
        {% for task in tasks %}
            <li>
                {{ task.title }}
            </li>
        {% else %}
            <li>タスクがありません。</li>
        {% endfor %}
    </ul>
{% endblock %}

<a href="{{ path('task_new') }}">新しいタスクを作成</a> を追加しただけです。

確認

http://localhost:8000/task にアクセスして確認します。

CleanShot 2024-12-30 at 17.32.00.gif

Goooood.

Createの作成までをコミット

Createの作成まで確認できたので、ここでいったんコミットしましょう。

git add app/symfony-sample/src/Controller/TaskController.php app/symfony-sample/templates/task/ app/symfony-sample/src/Form/
git commit -m "feat: Createの作成まで"

作業ログ

Update

続いて、CRUDのUを作っていきます。タスクの更新です。

Controllerの修正

task_editのルートを作成します。

<?php

namespace App\Controller;

use App\Entity\Task;
use App\Form\TaskType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class TaskController extends AbstractController
{
    // 〜

    #[Route('/task/{id}/edit', name: 'task_edit', methods: ['GET', 'PUT'])]
    public function edit(Task $task, Request $request, EntityManagerInterface $em): Response
    {
        $form = $this->createForm(TaskType::class, $task, [
            'method' => 'PUT',
        ]);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $em->flush();

            return $this->redirectToRoute('task_index');
        }

        return $this->render('task/edit.html.twig', [
            'form' => $form->createView(),
            'task' => $task,
        ]);
    }
}

フォームはsrc/Form/TaskType.phpを使いまわします。ポイントは'method' => 'PUT',をoptionで渡しているところです。後ほど説明します。

Viewの作成と修正

app/symfony-sample/templates/task/edit.html.twig を作成します。

{# templates/task/edit.html.twig #}

{% extends 'base.html.twig' %}

{% block title %}タスクを編集{% endblock %}

{% block body %}
    <h1>タスクを編集</h1>

    {{ form_start(form, {
        'method': 'PUT',
        'action': path('task_edit', {'id': task.id})
    }) }}
    {{ form_widget(form) }}
    <button type="submit">更新</button>
    {{ form_end(form) }}

    <a href="{{ path('task_index') }}">戻る</a>
{% endblock %}

app/symfony-sample/templates/task/index.html.twig を修正します。

{# templates/task/index.html.twig #}

{% extends 'base.html.twig' %}

{% block title %}タスク一覧{% endblock %}

{% block body %}
    <h1>タスク一覧</h1>

    <a href="{{ path('task_new') }}">新しいタスクを作成</a>

    <ul>
        {% for task in tasks %}
            <li>
                <a href="{{ path('task_edit', {'id': task.id}) }}">{{ task.title }}</a>
            </li>
        {% else %}
            <li>タスクがありません。</li>
        {% endfor %}
    </ul>
{% endblock %}

タイトルを、<a href="{{ path('task_edit', {'id': task.id}) }}">{{ task.title }}</a>としただけです。

PUTを使う

今回、意識高めに更新はPUTを使っています。ですが、このまま更新を実行しても何も起こりません。

HTMLフォームの制約として、ブラウザのHTMLフォームはGETとPOSTメソッドのみをサポートしているからです。method="PUT"と指定しても、ブラウザはそれをPOSTとして扱います。そのためHTTPメソッドをオーバーライドする必要があり、Symfonyはその術を提供しています。

以下が必要になります。

  • フォーム(src/Form/TaskType.php)にメソッドオプションを追加
    • 'method' => 'PUT',
  • フォーム(templates/task/edit.html.twig)でmethodフィールドを使用
    • form, {'method': 'PUT',
  • http_method_overrideの有効化
    • 特にこれが大事
http_method_override の有効化

config/packages/framework.yaml

# see https://symfony.com/doc/current/reference/configuration/framework.html
framework:
    secret: '%env(APP_SECRET)%'

    # Note that the session will be started ONLY if you read or write from it.
    session: true

    #esi: true
    #fragments: true
    
    http_method_override: true

when@test:
    framework:
        test: true
        session:
            storage_factory_id: session.storage.factory.mock_file

http_method_override: true を追加します。

確認

http://localhost:8000/task にアクセスして確認します。

CleanShot 2024-12-30 at 18.06.42.gif

Goooood.

Updateの作成までをコミット

Updateの作成まで確認できたので、ここでいったんコミットしましょう。

git add app/symfony-sample/config/packages/framework.yaml app/symfony-sample/src/Controller/TaskController.php app/symfony-sample/templates/task/
git commit -m "feat: Updateの作成まで"

作業ログ

Delete

最後に、CRUDのDを作っていきます。タスクの削除です。

Controllerの修正

task_deleteのルートを作成します。

<?php

namespace App\Controller;

use App\Entity\Task;
use App\Form\TaskType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class TaskController extends AbstractController
{
    // 〜

    #[Route('/task/{id}/delete', name: 'task_delete', methods: ['DELETE'])]
    public function delete(Task $task, Request $request, EntityManagerInterface $em): Response
    {
        if ($this->isCsrfTokenValid('delete'.$task->getId(), $request->request->get('_token'))) {
            $em->remove($task);
            $em->flush();
        }

        return $this->redirectToRoute('task_index');
    }
}

ポイントは、$this->isCsrfTokenValid('delete'.$task->getId(), $request->request->get('_token')) です。フォームを使えばcsrfチェックを勝手にしてくれますが、フォームを使わない場合はこのようにする必要があります。

Viewの修正

app/symfony-sample/templates/task/index.html.twig を修正します。

{# templates/task/index.html.twig #}

{% extends 'base.html.twig' %}

{% block title %}タスク一覧{% endblock %}

{% block body %}
    <h1>タスク一覧</h1>

    <a href="{{ path('task_new') }}">新しいタスクを作成</a>

    <ul>
        {% for task in tasks %}
            <li>
                <a href="{{ path('task_edit', {'id': task.id}) }}">{{ task.title }}</a>
                <form action="{{ path('task_delete', {'id': task.id}) }}" method="post" style="display:inline">
                    <input type="hidden" name="_method" value="DELETE">
                    <input type="hidden" name="_token" value="{{ csrf_token('delete' ~ task.id) }}">
                    <button type="submit">削除</button>
                </form>
            </li>
        {% else %}
            <li>タスクがありません。</li>
        {% endfor %}
    </ul>
{% endblock %}

task_deleteのformを追加しただけです。

確認

http://localhost:8000/task にアクセスして確認します。

CleanShot 2024-12-30 at 18.43.48.gif

Goooood.

Deleteの作成までをコミット

Deleteの作成まで確認できたので、ここでいったんコミットしましょう。

git add app/symfony-sample/src/Controller/TaskController.php app/symfony-sample/templates/task/index.html.twig
git commit -m "feat: Deleteの作成まで。CRUD、Done!"

作業ログ

これで完了です。お疲れ様でした。

追記 (2024/12/31)

コメントにて、

EntityのプロパティをPHPレベルでnon-nullableにするのは(make:entityのデフォルトがnullableになっていることからも)最近のSymfonyコミュニティの慣習から外れていそうなので、あえてそうしなくてもいいかなと思いました。

というのを頂きました。なるほどー、make:entityで作成されたデフォルトのEntityを修正するのは違和感があったのですよね。EntityはEntityなので、それをどう扱うかはドメイン側の仕事なのかなと思いました。

というわけで修正しています。

まとめ

  • カオナビ本の、Chapter05 モダンなPHPフレームワーク(05-04 Symfony) のサンプルコードをチュートリアル形式で実行してみました
  • エンティティを作成してマイグレーションファイルの生成、適用を行いました
    • 環境変数でDATABASE_HOSTを分けて実行したりもしました
    • 他にいいやり方があるかもしれません(誰か教えてください)
  • 簡単なCRUDを作成して、http_method_override: trueの注意点を知ることができました

おわりに

この年末年始は、カオナビ本を片手にお勉強をしてみるのはいかがでしょうか?

ちなみに、まだ:fire:火力(レビュー):fire:が足りません!辛辣なレビューでもいいので、ぜひお願いします!

それではみなさんよいお年を!

6
0
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?