39
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

EC-CUBE研修資料(Symfony入門ハンズオン)

Last updated at Posted at 2021-01-22

社内向けに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

image.png

簡単なCRUDアプリを作る

ここから簡易的なCRUDができるアプリを作っていきます。
Controller、Twig、FormType、Doctrine/ORMの使い方を学びます。

簡単な画面表示

まずは簡単な画面表示から。

src/Controller/TodoController.php
<?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を使ったテンプレートに置き換えます。

templates/todo.html.twig
hello world twig
src/Controller/TodoController.php
+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等)

src/Controller/TodoController.php
     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テンプレート内で変数として利用可能。
templates/todo.html.twig
-hello world twig
+{% if flg %}
+    {% for num in nums %}
+        {{ message }} world {{ num }}
+    {% endfor %}
+{% endif %}
  • if, forなどは{% %}
  • 変数出力は{{ value }}。デフォルトでエスケープされる。
  • 詳しくは→ https://twig.symfony.com/

twigテンプレートの利用(テンプレート継承)

templates/hello.html.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テンプレートを追加しておきます。

src/Controller/TodoController.php
+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はこちらで指定してる。
※このときの戻り値は配列

templates/create.html.twig
{% 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テンプレートから呼び出し

src/Form/Type/TodoType.php
<?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 を参照

src/Controller/TodoController.php
+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()
+        ];
     }
 }

templates/create.html.twig
 {% endblock %}
 
 {% block body %}
    <form method="post">
-        <input type="text" name="todo"/>
+        {{ form_widget(form.todo) }}
         <button type="submit">登録</button>
     </form>
 {% endblock %}

※form_widgetで画面描画できる

FormTypeを利用したフォーム作成(バリデーション)

バリデーションを追加

src/Form/Type/TodoType
 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 を参照

templates/create.html.twig
     <form method="post">
         {{ form_widget(form.todo) }}
+        {{ form_errors(form.todo) }}
         <button type="submit">登録</button>
     </form>

※エラーメッセージを表示するために、form_errorsを使う

src/Controller/TodoController.php
 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)

config/packages/translation.yaml
 framework:
-    default_locale: en
+    default_locale: ja
     translator:
         default_path: '%kernel.project_dir%/translations'
         fallbacks:

※エラーメッセージの日本語化

config/packages/framework.yaml
 framework:
     secret: '%env(APP_SECRET)%'
-    #csrf_protection: true
+    csrf_protection: true
     #http_method_override: true
templates/create.html.twig
 {% 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の利用(準備)

エンティティの作成、データベースの作成、テーブル作成を行う

src/Entity/Todo.php

<?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 を参照

src/Repository/TodoRepository.php
<?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・テーブル作成を行う

.env
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リストを表示させる。

src/Controller/TodoController.php

 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等が使える

templates/todo.html.twig
 {% 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で登録画面を作成します。

src/Form/Type/TodoType.php
+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を指定する

src/Controller/TodoController.php
+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 画面へ成功メッセージを出力

templates/create.html.twig
 {% 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を、編集もできるように拡張していきます。

src/Controller/TodoController.php
 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から取得したデータをフォームへ渡す。これで画面描画した際に、初期値として設定できる。

/templates/create.html.twig
             {{ 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先を切り替え

templates/todo.html.twig
 
 {% 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トークンチェックはこのような形で。

templates/todo.html.twig
     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とかは使えないので注意

参考文献・資料

EC-CUBE4の基礎

TODO

39
49
0

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
39
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?