[concrete 8.2+] Expressオブジェクトで定義した属性を使用したフォームをパッケージインストールで追加する


## はじめに
Expressオブジェクトをパッケージからインストール方法はこちらを参照。
https://qiita.com/calmtech/items/3df4394c275a5c3031b8

パッケージから処理する理由は、パッケージのアンインストールとともに、インストール時に作られたExpressオブジェクトなどが一式削除されるため。

本稼働後はアップグレードをしていくことになるのだが、開発段階ではまっさらな状態からテストしたりすることが多い。

パッケージで管理しておくと、アンインストールすればデータをなくせるので、試行コストが下がるし実装箇所が局所化される。

専用のコマンドラインからも同様のことはできるが、保守性を重視。

## この記事で書かれていること
* Expressオブジェクトで使用するフォームをインストールするサンプルコード

## この記事で割愛されていること
* Expressオブジェクトの操作、説明
* concreteでパッケージインストールの操作、説明


## パッケージから処理するために知っておくこと
https://documentation.concrete5.org/developers/express/programmatically-creating-express-objects

サンプルとなるコードは、"Creating as Express Object Form" に記載されているが、そのまま利用すると動作しない。
コメント欄でのやり取りを見ていると、コマンドラインから実行すると動作するということだ。
https://concrete5-japan.org/help/5-7/recipes/how-to-use-console-commands/

しかし、コマンドラインからの処理だと、独自開発部分をアンインストールしたい時に、削除処理を実装しなければならない。

## Expressオブジェクト用のフォーム
管理画面から「システムと設置＞エクスプレス＞（データオブジェクト選択）＞フォーム＞フォームを追加」で Expressオブジェクトを操作するフォームを作成できる。

さらに「フィールドセットを追加」として、フィールドセットのプラスアイコンから「フォームコントロールを追加＞属性＞（Expressオブジェクトで定義した属性）」と選択していくと、Expressオブジェクトの定義で作成した属性を入力項目として使用できる。

つまり関係として、以下のように連鎖していることになる。

Expressオブジェクト＞フォーム＞フィールドセット＞各種コントロール（入力項目）

各種コントロールを末端として、さかのぼっていけるように「コントロール＞フィールドセット」という参照を定義しておかねばならない。いずれもprotected なので基本的には「インスタンスを作ったら、親への参照をセットする」という手続きが必要になる。

## なぜサンプルコードが動かないのか
まだ未検証だが、フォームやフィールドセットを手軽に作成できるよう、「FormBuilder」「FieldSetBuilder」など「xxxBuilder」というユーティリティクラスが用意されている。

しかし、ソースを追っていくとバルクインサートのような処理になっており、上記のようなクラスでは「親への参照をセットする」という手続きが入っていない。

そのため、パッケージインストールだと例えば「コントロールをExpressオブジェクトに関連づけられなかった。まず親オブジェクトを永続化（データベースに保存）しないといけない」といったエラーが発生してしまう。

Ruby on Rails での実装経験がある方ならわかると思うが、ORMのリレーションでインスタンスを作っただけの「build」の状態で親子関係の定義を処理できない。なのでデータベースに保存する「create」の状態を実装する必要がある。

concrete Ver.8 では、ORMのインスタンスを管理する「EntityManager」というクラスから永続化の手続きを行う。

フォームやフィールドセットといった関係するオブジェクトの手続きがひととおり終わったら、EntityManagerから永続化するという処理を逐一行うことでパッケージインストールから無事処理できる。

```Exception
A new entity was found through the relationship 'Concrete\Core\Entity\Express\Control\AttributeKeyControl#attribute_key' that was not configured to cascade persist operations for entity: . To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example @ManyToOne(..,cascade={"persist"}).
```


## 参考

各種BuilderなどExpressオジェクトを操作するために使われているクラス

* /concrete/src/Express/ObjectBuilder.php
* /concrete/src/Express/ObjectBuilder/FormBuilder.php
* /concrete/src/Express/ObjectBuilder/FieldsetBuilder.php
* /concrete/src/Entity/Express/Control/AttributeKeyControl.php
* /concrete/src/Entity/Attribute/Key/ExpressKey.php
* /concrete/vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php

今回の手続きを実装するにあたって参考となるファイル。管理画面からExpressオブジェクトを作成したりするコントローラー。

* /concrete/controllers/single_page/dashboard/system/express/entities/forms.php


```packages/my/controller.php
use \Concrete\Core\Package\Package;
use \Concrete\Core\Block\BlockType\BlockType;
use \Concrete\Core\Entity\Attribute\Key\Settings\DateTimeSettings;
use \Concrete\Core\Entity\Attribute\Key\Settings\SelectSettings;
use Concrete\Core\Entity\Attribute\Value\Value\SelectValue;
use Concrete\Core\Entity\Attribute\Value\Value\SelectValueOptionList;
use Concrete\Core\Entity\Attribute\Value\Value\SelectValueOption;
use Concrete\Core\Entity\Express\Control\Control;
use Concrete\Core\Entity\Express\Form;
use Concrete\Core\Entity\Attribute\Key\ExpressKey;
use Concrete\Core\Entity\Express\Control\AttributeKeyControl;
use Concrete\Core\Entity\Express\FieldSet;
use Doctrine\ORM\Id\UuidGenerator;
use Express;

class Controller extends Package
{

...

	public function install()
    {
        $pkg = parent::install();
        $this->installExpressObjects();
    }

    private function installExpressObjects()
    {
        $object = Express::buildObject('object', 'objects', 'Expressオブジェクト', $pkg);

        $object->addAttribute('text', 'ふりがな', 'object_kana');
        $object->save();
        
        // 作成したExpressオブジェクトのEntityManagerインスタンス
        $em = $object->getEntityManager();
        
        // フォームを作成。Expressオブジェクトが親
        $form = new Form();
        $form->setName('フォーム');
        // ここで紐付けたいExpressオブジェクトへの参照をセット
        $form->setEntity($object->getEntity());
        
        // 一旦、フォームを永続化
        $em->persist($form);
        $em->flush($form);
        
        // フィールドセットを作成。フォームが親
        $fieldSet = new FieldSet();
        $fieldSet->setTitle('フィールドセット');
        // フィールドセットの並び順。複数の場合はインクリメンタルで定義
        $fieldSet->setPosition(0);

        // ここで紐付けたいフォームへの参照をセット
        $fieldSet->setForm($form);
        
        // 一旦、フィールドセットを永続化
        $em->persist($fieldSet);
        $em->flush($fieldSet);
        
        $position = 0;
        // Expressオブジェクトの属性をフィールドセットに追加
        foreach($object->getEntity()->getAttributes() as $ak)
        {
        	$control = new AttributeKeyControl();
        	$control->setAttributeKey($ak);
        	$control->setId((new UuidGenerator())->generate($em, $object->getEntity()));
        	// 属性コントロールを作成。フィールドセットが親
        	$control->setFieldSet($fieldSet);
        	$control->setPosition($position);
        	
        	// 一旦、属性コントロールを永続化
        	$em->persist($control);
        	$em->flush($control);
        	
        	$fieldSet->getControls()->add($control);
        	
        	// コントロールの並び順
        	$position++;
        }
        
        // 関連付けが終わったのでフィールドセットを永続化
        $em->persist($fieldSet);
        $em->flush($fieldSet);
        
        // Expressオブジェクトの表示・編集フォームとして設定
        $object->setDefaultViewForm($form);
        $object->setDefaultEditForm($form);
        
        // Expressオブジェクトを更新
        $object->save();
    }
    
...

}

```