Azukiシステムを使った開発を行う

例を使って実際の開発手順を記載していきます

ケース1.お知らせ機能を作る

以下の手順で作成していきます

要件・仕様を定義する

お知らせ機能の要件・仕様は以下とします

内容
1. システム管理画面にてお知らせの登録・編集・削除ができる
2. 登録できる内容は
1.日時(Y-m-d H 形式)
2.お知らせ内容
3.表示場所
4.表示・非表示の選択
とする
3. お知らせの表示場所は、
1.サイト管理者ダッシュボード
2.ユーザーマイページ
3.サイトトップ
とする
4. 表示順は日時の降順(最新のものが先)とする
5. 表示件数は最新のものから最大5件とする
6. お知らせが登録されていない場合は「お知らせはありません」と表示する
7. データはソフトデリートとする

テーブルを定義する

$ php artisan make:migration --create=informations CreateInformationsTable

を実行し、マイグレーションファイルを作成します
作成したファイルの内容を以下のように変更します


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

class CreateInformationsTable extends Migration
{
    protected $table = 'informations';

    /**
     * Run the migrations.
     *
     * @return  void
     */
    public function up()
    {
        Schema::create($this->table, function (Blueprint $table) {
            $table->engine = 'InnoDB ROW_FORMAT=DYNAMIC';

            $table->increments('id')->unsigned()->comment('一意の識別子');
            $table->timestamp('datetime')->comment('日時');
            $table->text('contents')->comment('お知らせ内容');
            $table->smallinteger('place')->unsigned()->index()->comment('表示場所.1:サイト管理。2:マイページ。3:サイト');
            $table->smallinteger('is_display')->unsigned()->index()->default(1)->comment('表示・非表示の選択.1:表示。2:非表示');
            $table->softDeletesTz();
            $table->timestampsTz();
        });

        DB::statement("ALTER TABLE `".$this->table."` COMMENT 'お知らせを管理するテーブル'");
    }

    /**
     * Reverse the migrations.
     *
     * @return  void
     */
    public function down()
    {
        Schema::dropIfExists($this->table);
    }
}
        

$ php artisan migrate

を実行し、テーブルを作成します

テーブルに対応するモデルを作成する

モデルクラスを作成します。まず

$ php artisan make:model Models\\Informations

を実行し、元となるモデルクラスを作成します。次に内容をテーブルに合わせて以下のように書き換えます


namespace App\Models;

use Azuki\App\Models\BaseSoftDeleteModel as Model;

class Informations extends Model
{
    /**
     * The database table account by the model.
     *
     * @var  string
     */
    protected $table = 'informations';

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'datetime',
        'contents',
        'place',
        'is_display',
    ];
    
    /**
     * function setCondition
     *
     * 検索条件を設定する
     *
     * @param  QueryBuilder $query
     * @param  Array        $condition
     * @return QueryBuilder $query
     *
     */
    public function setCondition($query, $condition)
    {
        $query = parent::setCondition($query, $condition);
        $query = $this->when($query, [$condition, 'id'], function($query, $value) {
            return $query->where('id', '=', $value);
        });
        $query = $this->when($query, [$condition, 'place'], function($query, $value) {
            return $query->whereIn('place', $value);
        });
        $query = $this->when($query, [$condition, 'is_display'], function($query, $value) {
            return $query->whereIn('is_display', $value);
        });

        return $query;
    }
}
        

継承元をuse Azuki\App\Models\BaseSoftDeleteModel as Model;として Azukiシステムのソフトデリートモデルの基礎クラスに変更します
※ハードデリートモデルの時は BaseHardDeleteModelを、 マスターデータを管理するモデルの場合はBaseMasterModelを継承します
$table $fillableは一般的なLaravelモデル作成と同様に定義します
setConditionメソッドはAzukiシステムで使用しており、検索のロジックを記述します。 上記ではID、place、is_displayでの検索を可能にしています。

コントローラを作成する

コントローラ作成前に、コントローラーで使用するマスターデータの定義を作成します
config/azuki.master.phpを以下の内容で作成します。


return [
    // お知らせ表示場所定義
    'informationPlaceLabel' => [
        'type' => 'file',
        'list' => [
            '1'  => ['order' => '1',  'value'=> '1', 'name' => 'サイト管理'],
            '2'  => ['order' => '2',  'value'=> '2', 'name' => 'マイページ'],
            '3'  => ['order' => '3',  'value'=> '3', 'name' => 'サイト'],
        ],
    ],
    // お知らせ表示・非表示定義
    'informationDisplayLabel' => [
        'type' => 'file',
        'list' => [
            '1'  => ['order' => '1',  'value'=> '1', 'name' => '表示'],
            '2'  => ['order' => '2',  'value'=> '2', 'name' => '非表示'],
        ],
    ],
];
        

次にコントローラーファイルの作成をします

$ php artisan make:controller System\\InformationController

を実行し、コントローラーファイルを作成します。
作成したコントローラーの内容を以下のように書き換えます


namespace App\Http\Controllers\System;

use Azuki\App\Http\Controllers\System\BaseController;
use Azuki\App\Http\Modules\ActionTrait\BaseActionPackageWithDetail;
use Azuki\App\Http\Validation\SharedValidator as Validator;
use App\Models\Informations as Model;

class InformationController extends BaseController
{
    /**
     * サポートサービスのオブジェクト
     *
     * @var CtrlCupporter ctrlSupporter
     */
    protected $ctrlSupporter;

    /**
     * 構成要素
     *
     * array $elements
     */
    protected $elements = [
        'datetime' => [
            'form'   => [
                'title' => [
                    'size' => 3,
                    'type' => 'title',
                    'name' => '日時',
                    'required' => IS_REQUIRED,
                ],
                'datetime'    => [
                    'required'          => IS_REQUIRED,
                    'size'              => 8,
                    'type'              => 'datetime',
                    'name'              => 'datetime', 
                    'column'            => 'datetime',
                    'validate'          => [
                        'rule'      => 'required|date',
                    ],
                ],
            ],
            'list' => [
                [
                    'type'           => CONTROL_TYPE_DATETIME,
                    'width'          => '12%',
                    'format'         => 'Y年m月d日 H時',
                    'orderable'      => true,
                ],
            ],
        ],
        'contents' => [
            'form'   => [
                'title' => [
                    'size' => 3,
                    'type' => 'title',
                    'name' => 'お知らせ内容',
                    'required' => IS_REQUIRED,
                ],
                'contents'    => [
                    'required'          => IS_REQUIRED,
                    'size'              => 8,
                    'type'              => 'textarea',
                    'name'              => 'contents', 
                    'column'            => 'contents',
                    'validate'          => [
                        'rule'      => 'required',
                    ],
                ],
            ],
            'list' => [
                [
                    'type'           => CONTROL_TYPE_TEXT,
                    'width'          => '50%',
                ],
            ],
        ],
        'place' => [
            'form'   => [
                'title' => [
                    'size' => 3,
                    'type' => 'title',
                    'name' => '表示場所',
                    'required' => IS_REQUIRED,
                ],
                'place'    => [
                    'required'          => IS_REQUIRED,
                    'size'              => 8,
                    'type'              => 'radio',
                    'name'              => 'place', 
                    'column'            => 'place',
                    'select'            => 'informationPlaceLabel',
                    'searchName'        => 'search[place]',
                    'searchType'        => 'checkbox',
                    'validate'          => [
                        'rule'      => 'required',
                    ],
                ],
            ],
            'list' => [
                [
                    'type'           => CONTROL_TYPE_SELECT,
                    'width'          => '12%',
                ],
            ],
        ],
        'is_display' => [
            'form'   => [
                'title' => [
                    'size' => 3,
                    'type' => 'title',
                    'name' => '表示',
                    'required' => IS_REQUIRED,
                ],
                'is_display'    => [
                    'required'          => IS_REQUIRED,
                    'size'              => 8,
                    'type'              => 'radio',
                    'name'              => 'is_display', 
                    'column'            => 'is_display',
                    'default'           => 1,
                    'select'            => 'informationDisplayLabel',
                    'searchName'        => 'search[is_display]',
                    'searchType'        => 'checkbox',
                    'validate'          => [
                        'rule'      => 'required',
                    ],
                ],
            ],
            'list' => [
                [
                    'type'           => CONTROL_TYPE_BOOLEAN,
                    'width'          => '8%',
                    'allowValue'     => 1,
                ],
            ],
        ],
    ];
    
    /**
     * 画面構成に必要な要素のモジュールの定義
     *
     */
    protected $elementModues = [
        \Azuki\App\Http\Modules\Forms\BasicElements::class,
    ];

    /**
     * 基本のアクションセットのトレイトを読み込みます
     */
    use BaseActionPackageWithDetail;

    /**
     * ページのh1に表示する文字列
     *
     * string pageTitle
     */
    protected $pageTitle = 'お知らせ管理';
    
    /**
     * 各画面タイプごとの要素のオーダー設定
     *
     * array $elementsOrder
     */
    protected $elementsOrder = [
        'search' => ['place', 'is_display' ],
        'detail' => ['id', 'datetime', 'contents', 'place', 'is_display' ],
        'form'   => ['datetime', 'contents', 'place', 'is_display'],
        'list'   => ['id', 'datetime', 'contents', 'place', 'is_display', 'ctrl' ],
    ];

    /**
     * Create a new authentication controller instance.
     *
     * @return void
     */
    public function __construct(Model $model, Validator $validator)
    {
        parent::__construct();
        
        $this->setCtrlSuppoter([
            'templatePrefix' => $this->viewPrefix.'templates.',
            'model'          => $model,
            'validator'      => $validator,
            'order'          => ['order' => 'datetime', 'condition' => ['datetime' => 'desc']],
            // 共通のアサイン設定変数
            'views' => [
                'pageName'   => 'お知らせ管理',
                'controller' => 'information',
                'search_shortcut' => false,
                'needDatetimePicker' => true,
            ],
        ]);
    }
}
        

いくつかのポイントを簡単に説明します

1.useと継承元

use Azuki\App\Http\Controllers\System\BaseController;
use Azuki\App\Http\Modules\ActionTrait\BaseActionPackageWithDetail;
use Azuki\App\Http\Validation\SharedValidator as Validator;
use App\Models\Informations as Model;

class InformationController extends BaseController
        

継承元をAzuki\App\Http\Controllers\System\BaseControllerに変更します。 これにより、Azukiシステムの提供する基本的なコントローラーの機能が提供されます
ModelクラスとValidationクラスを使用できるようにuseします。 ここで使っているAzuki\App\Http\Validation\SharedValidatorは共通利用できるように作成した汎用のバリデーターです。 個別に作成した専用のバリデーターを使用することも可能です
Azuki\App\Http\Modules\ActionTrait\BaseActionPackageWithDetailは各アクションを定義したトレイトです。 このトレイトはCtrlSupporterクラスを使用して各処理を実行しています。
この組み合わせを使うことで、コントローラーファイルには設定に関する定義を行ったり、任意の場所でのコールバックを登録する、 トレイトのメソッドをオーバーライドするといった部分的なコードの記述のみで管理画面を構築することが出来るようになります

2.メンバ変数

    /**
     * サポートサービスのオブジェクト
     *
     * @var CtrlCupporter ctrlSupporter
     */
    protected $ctrlSupporter;

    /**
     * 構成要素
     *
     * array $elements
     */
    protected $elements = [
    ];
    
    /**
     * 画面構成に必要な要素のモジュールの定義
     *
     */
    protected $elementModues = [
        \Azuki\App\Http\Modules\Forms\BasicElements::class,
    ];

    /**
     * 基本のアクションセットのトレイトを読み込みます
     */
    use BaseActionPackageWithDetail;

    /**
     * ページのh1に表示する文字列
     *
     * string pageTitle
     */
    protected $pageTitle = 'お知らせ管理';
    
    /**
     * 各画面タイプごとの要素のオーダー設定
     *
     * array $elementsOrder
     */
    protected $elementsOrder = [
        'search' => ['place', 'is_display' ],
        'detail' => ['id', 'datetime', 'contents', 'place', 'is_display' ],
        'form'   => ['datetime', 'contents', 'place', 'is_display'],
        'list'   => ['id', 'datetime', 'contents', 'place', 'is_display', 'ctrl' ],
    ];
        

上記メンバ変数、ならびにトレイトの読み込みは必須です。
$elementsにて、画面構成要素の定義を行います。この変数の設定に応じて検索フォーム、一覧表示、詳細表示、編集画面 の全ての表示要素のレイアウト、表示内容、フォームの型、placeHolderといった様々な内容が構築されます。
ここで指定できる詳細については別途記載します。
$elementModues$elementsに追加する構成要素を定義しているクラス配列です。 一般に利用する、idや作成日時、一覧の詳細・編集・削除ボタンが\Azuki\App\Http\Modules\Forms\BasicElements に定義されており、この配列に指定していることで利用可能になります。
他にも一般的に利用するようなパーツを別クラスで定義して読み込むことが可能です
$elementsOrderで、各場所において使う要素を定義します。ここで定義された要素がそれぞれの場所で$elements の定義に従って構成されることになります。
なお、$elementsOrder['form']は必須です。新規作成・編集画面がない場合空の配列で定義しておく必要があります。

3.コンストラクタ

    public function __construct(Model $model, Validator $validator)
    {
        parent::__construct();
        
        $this->setCtrlSuppoter([
            'templatePrefix' => $this->viewPrefix.'templates.',
            'model'          => $model,
            'validator'      => $validator,
            'order'          => ['order' => 'datetime', 'condition' => ['datetime' => 'desc']],
            // 共通のアサイン設定変数
            'views' => [
                'pageName'   => 'お知らせ管理',
                'controller' => 'information',
                'search_shortcut' => false,
                'needDatetimePicker' => true,
            ],
        ]);
    }
        

コンストラクタはほぼ固定で上述の通りになりますが、ここでCtrlSupporterへの初期設定登録をしており、 その設定を変更することで挙動をコントロールすることができます。
'templatePrefix' => $this->viewPrefix.'templates.'は使用するテンプレートのプレフィックスであり、 $this->viewPrefix.'templates.'vendor/azuki/system/templatesディレクトリのテンプレートを使うことを 設定しいます。
なお、vendor/azuki/system/templatesは汎用テンプレートになっています。
他に'pageName'ならびに'controller'は各コントローラーで個別になる場所ですので、合わせて設定が必要です。
'controller'の設定はURLのパスに使用されます。

今回汎用テンプレートを使う形にしているのでテンプレートファイルの作成をしませんが、専用のテンプレートを指定した場合は テンプレートファイルの作成が必要です。
テンプレートファイルは各画面に対してファイル名が決まっていますので、その点注意してください。

ルーティングを登録する

routes/web.phpに以下のルーティングルールを追加します


Route::group(['prefix' => 'system', 'middleware' => ['auth.system', 'acl']], function($route) {
    $route->get('information',
        ['uses' => 'System\\InformationController@getIndex', 'as' => 'system.information.index']);

    $route->get('information/list',
        ['uses' => 'System\\InformationController@getList', 'as' => 'system.information.list']);
    $route->post('information/list',
        ['uses' => 'System\\InformationController@postList', 'as' => 'system.information.p-list']);

    $route->get('information/regist',
        ['uses' => 'System\\InformationController@getRegist', 'as' => 'system.information.regist']);
    $route->post('information/regist',
        ['uses' => 'System\\InformationController@postRegist', 'as' => 'system.information.p-regist']);

    $route->get('information/detail/{id}',
        ['uses' => 'System\\InformationController@getDetail', 'where' => ['id', '[0-9]+'], 'as' => 'system.information.detail']);

    $route->post('information/edit',
        ['uses' => 'System\\InformationController@postEdit', 'as' => 'system.information.p-edit']);
    $route->get('information/edit/{id}',
        ['uses' => 'System\\InformationController@getEdit', 'where' => ['id' => '[0-9]+'], 'as' => 'system.information.edit']);
    $route->get('information/edit/{id}/prev/{prev}',
        ['uses' => 'System\\InformationController@getEdit', 'where' => ['id' => '[0-9]+', 'prev' => 'detail|list'], 'as' => 'system.information.edit']);

    $route->post('information/confirm',
        ['uses' => 'System\\InformationController@postConfirm', 'as' => 'system.information.p-confirm']);
    $route->post('information/done',
        ['uses' => 'System\\InformationController@postDone', 'as' => 'system.information.p-done']);
    $route->post('information/delete',
        ['uses' => 'System\\InformationController@postDelete', 'as' => 'system.information.p-delete']);
});
        

Azukiシステムのルーティングを使用した場合は以下の記述方法が可能です。
ただし、web.phpに記述するのではなく別ファイルに記述の上、App\Providers\RouteServiceProviderクラスの mapWebRoutesメソッドから読み込むようにする方がよいと考えます。


$routeController = new \Azuki\App\Services\RouteControll([
    'web' => [
        'namespace' => '\App\Http\Controllers',
        'group' => [
            // /system/~ に関するルーティング。ミドルウェアにてログイン、権限の制限がかかったURL
            [
                'prefix'     => 'system',
                'middleware' => ['auth.system', 'acl'],
                'list'       => [
                    [   // システムユーザー管理画面
                        'prefix'     => 'information',
                        'set'        => [
                            [ 'method' => 'get',  'url' => 'index' ],
                            [ 'method' => 'get',  'url' => 'list' ],
                            [ 'method' => 'post', 'url' => 'list' ],
                            [ 'method' => 'post', 'url' => 'regist' ],
                            [ 'method' => 'get',  'url' => 'regist' ],
                            [ 'method' => 'get',  'url' => 'detail/{id}', 'where' => ['id' => '[0-9]+'] ],
                            [ 'method' => 'post', 'url' => 'edit' ],
                            [ 'method' => 'get',  'url' => 'edit/{id}', 'where' => ['id' => '[0-9]+'] ],
                            [ 'method' => 'get',  'url' => 'edit/{id}/prev/{prev}', 'where' => ['id' => '[0-9]+', 'prev' => 'detail|list'] ],
                            [ 'method' => 'post', 'url' => 'confirm' ],
                            [ 'method' => 'post', 'url' => 'done' ],
                            [ 'method' => 'post', 'url' => 'delete' ],
                        ],
                        'controller' => 'InformationController',
                    ],
                ],
            ],
        ],
    ]
]);
$routeController->makeRouting(app('router'));
        

管理画面にアクセスして機能を確認する

http[s]://ドメイン名/system/informationにアクセスして、お知らせの登録・編集・削除が出来ることを確認します

お知らせを表示する部分を実装する

マイページへの表示のみ紹介します。

Models/Informationsに表示場所を引数指定して最新5件を取得できるメソッドを作成します。 これは共通で利用できるメソッドになります。


    /**
     * function getLastInformations
     *
     * 最新のお知らせ情報を取得する
     *
     * @params Integer $place 表示場所の指定
     * @params Integer $num   取得件数ーデフォルト5件
     *
     * @return Collection $ret Informationsクラスのインスタンスコレクション
     **/
    public function getLastInformations($place, $num = 5)
    {
        $ret = $this
            ->where('place', '=', $place)
            ->where('is_display', '=', 1)
            ->orderBy('datetime', 'desc')
            ->limit($num)
            ->get();
        
        return $ret;
    }
        

次にコントローラーを作成し、既存のマイページ用のコントローラーを継承し、必要なメソッドのみオーバーライドします。
App\Http\Controllers\Common\MypageControllerを以下の内容で作成します。


namespace App\Http\Controllers\Common;

use Azuki\App\Http\Controllers\Common\MypageController as Controller;
use App\Models\Informations;

class MypageController extends Controller
{
    /**
     * mypageのテンプレート個別設定
     * myapgeトップ画面のテンプレートのみお知らせ表示のために差し替える
     */
    protected $viewTemplates = [
        'mypage' => 'mypage.mypage',
    ];

    /**
     * function indexAction 初期表示のアクション
     * 
     * @Get("/", as="index")
     *
     * 初期表示のアクション
     * 初期表示は一覧とするため、一覧のアクションにリダイレクト
     * @return void
     */
    public function getIndex()
    {
        $id = $this->user->id;

        $forms = $this->getDetailForm();
        $this->ctrlSupporter->setDetailForm($forms);
        $this->ctrlSupporter->setResultStatus();

        $info = new Informations();
        $information = $info->getLastInformations(2);

        return $this->ctrlSupporter->view('mypage', [
            'flow'         => 'mypage',
//            'prev'         => $prev,
            'post'         => $this->ctrlSupporter->find($id),
            'detailLayout' => $this->getDetailLayout(),
            'detailForm'   => $forms,
            'informations' => $information,
        ]);
    }
}
        

対応するテンプレートファイルを作成します。上記のコンロトーラーで


    protected $viewTemplates = [
        'mypage' => 'mypage.mypage',
    ];
        

と設定しているので、対象のテンプレートはresources/views/mypage/mypage.blade.phpとなります。
このファイルを作成し、以下のような感じにします。


@extends($vendor.'.'mypage.mypage'')

@section('contents-body')
      <div class="in-contents-body">

        <h2>お知らせ</h2>
        @forelse($informations as $row)
          {{$row->datetime}}{!!nl2br(e($row->contents))!!}
        @empty
          お知らせはありません
        @endforelse
@parent

@stop
        

最後にルーティングを変更し、上記で作成したコントローラーに処理が渡るようにします。
routes/web.phpに以下を追加します。


Route::get('/mypage', ['uses' => 'Common\\MypageController@getIndex', 'as' => 'mypage']);
        

Ver.1.2.x以降ではルーティングの上書きをせずに、コントローラーの実体を切り替えることができます。
config/azuki.app.phpをpublishし以下を設定します。


'aliases' => [
    'controllers' => [
        'common' => [
            'mypage'         => \App\Http\Controllers\Common\MypageController::class, // [マイページ]
        ],
    ],
],
        

config/azuki.app.phpの他の設定を削除しないようにしてください

ケース2.ユーザー情報を拡張する