【Laravel】独自のDB用クエリ関数を実装する

# この記事でやりたいこと
こんな独自のクエリ関数を実装する手順です。

``` php
\DB::canConnection(); //(1)
\Schema::getColumnDefinitions('user'); //(2)
User::getColumnDefinitions(); //(3)
\DB::table('user')->getColumnDefinitions(); //(4)
```



# はじめに
以下のような関数は、Laravelですでに用意されています。

``` php
\DB::statement('drop table users');
\Schema::hasTable('testTable');
```

上記のような形式の、独自の関数を追加する方法です。

# 実装方法
それでは早速実装してみましょう。
なお、「この記事で書きたいこと」で書いた(1)～(4)の関数ですが、厄介なことに定義する場所は、それぞれ異なります。その点を考慮しておいてください。

今回作成するコードのフォルダ構成は、以下のようになります。

> ServiceProvider.php
> Database/
> 　　├ MySqlConnection.php
> 　　├ Eloquent/
> 　　│　└ MySqlBuilder.php
> 　　├ Query/
> 　　│　├ MySqlBuilder.php
> 　　│　├ Grammars
> 　　│　│　└ MySqlGrammar.php
> 　　│　└ Processors
> 　　│　　　└ MySqlGrammar.php
> 　　└ Schema/
> 　　　　├ MySqlBuilder.php
> 　　　　└ Grammars
> 　　　　　　└ MySqlGrammar.php
> Model/
> 　　└ CustomBuilderTrait.php

※今回はMySQL前提で作成していきます。MySQL以外のデータベースを使用したい場合、複数のクラスを作成してください。

※今回の例では、以下の3つの関数を作成します。

- データベースに接続できるかを判定する関数「canConnection」
- データベースのバージョンを取得する関数「getVersion」
- 指定のテーブルの列の定義を一覧取得する関数「getColumnDefinitions」

## 実装

- ServiceProviderに、独自のMySqlConnectionを呼び出すための処理を追加します。

``` php 
<?php

namespace CustomBuilder;

use Illuminate\Support\ServiceProvider;
use Illuminate\Database\Connection;

class CustomBuilderServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        // 追加
        Connection::resolverFor('mysql', function (...$parameters) {
            return new Database\MySqlConnection(...$parameters);
        });
    }

    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}
```


- Database\MySqlConnectionを作成します。  
この関数では、継承した「MySqlGrammar」などの定義、ならびに「\DB::XXXXXX」形式で呼び出す関数の定義を行います。

``` php 
<?php

namespace CustomBuilder\Database;

use CustomBuilder\Database\Query\Grammars\MySqlGrammar as QueryGrammar;
use CustomBuilder\Database\Schema\Grammars\MySqlGrammar as SchemaGrammar;
use CustomBuilder\Database\Schema\MySqlBuilder;
use CustomBuilder\Database\Query\MySqlBuilder as QueryBuilder;
use CustomBuilder\Database\Eloquent\MySqlBuilder as EloquentBuilder;
use CustomBuilder\Database\Query\Processors\MySqlProcessor;
use Illuminate\Database\MySqlConnection as BaseConnection;

class MySqlConnection extends BaseConnection
{
    /**
     * Get a schema builder instance for the connection.
     *
     * @return Builder
     */
    public function getSchemaBuilder()
    {
        if (is_null($this->schemaGrammar)) {
            $this->useDefaultSchemaGrammar();
        }

        return new MySqlBuilder($this);
    }

    /**
     * Get the default schema grammar instance.
     *
     * @return MySqlGrammar
     */
    protected function getDefaultSchemaGrammar()
    {
        return $this->withTablePrefix(new SchemaGrammar);
    }
    
    /**
     * Get the default query grammar instance.
     *
     * @return QueryGrammar
     */
    protected function getDefaultQueryGrammar()
    {
        return $this->withTablePrefix(new QueryGrammar);
    }

    /**
     * Get the default post processor instance.
     *
     * @return MySqlProcessor
     */
    protected function getDefaultPostProcessor()
    {
        return new MySqlProcessor;
    }

    /**
     * Get a new query builder instance.
     *
     * @return \Illuminate\Database\Query\Builder
     */
    public function query()
    {
        return new QueryBuilder(
            $this, $this->getQueryGrammar(), $this->getPostProcessor()
        );
    }

    /**
     * Get a new eloquent query builder instance.
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function eloquentBuilder($query)
    {
        return new EloquentBuilder($query);
    }

    
    // \DB::XXXXXX()関数はここで記載 -------------------------------------------
    public function canConnection()
    {
        try {
            $this->getSchemaBuilder()->getTableListing();
            return true;
        } catch (\Exception $ex) {
            return false;
        }
    }
    
    /**
     * Get database version.
     *
     * @return void
     */
    public function getVersion()
    {
        return $this->getSchemaBuilder()->getVersion();
    }
}
```

- Database\Schema\MySqlBuilderを作成します。  
このクラスに、\Schema::XXXXXX()で呼び出す関数の定義を行います。

``` php
<?php

namespace CustomBuilder\Database\Schema;

use Illuminate\Database\Schema\MySqlBuilder as BaseBuilder;

class MySqlBuilder extends BaseBuilder
{
    // \Schema::XXXXXX()関数の形式 -------------------------------------------

    /**
     * Get database version.
     *
     * @return void
     */
    public function getVersion()
    {
        $results = $this->connection->selectFromWriteConnection($this->grammar->compileGetVersion());

        return $this->connection->getPostProcessor()->processGetVersion($results);
    }

    /**
     * Get the table listing
     *
     * @return array
     */
    public function getTableListing()
    {
        $results = $this->connection->selectFromWriteConnection($this->grammar->compileGetTableListing());

        return $this->connection->getPostProcessor()->processTableListing($results);
    }

    /**
     * Get column difinitions
     *
     * @return array
     */
    public function getColumnDefinitions($table)
    {
        $baseTable = $table;
        $table = $this->connection->getTablePrefix().$table;
        $results = $this->connection->selectFromWriteConnection($this->grammar->compileColumnDefinitions($table));

        return $this->connection->getPostProcessor()->processColumnDefinitions($baseTable, $results);
    }
}
```

- Database\Schema\Grammars\MySqlGrammarを作成します。  
このクラスで、主に独自クラスで実行するSQLを定義します。

``` php
<?php

namespace CustomBuilder\Database\Schema\Grammars;

use Illuminate\Database\Schema\Grammars\MySqlGrammar as BaseGrammar;

class MySqlGrammar extends BaseGrammar
{
    /**
     * Compile the query to get version
     *
     * @return string
     */
    public function compileGetVersion()
    {
        return "select version()";
    }

    /**
     * Compile the query to show tables
     *
     * @return string
     */
    public function compileGetTableListing()
    {
        return "show tables";
    }
    
    /**
     * Compile the query to get column difinitions
     *
     * @return string
     */
    public function compileColumnDefinitions($tableName)
    {
        return "show columns from {$this->wrapTable($tableName)}";
    }
}

```


- Database\Schema\Grammars\MySqlGrammarを作成します。  
このクラスで、主に独自クラスで実行するSQLを定義します。

``` php
<?php

namespace CustomBuilder\Database\Schema\Grammars;

use Illuminate\Database\Schema\Grammars\MySqlGrammar as BaseGrammar;

class MySqlGrammar extends BaseGrammar
{
    /**
     * Compile the query to get version
     *
     * @return string
     */
    public function compileGetVersion()
    {
        return "select version()";
    }

    /**
     * Compile the query to show tables
     *
     * @return string
     */
    public function compileGetTableListing()
    {
        return "show tables";
    }
    
    /**
     * Compile the query to get column difinitions
     *
     * @return string
     */
    public function compileColumnDefinitions($tableName)
    {
        return "show columns from {$this->wrapTable($tableName)}";
    }
}

```


- Database\Query\Processors\MySqlProcessorを作成します。  
このクラスで、SQLを実行して取得した結果を、配列やCollectionに加工するなどを行います。  
また、このクラス内の関数によって、各データベース種類(MySQL、SQL Serverなど)によって列名などが異なるものを、すべて揃える目的があります。

``` php
<?php

namespace CustomBuilder\Database\Query\Processors;

use Illuminate\Database\Query\Processors\MySqlProcessor as BaseMySqlProcessor;

class MySqlProcessor extends BaseMySqlProcessor
{
    /**
     * Process the results of a get version.
     *
     * @param  array  $results
     * @return array
     */
    public function processGetVersion($results)
    {
        $versionArray = $this->versionAray($results);

        return $versionArray[0];
    }

    protected function versionAray($results)
    {
        $version = collect(collect($results)->first())->first();
        return explode('-', $version);
    }

    /**
     * Process the results of a table listing query.
     *
     * @param  array  $results
     * @return array
     */
    public function processTableListing($results)
    {
        return array_map(function ($result) {
            return collect((object) $result)->first();
        }, $results);
    }
    
    /**
     * Process the results of a Column Definitions query.
     *
     * @param  array  $results
     * @return array
     */
    public function processColumnDefinitions($tableName, $results)
    {
        return collect($results)->map(function ($result) use ($tableName) {
            return [
                'table_name' => $tableName,
                'column_name' => $result->Field,
                'type' => $result->Type,
                'nullable' => boolval($result->Null),
                'virtual' => strtoupper($result->Extra) == 'VIRTUAL GENERATED',
            ];
        })->toArray();
    }
    
}
```


- Database\Query\MySqlBuilderを作成します。  
このクラスに、\DB::table('user')::XXXXXX()で呼び出す関数の定義を行います。

``` php
<?php

namespace CustomBuilder\Database\Query;

use Illuminate\Database\Query\Builder as BaseBuilder;

class MySqlBuilder extends BaseBuilder
{
    // \DB::table('user')::XXXXXX()関数の形式 -------------------------------------------
    
    /**
     * Get column difinitions
     *
     * @return array
     */
    public function getColumnDefinitions()
    {
        $results = $this->connection->selectFromWriteConnection($this->grammar->compileColumnDefinitions($this->from));

        return $this->connection->getPostProcessor()->processColumnDefinitions($this->from, $results);
    }
}

```

- また、\Database\Query\Grammars\MySqlGrammarで、上記のDatabase\Query\MySqlBuilderで呼び出すGrammerを作成します。  
(\Database\Schema\Grammars\MySqlGrammarとまとめられないかな？という思いもありつつ)

``` php
<?php

namespace CustomBuilder\Database\Query\Grammars;

use Illuminate\Database\Query\Grammars\MySqlGrammar as BaseGrammar;

class MySqlGrammar extends BaseGrammar
{
    /**
     * Compile the query to get column difinitions
     *
     * @return string
     */
    public function compileColumnDefinitions($tableName)
    {
        return "show columns from {$this->wrapTable($tableName)}";
    }
}
```

- Database\Eloquent\MySqlBuilderを作成します。  
このクラスに、\User::XXXXXX()で呼び出す関数の定義を行います。

``` php
<?php

namespace CustomBuilder\Database\Eloquent;

use Illuminate\Database\Eloquent\Builder as BaseBuilder;

class MySqlBuilder extends BaseBuilder
{
    // \User::XXXXXX()関数の形式 -------------------------------------------
    
    /**
     * Get column difinitions
     *
     * @return array
     */
    public function getColumnDefinitions()
    {
        $table = $this->model->getTable();
        $connection = $this->query->connection;

        $results = $connection->selectFromWriteConnection($connection->getQueryGrammar()->compileColumnDefinitions($table));
        return $connection->getPostProcessor()->processColumnDefinitions($table, $results);
    }
}
```

- Model\CustomBuilderTraitを作成します。  
このTraitでは、各Modelで独自のEloquent\Builder、Query\Builderを呼び出すために必要となります。

``` php
<?php

namespace CustomBuilder\Model;

trait CustomBuilderTrait
{
    /**
     * Get a new query builder instance for the connection.
     *
     * @return \Illuminate\Database\Query\Builder
     */
    protected function newBaseQueryBuilder()
    {
        $connection = $this->getConnection();

        return $connection->query();
    }

    /**
     * Create a new Eloquent query builder for the model.
     *
     * @param  \Illuminate\Database\Query\Builder  $query
     * @return \Illuminate\Database\Eloquent\Builder|static
     */
    public function newEloquentBuilder($query)
    {
        $connection = $this->getConnection();
        return $connection->eloquentBuilder($query);
    }
}
```

独自関数を呼び出すModelで、このTraitをuseしてください。

``` php
<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use CustomBuilder\Model\CustomBuilderTrait;

class User extends Authenticatable
{
    use Notifiable;
    // 追加
    use CustomBuilderTrait;

    // 略
}
```


# まとめ

以上です！
Helperクラス的な関数を作ってもいいですが、出来ればこのように、クエリビルダを拡張することを行いたいですよね。
その場合は是非、こちらのコードを使用してください！

ちなみに今回のコードのGitHubはこちらです。
https://github.com/hirossyi73/custom-builder