PHP Slim 環境構築(5) PDOとDBアクセス

# PHP Slim 環境構築(5) PDOとDBアクセス

## Introduction

[前回](https://qiita.com/dynamitecoolguy/items/9652ea9e935f897c8be1)は、Autoloader周りを触りましたが、今回はPDOを使ってDBにアクセスしてみたいと思います。

## 変更点

### ソースツリー

前回からの変更・追加ソースは以下の通りです。

```
$(PROJECTROOT)
  /compose
    docker-compose.yml
  /src
    /hoge
      /lib
        /Controller
          PlayerController.php            (NEW!)
          PlayerCreatedLogController.php  (NEW!)
        /Model
          Player.php                      (NEW!)
          PlayerCreatedLog.php            (NEW!)
      /public
        index.php
    composer.json
    composer.lock
```

### docker-compose.yml

web_hogeコンテナから、DBコンテナ(mysqlとpostgresql)にアクセスするために、linksを設定します。
これで、ホスト名"mysql"と"postgresql"でそれぞれのDBを識別することができます。

```yml
  web_hoge:
    build:
      context: ./web_hoge
      args:
        - environment=local
    links:                                (追加)
      - mysql                             (追加)
      - postgresql                        (追加)
```

### composer.json/composer.lock

Slimフレームワークでは、PDOの生成はDIコンテナから行うのが一般的です。
PSR-11に対応しているDIコンテナとして、php-diをcomposerで追加します。
また、PDOおよびJSONのPHP拡張を使用するので、それらもcomposerで追加しておきます。

```sh
$ composer require php-di/php-di:6.0.9
$ composer require ext-pdo
$ composer require ext-json
```

### index.php

19～58行目:
DIコンテナにDBの設定とDBのfactoryを追加しています。本当はソースを分けたりいろいろすべきですが、べらっと書いてしまっています。

62～63行目:
レスポンスに自動的にContent-Lengthを付けるためのMiddlewareを追加しています。
PDO周りとは直接関係ないのですが、PDOから取得したデータをそのまま出すだけだと、Content-Lengthが付かないので、一応お行儀よくしました。

68～70行目:
前回のExampleBeforeMiddlewareとExampleAfterMiddlewareを"/"だけに限定して付けています。
こういうやりかたもできるというテストです。

72～76行目:
GET "/player/{id}"、POST "/player"、PUT "/player/{id}"のCRUD (Dはありませんが)の定義です。
PlayerControllerに処理を移譲しており、この中でMySQLへのアクセス処理を行っています。

78～81行目:
GET "/player_created/{id}"、GET "/player_created"の定義です。
PlayerCreatedLogControllerに処理を移譲しており、この中でPostgreSQLへのアクセス処理を行っています。

```php:/src/hoge/public/index.php
<?php

use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
use Slim\Routing\RouteCollectorProxy;
use DI\ContainerBuilder;

use Hoge\ExampleAfterMiddleware;
use Hoge\ExampleBeforeMiddleware;
use Hoge\Controller\PlayerController;
use Hoge\Controller\PlayerCreatedLogController;

/** @var Composer\Autoload\ClassLoader $loader */
$loader = require __DIR__ . '/../../vendor/autoload.php';
$loader->addPsr4('Hoge\\', __DIR__ . '/../lib');

$containerBuilder = new ContainerBuilder();
$containerBuilder->addDefinitions([
    'settings' => [
        'userdb' => [
            'host' => 'mysql',
            'dbname' => 'userdb',
            'user' => 'scott',
            'password' => 'tiger'
        ],
        'logdb' => [
            'host' => 'postgresql',
            'dbname' => 'logdb',
            'user' => 'root',
            'password' => 'hogehoge'
        ]
    ],
    'userdb' => function (ContainerInterface $container) {
        $settings = $container->get('settings')['userdb'];
        $dsn = 'mysql:host=' . $settings['host'] . ';dbname=' . $settings['dbname'];
        $options = [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false
        ];
        return new PDO($dsn, $settings['user'], $settings['password'], $options);
    },
    'logdb' => function (ContainerInterface $container) {
        $settings = $container->get('settings')['logdb'];
        $dsn = 'pgsql:host=' . $settings['host'] . ';dbname=' . $settings['dbname'];
        $options = [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false
        ];
        return new PDO($dsn, $settings['user'], $settings['password'], $options);
    }
]);
$container = $containerBuilder->build();

AppFactory::setContainer($container);
$app = AppFactory::create();

// Middleware
$contentLengthMiddleware = new \Slim\Middleware\ContentLengthMiddleware();
$app->add($contentLengthMiddleware);

$app->get('/', function (Request $request, Response $response, array $args) {
    $response->getBody()->write('Hello, World!');
    return $response;
})
    ->add(new ExampleBeforeMiddleware())
    ->add(new ExampleAfterMiddleware());

$app->group('/player', function (RouteCollectorProxy $group) {
    $group->get('/{id}', PlayerController::class . ':get');
    $group->post('', PlayerController::class . ':post');
    $group->put('/{id}', PlayerController::class . ':put');
});

$app->group('/player_created', function (RouteCollectorProxy $group) {
    $group->get('/{id}', PlayerCreatedLogController::class . ':get');
    $group->get('', PlayerCreatedLogController::class . ':list');
});

$app->run();
```

### PlayerController

httpリクエストを受けて、MySQLのplayerテーブルにアクセスするためのControllerです。
CRUDのCRUに相当するメソッドを実装しています。

例外処理をしていなかったり、コードの重複があったりといろいろ手を抜いていますが、あくまでサンプルということでそのままにしています。

なお、それぞれのメソッドは以下のようにアクセスすることができます。

```sh
$ wget http://hoge.localhost/user  // get
$ wget http://hoge.localhost/user --post-data='{"player_name":"名前"}'   // post
$ wget http://hoge.localhost/user/1 --method=put --body-data='{"player_name":"新名前"}'
```


```php:/src/hoge/lib/Controller/PlayerController.php
<?php

namespace Hoge\Controller;

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Container\ContainerInterface;

use \PDO;

use Hoge\Model\Player;

class PlayerController
{
    /**
     * @var ContainerInterface
     */
    protected $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public function get(Request $request, Response $response, array $args) : Response
    {
        // parameters (id)
        $id = $args['id'];

        $pdo = $this->container->get('userdb'); /** @var PDO $pdo */
        $stmt = $pdo->prepare('select * from player where player_id=:id');
        $stmt->bindParam(':id', $id, PDO::PARAM_INT);
        $stmt->execute();
        $stmt->setFetchMode(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, Player::class);

        $player = $stmt->fetch();  /** @var Player|false $player */
        if (!$player) {
            return $response->withStatus(404);
        }

        $response->getBody()->write(json_encode($player->toArray(), JSON_UNESCAPED_UNICODE));
        return $response;
    }

    public function post(Request $request, Response $response, array $args) : Response
    {
        $body = $request->getBody()->getContents();
        $input = json_decode($body);
        $name = $input->player_name;

        $pdo = $this->container->get('userdb'); /** @var PDO $pdo */
        $stmt = $pdo->prepare('insert into player(player_name) values(:name)');
        $stmt->bindParam(':name', $name, PDO::PARAM_STR);

        $pdo->beginTransaction();
        $stmt->execute();
        $pdo->commit();

        return $response;
    }

    public function put(Request $request, Response $response, array $args) : Response
    {
        $body = $request->getBody()->getContents();
        $input = json_decode($body);
        $id = $args['id'];
        $name = $input->player_name;

        $pdo = $this->container->get('userdb'); /** @var PDO $pdo */
        $stmt = $pdo->prepare('update player set player_name=:name where player_id=:id');
        $stmt->bindParam(':name', $name, PDO::PARAM_STR);
        $stmt->bindParam(':id', $id, PDO::PARAM_INT);

        $pdo->beginTransaction();
        $stmt->execute();
        $pdo->commit();

        return $response;
    }
}
```

### Player

playerテーブルのデータモデルです。

```php:/src/hoge/lib/Model/Player.php
<?php

namespace Hoge\Model;

class Player
{
    private $player_id;
    private $player_name;

    public function toArray()
    {
        return [
            'player_id' => $this->player_id,
            'player_name' => $this->player_name
        ];
    }
}
```

### PlayerCreatedLogController

PostgreSQLのplayer_created_logテーブルにアクセスするためのControllerクラスです。
サンプルのため、特定のidのレコード取得と、全レコード取得を実装しています。
PlayerControllerと同様に、かなり手抜きです。

```php:/src/hoge/lib/Controller/PlayerCreatedLogController.php
<?php

namespace Hoge\Controller;

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Container\ContainerInterface;

use \PDO;

use Hoge\Model\PlayerCreatedLog;

class PlayerCreatedLogController
{
    /**
     * @var ContainerInterface
     */
    protected $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public function get(Request $request, Response $response, array $args) : Response
    {
        // parameters (id)
        $id = $args['id'];

        $pdo = $this->container->get('logdb'); /** @var PDO $pdo */
        $stmt = $pdo->prepare('select * from player_created_log where player_id=:id');
        $stmt->bindParam(':id', $id, PDO::PARAM_INT);
        $stmt->execute();
        $stmt->setFetchMode(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, PlayerCreatedLog::class);

        $logList = $stmt->fetchAll();  /** @var PlayerCreatedLog[] $logList */

        $logArray = array_map(
            function ($log) { /** @var PlayerCreatedLog $log */
                return $log->toArray();
            },
            $logList
        );

        $response->getBody()->write(json_encode($logArray, JSON_UNESCAPED_UNICODE));
        return $response;
    }

    public function list(Request $request, Response $response, array $args) : Response
    {
        $pdo = $this->container->get('logdb'); /** @var PDO $pdo */
        $stmt = $pdo->prepare('select * from player_created_log');
        $stmt->execute();
        $stmt->setFetchMode(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, PlayerCreatedLog::class);

        $logList = $stmt->fetchAll();  /** @var PlayerCreatedLog[] $logList */

        $logArray = array_map(
            function ($log) { /** @var PlayerCreatedLog $log */
                return $log->toArray();
            },
            $logList
        );

        $response->getBody()->write(json_encode($logArray, JSON_UNESCAPED_UNICODE));
        return $response;
    }
}
```

### PlayerCreatedLog

player_created_logテーブルのデータモデルです。

```php:/src/hoge/lib/Model/PlayerCreatedLog.php
<?php

namespace Hoge\Model;

class PlayerCreatedLog
{
    private $player_id;
    private $created_at;

    public function toArray()
    {
        return [
            'player_id' => $this->player_id,
            'created_at' => $this->created_at
        ];
    }
}
```

## ここまでのソース

[こちら](https://github.com/dynamitecoolguy/slimtemplate/tree/chapter5)でどうぞ。