Hacking Laravel - Custom Relationships with Eloquent

22
Hacking Laravel Custom relationships with Eloquent Alex Weissman https://chat.userfrosting.com @userfrosting

Transcript of Hacking Laravel - Custom Relationships with Eloquent

Hacking LaravelCustom relationships with Eloquent

Alex Weissmanhttps://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 BroodChamber

Guard

Alice attendant BroodChamber

Feeder

David attendant BroodChamber

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 BelongsToManyrelationship 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 Eloquent\Builder!

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-manyStack 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

Eloquent\Builder::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-manyStack trace time!

BelongsToMany::performJoin

BelongsToMany::addConstraints

Relation::__construct

BelongsToMany::__construct

Model::belongsToMany

Constructing the query

Assembling the Collection

Relation::getEager

BelongsToMany::match

Eloquent\Builder::eagerLoadRelation

Eloquent\Builder::eagerLoadRelations

Eloquent\Builder::get

$workers = Worker::with(‘jobs’)->get();

match

Alice

David

$models

(from main Eloquent\Builder)

row1

$results

(from the joined query in BelongsToMany)

row2

row3

row4

row5

buildDictionary

Alice

David

$models

(from main Eloquent\Builder)

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