ECCUBE4 Symfony CRUDフォーム画面実装の流れ　メモ

#はじめに
個人的なメモであります。
ECCUBE4の管理画面Adminの話であります。
今回はDeleteは省略しています。

#前提
MySQLサーバー内に使用するdatabaseを作成済み、という地点からの作業メモです。
環境=>Win10,WSL,ubuntu,vscode+sftp,EC2,MySQL5.7,Symfony2,ECCUBE4

#フォームを実装するためにやること
結論
・DBサーバーとDatabaseとDB設計(ER図)は準備しておく
・モデルを書く(Entity,Repositoty)
・コントローラーを書く(Controller)
・フォーム部品を書く(Form/type)
・ビューテンプレートを書く(twig)


#モデル（MVCのM）
DB:MySQLに「Userテーブル」をつくりたい
カラムは、主キーのid、nameカラムを想定
こんな感じにしたい

|  id |  name |
| ---- | ---- |
|  1  |  NULL  |
|  2  |  NULL  |

Customize/EntityディレクトリにUser.phpファイルを新規作成して編集します。

```PHP:Customize/Entity/User.php
 <?php

namespace Customize\Entity;                            //名前空間

use Doctrine\ORM\Mapping as ORM;                       //ORMマッピング
use Eccube\Annotation as Eccube;                       //アノテーション（注釈）
use Symfony\Component\Validator\Constraints as Assert; //フォーム側バリデーション


/**                                                    //アノテーション
 * User
 *
 * @ORM\Table(name="mtb_User")                         //mtb=マスタテーブルのUser
 * @ORM\Entity(repositoryClass="Customize\Repository\UserRepository")
 */                                                    //リポジトリ定義
class User extends \Eccube\Entity\AbstractEntity       //Entityクラス定義
{

//idカラム

    /**
     * @ORM\Column(name="id", type="integer"          //カラム名、データ型
     * @ORM\Id                                        //主キー
     * @ORM\GeneratedValue(strategy="IDENTITY")       //オートインクリメントid生成
     */
    private $id;                                      //インスタンス変数


//nameカラム

    /**
     * @ORM\Column(name="name", type="string", length=64, nullable=false) //nameカラム、データ型、DBカラム制約=空白許可
     * @Assert\NotBlank(message="なまえを入力してください")　              //フォーム空白時エラー
     * @Assert\Length(min = 2, max = 64)　　　　　　　　　               　//フォーム入力制限
     */
    private $name;                                                       //インスタンス変数
```
##エンティティの拡張
**EntityからGetter/Setter/Repositoryを自動生成**

< for EC-CUBE 4.0 Developers />
EC-CUBE 4.0 開発者向けドキュメントサイト
https://doc4.ec-cube.net/customize_entity#%E5%9F%BA%E6%9C%AC%E3%81%AE%E6%8B%A1%E5%BC%B5%E6%96%B9%E6%B3%95

**スキーマが更新されていることを確認**
**Entityのメンバのアノテーションを記載後**

```PHP:Customize/Entity以下にEntityを追加後以下を実行

## Proxy クラスの生成
 $ bin/console eccube:generate:proxies

## 作成したクラスを確実に認識できるようキャッシュを削除
 $ bin/console cache:clear --no-warmup

## 実行する SQL を確認
 $ bin/console doctrine:schema:update --dump-sql

## SQL を実行
 $ bin/console doctrine:schema:update --dump-sql --force
```

**それぞれのコマンドで、記述に不備があるとエラーになります**
**ひとつずつエラー解決してコマンドを通していきます。**

```言語:EntityからGetter/Setter/Repositoryを自動生成

 $ bin/console make:entity --regenerate

//再生成するクラスまたは名前空間を入力します
 Enter a class or namespace to regenerate [App\Entity]:
 > 

Customize\Entity を入力enterキー
```

```PHP:実行結果
$ bin/console make:entity --regenerate
PHP Warning:  Module 'intl' already loaded in Unknown on line 0
 ! [NOTE] It looks like your app may be using a namespace other than "App".                                             
 !                                                                                                                      
 !        To configure this and make your life easier, see:                                                             
 !        https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html#configuration                           
//このコマンドは、名前空間内のクラスまたはすべてのクラスに対して欠落しているメソッド（getterやsetterなど）を生成します。
 This command will generate any missing methods (e.g. getters & setters) for a class or all classes in a namespace.     

 To overwrite any existing methods, re-run this command with the --overwrite flag                                       

 Enter a class or namespace to regenerate [App\Entity]:
 > Customize\Entity を入力enterキー

 created: app/Customize/Repository/UserRepository.php
 updated: app/Customize/Entity/User.php
```

Entityファイルにセッター、ゲッターが上書きされました。
これでDB保存と呼び出しが実行できます。

```PHP:Customize/Entity/User.php
    public function getId(): ?int
    {
        return $this->id;
    }
    public function getName(): ?string
    {
        return $this->name;
    }
    public function setName(string $name): self
    {
        $this->name = $name;
        return $this;
    }
```
app/Customize/Repositoryディレクトリに
UserRepository.phpファイルが自動生成されました。
型が用意されて中身は無く、参考コードがコメントアウトされています。

```PHP:ec-cube/app/Customize/Repository/UserRepository.php
<?php

use Customize\Entity\User;
use Eccube\Repository\AbstractRepository;
use Symfony\Bridge\Doctrine\RegistryInterface;

class UserRepository extends AbstractRepository
{
    public function __construct()
    {
    }

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

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

# コントローラー(MVCのC)
コントローラでは、googleクロムなどのブラウザ側でユーザーリクエストに応じて画面遷移するHTTPメソッド"GET"のルーティングと、
フォーム送信＝HTTPメソッド"POST"を受けたときの、新規保存createメソッド、更新updateメソッドなどの処理を記述します。

**SymfonyではHTTPメソッド、の"CREATE"、"UPDATE"、"PUT"、をひとくくりに"POST"で対応するようです**

ec-cube/app/Customize/Controller/Admin/配下に
userController.phpを新規作成して編集します。

##ルーティング(画面遷移経路)とフォームの処理

```PHP:Customize/Controller/Admin/userController.php
<?php

namespace Customize\Controller\Admin;

use Customize\Entity\User;                                      //Userエンティティファイル
use Customize\Form\Type\Admin\UserEditType;                     //フォーム定義ファイル（あとで作ります）
use Eccube\Controller\AbstractController;                       //拡張機能
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;    //拡張機能
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;     //拡張機能
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;  //拡張機能
use Symfony\Component\HttpFoundation\Request;                   //リクエストを取得できる

class UserController extends AbstractController                 //クラス定義
{
//新規登録編集画面表示
   /**
     * @Method("GET")                                                                                      //HTTPメソッド
     * @Route("/%eccube_admin_route%/user/new", name="admin_user_new")                                     //新規作成newルーティング(URLパラメーター, ルーティング名)
     * @Route("/%eccube_admin_route%/user/{id}/edit", requirements={"id" = "\d+"}, name="admin_user_edit") //編集editルーティング(URLパラメーター, ルーティング名)
     * @Template("@admin/user/user.twig")                                                                  //対応するビューファイル
     */
    public function edit(Request $request, ?int $id = null)                                 //第一引数：HttpFoundation\Request機能の$request
                                                                                            //第二引数：タイプヒンティング（数値型を要求）した空の変数$id
    {
        //新規登録
        if (is_null($id)) {                                //idがnullならば
            $user = new user();                            //Entity,Userクラスインスタンス作成
        
                                                           //新規登録、編集ともに
        $form = $this->formFactory                         //フォームファクトリ機能のgrtFrom=フォームレンダリングの準備
            ->createBuilder(userEditType::class, $user)  
            ->getForm();

        return [
            'form' => $form->createView(),               //ビューにformオブジェクトを返す
            'user' => $user,                             //ビューにuserオブジェクトを返す
        ];
    }
```


```PHP:Customize/Controller/Admin/userController.php
//登録ボタンで実行するPOSTメソッドの処理
    /**
     * @Method("POST")                                                                                            //HTTPメソッド
     * @Route("/%eccube_admin_route%/user/{id}/update", requirements={"id" = "\d+"}, name="admin_user_update")    //更新ルーティング
     * @Route("/%eccube_admin_route%/user/create", name="admin_user_create")                                      //新規登録ルーティング
     * @Template("@admin/User/user.twig")                                                                         //対応するビューファイル
     */
    public function update(Request $request, ?int $id = null)
    {
        if (is_null($id)) {
            $user = new User();                          //新規作成のときはEntity,Userクラスインスタンス作成
                                                         //新規登録、編集ともに
        $form = $this->formFactory                       //フォームファクトリ機能のgrtFrom=フォームレンダリングの準備
            ->createBuilder(UserEditType::class, $user)
            ->getForm()
            ->handleRequest($request);                   //POSTリクエストを受け取る

        if (!$form->isValid()) {                         //もしバリデーションされたら
            return [
                'form' => $form->createView(),           //はい、もっかいやりなおし!
                'user' => $user,
            ];
        }

            $this->entityManager->persist($user);                     //永続化コマンド
            $this->entityManager->flush();                            //エンティティオブジェクトの内容をデータベースに反映
            $this->addSuccess('admin.common.save_complete', 'admin'); //ECCUBEデフォルトの登録しましたメッセージ表示

        //登録成功したら、登録したレコードidで編集画面にリダイレクトする
        return $this->redirectToRoute('admin_user_edit', ['id' => $user->getId()]);
    }

}
```
公式ドキュ
https://symfony.com/doc/current/components/http_foundation.html

#フォーム(Form)
ec-cube/app/Customize/Form/Type/Admin/ディレクトリに
UserEditType.phpを新規作成して編集します。

```PHP:app/Customize/Form/Type/Admin/UserEditType.php
<?php

namespace Customize\Form\Type\Admin;                       //名前空間

use Customize\Entity\user;                                 //Userエンティティ
use Symfony\Component\Form\AbstractType;                   //拡張機能
use Symfony\Component\Form\Extension\Core\Type\TextType;   //フォーム型
use Symfony\Component\Form\FormBuilderInterface;           //フォームビルダー

class UserEditType extends AbstractType                    //クラス定義
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder                                           //フォームビルダ変数(オブジェクト)
            ->add('name', TextType::class, [               //第一引数はEntityの変数$nameの$を除いたnameを、第二引数にはフォーム型をかく
                'attr' => ['class' => 'tinymce'],          //HTML属性にclass追加
                'required' => true,                        //入力必須とする
            ]);
     }
```
↓公式ドキュ：テキストタイプで使用可能なオプションでをみる
https://symfony.com/doc/current/reference/forms/types/text.html


# ビュー（MVCのV)

HTML/CSSのかわりに、テンプレートエンジンtwig（トゥイグ）形式のファイルを編集します。
今回はECCUBE管理画面で
app/template/admin/配下に
User/ディレクトリ新規作成、その配下に、UserEdit.twigを新規作成します

```Twig:app/template/admin/User/UserEdit.twig
{% extends '@admin/default_frame.twig' %}              <!-- ECCUBEでデフォルトテンプレート参照 -->

{% set menus = ['user', 'user_edit'] %}                <!-- menu変数に値セット -->

{% block title %}ユーザー管理{% endblock %}             <!-- 見出し -->
{% block sub_title %}ユーザー登録編集{% endblock %}     <!-- 見出し２ -->
<!-- フォームテーマを明示的に指定 -->
{% form_theme form '@admin/Form/bootstrap_4_horizontal_layout.html.twig' %} 
<!-- スタイルシート参照 -->
{% block stylesheet %}
    <link rel="stylesheet" href="{{ asset('assets/css/tempusdominus-bootstrap-4.min.css', 'admin') }}">
{% endblock stylesheet %}

{% block javascript %}
<script> 
<!-- 必要なjavascriptを書く -->
</script>
{% endblock %}

{% block main %}
<!-- フォームタグ,メソッドPOST,idが存在するとき編集へ、無ければ新規登録へルーティング -->
<form method="post" 
      action="{%- if user.id %}{{url('admin_user_update', {id : user.id})}}
              {% else %}{{url('admin_user_create')}}{% endif -%}">  
    <div class="row">
        <div class="col-3 ml-5 mb-3">
            <label class="col-form-label">ユーザー名</label>
               {{ form_widget(form.eventName) }}            <!-- フォーム部品の表示 -->         
               {{ form_errors(form.eventName) }}            <!--  標準エラーの表示-->        
        </div>

        <!-- 登録ボタン -->
        <div>
　　　　　　<button type="submit" >{{'登録'}}</button>
       </div>
   </div>
</form>
{% endblock %}
```
こんな感じになります。

```:ブラウザアドレスバーのURL
IPアドレス/admin/user/new
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617331/84e26480-4c67-4855-73f5-cbe1b20949af.png)

登録が完了したら、そのままedit画面にリダイレクト、画面遷移します。

```:ブラウザアドレスバーのURL
IPアドレス/admin/user/1/edit
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617331/f3489231-b0c5-e2d0-cbad-35bb41043b66.png)

# Doctrine　疎結合について
エンティティとモデルの違い
エンティティ(entity) は実体という意味
オブジェクトと区別をつけるとすればオブジェクトはプログラムの中で何か役割を持って仕事をする実体であるのに対し、エンティティは単に何らかの情報のカタマリを表す実体をささす。
アプリケーションを実装する際に、情報のカタマリであるエンティティごとにクラス
を実装する、このクラスのことをエンティティクラスと呼ぶ。

DoctrineはORM (Object-Relational Mapping、オブジェクトリレーショナルマッピング) ライブラリである。
ActiveRecord方式ではテーブルの1行1行を表すレコードオブジェクト自体がデータベースとやりとりするのに対し、DataMapper方式ではレコードオブジェクトとデータベースのやりとりをする機能とが切り離されている。
DataMapper方式は、ctiveRecord方式の持つ手軽さを少しだけ犠牲にして、その分メンテナンスのしやすさを向上させている。
DataMapper方式の根底にあるのはデータベースのスキーマとアプリケーション内で扱うオブジェクトとを切り離して扱える疎結合という考え方で、 これは結合が弱いという意味である。
ソフトウェア開発においては、モジュールやクラスのメンテナンス性の高さに結び付く1つの大事な考え方でSymfony全体がこの疎結合性を重視しており、その考え方にマッチするDoctrineが選ばれている。




#おわりに
ECCUBE4、SymfonyフレームワークについてはRubyOnRailsと違って
参考書や日本語ソースや新しい情報が少ないです
Symfony公式ドキュのが情報網羅性がう～～んな感じ。
現在ネットに公表されている情報が貴重でとても参考になっております。
公表してくださっている先輩エンジニアの方々にお礼申し上げます。