- 投稿日:2019-06-07T19:09:18+09:00
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: 共通で処理したいイベント
- 投稿日:2019-06-07T14:53:22+09:00
Laravel-adminで1対多を色々な形で表示してみる
概要
Laravel-adminの1対多で結びついているデータを色々な形で表示してみました.
環境
Laravel 5.3 , Laravel-admin 1.6.9
モデル
Author.phpclass Author extends Model { public function books() { return $this->hasMany(Book::class,'author_id')->orderBy('id','asc'); } }Book.phpclass Book extends Model { protected $fillable=['title','author_id','page','subtitle','price']; public function author() { return $this->belongTo(Author::class,'author_id'); } }通常の使用
公式ドキュメントなどにあるように実装するとこのような画面が出来上がります.
子のテーブルのカラムが少ない場合はこれで十分使えると思います.AuthorController.phpprotected 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つのレコードが表示されるため,ノーマルモードに比べて下に伸びにくくなっています.
一方で,親が子を多く持つとタブの数が増加するため,このようなときには使いづらくなります.また,目的のレコードが探しづらいという短点もあります.AuthorController.phpprotected 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つに比べてスッキリとしたデザインで使いやすいと思います.
しかし,子のカラムが増えると横幅が足りなくなり,入力フォームが非常に小さくなることがあります.AuthorController.phpprotected 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つのどれかで実装するのが良いと思われます.AuthorController.phpprotected 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を書いたほうが良い気もします.
- 投稿日:2019-06-07T14:12:01+09:00
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=stagingproduction は、serverを指定してもOK
解説
実行対象を分けるだけであれば、実はそれほど難しくありません。
@story
を分けて@story('production', ['on' => 'production']) ... @endstory @story('staging', ['on' => 'staging']) ... @endstoryのように記述すればできます。
ただ、この方法ではSlack通知のメッセージを出し分けることができませんでした。
理由は以下の制約があるからです。
@setup
内じゃないと変数代入できない。@finished
内じゃないと@slack
が使えない。ということでコマンドのオプション変数で対応しました。
(まさかon
に変数を直接書けるとは思わなかった)
--server
オプションを必ず指定するのであれば、各@task
のon
を削除して以下の記述でもいけます。@story('deploy', ['on' => $server]) ... @endstoryこの場合
--server
が未指定の場合に@setup
で補完する処理を記述しても、すでにon
の記載されている@story
が呼ばれているためなのか効果がありませんでした。まとめ
これで、1ファイルでデプロイ先のサーバを切り替えることができそうです。
もし、もっとスマートなやり方を知っている方は教えてください!
- 投稿日:2019-06-07T12:03:58+09:00
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行しかないメソッドもどうかと思いますが。
不備、間違い等ございましたらぜひご指摘いただけたらと思います。