ConfluenceとJIRAがいつの間にかバージョンアップしてるので、把握できるようなRSSを配信できるスクリプト組んだ。
httpが面倒だったのでwget前提。
以下の3つのファイルを置いて atlassian-products-jira.php atlassian-products-confluence.php 二アクセスすればOK
FeedのネタにしてるJSONが変わったら適当に変更しなければならない。
Routingクロージャや、Controllerメソッドで
<?php Route::get('/', function (Authenticatable $user) {
とか
<?php public function index(Authenticatable $user)
とした時に、どうやってこの$userを良い感じで\App\Userにしてくれるのかを追ってみた。
Reflectionを使うところから知らなかったのでまずそこから。
Reflection使ってMethodやConstructorの情報を得るのは下記のようにする。参照
<?php class Foo { public function __construct(Bar $bar, Foo $foo) { } public function methodA(Bar $bar, Foo $foo){ } } class Bar { public function __construct() { } public function methodA(Bar $bar){ } } getRef('Foo'); getRef('Bar'); function getRef($class) { $reflection = new ReflectionClass($class); $params = $reflection->getConstructor()->getParameters(); echo "Constructor params\n"; foreach ($params AS $param) { echo " ". $param->getClass()->name . "\n"; } echo "---\n"; $params = $reflection->getMethod('methodA')->getParameters(); echo "methodA params\n"; foreach ($params AS $param) { echo " ". $param->getClass()->name . "\n"; } echo "---\n"; }
というので、Reflectionを使うことでMethodが何のクラスを欲しがっているかを解決できる。
Laravelでは、Illuminate/Routing/RouteDependencyResolverTrait.phpのReflectionでどうこうやってるみたい。
<?php Route::get('/', function (Authenticatable $user) {
この場合、Illuminate/Routing/Route.php のrunCallable()がコールされる。
<?php /** * Run the route action and return the response. * * @return mixed */ protected function runCallable() { $parameters = $this->resolveMethodDependencies( $this->parametersWithoutNulls(), new ReflectionFunction($this->action['uses']) ); $callable = $this->action['uses']; return $callable(...array_values($parameters)); }
このresolveMethodDependenciesにより、$this->action['uses']はタダのタイプヒントからObjectになっている。
Illuminate/Foundation/Application の$bindingプロパティに依る。
この$bindingにこのauthの対応を登録しているのは、Illuminate/Auth/AuthServiceProvider.phpの
<?php protected function registerUserResolver() { $this->app->bind( AuthenticatableContract::class, function ($app) { return call_user_func($app['auth']->userResolver()); } ); }
になる。
ここでようやく、$app['auth']がregisterCoreContainerAliases()でヒモづけられた'Illuminate\Auth\AuthManager'であり、そのAuthManagerがconfig/auth.phpのproviders/users/modelを参照しているということになる。
ここまで抽象化されるとわからんな。。。
さて、だいたい慣れてきた。折角なのでここからは与えられたサンプルコードを使わずに一から作っていこうと思う。
もちろんサンプルコードは参考にする。
すっかり忘れてしまったのだけど、.bashrcに
export COMPOSER_HOME=~/.composer export PATH=$PATH:~/bin:$COMPOSER_HOME/vendor/bin
と書き、~/.composer/composer.json に
{ "require": { "laravel/installer": "~1.1" } }
としているので、~/.composerにて $composer updateする。もちろん、~/bin/にcomposer.pharを入れている。すると、Laravel Installerによるlaravelコマンドが任意の場所で使える。
$ laravel new ~/laravel/auth_sample
でとりあえず、雛形作成。キー生成も済んでいる。
作るテーブルの情報はP.158の表4.2と表4.3の通り。
usersテーブルを作るMigration
$ ./artisan make:migration create_user_table --create=users Created Migration: 2016_12_29_124332_create_user_table
password_resetsテーブルを作るMigration
$ ./artisan make:migration create_password_resets_table --create=password_resets [InvalidArgumentException] A CreatePasswordResetsTable migration already exists.
???あれ、既にある。
$ ls database/migrations/ 2014_10_12_000000_create_users_table.php 2014_10_12_100000_create_password_resets_table.php 2016_12_29_124332_create_user_table.php $ ./artisan make:migration create_users_table --create=users [InvalidArgumentException] A CreateUsersTable migration already exists.
あるじゃん・・・というか、usersテーブルもあるんだ。
ということで、さっき作った2016_12_29_124332_create_user_table.phpを削除して、composer dump-autoloadする。
$ ./artisan migrate Migration table created successfully. Migrated: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_100000_create_password_resets_table $ mysql -u **** -p**** laravel_authsample -e "show tables" +------------------------------+ | Tables_in_laravel_authsample | +------------------------------+ | migrations | | password_resets | | users | +------------------------------+
作成完了。
$ ./artisan make:model User Model already exists!
・・・あれ、もうできてる。
書籍によると、
の2つが既にあるらしい。
5.3だとそれらは無いのだが、
の2つがある。見た感じそれぞれが対応しそうである。
config/auth.php が本の5.1とは随分と異なっている。
肝心な部分はここだと思われる。
<?php 'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\User::class, ], // 'users' => [ // 'driver' => 'database', // 'table' => 'users', // ], ],
このままでおそらく本書と同じであろうからそのまま。
resources/lang/ja/passwords.php を作成。面倒なので、本書のサンプルコードをそのままコピー。
Routingを行う。
routes/web.php に
<?php Route::resource('/register', 'Auth\RegisterController');
を追加。
雛形のAuth\RegisterControllerは、Illuminate/Foundation/Auth/RegistersUsers をtraitしているのでこのクラスを使ってみる。
GET/indexを追加
<?php protected function index() { return $this->showRegistrationForm(); }
で、次はpost周りを・・・と思ったけど Illuminate/Foundation/Auth/RegistersUsers.phpが5.1と5.3では大きく変わってる。
あれー、と思って調べるとHow to register a user in Laravel 5.3?で「Route::auth()」使うんやでっていうコメントが。
Illuminate/Routing/Router.php のauth()を見てみると、あー。。。なんか色々とRoutingしてくれている。
ということで、 routes/web.php を
<?php // Route::resource('/register', 'Auth\RegisterController'); Route::auth();
と変更。Auth\RegisterControllerに付け加えた protected function index()も削除。
findすると vendor/laravel/framework/src/Illuminate/Auth/Console/stubs/make/views/auth/register.stub なるスタブもある。
んんん~~。と思い、調べると、artisanにmake:authコマンドがある。
./resources/views/layouts/app.blade.php ./resources/views/auth/register.blade.php ./resources/views/auth/passwords/reset.blade.php ./resources/views/auth/passwords/email.blade.php ./resources/views/auth/login.blade.php ./resources/views/home.blade.php ./app/Http/Controllers/HomeController.php
が追加されていた。
routes/web.phpには以下の2行が追記されていた。もはや、Route::auth()メソッドですらない。
<?php Auth::routes(); Route::get('/home', 'HomeController@index');
Authファサードでよく使いそうな機能について
普通のHTTPの場合なので、実態は、Illuminate/Auth/SessionGuard.php
実際には、Illuminate/Foundation/Auth/AuthenticatesUsers.php のlogin()にて使われている。 これを直接使うのはあまり無いかも知れない。
ログイン状態のチェックをする。guest()メソッドはこれの反転。Bladeテンプレートでも @if (Auth::guest()) とかで簡単に使える。
Tokenによるログインを行ったかどうか。
セッションが切れてCookieのRememberトークンによるログインになった場合はこれがtrueになる。
App\Userオブジェクトを返してくれる。
これ、どうやって実装してるんだろう・・・・。
Middlewareではなく、config/auth.phpで設定される何かから引っ張ってきているんだろうとは思うんだけど。
何となくこんな感じというのを調べてみた。 LaravelのメソッドのTypeHintがどうやってパラメータをうまい具合にセットしているのか? - tohokuaikiのチラシの裏
RoutingのClosureで使う場合
<?php use Illuminate\Http\Request; Route::get('/', function (Request $request) { dd($request->user()); return view('welcome'); });
Controllerメソッドで使う場合。
<?php use Illuminate\Http\Request; class HomeController extends Controller { /** * Show the application dashboard. * * @return \Illuminate\Http\Response */ public function index(Request $request) { dd($request->user()); return view('home'); }
ただし、この場合は認証を通過していないと引数にnullが入ってしまい
Controller::index() must implement interface Illuminate\Contracts\Auth\Authenticatable, none given
のエラーが出てしまう。
従って、
<?php public function __construct() { $this->middleware('auth'); }
として、middlewareにてAuthチェックを行うなどする必要がある。
どちらも任意のログインユーザーでログインする。テストを書く時に使う。
<?php Auth::login(new \App\User::find(1)); Auth::loginUsingId(1);
1リクエスト内で有効なログイン状態を作る。それ以外はAuth::attempt()と同じ。
雛形は、
$ ./artisan make:event QueryLogTracker
で作る。
<?php namespace App\Handlers\Events; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; class QueryLogTracker { /** * Create the event handler. * * @return void */ public function __construct() { // } /** * Handle the event. * * @param illuminate.query $event * @return void */ public function handle($query, $bindings, $time, $name) { Log::debug(sprintf("Query: %s | Bindings: %s", $query, json_encode($bindings) )); } }
<?php namespace App\Providers; use Illuminate\Contracts\Events\Dispatcher as DispatcherContract; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use DB; class EventServiceProvider extends ServiceProvider { /** * The event listener mappings for the application. * * @var array */ protected $listen = [ 'App\Events\SomeEvent' => [ 'App\Listeners\EventListener', ], ]; /** * @brief * @param * @retval */ public function __construct($app) { parent::__construct($app); if (env('APP_DEBUG')){ $this->listen['illuminate.query'] = [ 'App\Handlers\Events\QueryLogTracker', ]; } } /** * Register any other events for your application. * * @param \Illuminate\Contracts\Events\Dispatcher $events * @return void */ public function boot(DispatcherContract $events) { parent::boot($events); } }
つらつらっと読んで気になったところだけメモ
コマンド引数の実行内容(下記の場合はcreate_authors_table)は同じものが被るとClass名として被ってしまい、Fatalになるので気を付ける。
./artisan make:migration create_authors_table --create=authors
マイグレーションファイルを削除したのに亡霊のように出てきてオカシイ
$ ./artisan migrate:refresh [ErrorException] include(/home/vagrant/laravel/sample/3/vendor/composer/../../database/migrations/2016_12_26_105 300_create_authors_table.php): failed to open stream: No such file or directory
何かと思ったら、 ./vendor/composer/autoload_static.php ./vendor/composer/autoload_classmap.php でこの作ったファイルをローディングしていた。
$ composer dump-autoload
して解決。
SQLを見せてくれるはずのオプションなんだけど
$ ./artisan migrate --pretend [Doctrine\DBAL\Schema\SchemaException] There is no column with name 'name' on table 'authors'.
ちゃんと動かない・・・。
多分、ALTERが入っている時にはそのTableが既に作られて無いとダメなんだと思う。
同じように、
<?php public function up() { if (Schema::hasTable('authors')) {
ってやったらちゃんと判定してくれなかった。
実migrate実行時には正しく判定してたのでおそらくConnectionをしてない状態でやってるからだと思う。
これもメモ程度で。
<?php public function up() { Schema::table('authors', function (Blueprint $table) { $table->index('name'); $table->string('katakana')->index(); $table->string('furigana', 10)->change()->nullable(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('authors', function (Blueprint $table) { $table->dropIndex('authors_name_index'); $table->dropIndex('authors_katakana_index'); }); }
これ、実際には何度かmigrate → rollbackを繰り返してチェックしないときれいなMigrationは作れないな。
./artisan db:seed
でシード投入
./artisan db:seed --class=AuthorTableSeeder
でAuthorTableSeederのみ実行。
./artisan migrate:refresh --seed
でマイグレーションの再実行(rollback&migrate)からseedの投入まで一発
<?php $result = DB::select(select文); $result = DB::select(プレースホルダ付きselect文, [param1 => "foo" , param2 => "bar"]); foreach ($result as $r){ echo $r->name; }
プレースホルダは、
select from authors where name=:name and email=:email
というSQLに対して、第二引数を
<?php ["name" => "ITOH", "email" => "itoh@example.com"]
という感じで与える。
あるいは、
select from authors where name=? and email=?
に引数を
<?php ["ITOH", "itoh@example.com"]
いずれもプレースホルダはクオートで囲む必要は無い。
返り値は、config/database.phpの'fetch' => PDO::FETCH_CLASSで設定したものになる。
read/writeでDBを分けている場合、readの方に繋ぐ。強制的にwriteの方に繋ぎたい場合は、DB::selectFromWriteConnection()を使う。
DB::insert(INSERT文); でBooleanが返ってくる。
DB::update(UPDATE文); で影響を受けた行数が返ってくる。
DB::delete(DELETE文); で影響を受けた行数が返ってくる。
DB::statement(SET文やDROP文など); でBooleanが返ってくる。
普通にDB::select()などをした場合、config/database.phpの「'default' => env('DB_CONNECTION', 'mysql'),」で指定されているものに繋ぎに行く。この場合、.envにDB_CONNECTIONが無かったらmysqlに行くようになる。
config/database.phpのconnectionsキーの別のものに繋ぎにいきたい場合、DB::connection()を使う。
<?php DB::connection('mysql2')->select(...)
DB::transaction()とクロージャを使う
<?php DB::transaction(function(){ /// ここにさまざまなSQL処理を行う。 });
特にRollbackやBeginを行う必要は無い。一応手動で、DB::commit()/ DB::rollback()/ DB::begin()はある。
<?php DB::enableQueryLog(); // これでクエリログが残る ////// ここにさまざまなSQL処理を行う。 $logs = DB::getQueryLog(); // クエリログを取得 dd($logs); // ddはブラウザにきれいに出力してくれるvar_dump()
<?php $sql = DB::pretend(function(){ /// ここにさまざまなSQL処理を行う。 }); Log::debug($sql); // ログファイルにデバッグ出力
プレースホルダを使わなくてもいいっていう所が若干楽かも。
やっぱりクエリビルダよりもORM欲しいですよね。
LaravelはFacadeとフォーム周り(特にValidateorとエラー処理)が肌感覚にあうので、あとはこのORMがしっかりしてればもう言うこと無し。
テーブルや主キーについて、以下の設定で作られている。
サンプルのUser.phpは既に対策済みなのでこのエラーが出なかった。
要は、update()やsave()においてHTTPからの入力を許可して良いものと悪いものがあるということか。
正確には、HTTPからというより $model->propertyA みたいに直接プロパティを変更しようとするとダメってことね。
$fillable/$guardedのどちらかを使うとこのエラーは出なくなる。
Model継承クラスに $fillable プロパティを設定し、このプロパティの中に直接プロパティを変更してもよいものを羅列する。 これ以外のプロパティを変更しようとしても変更はできない。
$fillableとどうように$guardedを作り、この中のプロパティは設定しようとしても変更できない。
$guardedと$fillableは同時には使えない。エラーとかにはならない。$guardedが優先されるようだ。
Modelに独自にSetterなどのメソッドを用意してやればいい。
<?php Author::find(1)->delete(); Author::destroy(1); // これは上と全く同じ
Illuminate\Database\Eloquent\SoftDeletesを使う
すると、destroy()やdelete()が論理削除になる。
論理削除すると、Modelのメソッドのfindやallではselectの対象にならない。その他、以下のような特徴がある。
usersテーブルを例にとってやってみる。
./artisan make:migration alter_table_user_add_softdeletes --table=users
できたClassファイルに
<?php use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class AlterTableUserAddSoftdeletes extends Migration { public function up() { Schema::table('users', function (Blueprint $table) { $table->softDeletes()->after('id'); // add (afterは無くても大丈夫) }); } public function down() { Schema::table('users', function (Blueprint $table) { $table->dropSoftDeletes(); // add }); } }
とする。2行加えただけ。 ./artisan migrate で実行。
NamespaceでuseしてTraitでuse。$datesプロパティ設置
<?php use Illuminate\Database\Eloquent\SoftDeletes; class User extends Model { use SoftDeletes; $dates = ['deleted_at']; }
get(set)プロパティ名Attributeがそれにあたる。これは、Modelのset/getマジックメソッドで使っているものでその媒介に使われるもの。
getの場合はAttributeの生値$valueを受けてそれをどう変形するかを記載する。
setの方は、Model::setAttributeを上書きする形で$this->attributesに値を納めるような処理をする。
$datesプロパティを配列にしてにフィールドを入れると、$model->date_at がCarbonクラスのンスタンスになる。$model->created_at,$model->updated_atと同じ扱い。
toArray/toJsonメソッドを使う。1つのModelでも複数のModelでも同じ。
<?php $u = User::find(1)->toArray(); $a = Author::all()->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); dd($u, $a);
toJsonの引数は表示オプション。
ブラックリスト方式 → $hidden にそのProperty名を入れる。
ホワイトリスト方式 → $visibleにそのProperty名を入れる。
リレーションを作りたい場合は、Modelクラスに対応するTableの名称のメソッドを作る。
<?php class Author extends Model { public function phone() { // コード規程に則ったものであれば、author_id, idの2つの引数は不要。 return $this->hasOne(Phone::class, 'author_id', 'id'); }
<?php class Author extends Model { public function books() { // コード規程に則ったものであれば、author_id, idの2つの引数は不要。 return $this->hasMany(Book::class, 'author_id', 'id'); }
メソッドとしてコールした場合、クエリビルダが返ってくるので、こういうこともできる。
<?php Author::find(1)->books()->where('title', 'LIKE', 'Laravel%');
(Properyコールのマジックメソッドとして呼ばれた場合はModelのCollectionが返ってくる。)
phoneテーブルにauthor_idがあり、PhoneオブジェクトからAuthorを取得しようとする場合。
<?php class Phone extends Model { public function author() { // コード規程に則ったものであれば、author_id, idの2つの引数は不要。 return $this->belongsTo(Author::class, 'author_id', 'id');
何故EloquentModelはクラス名を複数形にしたテーブルを自動で見に行けるのか - QiitaでPluralizerクラスがやってるっていうのでそこだけ抜き出して調べてみた。
<?php use Illuminate\Support\Pluralizer; $r = ['fish', 'carp', 'salmon', 'trout', 'sheep', 'cattle', 'deer', 'Japanese', 'Chinese', 'Korean' ]; foreach ($r as $_r){ printf("%s => %s<br>", $_r, Pluralizer::plural($_r)); }
fish => fish carp => carp salmon => salmon trout => trout sheep => sheep cattle => cattles deer => deer Japanese => Japanese Chinesse => Chinesses Korean => Koreans
実態は、vendor/doctrine/inflector/lib/Doctrine/Common/Inflector/Inflector.php らしい。これ見たら沢山単語があった。
ということで、テーブルの複数形が気になったらこのクラスを使って実際に複数形を見てみることが大切なような気がする。
このリレーション先を取得するメソッドを作成し、belongsToManyの第一引数に中間テーブル越しのリレーション先のClassを指定する。
以下は、authors => author_author_type => author_types とRDBMSの存在する際の例。
<?php class Author extends Model { public function types() { return $this->belongsToMany(AuthorType::class); }
中間テーブルにある別のフィールドが欲しい場合は
<?php class Author extends Model { public function types() { return $this->belongsToMany(AuthorType::class)->withPivot('id'); }
とすると、
<?php foreach (Author::find(1)->types as $type){ echo $type->pivot->id ; }
とすることで、中間テーブルのIDが取得できる。
タイムスタンプが欲しい場合は
<?php class Author extends Model { public function types() { return $this->belongsToMany(AuthorType::class)->withTimestamps(); }
とすると、$type->pivot->created_atが使えたりする。
ちなみに、types()メソッドはBuilderを返すので、
<?php Author::find(1)->types()->where('name', 'like', '%i%')->get();
とすることで、AuthorTypesクラスのインスタンスをそのまま配列で受け取れる。
->typesも->types()もどちらもAuthorTypesインスタンスのコレクションを返すようになっている。全く同じであり、typesはtypes()の省略形(最後の->get()が不要)である。
簡単にAuthorTypesの単一フィールドnameだけを一覧で欲しい場合は、
<?php Author::find(1)->types()->where('name', 'like', '%i%')->lists('name');
とするとStringの配列で取得できる。
<?php $book_authors = Author::has('books')->get(); // 3つ以上あるauthorを取得 $book_authors = Author::has('books', '>=', 3)->get();
<?php $php_book_authors = Author::whereHas('books', function($query){ $query->where('title', 'like', '%PHP%'); })->get();
これで作られるSQLは
select * from `authors` where (select count(*) from `books` where `books`.`author_id` = `authors`.`id` and `title` like "%18%") >= 1
使う時はその後foreachでまわすような場合。
<?php Book::with('author')->all();
<?php $pref = Prefecture::with('author', 'author.phone')->where('id', 13)->get(); // これだと動作しない。Builderからwith()は効かない。 // $pref = Prefecture::find(13)->with('author', 'author.phone'); foreach ($pref as $p){ foreach ($p->author as $a){ if ($a->phone){ echo $a->phone->phone_number."<br>";
これで使われるSQLは
select * from `prefectures` where `id` = 13; select * from `authors` where `authors`.`deleted_at` is null and `authors`.`prefecture_id` in (13); select * from `phones` where `phones`.`author_id` in ("1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21");
の3本。author.phoneが無くても動作するが、SQLの発行数が多くなる。
対象となるテーブルの指定を配列にして、'table' => クロージャで指定。
<?php $pref = Prefecture::with(['author' => function($query){ $query->where('name', 'like', '%3'); }], 'author.phone')->where('id', 13)->get();
取得結果に対してloadメソッドを使う。
<?php $pref = Prefecture::where("name", 'LIKE', '%県')->get(); $pref->load('author');
1つのModelのIDに複数のModelがからみ、さらにそのModelに複数のModelが絡んでいる場合。
「(著者の)出身県」→「著者一覧」→「(著者の)本一覧」
というリレーションの場合に、出身県別の本を取得したい場合。
<?php class Prefecture extends Model { public function books() { return $this->hasManyThrough(Book::class, Author::class); // 引数を全て書く場合は下記のとおり。中間テーブルが持つ自分の主キー・最終テーブルが持つ中間テーブルの主キー・自分の主キー // return $this->hasManyThrough(Book::class, Author::class, 'prefecture_id', 'author_id', 'id');
Preferctureから直接Bookを取るメソッドをはやす。このメソッドでは、hasManyThroughを使う。引数の順番は「最終的に取るもの」が最初。
主キーが規則どおりで無い場合は逐一指定する。
親となるIDを含んだまま更新して欲しいということで。
こんな感じ。
<?php $author = new Author(); $author->fill([ 'name' => '伊藤貴史', 'prefecture_id' => 14, 'furigana' => 'フリガナ', 'romaji' => 'itoh takashi', ])->save(); $phone = new Phone(); $phone->fill([ 'phone_number' => '09000000000' ]); $author->phone()->save($phone);
あるいは $author=Author::create(['name'...])で得てもかまわない。
ただ、$phoneは自ら$phone->save()しようとすると、外部キー制約に引っかかりINSERTできない。必ず、$author->phone()からsaveしてやる必要がある。
自分がbelongsしている親を変える場合は、associate()メソッドを使う。
<?php $phone = Phone::find(28); echo $phone->author_id; // 36 $phone->author()->associate(Author::find(35)); echo $phone->author_id; // 35
中間テーブルがある場合、どちら側からにしても
<?php $author = Author::find(1); $type_ids = $author->types()->lists('name'); // 0,1,2,3,4 $author->types()->detach(6); $type_ids = $author->types()->lists('name'); // 0,1,2,3,4,6
逆側から
<?php $type = AuthorType::find(1); $type->authors()->attach(20);
自分がbelongsTo()しているテーブルに対してその通知を記録したい場合
<?php class Phone extends Model { protected $touches = ["author"]; public function author() { $this->belongsTo(Author::class); }
$touchesプロパティに対象(単数形)を入れることで自動的に記録される。
リレーションがある場合、
$model->relation か $model->relation() かどっちを使う?っていう場合。
単純にインスタンスとしてのリレーションが欲しかったら、$model->relationで、relationの先に何かをつなぎたい場合。。。。relationをwhere句で狭めたり、relationのAssosiationIDを変更したり、という場合は $model->relation()を使うって言う感じかな。