この記事は カオナビ Advent Calendar 2024 に書くのが間に合わなかった記事になります。ちゃんと埋まってよかったね。
はじめに
はいさい。しまぶ@shimabox だよ。
というわけで本を書きました。こちらです。以降、カオナビ本と呼びますね。
本を書いた経緯などは、こちらが詳しいです。
ありがたいことに、レビューを書いてくれている人もちらほら見かけています。ほんまにありがてぇ。
さて、このカオナビ本では、フレームワークの紹介としてSymfonyのサンプルコードを載せています。(Chapter05 モダンなPHPフレームワーク)
ただ、紙面の都合で一部を省略しているんですよね。つまり、そのまま写経しても動かないんです。なんだか少し残念ですよね。
そこで今回はカオナビ本を片手に、Symfonyのサンプルコードを動かすところまでを、チュートリアル形式で一緒に進めていきたいと思います。
つまり、この記事のタイトルを正確に書くのなら
カオナビ本を片手にSymfonyで簡単なCRUDを作る
となります!
それでは、いってみましょう!レッツゴー!
前提条件
- GitHubアカウントがある
- Dockerが利用できる
- https://github.com/php-tech-master24/devenv を利用します
- 環境構築手順 が終わっていればOKです
つくるもの
カオナビ本でいうところの、
- Chapter05 モダンなPHPフレームワーク
- 05-04 Symfony
が、該当します。以下のタスク一覧画面を作成します。
タスク一覧画面
- タスクのタイトルと内容が書き込める
- タスクのタイトルは必須、内容は任意
- 登録したタスクは編集ができる
- 登録したタスクの一覧を確認できる
- タスクの削除ができる
今回の作業ログ
最初に貼っときます。今回、作業したリポジトリのログは以下になります。
環境を用意(Forkする)
まず環境を用意しましょう。こちらをForkします。
Forkする
-
https://github.com/php-tech-master24/devenv にアクセスしたら右上の
Fork
をクリックします。
-
以降、こちらのリポジトリをつかいます
作業ディレクトリを作る
ローカルに作業ディレクトリを作ります。
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) にアクセスしてみます。
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
よりも優先されます。
参考
-
Symfonyの環境変数の扱いについてまとめたよ | QUARTETCOM TECH BLOG
.env < .env.local < .env.$APP_ENV < .env.$APP_ENV.local < マシンに設定されている環境変数
Appコンテナーの中に入る
make php-cli
cd symfony-sample/
.env.local の作成
touch .env.local
DB接続情報を書き込む
vim .env.local
DATABASE_HOST
とDATABASE_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 の値が読み込まれていてランダムな値が表示されているはずです。
この後は、
- エンティティ(リポジトリ)の作成
- マイグレーションファイルの生成
- マイグレーションファイルの適用
を行って、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をひとつひとつ作っていきます。
- Read
- Create
- Update
- 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 にアクセスして確認します。
タスクがないとき
タスクがあるとき
まだタスクが無いので作ります。
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 に再度アクセス。
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 にアクセスして確認します。
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 にアクセスして確認します。
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 にアクセスして確認します。
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
の注意点を知ることができました
おわりに
この年末年始は、カオナビ本を片手にお勉強をしてみるのはいかがでしょうか?
ちなみに、まだ火力(レビュー)が足りません!辛辣なレビューでもいいので、ぜひお願いします!
それではみなさんよいお年を!