20190607のlaravelに関する記事は4件です。

Laravel の Listener, Subscriber, Observer それぞれの使い分けについてざっくり

この記事について

Laravel にはイベントを捕捉する仕組みがいくつかあるんですが、まとめて違いや使い方を説明してるページがないな、と思ったので、本当にざっくりですが、書いてみました。

Listener

単体のイベントを捕捉する

Providers/EventServiceProvider.php

protected $listen = [
    Event::class => [
        Listener::class,
    ],
];

呼び方

event(new Event($something));

Subscriber

複数のイベントを捕捉する - 何らかの共通の処理を挟んだりするときに有用

Providers/EventServiceProvider.php

protected $subscribe = [
    Subscriber::class,
];

Subscriber.php

public function subscribe($events): void
{
    $events->listen(FooEvent::class, 'App\Listeners\Subscriber@handleFoo');
    $events->listen(BarEvent::class, 'App\Listeners\Subscriber@handleBar');
}

呼び方

event(new FooEvent($something));

Observer

特定のモデルのイベントを捕捉する - モデルの変化にシームレスに追従して何らかの処理を行いたいときに有用

AppServiceProvider.php

SomeModel::observe(SomeModelObserver::class);

明示的にイベントを発火する必要はなく、それぞれのイベントがしかるべきタイミングで勝手に呼ばれます。

デフォルトで捕捉してくれるイベントは下記の13個

HasEvents.php

    public function getObservableEvents()
    {
        return array_merge(
            [
                'retrieved', 'creating', 'created', 'updating', 'updated',
                'saving', 'saved', 'restoring', 'restored', 'replicating',
                'deleting', 'deleted', 'forceDeleted',
            ],
            $this->observables
        );
    }

setObservableEvents, addObservableEvents, removeObservableEvents あたりをつかって増やしたり減らしたりできます。 getObservableEvents() が呼ばれる前に使ってください。

おまけ: ワイルドカード Listener

イベント名のプレフィックスを使ってマッチしたイベントを捕捉する

Event::listen('message.*', function ($event, $payload) {
  // handle event with prefix 'message.'
});

通常は、

event(new MessageSent());

のようにオブジェクトを渡しますが、ワイルドカードを使う場合は、

event('message.sent', ['message' => 'hi']);

のように、イベント名を表す文字列で発火させるようにします。

まとめ

  • Listener: 単独のイベント
  • Subscriber: 複数の関連したイベント
  • Observer: モデルのイベント
  • ワイルドカード Listener: 共通で処理したいイベント
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel-adminで1対多を色々な形で表示してみる

概要

Laravel-adminの1対多で結びついているデータを色々な形で表示してみました.

環境

Laravel 5.3 , Laravel-admin 1.6.9

モデル

Author.php
class Author extends Model
{
    public function books() {
        return $this->hasMany(Book::class,'author_id')->orderBy('id','asc');
    }
}
Book.php
class Book extends Model
{
    protected $fillable=['title','author_id','page','subtitle','price'];

    public function author() {
        return $this->belongTo(Author::class,'author_id');
    }
}

通常の使用

公式ドキュメントなどにあるように実装するとこのような画面が出来上がります.
子のテーブルのカラムが少ない場合はこれで十分使えると思います.

ノーマルモード.PNG

AuthorController.php
protected function form()
{
    $form = new Form(new Author);

    $form->tab('氏名',function($form) {
        $form->hidden('id');
        $form->text('first_name', '名');
        $form->text('last_name', '氏');
    })->tab('本',function($form) {
        $form->hasMany('books','BOOK',function(Form\NestedForm $nestedForm) {
            $nestedForm->hidden('id');
            $nestedForm->text('title','タイトル');
            $nestedForm->text('subtitle','サブタイトル');
            $nestedForm->number('page','ページ数');
            $nestedForm->currency('price','価格');
        });
    });

    return $form;
}

タブモード

ノーマルモードのフォーム1つ1つをタブに入れて表示しています.子のカラムが増えても1ページに1つのレコードが表示されるため,ノーマルモードに比べて下に伸びにくくなっています.
一方で,親が子を多く持つとタブの数が増加するため,このようなときには使いづらくなります.また,目的のレコードが探しづらいという短点もあります.

タブモード.PNG

AuthorController.php
protected function form()
{
    $form = new Form(new Author);

    $form->tab('氏名',function($form) {
        $form->hidden('id');
        $form->text('first_name', '名');
        $form->text('last_name', '氏');
    })->tab('本',function($form) {
        $form->hasMany('books','BOOK',function(Form\NestedForm $nestedForm) {
            $nestedForm->hidden('id');
            $nestedForm->text('title','タイトル');
            $nestedForm->text('subtitle','サブタイトル');
            $nestedForm->number('page','ページ数');
            $nestedForm->currency('price','価格');
        })->useTab();
    });

    return $form;
}

デーブルモード

laravel-admin 1.6.10にて追加された機能.上記の2つに比べてスッキリとしたデザインで使いやすいと思います.
しかし,子のカラムが増えると横幅が足りなくなり,入力フォームが非常に小さくなることがあります.

テーブルモード.PNG

AuthorController.php
protected function form()
{
    $form = new Form(new Author);

    $form->tab('氏名',function($form) {
        $form->hidden('id');
        $form->text('first_name', '名');
        $form->text('last_name', '氏');
    })->tab('本',function($form) {
        $form->hasMany('books','BOOK',function(Form\NestedForm $nestedForm) {
            $nestedForm->hidden('id');
            $nestedForm->text('title','タイトル');
            $nestedForm->text('subtitle','サブタイトル');
            $nestedForm->number('page','ページ数');
            $nestedForm->currency('price','価格');
        })->useTable();
    });

    return $form;
}

index画面の埋めこみ

index画面にあるgridをそのままフォームに埋め込んだものとなっています.
index画面を埋め込んでいるため動作はindex画面に準じたもので,他のフォームとは独立した編集画面として動作します.
例えばindex画面と同様に編集を行うと,送信ボタンを押さずともデータベースが更新され,新規ボタンを押すとBookControllerのcreateメソッドに飛びます.
あくまで表示だけにとどめて,新規・編集機能は上記の3つのどれかで実装するのが良いと思われます.

grid埋めこみ.PNG

AuthorController.php
protected function form($id = null)
{
    if($id) {
        //Bookのgird画面のHTMLレンダリングを先に行う
        $html = '';

        //Book用のgridを作成する
        $grid = new Grid(new Book);
        $grid->setName('BOOK')
            ->setTitle('本')
            ->setRelation(Author::find($id)->books())
            ->resource('/admin/books');

        $grid->title('タイトル')->editable();
        $grid->subtitle('サブタイトル')->editable();
        $grid->page('ページ数')->editable();
        $grid->price('価格')->editable();

        //ボタンを無効
        $grid->disableExport();
        $grid->disableFilter();
        $grid->disablePagination();
        $grid->disableRowSelector();
        $grid->tools(function ($tools) {
            $tools->disableRefreshButton();
        });

        //操作を無効化
        $grid->actions(function ($actions) {
            $actions->disableView();
            $actions->disableEdit();
            });

        $html = $grid->render();
    }

    $form = new Form(new Author);

    $form->tab('氏名',function($form) {
        $form->hidden('id');
        $form->text('first_name', '名');
        $form->text('last_name', '氏');
    })->tab('本',function($form) use ($html) {
        $form->html(function() use ($html) {
            return $html;
        })->setWidth(12, 0);
    });

    return $form;
}

まとめ

4つの表示方法を紹介しましたが,いずれも一長一短で状況に応じて使い分けるのが良いと思います.
また,index画面の埋めこみはかなりトリッキーな実装をしているので,ここまでするなら素直にbladeを書いたほうが良い気もします.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel Envoy でデプロイ対象サーバを指定してデプロイする

Envoy はとてもシンプルなデプロイツールでLaravel の Blade を使ったことがあれば直感的に記述できます。

https://laravel.com/docs/envoy

ただ、複数のサーバを対象とする場合、基本的には記述されたすべてのServerがデプロイ対象となり、対象を選択するようなオプションが標準では準備されていません。
@task のオプションである on を利用すればタスク毎に対象サーバを絞ることができますが、実行時に対象サーバの選択はできない。)

そこで試行錯誤して複数環境にリリースできるようにしてみました。

Envoy.blade.php
@servers(['production' => 'example.com', 'staging' => 'staging.example.com'])

@story('deploy')
    git
    composer
    migration
    cache
    autoload
@endstory

@setup
    $docRoot = '/var/www/html';

    if (empty($server)) {
        $server = 'production';
    }
@endsetup

@task('git', ['on' =>  $server])
    cd {{ $docRoot }}
    git pull origin master
@endtask

@task('composer', ['on' =>  $server])
    cd {{ $docRoot }}
    composer install --no-interaction --no-dev
@endtask

@task('migration', ['on' =>  $server, 'confirm' => true])
    cd {{ $docRoot }}
    php artisan migrate
@endtask

@task('cache', ['on' =>  $server])
    cd {{ $docRoot }}
    php artisan cache:clear
@endtask

@task('autoload', ['on' =>  $server])
    cd {{ $docRoot }}
    composer dump-autoload
@endtask

@finished
    @slack('webhook-url', '#example', $server . 'にデプロイしました')
@endfinished

実行方法

# production
$ envoy run deploy

# staging
$ envoy run deploy --server=staging

production は、serverを指定してもOK

解説

実行対象を分けるだけであれば、実はそれほど難しくありません。
@story を分けて

@story('production', ['on' => 'production'])
    ...
@endstory

@story('staging', ['on' => 'staging'])
    ...
@endstory

のように記述すればできます。

ただ、この方法ではSlack通知のメッセージを出し分けることができませんでした。
理由は以下の制約があるからです。

  • @setup 内じゃないと変数代入できない。
  • @finished 内じゃないと @slack が使えない。

ということでコマンドのオプション変数で対応しました。
(まさか on に変数を直接書けるとは思わなかった)

--server オプションを必ず指定するのであれば、各 @taskon を削除して以下の記述でもいけます。

@story('deploy', ['on' => $server])
    ...
@endstory

この場合 --server が未指定の場合に @setup で補完する処理を記述しても、すでに on の記載されている @story が呼ばれているためなのか効果がありませんでした。

まとめ

これで、1ファイルでデプロイ先のサーバを切り替えることができそうです。
もし、もっとスマートなやり方を知っている方は教えてください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaravelでRepository Patternを採用した時に手間が発生

なぜしたのか?

  • Repository Patternを実装する際InterfaceとRepositoryのfileを作成する手間が発生しめんどーだと思ったから。

何をしたのか?

  • Laravelでartisan コマンドを作成しInterfaceとRepositoryのfileをコマンドで作成できるようにしました
    • php artisan make:command CommandNameで作成し、実装後にapp/Console/Kernel.phpに追記して使用できるようにします。

備考

  • php artisan make:repository RepositoryNameというコマンドを作成しようと思います
    • 本来ならば実装した方がいいですが今回は、Service Classは書いておらず、またPHPUnitも書いてません
  • 今回は、InterfaceとRepositoryを同じディレクトリに作成します。
  • 参考にしたサイト(ほぼ使わせていただいてます)

完成イメージ

projectName > php artisan make:repository Hoge

new directory name. or use directory name:
> Hoge # 入力
  • 実行結果
app/Repositories/
└── Hoge
    ├── HogeInterface.php
    └── HogeRepository.php

作成したFile

  • 今回1fileで書いています。
<?php
declare(strict_types=1);

namespace App\Console\Commands;

use Illuminate\Console\Command;

/**
 * CreateRepositoryFileCommand class
 */
class CreateRepositoryFileCommand extends Command
{
    /**
     * @const string repository dir path
     */
    const REPOSITORIES_PATH = 'app/Repositories/';

    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'make:repository {repositoryName : The name of repository}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Create repository files';

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

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

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

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

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $this->fileName = $this->argument('repositoryName');

        if (is_null($this->fileName)) {
            $this->error('Repository Name invalid');
        }
        $this->dirName = $this->ask('new directory name. or use directory name');

        if (is_null($this->dirName)) {
            $this->error('Directory required!');
        }

        if (!$this->isExistDirectory()) {
            $this->createDirectory();
        }

        $this->repositoryFileName = self::REPOSITORIES_PATH . $this->dirName . '/' . $this->fileName . 'Repository.php';
        $this->interfaceFileName = self::REPOSITORIES_PATH . $this->dirName . '/' . $this->fileName . 'Interface.php';
        if ($this->isExistFiles()) {
            $this->error('already exist');
            return;
        }

        $this->creatRepositoryFile();
        $this->createInterFaceFile();
        $this->info('create successfully');
    }

    /**
     * Repositoryのfileを作成する
     * @return void
     */
    private function creatRepositoryFile(): void
    {
        $content = "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Repositories\\$this->dirName;\n\class $this->fileName" . "Repository implements $this->fileName" . "Interface\n{\n}\n";
        file_put_contents($this->repositoryFileName, $content);
    }

    /**
     * Interfaceのfileを作成する
     * @return void
     */
    private function createInterFaceFile(): void
    {
        $content = "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Repositories\\$this->dirName;\n\ninterface $this->fileName" . "Interface\n{\n}\n";
        file_put_contents($this->interfaceFileName, $content);
    }

    /**
     * 同名fileの確認
     * @return bool
     */
    private function isExistFiles(): bool
    {
        return file_exists($this->repositoryFileName) && file_exists($this->interfaceFileName);
    }

    /**
     * directoryの存在確認
     * @return bool
     */
    private function isExistDirectory(): bool
    {
        return file_exists(self::REPOSITORIES_PATH . $this->dirName);
    }

    /**
     * 指定名でdirectoryの作成
     * @return void
     */
    private function createDirectory(): void
    {
        mkdir(self::REPOSITORIES_PATH . $this->dirName, 0755, true);
    }
}

作成されるfile

  • あとはおすきなように書いてください。
    • ちなみに自分は、RepositoryでEloquent Modelを継承したclassをDIして使用しています。
<?php
declare(strict_types=1);

namespace App\Repositories\Hoge;

interface HogeInterface
{
}

<?php
declare(strict_types=1);

namespace App\Repositories\Hoge;

class HogeRepository implements HogeInterface
{
}

まとめ

  • 自身が設計知識に精通しているわけではないので同じディレクトリで良いのだろうかなど色々悩みましたが、まぁーちゃんとした答えがあるとは、思っていないのでひとまずは、同じディレクトリでいいかなっておもい同じディレクトリにしました

    • 「いやいや待て待て」と思う方はぜひご指摘の方よろしくお願いいたします!
  • 久々にQiitaに投稿しましたがアウトプットは大切だなと思いました。

    • 自分のコードを見直してて1行しかないメソッドもどうかと思いますが。
  • 不備、間違い等ございましたらぜひご指摘いただけたらと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む