KEMBAR78
Hacking Laravel - Custom Relationships with Eloquent | PPTX
Hacking Laravel
Custom relationships with Eloquent
Alex Weissman
https://chat.userfrosting.com
@userfrosting
Basic Relationships
student subject
Alice Freestyling
Alice Beatboxing
David Beatboxing
David Turntabling
name team
Alice London
David Liverpool
Abdullah London
person partner
Alice David
Abdullah Louis
Model::hasOne
Model::hasMany
Model::belongsToMany
Ternary Relationships
Job
Worker
Locationm:m:m
Ternary Relationships
worker job location title
Alice soldier Hatchery Grunt
Alice soldier Brood
Chamber
Guard
Alice attendant Brood
Chamber
Feeder
David attendant Brood
Chamber
Midwife
David attendant Pantry Inspector
[
{
‘name’: ‘Alice’,
‘jobs’: [
{
‘name’: ‘soldier’,
‘locations’: [
‘Hatchery’,
‘Brood Chamber’
]
},
{
‘name’: ‘attendant’,
‘locations’: [
‘Brood Chamber’
]
}
]
},
{
‘name’: ‘David’,
‘jobs’: [
{
‘name’: ‘attendant’,
‘locations’: [
‘Brood Chamber’,
‘Pantry’
]
}
]
}
]
Ternary Relationships
• Why can’t we just model this as two m:m relationships
instead?
• What happens if we try to use a BelongsToMany
relationship on a ternary pivot table?
public function jobs()
{
$this->belongsToMany(EloquentTestJob::class, ’assignments',
'worker_id', 'job_id');
}
…
$worker->jobs()->get();
$worker->load(jobs.locations)->get();
Using Two BelongsToMany
// $worker->jobs()->get();
{
'name': 'soldier'
},
{
'name': 'soldier'
},
{
'name': 'attendant'
}
Using Two BelongsToMany
// $worker->load(jobs.locations)->get();
{
'name': 'soldier',
'locations': {
'Hatchery',
'Brood Chamber'
}
},
{
'name': 'soldier',
'locations': {
'Hatchery',
'Brood Chamber'
}
},
{
'name': 'attendant',
'locations': {
'Brood Chamber',
'Brood Chamber',
'Pantry'
}
}
Using BelongsToTernary
// $worker->jobs()->withTertiary(‘locations’)->get();
{
'name': 'soldier',
'locations': {
'Hatchery',
'Brood Chamber'
}
},
{
'name': 'attendant',
'locations': {
'Brood Chamber’
}
}
Goals
• Understand Eloquent’s Model and query builder classes
• Understand how Eloquent implements database relationships
• Understand how Eloquent solves the N+1 problem
• Implement a basic BelongsToTernary relationship
• Implement eager loading for BelongsToTernary
• Implement loading of the tertiary models as a nested
collection
https://github.com/alexweissman/phpworld2017
Architecture of Eloquent
Retrieving a relation on a single model
$user = User::find(1);
$roles = $user->roles()->get();
$users = User::where(‘active’, ‘1’)
->with(‘roles’)
->get();
Retrieving a relation on a collection of models (eager load)
$users = User::where(‘active’, ‘1’)->get();
$users->load(‘roles’);
get() is a method of Relation!
get() is a method of EloquentBuilder!
Need to override this!
Don’t need to
override this.
Retrieving a relation on a single model
select * from `jobs`
inner join `job_workers`
on `job_workers`.`job_id` = `jobs`.`id`
and `job_workers`.`worker_id` = 1
many-to-many
$user = User::find(1);
$emails = $user->emails()->get();
select * from `emails`
where `user_id` = 1
one-to-many
$worker = Worker::find(1);
$jobs = $worker->jobs()->get();
Retrieving a relation on a single model, many-to-many
Stack trace time!
$worker = Worker::find(1);
$jobs = $worker->jobs()->get();
BelongsToMany::performJoin
BelongsToMany::addConstraints
Relation::__construct
BelongsToMany::__construct
Model::belongsToMany
Constructing the query
Assembling the Collection
EloquentBuilder::getModels
BelongsToMany::get
Retrieving a relation on a collection, many-to-many
select * from `workers`;
select * from `jobs`
inner join `job_workers`
on `job_workers`.`job_id` = `jobs`.`id`
and `job_workers`.`worker_id` in (1,2);
many-to-many
$users = User::with(‘emails’)->get();
select * from `users`;
select * from `emails` where `user_id` in (1,2);
one-to-many
$workers = Worker::with(‘jobs’)->get();
Retrieving a relation on a collection, many-to-many
select * from `workers`;
select * from `jobs`
inner join `job_workers`
on `job_workers`.`job_id` = `jobs`.`id`
and `job_workers`.`worker_id` in (1,2);
many-to-many
$users = User::with(‘emails’)->get();
select * from `users`;
select * from `emails` where `user_id` in (1,2);
one-to-many
$workers = Worker::with(‘jobs’)->get();
solves the n+1
problem!
Retrieving a relation on a collection, many-to-many
Stack trace time!
BelongsToMany::performJoin
BelongsToMany::addConstraints
Relation::__construct
BelongsToMany::__construct
Model::belongsToMany
Constructing the query
Assembling the Collection
Relation::getEager
BelongsToMany::match
EloquentBuilder::eagerLoadRelation
EloquentBuilder::eagerLoadRelations
EloquentBuilder::get
$workers = Worker::with(‘jobs’)->get();
match
Alice
David
$models
(from main EloquentBuilder)
row1
$results
(from the joined query in BelongsToMany)
row2
row3
row4
row5
buildDictionary
Alice
David
$models
(from main EloquentBuilder)
row1
$results
(from the joined query in BelongsToMany)
row2
row3
row4
row5
Task 1
Implement BelongsToTernary::condenseModels,
which collapses these rows into a single model.
For now, don't worry about extracting the
tertiary models (locations) for the sub-
relationship.
Task 2
Modify BelongsToTernary::match, which is
responsible for matching eager-loaded models
to their parents.
Again, we have provided you with the default
implementation from BelongsToMany::match,
but you must modify it to collapse rows with the
same worker_id and job_id (for example) into a
single child model.
Task 3
By default, BelongsToTernary::buildDictionary returns a dictionary that maps parent models to their
children. Modify it so that it also returns a nestedDictionary, which maps parent->child->tertiary
models.
For example:
[
// Worker 1
'1' => [
// Job 3
'3' => [
Location1,
Location2
],
...
],
...
]
You will also need to further modify condenseModels to retrieve the tertiary dictionary and call
matchTertiaryModels to match the tertiary models with each of the child models, if withTertiary is being
used.
Try this at home
BelongsToManyThrough
$user->permissions()->get();
User m:m Role m:m Permission
Full implementations in https://github.com/userfrosting/UserFrosting

Hacking Laravel - Custom Relationships with Eloquent

  • 1.
    Hacking Laravel Custom relationshipswith Eloquent Alex Weissman https://chat.userfrosting.com @userfrosting
  • 2.
    Basic Relationships student subject AliceFreestyling Alice Beatboxing David Beatboxing David Turntabling name team Alice London David Liverpool Abdullah London person partner Alice David Abdullah Louis Model::hasOne Model::hasMany Model::belongsToMany
  • 3.
  • 4.
    Ternary Relationships worker joblocation title Alice soldier Hatchery Grunt Alice soldier Brood Chamber Guard Alice attendant Brood Chamber Feeder David attendant Brood Chamber Midwife David attendant Pantry Inspector [ { ‘name’: ‘Alice’, ‘jobs’: [ { ‘name’: ‘soldier’, ‘locations’: [ ‘Hatchery’, ‘Brood Chamber’ ] }, { ‘name’: ‘attendant’, ‘locations’: [ ‘Brood Chamber’ ] } ] }, { ‘name’: ‘David’, ‘jobs’: [ { ‘name’: ‘attendant’, ‘locations’: [ ‘Brood Chamber’, ‘Pantry’ ] } ] } ]
  • 5.
    Ternary Relationships • Whycan’t we just model this as two m:m relationships instead? • What happens if we try to use a BelongsToMany relationship on a ternary pivot table? public function jobs() { $this->belongsToMany(EloquentTestJob::class, ’assignments', 'worker_id', 'job_id'); } … $worker->jobs()->get(); $worker->load(jobs.locations)->get();
  • 6.
    Using Two BelongsToMany //$worker->jobs()->get(); { 'name': 'soldier' }, { 'name': 'soldier' }, { 'name': 'attendant' }
  • 7.
    Using Two BelongsToMany //$worker->load(jobs.locations)->get(); { 'name': 'soldier', 'locations': { 'Hatchery', 'Brood Chamber' } }, { 'name': 'soldier', 'locations': { 'Hatchery', 'Brood Chamber' } }, { 'name': 'attendant', 'locations': { 'Brood Chamber', 'Brood Chamber', 'Pantry' } }
  • 8.
    Using BelongsToTernary // $worker->jobs()->withTertiary(‘locations’)->get(); { 'name':'soldier', 'locations': { 'Hatchery', 'Brood Chamber' } }, { 'name': 'attendant', 'locations': { 'Brood Chamber’ } }
  • 9.
    Goals • Understand Eloquent’sModel and query builder classes • Understand how Eloquent implements database relationships • Understand how Eloquent solves the N+1 problem • Implement a basic BelongsToTernary relationship • Implement eager loading for BelongsToTernary • Implement loading of the tertiary models as a nested collection https://github.com/alexweissman/phpworld2017
  • 10.
  • 11.
    Retrieving a relationon a single model $user = User::find(1); $roles = $user->roles()->get(); $users = User::where(‘active’, ‘1’) ->with(‘roles’) ->get(); Retrieving a relation on a collection of models (eager load) $users = User::where(‘active’, ‘1’)->get(); $users->load(‘roles’); get() is a method of Relation! get() is a method of EloquentBuilder! Need to override this! Don’t need to override this.
  • 12.
    Retrieving a relationon a single model select * from `jobs` inner join `job_workers` on `job_workers`.`job_id` = `jobs`.`id` and `job_workers`.`worker_id` = 1 many-to-many $user = User::find(1); $emails = $user->emails()->get(); select * from `emails` where `user_id` = 1 one-to-many $worker = Worker::find(1); $jobs = $worker->jobs()->get();
  • 13.
    Retrieving a relationon a single model, many-to-many Stack trace time! $worker = Worker::find(1); $jobs = $worker->jobs()->get(); BelongsToMany::performJoin BelongsToMany::addConstraints Relation::__construct BelongsToMany::__construct Model::belongsToMany Constructing the query Assembling the Collection EloquentBuilder::getModels BelongsToMany::get
  • 14.
    Retrieving a relationon a collection, many-to-many select * from `workers`; select * from `jobs` inner join `job_workers` on `job_workers`.`job_id` = `jobs`.`id` and `job_workers`.`worker_id` in (1,2); many-to-many $users = User::with(‘emails’)->get(); select * from `users`; select * from `emails` where `user_id` in (1,2); one-to-many $workers = Worker::with(‘jobs’)->get();
  • 15.
    Retrieving a relationon a collection, many-to-many select * from `workers`; select * from `jobs` inner join `job_workers` on `job_workers`.`job_id` = `jobs`.`id` and `job_workers`.`worker_id` in (1,2); many-to-many $users = User::with(‘emails’)->get(); select * from `users`; select * from `emails` where `user_id` in (1,2); one-to-many $workers = Worker::with(‘jobs’)->get(); solves the n+1 problem!
  • 16.
    Retrieving a relationon a collection, many-to-many Stack trace time! BelongsToMany::performJoin BelongsToMany::addConstraints Relation::__construct BelongsToMany::__construct Model::belongsToMany Constructing the query Assembling the Collection Relation::getEager BelongsToMany::match EloquentBuilder::eagerLoadRelation EloquentBuilder::eagerLoadRelations EloquentBuilder::get $workers = Worker::with(‘jobs’)->get();
  • 17.
    match Alice David $models (from main EloquentBuilder) row1 $results (fromthe joined query in BelongsToMany) row2 row3 row4 row5
  • 18.
  • 19.
    Task 1 Implement BelongsToTernary::condenseModels, whichcollapses these rows into a single model. For now, don't worry about extracting the tertiary models (locations) for the sub- relationship.
  • 20.
    Task 2 Modify BelongsToTernary::match,which is responsible for matching eager-loaded models to their parents. Again, we have provided you with the default implementation from BelongsToMany::match, but you must modify it to collapse rows with the same worker_id and job_id (for example) into a single child model.
  • 21.
    Task 3 By default,BelongsToTernary::buildDictionary returns a dictionary that maps parent models to their children. Modify it so that it also returns a nestedDictionary, which maps parent->child->tertiary models. For example: [ // Worker 1 '1' => [ // Job 3 '3' => [ Location1, Location2 ], ... ], ... ] You will also need to further modify condenseModels to retrieve the tertiary dictionary and call matchTertiaryModels to match the tertiary models with each of the child models, if withTertiary is being used.
  • 22.
    Try this athome BelongsToManyThrough $user->permissions()->get(); User m:m Role m:m Permission Full implementations in https://github.com/userfrosting/UserFrosting