ytake Hatena

Web Application Developer

Laravel5.4 Data MapperライクなDatabaseアプローチ

Laravelで使われているilluminate/databaseはPDOを利用して実装されています。
つまりPDOでできることは全て利用可能です。

Laravelの標準の機能では、データベースのレコードはCollectionクラスで、
stdClassまたは、配列でカラムと値が共に返却されます。

Data Mapperライクに任意のオブジェクトで返却する様にするには、
Illuminate\Database\Events\StatementPrepared をlistenする必要がありますが、
Database処理にEventが依存してしまうため(eventヘルパーを使ってもクラスに依存していることになります)、
fetchModeを変更できるメソッドが欲しくなります(laravel5.3まではありましたが変更されました)

5.4で利用したい場合は次の様な拡張で簡単に追加することができます。

<?php
declare(strict_types=1);

namespace App\Foundation;

use PDOStatement;

trait QueryPrepared
{
    /** @var string */
    protected $fetchStyleClass = '';

    /**
     * @param string $class
     *
     * @return QueryPrepared
     */
    public function fetchClass(string $class): self
    {
        $this->fetchStyleClass = $class;

        return $this;
    }

    /**
     * @param PDOStatement $statement
     *
     * @return PDOStatement
     */
    protected function prepared(PDOStatement $statement): PDOStatement
    {
        $statement->setFetchMode($this->fetchMode);
        if (!empty($this->fetchStyleClass)) {
            $statement->setFetchMode(\PDO::FETCH_CLASS, $this->fetchStyleClass);
        }

        return $statement;
    }
}

5.4ではquery発行の直前で Illuminate\Database\Connection クラスの preparedメソッドが実行され、
このメソッドで前述のeventが発火されますので、この動作を変更します。

この例ではeventを利用せずに、fetch styleを変更します。

次にConnectionクラスを継承したクラスを作成し、トレイトを使って変更します。
(当然トレイトではなく、通常の継承でも構いません)

<?php
declare(strict_types=1);

namespace App\Foundation;

/**
 * Class SqliteConnection
 */
final class SqliteConnection extends \Illuminate\Database\SQLiteConnection
{
    use QueryPrepared;
}

この例ではsqliteだけを変更します。

次にService Containerを使って、実行クラスを変更します。

<?php
declare(strict_types=1);

namespace App\Providers;

use App\Foundation\SqliteConnection;
use Illuminate\Support\ServiceProvider;
use Illuminate\Database\DatabaseManager;
use Illuminate\Database\Connectors\SQLiteConnector;

/**
 * Class AppServiceProvider
 */
final class AppServiceProvider extends ServiceProvider
{
    /**
     *
     */
    public function register()
    {
        /** @var DatabaseManager $dbManager */
        $dbManager = $this->app['db'];
        $dbManager->extend('sqlite', function (array $config, $name) {
            $connectior = new SQLiteConnector;
            return new SqliteConnection($connectior->connect($config));
        });
    }
}

これでsqlite利用時は拡張したクラスが利用される様になります。
(SqliteConnectionクラスの引数は必要に応じて指定してください)

データベースで返却されるカラムなどに応じてFETCH_CLASSで指定するクラスを作成します。

次の例は、返却されるカラムが二つだけのシンプルなものです。

<?php
declare(strict_types=1);

namespace App\Mapper;

/**
 * Class UserMapper
 */
class UserMapper
{
    /** @var string */
    private $id;

    /** @var string */
    private $name;

    /**
     * @return string
     */
    public function getId(): string
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }
}

最後に利用したい処理に記述します。

<?php

use App\Mapper\UserMapper;
use App\Foundation\SqliteConnection;
use Illuminate\Database\DatabaseManager;

public function __invoke(DatabaseManager $databaseManager)
{
    /** @var SQLiteConnection $connection */
    $connection = $databaseManager->connection();
    $connection->fetchClass(UserMapper::class);
    $generator = $connection->table('users')->cursor();

    /** @var UserMapper $item */
    foreach ($generator as $item) {
        dump($item);
    }
}

cursorはLaravel5.3から追加されたGeneratorで返却するメソッドです。
返却されるオブジェクトが指定したものになっていることが確認できます。

UserMapper {#107 ▼
  -id: "1"
  -name: "onamae"
}