【PHP】PHPフレームワーク「Flow」を試してみる～DB接続＆データ登録編～

# 初めに
前回は[こちら](https://qiita.com/nnhkrnk/items/2b9f647d6130a098345f)の記事でマイナフレームワークFlowを使った簡単なAPI作成を行いました。
今回はMySQLにデータを登録するAPIを作成していきます。

# FlowにおけるDB接続処理
Flowはdoctrineと呼ばれるORMを内包しています。
今回はこのライブラリを用いてTBL作成やDB接続を行っていきます。

https://www.doctrine-project.org/

## doctrineでできること
doctrineはORM形式でDB操作を行います。
具体的には以下のような特徴を持ちます。
* SELECTなどのDMLを記載する必要がなく、doctrineで用意されているメソッドを利用してDB操作を行う。
* 事前のテーブル作成が不要。１つのTBL対応するデータクラスを作成し、doctrine　のmigrateコマンドを実行することでTBL作成を行う。

# DB接続試してみた
ということで、DB接続を行うAPIを作成していきましょう。
今回は商品情報を登録するAPIを作成していきます。
* リクエストで商品情報を受け取る
* 商品TBLにデータを登録
* レスポンスで商品情報を返す

商品TBLは以下です。
|論理名|物理名|データ型|その他制約|
|--|--|--|--|
|商品名|item_name|varchar(20)|NOT NULL|
|商品説明|description|varchar(100)||
|価格|price|int(11)|NOT NULL|


プロジェクト構成は以下です。
ControllerからRepositoryを呼び出してデータを登録するというシンプルな作りにします。

```
Quickstart
    ├ Configration/
    |    └ Settings.yaml（★）
    └ Packages/
         ├ Application/
         |    └ Neos.Welcome/
         |         └ Classes/
         |              ├ Controller/
         |              |    └ ItemController.php（★）
         |              └ Domain/
         |                   ├ Model
         |                   |    └ Item.php（★）
         |                   └ Repository
         |                        └ ItemRepository.php（★）
         ├ Framework/
         └ Libraries/

```

## 環境
* OS：Windows11 Pro
* PHP：8.1.25
* Flow：8.3.5
* composer：2.6.5
* MySQL：5.7.43
* doctrine：2.17.1



## ０．事前手順
まずは事前準備として以下の2つを行います。
* MySQLの構築
* Flow側のMySQL接続設定

### MySQLの構築
ローカルにMySQLを構築しておきましょう。
今回はdockerを用いて作成しました。

以下の```docker-compose.yml```を用意し、```docker compose up -d```を実行しました。
```yaml: docker-compose.yml
# docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7
    volumes:
      - db_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: mydatabase
    ports:
      - "3306:3306"

volumes:
  db_data: {}
```

```docker compose up -d```コマンドを実行しコンテナを起動します。
```sh
$ docker compose up -d
[+] Running 3/3
 ✔ Network docker_default   Created                                                                                                                     0.0s
 ✔ Volume "docker_db_data"  Created                                                                                                                     0.0s
 ✔ Container docker-db-1    Started
```

### Flow側のMySQL接続設定
FlowではConfigration直下にある```Settings.yaml```にMySQLの接続設定を記載します。
以下のように記載しました。

```yaml:Settings.yaml
Neos:
  Flow:
    persistence:
      backendOptions:
        driver: 'pdo_mysql'
        charset: 'utf8mb4'
        host: '127.0.0.1'    # Docker service name
        dbname: 'mydatabase'  # Database name
        user: 'root'  # MySQL user
        password: 'root'  # MySQL password
        port: '3306'  # MySQL port
        driverOptions:
          1002: 'SET SESSION wait_timeout=5'
```


## １．Modelクラス作成
doctrineを使用してTBL作成を行います。
flowに用意されている```kickstart:model```コマンドを実行してItemクラスを作成しました。
```sh
$ ./flow kickstart:model Neos.Welcome Item
Created .../Neos.Welcome/Classes/Domain/Model/Item.php
Created .../Neos.Welcome/Tests/Unit/Domain/Model/ItemTest.php
As a new model was generated, don't forget to update the database schema with the respective doctrine:* commands.
```

```php: Item.php
<?php
namespace Neos\Welcome\Domain\Model;

/*
 * This file is part of the Neos.Welcome package.
 */

use Neos\Flow\Annotations as Flow;
use Doctrine\ORM\Mapping as ORM;

/**
 * @Flow\Entity
 */
class Item
{


}
```

モデルが作成できました。
このままではdoctrineを用いたマイグレーションができないので、いくつか修正していきます。

### テーブルとの紐づけ
classに```@ORM\Table```というアノテーションを追加し、紐づけるテーブル名を記載しました。

```php
/**
 * @Flow\Entity
 * @ORM\Table(name="item")
 */
class Item
```

### テーブルカラムの作成
```@ORM\Column```アノテーションを付与したメンバ変数をいくつか作成しました。
これが商品TBLのカラムになります。
```@ORM\Column```でtypeやuniqueなどを指定することで、カラムの属性を決めることができます。


```php
    /**
     * @ORM\Column(type="string", name="item_name", length=20, nullable=false, unique=true)
     * @var string
     */
    protected $name;
```

:::note warn
```@var```がない場合や、アクセス修飾子がの場合、privateとdoctrineがカラムとして読み込まれませんので注意が必要です。
:::

コンストラクタも追加し、最終的には以下のようになりました。

```php
<?php
namespace Neos\Welcome\Domain\Model;

use Neos\Flow\Annotations as Flow;
use Doctrine\ORM\Mapping as ORM;

/**
 * @Flow\Entity
 * @ORM\Table(name="item")
 */
class Item
{

    /**
     * @ORM\Column(type="string", name="item_name", length=20, nullable=false, unique=true)
     * @var string
     */
    protected $name;

    /**
     * @ORM\Column(type="string", length=100)
     * @var string
     */
    protected $description;

    /**
     * @ORM\Column(type="integer", nullable=false)
     * @var int
     */
    protected $price;

    public function __construct(string $name, string $description, int $price)
    {
        $this->name = $name;
        $this->description = $description;
        $this->price = $price;
    }

}

```

この時、```./flow doctrine:validate```を実行することで、文法的に問題がないかどうかを確認することができます。

```sh
$ ./flow doctrine:validate

Mapping validation passed, no errors were found.
```

## ２．TBL作成
作成されたmodel元にテーブルを作成してきます。
テーブル作成は以下の２ステップです。
* doctrine:migrateの実行
* doctrine:updateの実行

### doctrine:migrateの実行
まず初めに、flowの```doctrine:migrate```コマンドを実行しましょう。
このコマンドで、doctrineの実行自体に必要なTBLなどのデータが作成されます。

```sh
$ ./flow doctrine:migrate
Migrating up to Neos\Flow\Persistence\Doctrine\Migrations\Version20200908155620

     -> CREATE TABLE flow3_resource_resourcepointer (hash VARCHAR(255) NOT NULL, PRIMARY KEY(hash)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB


～中略～


[warning] Migration Neos\Flow\Persistence\Doctrine\Migrations\Version20150611154419 was executed but did not result in any SQL statements.
[notice] finished in 1309.4ms, used 36M memory, 27 migrations executed, 93 sql queries
```
warningは出てますが、実行自体は完了しTBLが作成されました。


```sh
mysql> show tables
    -> ;
+-------------------------------------------------+
| Tables_in_mydatabase                            |
+-------------------------------------------------+
| flow_doctrine_migrationstatus                   |
| neos_flow_mvc_routing_objectpathmapping         |
| neos_flow_resourcemanagement_persistentresource |
| neos_flow_security_account                      |
+-------------------------------------------------+
4 rows in set (0.00 sec)
```

### doctrine:updateの実行
続いてdoctrine:updateコマンドを実行します。
このコマンドにより、先ほど作成した商品TBLが作成されます。

```sh
$ ./flow doctrine:update
Executed a database schema update.
```

```
mysql> show tables
    -> ;
+-------------------------------------------------+
| Tables_in_mydatabase                            |
+-------------------------------------------------+
| flow_doctrine_migrationstatus                   |
| item                                            |
| neos_flow_mvc_routing_objectpathmapping         |
| neos_flow_resourcemanagement_persistentresource |
| neos_flow_security_account                      |
+-------------------------------------------------+
5 rows in set (0.00 sec)
mysql> 
```

商品TBLの詳細は以下です。モデルので定義した情報が設定されていることが確認できました。
```

mysql> show create table item\G
*************************** 1. row ***************************
       Table: item
Create Table: CREATE TABLE `item` (
  `persistence_object_identifier` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
  `item_name` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
  `description` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
  `price` int(11) NOT NULL,
  PRIMARY KEY (`persistence_object_identifier`),
  UNIQUE KEY `UNIQ_1F1B251E96133AFD` (`item_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.00 sec)
```

:::note info
```persistence_object_identifier```というカラムが作成されてますが、これはdoctrineがテーブル管理に用いている主キーのようです。doctrineを用いてTBLを作成する際はこのIDのカラムが自動的に付与されます。
:::


## ３．Repositoryクラス作成
続いて商品TBLを操作するためのItemRepositoryを作成していきます。
flowの```kickstart:repository```コマンドを用いて作成しました。

```sh
$ ./flow kickstart:repository Neos.Welcome Item
Created .../Neos.Welcome/Classes/Domain/Repository/ItemRepository.php
```

```php:ItemRepository.php
<?php
namespace Neos\Welcome\Domain\Repository;

/*
 * This file is part of the Neos.Welcome package.
 */

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Persistence\Repository;

/**
 * @Flow\Scope("singleton")
 */
class ItemRepository extends Repository
{

    // add customized methods here
}

```

このままではDB登録できないので、データを登録する用のメソッドを追加しました。

```php

<?php
namespace Neos\Welcome\Domain\Repository;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Persistence\Repository;
use Neos\Welcome\Domain\Model\Item;

/**
 * @Flow\Scope("singleton")
 */
class ItemRepository extends Repository
{
    /**
     * @param string $name
     * @param string $description
     * @return void
     */
    public function addItem(string $name, string $description, int $price): void
    {
        // 商品モデルにデータを詰める
        $item = new Item($name, $description, $price);

        // 商品TBLへデータをINSERT
        $this->add($item);
    }

}
```

```$this->add($item);```にて、Flowのフレームワークを介してdoctrineで定義されたデータ登録用のメソッドを呼び出しているみたいです。

## ４．Controller作成
リクエストを受け取るためのControlerを作成します。
この辺は[前回の記事](https://qiita.com/nnhkrnk/items/2b9f647d6130a098345f)で書いたような手順で作成しました。

```php:ItemController.php
<?php
namespace Neos\Welcome\Controller;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\Controller\ActionController;

class ItemController extends ActionController
{

    /**
     * @Flow\Inject
     * @var \Neos\Flow\Mvc\View\JsonView
     */
    protected $view;

    /**
     * @Flow\Inject
     * @var \Neos\Welcome\Domain\Repository\ItemRepository
     */
    protected $itemRepository;
    
    public function postItemAction(string $name, string $description, int $price)
    {
        $this->itemRepository->addItem($name, $description, $price);
        $this->view->assign('value', array(
            "postedItem" => array(
                "name" => $name,
                "description" => $description,
                "price" => $price
            )
        ));
    }
}

```

## ５．動作確認
ローカルで動作確認していきます。
まずはサーバを起動します。
```
$ ./flow server:run
Server running. Please go to http://127.0.0.1:8081 to browse the application.
[Sat Nov 25 14:06:38 2023] PHP 8.1.25 Development Server (http://127.0.0.1:8081) started
```

サーバが起動できたので、リクエストを送ってみます。
リクエストパラメータは以下です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430841/ae2af393-9ffc-7a09-8627-1d7f6afd0865.png)


<details><summary>実際に送ったjsonはこちら</summary>

```json:リクエストBody
{
    "name": "Item Name",
    "description": "Item Description",
    "price": 100
}
```
</details>

レスポンスは200で返ってきました。
Bodyも問題なさそうです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430841/f891dd3a-8c01-579e-5725-1e2d8f886118.png)

DBにも問題なくデータが登録されていることが確認できました。
```sh
mysql> select * from item
    -> ;
+--------------------------------------+-----------+------------------+-------+
| persistence_object_identifier        | item_name | description      | price |
+--------------------------------------+-----------+------------------+-------+
| 1d89b35b-0df6-4233-9ea9-8927e52cc4cc | Item Name | Item Description |   100 |
+--------------------------------------+-----------+------------------+-------+
1 row in set (0.00 sec)
```

# 終わりに
今回はFlowにおけるDB登録を行いました。
今回は簡単なTBLを使用してデータ登録を行いましたが、リレーションがもっと複雑な場合はどうなるかなどはまだ理解しきれていません。
次はそのあたりも調べたいですね。
分かり次第またまとめようと思います。

# 参考資料
https://flowframework.readthedocs.io/en/stable/Quickstart/index.html
https://webukatu.com/wordpress/blog/30447/
https://weblabo.oscasierra.net/php-composer-windows-install/
https://www.doctrine-project.org/