Laravel-adminを使って実用的な画面を作る。（1対多のリレーションシップに対応）


Laravel-adminのCRUDについて
よく見る情報は1テーブルを操作するものしか載っていないので、リレーションシップをどれくらい対応できているのか試行錯誤しました。
リレーションシップに関連する情報が少ないので探すのがとても大変でした。
ドキュメントを参考にしつつ、ドキュメントに無いものはソースを読みながら……。
試したみたところ結構実用的なレベルだと思いました。
1対多のデータに対応するための面倒なフォームも少しコードを書くだけで表示できるし、レイアウトもそこそこカスタマイズできるのでテンプレートの作成が不要！
開発工数を短縮できそう。
これはもうへっぽこなデザインで作って「時間がないので…」とか言い訳できないぞっ

[Laravel-adminのインストール手順＆ドキュメント](http://laravel-admin.org/docs/#/en/installation)

# 今回試したこと

Laravel-adminの機能を使ってユーザー管理する画面を作る。
・一覧画面、検索（フィルター）
・編集画面
・詳細表示画面

よくある1対1、1対多のリレーションシップに対応できること。
サンプルでは1個ずつ入れてますが、複数あっても問題なく実装出来ると思う。

<details><summary>サンプルコード一式（とても長いので折り畳み）</summary><div>

```php:migration
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateExampleTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('example_users', function (Blueprint $table) {
            $table->increments('id')->comment('ユーザーID');
            $table->string('name', 32)->comment('ユーザー名');
            $table->unsignedInteger('example_sections_id')->comment('セクションID');
            $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP'));
            $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP'));
        });

        Schema::create('example_user_items', function (Blueprint $table) {
            $table->increments('id');
            $table->unsignedInteger('example_users_id')->comment('ユーザーID');
            $table->unsignedInteger('example_items_id')->comment('アイテムID');
            $table->unsignedInteger('num')->comment('所持数');
            $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP'));
            $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP'));
            $table->unique(['example_users_id', 'example_items_id'], 'unique_id');
            $table->index(['example_items_id'], 'item_id');
        });

        Schema::create('example_items', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name', 32)->comment('アイテム名');
            $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP'));
            $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP'));
        });

        Schema::create('example_sections', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name', 32)->comment('セクション名');
            $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP'));
            $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP'));
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('example_users');
        Schema::dropIfExists('example_user_items');
        Schema::dropIfExists('example_items');
        Schema::dropIfExists('example_sections');
    }
}
```

```sql:サンプルマスタデータ
-- example_sections
INSERT INTO `example_sections` (`name`) VALUES ('SectionA');
INSERT INTO `example_sections` (`name`) VALUES ('SectionB');
INSERT INTO `example_sections` (`name`) VALUES ('SectionC');

-- example_items
INSERT INTO `example_items` (`name`) VALUES ('ItemA');
INSERT INTO `example_items` (`name`) VALUES ('ItemB');
INSERT INTO `example_items` (`name`) VALUES ('ItemC');
INSERT INTO `example_items` (`name`) VALUES ('ItemD');
INSERT INTO `example_items` (`name`) VALUES ('ItemE');
```

```php:model
<?php

namespace App\Models\Example;

use Illuminate\Database\Eloquent\Model;

class ExampleUsers extends Model
{
    protected $table = 'example_users';
    public $timestamps = false;
    protected $fillable = [
        'name',
        'example_sections_id',
    ];
    protected $guarded = ['id'];

    public function example_user_items()
    {
        return $this->hasMany(ExampleUserItems::class, 'example_users_id');
    }

    public function example_sections()
    {
        return $this->hasOne(ExampleSections::class, 'id', 'example_sections_id');
    }
}

<?php

namespace App\Models\Example;

use Illuminate\Database\Eloquent\Model;

class ExampleUserItems extends Model
{
    protected $table = 'example_user_items';
    public $timestamps = false;
    protected $fillable = [
        'example_users_id',
        'example_items_id',
        'num',
    ];
    protected $guarded = ['id'];

    public function example_users()
    {
        return $this->belongsTo(ExampleUsers::class, 'example_users_id');
    }

    public function example_items()
    {
        return $this->hasOne(ExampleItems::class, 'example_items_id');
    }
}

<?php

namespace App\Models\Example;

use Illuminate\Database\Eloquent\Model;

class ExampleSections extends Model
{
    protected $table = 'example_sections';
    public $timestamps = false;
    protected $fillable = [
        'name',
    ];
    protected $guarded = ['id'];

    public function example_users()
    {
        return $this->belongsTo(ExampleUsers::class, 'example_users_id');
    }
}

<?php

namespace App\Models\Example;

use Illuminate\Database\Eloquent\Model;

class ExampleItems extends Model
{
    protected $table = 'example_items';
    public $timestamps = false;
    protected $fillable = [
        'name',
    ];
    protected $guarded = ['id'];

    public function example_user_items()
    {
        return $this->belongsTo(ExampleUserItems::class, 'example_items_id');
    }
}
```

```php:controller
php artisan admin:make ExampleUsersController --model=\\App\\Models\\Example\\ExampleUsers 

<?php

namespace App\Admin\Controllers;

use Encore\Admin\Form;
use Encore\Admin\Grid;
use Encore\Admin\Facades\Admin;
use Encore\Admin\Layout\Content;
use Encore\Admin\Layout\Column;
use Encore\Admin\Layout\Row;
use Encore\Admin\Controllers\ModelForm;
use Encore\Admin\Grid\Displayers\Actions;
use Encore\Admin\Widgets\Box;
use Encore\Admin\Widgets\Table;
use App\Http\Controllers\Controller;
use App\Models\Example\ExampleSections;
use App\Models\Example\ExampleItems;
use App\Models\Example\ExampleUsers;
use Illuminate\Support\Facades\Input;
use \Validator;

class ExampleUsersController extends Controller
{
    use ModelForm;

    /**
     * Index interface.
     *
     * @return Content
     */
    public function index()
    {
        return Admin::content(function (Content $content) {

            $content->header('ユーザー一覧');
            $content->description('index');

            $content->body($this->grid());
        });
    }

    /**
     * Show interface.
     *
     * @param $id
     * @return Content
     */
    public function show($id)
    {
        return Admin::content(function (Content $content) use ($id) {
            $content->header('ユーザー詳細');
            $content->description('show');

            $example_items = [];
            $tmp = ExampleItems::get(['id', 'name']);
            if ($tmp->isNotEmpty()) {
                $list = $tmp->all();
                foreach ($list as $v) {
                    $example_items[$v->id] = $v->name;
                }
            }
            $users = ExampleUsers::find($id);
            $user_data = $users->first(['name']);
            $user_sections = $users->example_sections;
            $user_items = $users->example_user_items;

            $profile = [];
            $profile[] = [
                'ユーザー名',
                $user_data['name'],
            ];
            $profile[] = [
                'グループ名',
                $user_sections['name'],
            ];
            $items = [];
            foreach ($user_items as $v) {
                $items[] = [
                    'name' => $example_items[$v['example_items_id']],
                    'value' => $v['num'],
                ];
            }

            $content->breadcrumb(
                ['text' => 'ユーザー一覧', 'url' => '/ExampleUsers'],
                ['text' => 'ユーザー詳細']
            );

            $content->row(function (Row $row) use ($profile, $items) {
                $row->column(3, function (Column $column) use ($profile) {
                    $table = new Table([], $profile);
                    $box = new Box('ユーザー情報', $table->render());
                    $box->collapsable();
                    $box->style('primary');
                    $box->solid();
                    $column->append($box);
                });
                $row->column(2, function (Column $column) use ($items) {
                    $table = new Table(['アイテム名', '所持数'], $items);
                    $box = new Box('アイテム情報', $table->render());
                    $box->collapsable();
                    $box->style('info');
                    $box->solid();
                    $column->append($box);
                });
            });
        });
    }

    /**
     * Edit interface.
     *
     * @param $id
     * @return Content
     */
    public function edit($id)
    {
        return Admin::content(function (Content $content) use ($id) {

            $content->header('ユーザー情報');
            $content->description('edit');

            $content->body($this->form()->edit($id));
        });
    }

    /**
     * Create interface.
     *
     * @return Content
     */
    public function create()
    {
        return Admin::content(function (Content $content) {

            $content->header('ユーザー情報');
            $content->description('create');

            $content->body($this->form());
        });
    }

    /**
     * Make a grid builder.
     *
     * @return Grid
     */
    protected function grid()
    {
        return Admin::grid(ExampleUsers::class, function (Grid $grid) {
            $grid->id('ID')->sortable();
            $grid->name('ユーザー名');
            $grid->example_sections('グループ名')->display(function ($example_sections) {
                return $example_sections['name'];
            });
            $example_items = [];
            $tmp = ExampleItems::get(['id', 'name']);
            if ($tmp->isNotEmpty()) {
                $list = $tmp->all();
                foreach ($list as $v) {
                    $example_items[$v->id] = $v->name;
                }
            }
            $grid->example_user_items('所持アイテム(個数)')->display(function ($example_user_items) use ($example_items) {
                $result = '';
                foreach ($example_user_items as $user_item) {
                    $item_name = $example_items[$user_item['example_items_id']];
                    $item_num = $user_item['num'];
                    if (!empty($result)) {
                        $result .= ', ';
                    }
                    $result .= $item_name . '(' . $item_num . ')';
                }
                return $result;
            });
            $grid->created_at('登録日時');
            $grid->updated_at('更新日時');

            $grid->actions(function (Actions $actions) {
                $actions->prepend('<a href="ExampleUsers/' . $actions->getKey() . '"><i class="fa fa-file-text-o"></i></a>');
            });

            $example_sections_id_options = [];
            $tmp = ExampleSections::orderBy('id')->get(['id', 'name']);
            if ($tmp->isNotEmpty()) {
                $list = $tmp->all();
                foreach ($list as $v) {
                    $example_sections_id_options[$v->id] = $v->name;
                }
            }
            $example_items_id_options = [];
            $tmp = ExampleItems::orderBy('id')->get(['id', 'name']);
            if ($tmp->isNotEmpty()) {
                $list = $tmp->all();
                foreach ($list as $v) {
                    $example_items_id_options[$v->id] = $v->name;
                }
            }
            $grid->filter(function ($filter) use ($example_sections_id_options, $example_items_id_options) {
                $filter->like('name', 'ユーザー名');
                $filter->in('example_sections_id', 'グループ名')->multipleSelect($example_sections_id_options);

                $filter->where(function ($query) {
                    $query->whereHas('example_user_items', function ($query) {
                        $query->whereIn('example_items_id', $this->input);
                    });
                }, 'アイテム名')->multipleSelect($example_items_id_options);

                $filter->between('created_at', '登録日時')->datetime();
            });
        });
    }

    /**
     * Make a form builder.
     *
     * @return Form
     */
    protected function form()
    {
        return Admin::form(ExampleUsers::class, function (Form $form) {
            $form->tab('ユーザー情報', function ($form) {
                $form->display('id', 'ID');
                $form->text('name', 'ユーザー名')->rules('required|max:32');
                $example_sections_id_options = [];
                $tmp = ExampleSections::orderBy('id')->get(['id', 'name']);
                if ($tmp->isNotEmpty()) {
                    $list = $tmp->all();
                    foreach ($list as $v) {
                        $example_sections_id_options[$v->id] = $v->name;
                    }
                }
                $form->select('example_sections_id', 'グループ名')->options($example_sections_id_options)->rules('required');
                $form->display('created_at', '登録日時');
                $form->display('updated_at', '更新日時');
            })->tab('所持アイテム', function ($form) {
                $example_items_id_options = [];
                $tmp = ExampleItems::orderBy('id')->get(['id', 'name']);
                if ($tmp->isNotEmpty()) {
                    $list = $tmp->all();
                    foreach ($list as $v) {
                        $example_items_id_options[$v->id] = $v->name;
                    }
                }
                $form->hasMany('example_user_items',
                    null,
                    function (Form\NestedForm $form) use ($example_items_id_options) {
                        $form->select('example_items_id', 'アイテム名')->options($example_items_id_options)->rules('required');
                        $form->number('num', '個数')->rules('required_with:example_user_items.*.example_items_id|integer|min:0');
                    }
                );
            });
        });
    }

    /**
     * Update the specified resource in storage.
     *
     * @param int $id
     *
     * @return \Illuminate\Http\Response|\Symfony\Component\HttpFoundation\Response
     */
    public function update($id)
    {
        $validator = Validator::make(Input::all(), [
            'example_user_items.*.example_items_id' => 'distinct',
        ], [
            'distinct' => 'アイテムが重複しています。',
        ]);
        if ($validator->fails()) {
            return redirect()->back()->withErrors($validator)->withInput();
        }
        return $this->form()->update($id);
    }
}
```

```php:app\Admin\routes.phpにresourceを追記
    $router->resource('ExampleUsers', ExampleUsersController::class);
```

</div></details>


# 解説

## フォームの設定

基本的な使い方は[Laravel-adminのドキュメント](http://laravel-admin.org/docs/#/en/model-form-fields)を見て！

![Laravel-admin_編集_プロフィール.png](https://qiita-image-store.s3.amazonaws.com/0/178991/0432e9cc-6ace-14a6-06b1-14534581da4e.png)

### 1対1のリレーション
　Select, Radioなどで実装出来ると思います。
　optionsにマスタデータのidとnameの配列を入れます。

```php:form()の中
$example_sections_id_options = [];
$tmp = ExampleSections::orderBy('id')->get(['id', 'name']);
if ($tmp->isNotEmpty()) {
    $list = $tmp->all();
    foreach ($list as $v) {
        $example_sections_id_options[$v->id] = $v->name;
    }
}
$form->select('example_sections_id', 'グループ名')->options($example_sections_id_options)->rules('required');
```

### 1対多のリレーション
　hasManyを使います。
　コールバックでNestedFormを設定すると自由に何個でも追加できるフォームを表示できます。
　今回はフォームをタブで分けているので2番目の引数をnullにして項目名の表示を無しにしました。（正確には空白が表示されているのかな）
　バリデーションも書けます。
　同じアイテムを重複登録できないようにするには、`update()`をオーバーライドしてその中に`Validator::make`を追加します。
 KEYは`リレーション名.*.カラム名`です。

```php:form()の中
$example_items_id_options = [];
$tmp = ExampleItems::orderBy('id')->get(['id', 'name']);
if ($tmp->isNotEmpty()) {
    $list = $tmp->all();
    foreach ($list as $v) {
        $example_items_id_options[$v->id] = $v->name;
    }
}
$form->hasMany('example_user_items',
    null,
    function (Form\NestedForm $form) use ($example_items_id_options) {
        $form->select('example_items_id', 'アイテム名')->options($example_items_id_options)->rules('required');
        $form->number('num', '個数')->rules('required_with:example_user_items.*.example_items_id|integer|min:0');
    }
);
}
```

```php:コントローラーにupdate()を追加
public function update($id)
{
    $validator = Validator::make(Input::all(), [
        'example_user_items.*.example_items_id' => 'distinct',
    ], [
        'distinct' => 'アイテムが重複しています。',
    ]);
    if ($validator->fails()) {
        return redirect()->back()->withErrors($validator)->withInput();
    }
    return $this->form()->update($id);
}
```

![Laravel-admin_編集_アイテム.png](https://qiita-image-store.s3.amazonaws.com/0/178991/1266f60e-954e-b183-c70f-76380610521d.png)

![Laravel-admin_編集_アイテム重複.png](https://qiita-image-store.s3.amazonaws.com/0/178991/fc513568-ba7d-9b0b-3bd6-9dbabed54430.png)

## 一覧表示の設定

基本的な使い方は[Laravel-adminのドキュメント](http://laravel-admin.org/docs/#/en/model-grid)を見て！

![Laravel-admin_一覧.png](https://qiita-image-store.s3.amazonaws.com/0/178991/6a4657ba-e90a-78a1-458f-fa8efbac05a4.png)

### 1対1のリレーション
modelで設定したメソッド（今回はexample_sections）を呼び、displayのコールバックで表示したい項目を処理します。

```php:grid()の中
$grid->example_sections('グループ名')->display(function ($example_sections) {
    return $example_sections['name'];
});
```

### 1対多のリレーション
modelで設定したメソッド（今回はexample_user_items）を呼び、displayのコールバックで表示したい項目を処理します。
試していないですが、[Table](http://laravel-admin.org/docs/#/en/widgets?id=table)を使ってキレイに表示できるかも。

```php:grid()の中
$example_items = [];
$tmp = ExampleItems::get(['id', 'name']);
if ($tmp->isNotEmpty()) {
    $list = $tmp->all();
    foreach ($list as $v) {
        $example_items[$v->id] = $v->name;
    }
}
$grid->example_user_items('所持アイテム(個数)')->display(function ($example_user_items) use ($example_items) {
    $result = '';
    foreach ($example_user_items as $user_item) {
        $item_name = $example_items[$user_item['example_items_id']];
        $item_num = $user_item['num'];
        if (!empty($result)) {
            $result .= ', ';
        }
        $result .= $item_name . '(' . $item_num . ')';
    }
    return $result;
});
```

### アクションのセルにリンクを追加

基本的な使い方は[Laravel-adminのドキュメント](http://laravel-admin.org/docs/#/en/model-grid-actions)を見て！
今回は詳細を表示する画面へのリンクを追加しています。

```php:grid()の中
$grid->actions(function (Actions $actions) {
    $actions->prepend('<a href="ExampleUsers/' . $actions->getKey() . '"><i class="fa fa-file-text-o"></i></a>');
});
```

![Laravel-admin_一覧_アクション.png](https://qiita-image-store.s3.amazonaws.com/0/178991/30b51460-0db1-182f-3d0b-b845b1a83359.png)

### フィルターに項目を追加

基本的な使い方は[Laravel-adminのドキュメント](http://laravel-admin.org/docs/#/en/model-grid-filters)を見て！

ユーザー名の検索（like）、グループの複数検索（in）、アイテムの複数検索（where）、登録日時の範囲検索（between、datetime）
特に`where`が強力で`whereHas`と`whereIn`の組み合わせを書くとテーブルをjoinして検索してくれます。
この辺の機能を自前で作ろうとするとそこそこ時間がかかるので、たった数行だけで実装できてしまうのがすごい！

```php:grid()の中
$example_sections_id_options = [];
$tmp = ExampleSections::orderBy('id')->get(['id', 'name']);
if ($tmp->isNotEmpty()) {
    $list = $tmp->all();
    foreach ($list as $v) {
        $example_sections_id_options[$v->id] = $v->name;
    }
}
$example_items_id_options = [];
$tmp = ExampleItems::orderBy('id')->get(['id', 'name']);
if ($tmp->isNotEmpty()) {
    $list = $tmp->all();
    foreach ($list as $v) {
        $example_items_id_options[$v->id] = $v->name;
    }
}
$grid->filter(function ($filter) use ($example_sections_id_options, $example_items_id_options) {
    $filter->like('name', 'ユーザー名');
    $filter->in('example_sections_id', 'グループ名')->multipleSelect($example_sections_id_options);
    $filter->where(function ($query) {
        $query->whereHas('example_user_items', function ($query) {
            $query->whereIn('example_items_id', $this->input);
        });
    }, 'アイテム名')->multipleSelect($example_items_id_options);
    $filter->between('created_at', '登録日時')->datetime();
});
```

![Laravel-admin_フィルター.png](https://qiita-image-store.s3.amazonaws.com/0/178991/b6e711a6-99bd-5472-c264-2916a53de177.png)

## 詳細表示の設定

詳細情報が見れる画面を作ります。
今回はアイテムしかないけど、1対多のデータがいっぱい並ぶことをイメージ。
基本的な使い方は[Laravel-adminのドキュメント](http://laravel-admin.org/docs/#/en/content-layout?id=layout)を見て！

`row`と`column`でレイアウト設定、`Box`で枠を作り、`Table`でデータを並べます。
`Box`以外にも`Collapse`、`Infobox`、`Tab`などがあるので、よほど凝ったレイアウトでもなければこれだけで作れると思います。
これでPHPコード書くだけで詳細画面が作れます！

```php:show()の中
return Admin::content(function (Content $content) use ($id) {
    $content->header('ユーザー詳細');
    $content->description('show');
    $example_items = [];
    $tmp = ExampleItems::get(['id', 'name']);
    if ($tmp->isNotEmpty()) {
        $list = $tmp->all();
        foreach ($list as $v) {
            $example_items[$v->id] = $v->name;
        }
    }
    $users = ExampleUsers::find($id);
    $user_data = $users->first(['name']);
    $user_sections = $users->example_sections;
    $user_items = $users->example_user_items;
    $profile = [];
    $profile[] = [
        'ユーザー名',
        $user_data['name'],
    ];
    $profile[] = [
        'グループ名',
        $user_sections['name'],
    ];
    $items = [];
    foreach ($user_items as $v) {
        $items[] = [
            'name' => $example_items[$v['example_items_id']],
            'value' => $v['num'],
        ];
    }
    $content->breadcrumb(
        ['text' => 'ユーザー一覧', 'url' => '/ExampleUsers'],
        ['text' => 'ユーザー詳細']
    );
    $content->row(function (Row $row) use ($profile, $items) {
        $row->column(3, function (Column $column) use ($profile) {
            $table = new Table([], $profile);
            $box = new Box('ユーザー情報', $table->render());
            $box->collapsable();
            $box->style('primary');
            $box->solid();
            $column->append($box);
        });
        $row->column(2, function (Column $column) use ($items) {
            $table = new Table(['アイテム名', '所持数'], $items);
            $box = new Box('アイテム情報', $table->render());
            $box->collapsable();
            $box->style('info');
            $box->solid();
            $column->append($box);
        });
    });
});
```

![Laravel-admin_詳細表示.png](https://qiita-image-store.s3.amazonaws.com/0/178991/697f7553-c119-407c-1f8a-4c3017d0f434.png)

今回はテンプレートを作らずに出来ることを試したかったのでこうしましたが、
`$column->append()`や`$content->body()`の引数に`view()`の結果を渡せばテンプレートでも出来ます。

これだけでもかなり実用的な管理画面が作れるのではないでしょうか。
ぜひ実践で使ってみて報告ください。（管理画面を作る仕事きたら試してみたいなぁ