社内向けにEC-CUBE4の入門・トレーニングを行うことになったのでメモとして。
前提
他言語や他フレームワークである程度開発経験のある方向けの資料です。
EC-CUBEを把握するために、Symfonyの機能をざっくり理解していただくことを目的にしています。
動作環境
以下の環境で動作確認しています。
- Mac Catalina
- PHP 7.3.17
- SQLite3 3.28.0
- Google Chrome 88
Symfonyの基礎
セットアップ
symfonyコマンドの導入。
以下はMacの例。Winの場合は https://symfony.com/download を参照。
$ curl -sS https://get.symfony.com/cli/installer | bash
プロジェクトの作成。EC-CUBE4.0はSymfony3.4ベースのため、バージョンは3.4を指定。
symfony new my_project_name --version=3.4 --full
ディレクトリ構成
ディレクトリ構成は以下の通り。
├── bin
├── config
├── migrations
├── public
├── src
│ ├── Controller
│ ├── Entity
│ ├── Form
│ └── Repository
├── templates
├── tests
├── translations
├── var
└── vendor
ディレクトリ | 概要 |
---|---|
bin | コンソールコマンド |
config | 設定ファイル |
migrations | マイグレーションファイル |
public | ドキュメントルート |
src | プロダクトコード |
templates | twigテンプレート |
tests | テストコード |
translations | 言語ファイル |
var | ログファイルやキャッシュ等 |
vendor | composerでインストールされる外部ライブラリ |
起動
Symfony Local Web Server で起動。
cd my_project_name
symfony serve
簡単なCRUDアプリを作る
ここから簡易的なCRUDができるアプリを作っていきます。
Controller、Twig、FormType、Doctrine/ORMの使い方を学びます。
簡単な画面表示
まずは簡単な画面表示から。
<?php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class TodoController
{
/**
* @Route("/todo", name="todo") # ※1
*/
public function index() # ※2
{
return new Response('hello world'); # ※3
}
}
/todoにアクセスし、hello worldが表示されればOK。
- ※1 パスの指定は
@Route
アノテーションで行う。 - ※1 第2引数のname属性は、twigテンプレート等から指定するとURLやパスに変換できる(後述)。
- ※2 メソッド名の命名はなんでもよい
- ※3 戻り値はResponseオブジェクト
twigテンプレートの利用
Symfonyでは、テンプレートエンジンにtwigを利用しています。
出力結果をtwigを使ったテンプレートに置き換えます。
hello world twig
+use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
-class TodoController
+class TodoController extends Controller # ※1
{
/**
* @Route("/todo", name="todo")
*/
public function index()
{
- return new Response('hello world');
+ return $this->render('todo.html.twig'); # ※2
}
}
/todoにアクセスし、hello world twigが表示されればOK。
- ※1 Controllerクラスを継承することで各種ユーティリティメソッドを追加できる
- ※2 render('テンプレート名')でtwigテンプレートで出力できる
twigテンプレートの利用(変数、if、for等)
public function index()
{
- return $this->render('todo.html.twig'); # ※1
+ return $this->render('todo.html.twig', [
+ 'message' => 'hello',
+ 'nums' => [1,2,3],
+ 'flg' => true,
+ ])
- 第2引数に key - valueで渡すことで、twigテンプレート内で変数として利用可能。
-hello world twig
+{% if flg %}
+ {% for num in nums %}
+ {{ message }} world {{ num }}
+ {% endfor %}
+{% endif %}
- if, forなどは{% %}
- 変数出力は{{ value }}。デフォルトでエスケープされる。
- 詳しくは→ https://twig.symfony.com/
twigテンプレートの利用(テンプレート継承)
-{% if flg %}
- {% for num in nums %}
- {{ message }} world {{ num }}
- {% endfor %}
-{% endif %}
+{% extends 'base.html.twig' %}
+
+{% block title %}
+ hello
+{% endblock %}
+
+{% block body %}
+ {% if flg %}
+ {% for num in nums %}
+ {{ message }} world {{ num }}
+ {% endfor %}
+ {% endif %}
+{% endblock %}
- 親のテンプレートを指定して共通化できる
- extendsで親テンプレートを指定
- 親テンプレートでblock定義されている箇所に差し込むことができる
FormTypeを利用したフォーム作成(準備)
登録フォームを作成します。
登録画面のルーティングとtwigテンプレートを追加しておきます。
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
@@ -19,4 +20,13 @@ class TodoController extends Controller
'flg' => true,
]);
}
+
+ /**
+ * @Route("/todo/create", name="todo_create")
+ * @Template("create.html.twig")
+ */
+ public function create()
+ {
+ return [];
+ }
}
※twigテンプレートの指定は、@Template
アノテーションでも可能です。EC-CUBEはこちらで指定してる。
※このときの戻り値は配列
{% extends 'base.html.twig' %}
{% block title %}
todo create
{% endblock %}
{% block body %}
<form method="post">
<input type="text" name="todo"/>
<button type="submit">登録</button>
</form>
{% endblock %}
FormTypeを利用したフォーム作成(FormTypeの作成)
FormTypeを作成して、twigテンプレートから呼び出し
<?php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
class TodoType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('todo', TextType::class, []);
}
}
※input type="text"はTextTypeを利用
※その他のフォームタイプは https://symfony.com/doc/current/reference/forms/types.html を参照
+use App\Form\Type\TodoType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
class TodoController extends Controller
*/
public function create()
{
- return [];
+ $form = $this->createForm(TodoType::class);
+
+ return [
+ 'form' => $form->createView()
+ ];
}
}
{% endblock %}
{% block body %}
<form method="post">
- <input type="text" name="todo"/>
+ {{ form_widget(form.todo) }}
<button type="submit">登録</button>
</form>
{% endblock %}
※form_widgetで画面描画できる
FormTypeを利用したフォーム作成(バリデーション)
バリデーションを追加
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Validator\Constraints\NotBlank;
class TodoType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
- $builder->add('todo', TextType::class, []);
+ $builder->add('todo', TextType::class, [
+ 'required' => false,
+ 'constraints' => [
+ new NotBlank()
+ ]
+ ]);
}
}
※constraintsオプションでバリデーションを追加できる
※required => falseはhtml5のバリデーション。ここではサーバへPOSTされないのでデモ用として無効化しているだけ。
※その他オプションは https://symfony.com/doc/4.0/validation.html を参照
<form method="post">
{{ form_widget(form.todo) }}
+ {{ form_errors(form.todo) }}
<button type="submit">登録</button>
</form>
※エラーメッセージを表示するために、form_errorsを使う
use App\Form\Type\TodoType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
@@ -26,9 +27,14 @@ class TodoController extends Controller
* @Route("/todo/create", name="todo_create")
* @Template("create.html.twig")
*/
- public function create()
+ public function create(Request $request)
{
$form = $this->createForm(TodoType::class);
+ $form->handleRequest($request);
+
+ if ($form->isSubmitted() && $form->isValid()) {
+ // do something
+ }
return [
※フォームからのリクエストを、FormTypeにマッピングする
FormTypeを利用したフォーム作成(日本語化とcsrf protection)
framework:
- default_locale: en
+ default_locale: ja
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
※エラーメッセージの日本語化
framework:
secret: '%env(APP_SECRET)%'
- #csrf_protection: true
+ csrf_protection: true
#http_method_override: true
{% block body %}
<form method="post">
+ {{ form_widget(form._token) }}
{{ form_widget(form.todo) }}
{{ form_errors(form.todo) }}
<button type="submit">登録</button>
※csrf_protectionの有効化。form._tokenでトークン発行できる。
Doctrine/ORMの利用(準備)
エンティティの作成、データベースの作成、テーブル作成を行う
<?php
namespace App\Entity;
use App\Repository\TodoRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=TodoRepository::class)
*/
class Todo
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $todo;
public function getId(): ?int
{
return $this->id;
}
public function getTodo(): ?string
{
return $this->todo;
}
public function setTodo(string $todo): self
{
$this->todo = $todo;
return $this;
}
}
※@ORM\Entity
で対象のテーブルを指定(上記では省略されてるが、テーブル名を指定することも可能)
※@ORM\Column
でカラムのデータ型等を指定
※アノテーションの詳細は https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/annotations-reference.html を参照
<?php
namespace App\Repository;
use App\Entity\Todo;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method Todo|null find($id, $lockMode = null, $lockVersion = null)
* @method Todo|null findOneBy(array $criteria, array $orderBy = null)
* @method Todo[] findAll()
* @method Todo[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class TodoRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Todo::class);
}
}
※レポジトリを通じてデータの取得を行う
エンティティ・レポジトリを作成したら、DB・テーブル作成を行う
DATABASE_URL=sqlite:///%kernel.project_dir%/var/data.db
※.envにsqliteのパスを追加
bin/console doctrine:database:create
※データベース作成
bin/console doctrine:schema:create
※テーブル作成
sqlite3 var/data.db
sqlite> insert into todo (todo) values ('todo1');
sqlite> insert into todo (todo) values ('todo2');
sqlite> insert into todo (todo) values ('todo3');
sqlite> select * from todo;
1|todo1
2|todo2
3|todo3
※3件ほどデータを追加しておく
Doctrine/ORMの利用(Read)
最初に作成した、TodoController::indexで、todoリストを表示させる。
namespace App\Controller;
use App\Form\Type\TodoType;
+use App\Repository\TodoRepository;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
@@ -11,16 +12,27 @@ use Symfony\Component\Routing\Annotation\Route;
class TodoController extends Controller
{
+ /**
+ * @var TodoRepository
+ */
+ private $todoRepository;
+
+ public function __construct(TodoRepository $todoRepository)
+ {
+ $this->todoRepository = $todoRepository;
+ }
+
/**
* @Route("/todo", name="todo")
+ * @Template("todo.html.twig")
*/
public function index()
{
- return $this->render('todo.html.twig', [
- 'message' => 'hello',
- 'nums' => [1,2,3],
- 'flg' => true,
- ]);
+ $todoList = $this->todoRepository->findBy([], ['id' => 'desc']);
+
+ return [
+ 'todoList' => $todoList
+ ];
}
/**
※コンストラクタでTodoRepositoryをインジェクションする
※find/findAll/findBy等が使える
{% block body %}
- {% if flg %}
- {% for num in nums %}
- {{ message }} world {{ num }}
- {% endfor %}
- {% endif %}
+ <ul>
+ {% for todo in todoList %}
+ <li>{{ todo.id }} - {{ todo.todo }}</li>
+ {% endfor %}
+ </ul>
{% endblock %}
Doctrine/ORMの利用(Create)
TodoController::createで登録画面を作成します。
+use App\Entity\Todo;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
class TodoType extends AbstractType
@@ -18,4 +20,11 @@ class TodoType extends AbstractType
]
]);
}
+
+ public function configureOptions(OptionsResolver $resolver)
+ {
+ $resolver->setDefaults([
+ 'data_class' => Todo::class
+ ]);
+ }
}
フォームからリクエストされた値をエンティティにマッピングするため、TodoTypeでdata_classを指定する
+use App\Entity\Todo;
use App\Form\Type\TodoType;
use App\Repository\TodoRepository;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
@@ -41,11 +42,18 @@ class TodoController extends Controller
*/
public function create(Request $request)
{
- $form = $this->createForm(TodoType::class);
+ $form = $this->createForm(TodoType::class, new Todo()); # ※1
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
- // do something
+ $todo = $form->getData(); # ※2
+ $entityManager = $this->getDoctrine()->getManager(); # ※3
+ $entityManager->persist($todo); # ※4
+ $entityManager->flush(); # ※5
+
+ $this->addFlash('success', '登録しました。'); # ※6
+
+ return $this->redirectToRoute('todo_create');
}
※1 Todoエンティティの空オブジェクトを渡す
※2 $form->getData()で、Todoエンティティにフォームからのリクエストをセットした状態で取得できる
※3 Doctrineのエンティティマネージャを取得
※4 persist()でエンティティが永続化対象であることをDoctrineに認識させる
※5 flush()でSQL発行
※6 画面へ成功メッセージを出力
{% endblock %}
{% block body %}
+ {% for message in app.flashes('success') %}
+ <div class="alert alert-success">
+ {{ message }}
+ </div>
+ {% endfor %}
<form method="post">
{{ form_widget(form._token) }}
{{ form_widget(form.todo) }}
{{ form_errors(form.todo) }}
<button type="submit">登録</button>
</form>
+ <a href="{{ url('todo') }}">一覧へ戻る</a>
{% endblock %}
成功メッセージを出力。
ついでに一覧画面へのリンクを追加。url()関数にルーティング名を渡すとurlを生成できる。
Doctrine/ORMの利用(Update)
TodoController::createを、編集もできるように拡張していきます。
use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
class TodoController extends Controller
@@ -38,11 +39,20 @@ class TodoController extends Controller
/**
* @Route("/todo/create", name="todo_create")
+ * @Route("/todo/{id}/edit", name="todo_edit", requirements={"id" = "\d+"}) # ※1
* @Template("create.html.twig")
*/
- public function create(Request $request)
+ public function create(Request $request, $id = null) # ※2
{
- $form = $this->createForm(TodoType::class, new Todo());
+ if ($id === null) {
+ $todo = new Todo();
+ } else {
+ $todo = $this->todoRepository->find($id); # ※3
+ if ($todo === null) {
+ throw new NotFoundHttpException();
+ }
+ }
+ $form = $this->createForm(TodoType::class, $todo); # ※4
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
@@ -51,13 +61,15 @@ class TodoController extends Controller
$entityManager->persist($todo);
$entityManager->flush();
- $this->addFlash('success', '登録しました。');
+ $operation = $id ? '更新' : '登録';
+ $this->addFlash('success', $operation.'しました。');
- return $this->redirectToRoute('todo_create');
+ return $this->redirectToRoute('todo_edit', ['id' => $todo->getId()]);
}
return [
- 'form' => $form->createView()
+ 'form' => $form->createView(),
+ 'todo' => $todo,
];
※1※2 編集用のルーティングを追加。{id}
でコントローラの引数としてあつかえる。requirements={"id" = "\d+"}
で文字種を数値のみに限定。
※3 編集時はDBからデータを取得
※4 DBから取得したデータをフォームへ渡す。これで画面描画した際に、初期値として設定できる。
{{ message }}
</div>
{% endfor %}
- <form method="post">
+ <form method="post" action="{{ todo.id ? url('todo_edit', { id: todo.id }) : url('todo_create') }}">
{{ form_widget(form._token) }}
{{ form_widget(form.todo) }}
{{ form_errors(form.todo) }}
登録フォームを編集時でも使えるように、idの有無でPOST先を切り替え
{% block body %}
+ <a href="{{ url('todo_create') }}">新規登録</a>
<ul>
{% for todo in todoList %}
- <li>{{ todo.id }} - {{ todo.todo }}</li>
+ <li>{{ todo.id }} - <a href="{{ url('todo_edit', { id: todo.id }) }}">{{ todo.todo }}</a></li>
{% endfor %}
</ul>
登録画面、編集画面への導線を追加
編集時のコントローラの流れをざっくり説明すると以下のとおり。
// id = 123 のtodoを編集する場合
// フォームからsubmitすると/todo/123/editにPOSTされ、引数$idに123がセットされる
public function create(Request $request, $id = null)
{
if ($id === null) {
$todo = new Todo();
} else {
// id = 123 をDBから検索
$todo = $this->todoRepository->find($id);
if ($todo === null) {
throw new NotFoundHttpException();
}
}
$form = $this->createForm(TodoType::class, $todo);
// POSTされた値を、todoエンティティのプロパティに反映
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// 反映後のtodoエンティティを取得
$todo = $form->getData();
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($todo);
// エンティティが更新されている場合、UPDATE文をDBに発行
$entityManager->flush();
Doctrine/ORMの利用(Delete)
削除機能を作ります。
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
@@ -72,4 +73,26 @@ class TodoController extends Controller
'todo' => $todo,
];
}
+
+ /**
+ * @Route("/todo/{id}/remove", name="todo_remove", requirements={"id" = "\d+"}, methods={"POST"}) # ※1
+ */
+ public function remove(Request $request, $id)
+ {
+ $token = $request->headers->get('x-csrf-token'); # ※2
+ if (!$this->isCsrfTokenValid('x-csrf-token', $token)) {
+ throw new BadRequestHttpException();
+ }
+
+ $todo = $this->todoRepository->find($id);
+ if ($todo == null) {
+ throw new NotFoundHttpException();
+ }
+
+ $entityManager = $this->getDoctrine()->getManager();
+ $entityManager->remove($todo);
+ $entityManager->flush();
+
+ return $this->json(['success' => true]);
+ }
※1 HTTP METHODを指定したいときはMethod={}で指定する。EC-CUBEでは、削除系はDELETEメソッドを指定している。
※2 ajax処理の際のCSFRトークンチェックはこのような形で。
hello
{% endblock %}
+{% block javascripts %}
+ <script>
+ window.onload = event => {
+ const btns = document.querySelectorAll('button')
+ btns.forEach((btn) => {
+ btn.addEventListener('click', event => {
+ if (!window.confirm('削除しますか?')) {
+ return;
+ }
+ fetch(btn.dataset.url, {
+ method: 'post',
+ headers: {
+ 'x-csrf-token': '{{ csrf_token('x-csrf-token') }}'
+ }
+ })
+ .then((res) => {
+ if (res.ok) {
+ alert('削除しました')
+ btn.parentElement.remove();
+ } else {
+ alert('削除に失敗しました')
+ }
+ })
+ })
+ })
+ }
+ </script>
+{% endblock %}
+
{% block body %}
<a href="{{ url('todo_create') }}">新規登録</a>
<ul>
{% for todo in todoList %}
- <li>{{ todo.id }} - <a href="{{ url('todo_edit', { id: todo.id }) }}">{{ todo.todo }}</a></li>
+ <li>{{ todo.id }} - <a href="{{ url('todo_edit', { id: todo.id }) }}">{{ todo.todo }}</a>
+ <button type="button" data-url="{{ url('todo_remove', { id: todo.id }) }}">削除</button>
+ </li>
{% endfor %}
</ul>
{% endblock %}
※ EC-CUBE 4.0では IE11がシステム要件に入っているので constとかは使えないので注意
参考文献・資料
- https://www.shuwasystem.co.jp/book/9784798056692.html
-
https://symfony.com/doc/4.0/index.html
- ※Symfony3.4は過渡期のバージョンのため、EC-CUBEで適用できないことが多い。4.0のドキュメントを参照するのがよい
- https://twig.symfony.com/
- https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/index.html
EC-CUBE4の基礎
TODO