Keeping Eloquent Eloquent

89
@colindecarlo colindecarlo inking abt tweeting? How abt …. Cannot believe that I am in the same room as @colindecarlo, what a rush!!! #laracon

Transcript of Keeping Eloquent Eloquent

Page 1: Keeping Eloquent Eloquent

@colindecarlo colindecarlo

Thinking about tweeting?

How about ….

Cannot believe that I am in the same room as @colindecarlo, what a rush!!!

#laracon

Page 2: Keeping Eloquent Eloquent

Hi!

@colindecarlo colindecarlo

Colin DeCarloSoftware DeveloperPHP & JavaScriptLocal Meetup Organizer

Hi, I’m Colin and this is a picture of me. I make my living as a software developer primarily working with PHP and JavaScript. For the last 4 years or so I’ve also been organizing a local php meetup. On twitter and github, I’m colindecarlo and I would love it if you decided to follow me.

Page 3: Keeping Eloquent Eloquent

@colindecarlo colindecarlo

I work with a very talented group of people at Vehikl in Waterloo, Ontario, Canada. Vehikl is a software consultancy and we build custom applications primarily using Laravel as our backend PHP Framework and mix of Angular and React on the frontend.

Page 4: Keeping Eloquent Eloquent

Ok, I’ve never been in front of such a big group before so I’d like to try an experiment. Is that ok?

I’d like you to close your eyes, seriously, close your eyes.

Alright, now I want you to think about yourself, specifically, your profession, what you do for a living. Got it?

Now, what I’d like you to do is picture yourself without that profession, it just doesn’t exist anymore and you’ve got a void in your life that you need to fill. What are you going to fill it with? If you weren’t able to do what you’re doing right now, what sort of profession would you have? Think about that, I’ve give you a bit to sort it out.

Ok, you can open your eyes, thanks.

By a show of hands, who actually participated and thought about their alternate life?

Alright great, now of those people, whose alternate job was in a creative field? Like maybe you saw yourself working in a hacklab, or a machine shop or maybe in a marketing/advertising type profession.

Wow, great.

So, that’s pretty much my hypothesis, as a group, we chose development as an outlet for our creativity, and if we didn’t have that outlet, we’d find a different one that allows use to exercise our creative drive.

Page 5: Keeping Eloquent Eloquent

I love well made, hand crafted furniture. Specifically, wooden furniture but I don’t discriminate too much. If you want to upholster a bench, I’m not going to hate, I can appreciate that sort of thing, but for me, wood furniture is where is my heart is.

There’s just something about it that’s a little hard to explain. I just feel like there is something deep and honest about the entire process. From starting with an image in your head to choosing the right wood use and working it meticulously over time with a huge assortment of specialized tools until finally you’re done and you’ve got more than just a piece of furniture, you’ve got a story to go with it, I don’t know, it’s romantic.

So, I’ve poured my heart out and you’ve got an idea of who I am. And after hearing me describe my love for furniture you might expect my house to be full of wondrous story-filled furniture and that I’ve got a woodshop where all this is made, but you’d be wrong.

Page 6: Keeping Eloquent Eloquent

This is where I buy pretty much all of the furnishings for my house. I have never made a single bench, cabinet, armoire, coffee table, anything. I’ve assembled most of those things mind you, buy I’ve never built them.

You can attribute the reason for that to many things I’m sure, but I like to say the primary reason is that the furniture I’m describing is built in a place like this.

Page 7: Keeping Eloquent Eloquent

And I don’t have one of these.

Page 8: Keeping Eloquent Eloquent

I have this.

And let’s say that tomorrow some freaky friday thing happened to me and I woke up in the morning and I wasn’t a software developer anymore. I walked out of my house and in my backyard there is a shop and I open the doors to the shop

Page 9: Keeping Eloquent Eloquent

And I walk into this.

Do you think I’m going to be able to build gorgeous wood furniture?

No, of course not. I’m going to try, but most likely, I’m going to build some pretty janky things. My tables will “work” but they may wobble a little, the chairs are going to be uncomfortable and the doors on the cabinets are either going to to not shut at all or have huge gaps around them.

Even if this was my reality, it’s going to take a lot of practice and a lot of mistakes before I have the experience required to build beautiful wood furniture.

Page 10: Keeping Eloquent Eloquent

BRING IT HOME DECARLO

Page 11: Keeping Eloquent Eloquent

Having the correct tools

is not the same as

using the tools correctly.

@colindecarlo colindecarlo

So, where am I going with all this?

(pause)

As developers working with Laravel, we have been given that woodshop to work with.

Laravel comes with a huge assortment of awesome tools that we can use to build great applications. But just because we have all the tools doesn’t mean our first project is going to be a work of art.

It’ll work but there’s going to be some dark corners in there. And that’s going to be caused by the same reason my first coffee table would have wobbled, we need practice with the tools before we’re going to be able to use them well.

Eloquent is no exception here and that’s the tool I’d like to focus on over the next while. I’m going to touch on a few trouble areas that I’ve personally ran into and how I’ve learned to use Eloquent more effectively.

Page 12: Keeping Eloquent Eloquent

Keeping Eloquent EloquentLaracon 2016

@colindecarlo colindecarlo

Page 13: Keeping Eloquent Eloquent

Model Defaults

@colindecarlo colindecarlo

Why would having sane default attributes on your model be so important?

For me, it’s a matter of eliminating an entire set of possible error conditions my app could run into. I like to program confidently as opposed to defensively, which means that any time a method in my application receives a model to work with, I presume that the model is in a valid state and ready to go. I don’t want to have to ask the model if it’s ready and I don’t want the model to be a state that doesn’t make sense to the domain of my application.

I also don’t want to have to be burdened with passing the same default values into the model’s constructor time and time again on every instantiation.

Having default model attributes allows you to declutter your code and focus on the data that really matters at the time you build your models and, at the same time, guarantees you’ll have valid objects coming back to you.

So how are we going to implement default attributes for our models?

Page 14: Keeping Eloquent Eloquent

use Illuminate\Database\Schema\Blueprint;

use Illuminate\Database\Migrations\Migration;

class CreatePostsTable extends Migration

{

public function up()

{

Schema::create('posts', function (Blueprint $table) {

$table->increments('id');

$table->string('title');

$table->text('body')->allowNull();

$table->boolean('published')->default(false);

$table->timestamps();

});

}

public function down()

{

Schema::drop('posts');

}

}

The first solution I thought of was storing the default values in the database table schema.

But there’s a serious problem with relying on this method, can you think of what it is?

The problem is that the default values are set *in the database record*, but they aren’t set in the newly instantiated model. The values that I was allowing the database to set for me were set to null in the model I just created and this lead to some pretty hard to find bugs.

Page 15: Keeping Eloquent Eloquent

<?php

use App\Post;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider

{

public function boot()

{

Post::saved(function ($post) {

$saved = Post::find($post->id);

$post->forceFill($saved->getAttributes());

});

}

public function register() {}

}

So I fixed it … with duct tape.

In my app service provider I created an observer like this that hooked into the “saved” event that the model fires after it’s been persisted to the database.

You can see what I’m doing here is going back to the database every time a model is saved and effectively refreshing it.

If you’re counting, that means that every write to my database triggered a read as well. Now, I’m no database expert but I’m pretty sure that’s not a solution that scales very well.

Page 16: Keeping Eloquent Eloquent

<?php

namespace App;

use Illuminate\Database\Eloquent\Model

class Post extends Model

{

protected $defaults = [

'published' => false,

];

public function __construct(array $attributes = [])

{

$attributes = array_merge($this->defaults, $attributes);

parent::__construct($attributes);

}

}

So I soon considered an alternative approach, similar to this.

On each model in my application I created a `$defaults` property which was an associative array storing the default values I wanted that model to be instantiated with.

Then each time a model was instantiated I would merge the attributes passed into the constructor with the model defaults and call the parent constructor with the merged data.

And this works, it works quite well actually.

Page 17: Keeping Eloquent Eloquent

but it’s kinda like this.

This chair/couch thing works, but it doesn’t feel right and I don’t really want it in my house.

Page 18: Keeping Eloquent Eloquent

<?php

namespace App;

use Illuminate\Database\Eloquent\Model

class Post extends Model

{

protected $defaults = [

'published' => false,

];

public function __construct(array $attributes = [])

{

$attributes = array_merge($this->defaults, $attributes);

parent::__construct($attributes);

}

}

The thing that didn’t sit well with me was that just to support default values, I had to create this whole extra property on the class and then override the constructor function of a core element of the framework … in every class that I wanted to have defaults.

That just couldn’t be right and I needed to find the right way to do it.

Page 19: Keeping Eloquent Eloquent

Requirements

• Access to default values after instantiation

• Persist default values to the database on creation

@colindecarlo colindecarlo

I decided to tackle this just like any other problem I face as a developer.

I wrote down my requirements, stared at them for a few minutes and then went for a walk to clear my head.

When I came back I was sure I had the proper solution but it was so simple I was also certain there something wrong with it.

Page 20: Keeping Eloquent Eloquent

<?php

namespace App;

use Illuminate\Database\Eloquent\Model

class Post extends Model

{

protected $attributes = [

'published' => false,

];

}

If in my previous solution I was merging the attributes used to create the model with my defaults array, why shouldn’t I be able to just set the default values in the `$attributes` array that Eloquent uses internally to store this data?

Could it be that simple?

The docs didn’t say anything about it …

Page 21: Keeping Eloquent Eloquent

so i was forced to use the source.

Page 22: Keeping Eloquent Eloquent

/** * Indicates if all mass assignment is enabled. * * @var bool */ protected static $unguarded = false;

/** * The cache of the mutated attributes for each class. * * @var array */ protected static $mutatorCache = [];

/** * The many to many relationship methods. * * @var array */ public static $manyMethods = ['belongsToMany', 'morphToMany', 'morphedByMany'];

/** * The name of the "created at" column. * * @var string */ const CREATED_AT = 'created_at';

/** * The name of the "updated at" column. * * @var string */ const UPDATED_AT = 'updated_at';

/** * Create a new Eloquent model instance. * * @param array $attributes * @return void */ public function __construct(array $attributes = []) { $this->bootIfNotBooted();

$this->syncOriginal();

$this->fill($attributes); }

/** * Check if the model needs to be booted and if so, do it. * * @return void */ protected function bootIfNotBooted() { if (! isset(static::$booted[static::class])) { static::$booted[static::class] = true;

$this->fireModelEvent('booting', false);

static::boot();

$this->fireModelEvent('booted', false); } }

/** * The "booting" method of the model. * * @return void */ protected static function boot() { static::bootTraits(); }

/** * Boot all of the bootable traits on the model. * * @return void */ protected static function bootTraits() { $class = static::class;

foreach (class_uses_recursive($class) as $trait) { if (method_exists($class, $method = 'boot'.class_basename($trait))) { forward_static_call([$class, $method]); } } }

/** * Clear the list of booted models so they will be re-booted. * * @return void */ public static function clearBootedModels() { static::$booted = []; static::$globalScopes = []; }

/** * Register a new global scope on the model. * * @param \Illuminate\Database\Eloquent\Scope|\Closure|string $scope * @param \Closure|null $implementation * @return mixed * * @throws \InvalidArgumentException */ public static function addGlobalScope($scope, Closure $implementation = null) { if (is_string($scope) && $implementation !== null) { return static::

$localKey); }

/** * Define a polymorphic one-to-one relationship. * * @param string $related * @param string $name * @param string $type * @param string $id * @param string $localKey * @return \Illuminate\Database\Eloquent\Relations\MorphOne */ public function morphOne($related, $name, $type = null, $id = null, $localKey = null) { $instance = new $related;

list($type, $id) = $this->getMorphs($name, $type, $id);

$table = $instance->getTable();

$localKey = $localKey ?: $this->getKeyName();

return new MorphOne($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $localKey); }

/** * Define an inverse one-to-one or many relationship. * * @param string $related * @param string $foreignKey * @param string $otherKey * @param string $relation * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function belongsTo($related, $foreignKey = null, $otherKey = null, $relation = null) { // If no relation name was given, we will use this debug backtrace to extract // the calling method's name and use that as the relationship name as most // of the time this will be what we desire to use for the relationships. if (is_null($relation)) { list($current, $caller) = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);

$relation = $caller['function']; }

// If no foreign key was supplied, we can use a backtrace to guess the proper // foreign key name by using the name of the relationship function, which // when combined with an "_id" should conventionally match the columns. if (is_null($foreignKey)) { $foreignKey = Str::snake($relation).'_id'; }

$instance = new $related;

// Once we have the foreign key names, we'll just create a new Eloquent query // for the related models and returns the relationship instance which will // actually be responsible for retrieving and hydrating every relations. $query = $instance->newQuery();

$otherKey = $otherKey ?: $instance->getKeyName();

return new BelongsTo($query, $this, $foreignKey, $otherKey, $relation); }

/** * Define a polymorphic, inverse one-to-one or many relationship. * * @param string $name * @param string $type * @param string $id * @return \Illuminate\Database\Eloquent\Relations\MorphTo */ public function morphTo($name = null, $type = null, $id = null) { // If no name is provided, we will use the backtrace to get the function name // since that is most likely the name of the polymorphic interface. We can // use that to get both the class and foreign key that will be utilized. if (is_null($name)) { list($current, $caller) = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);

$name = Str::snake($caller['function']); }

list($type, $id) = $this->getMorphs($name, $type, $id);

// If the type value is null it is probably safe to assume we're eager loading // the relationship. When that is the case we will pass in a dummy query as // there are multiple types in the morph and we can't use single queries. if (empty($class = $this->$type)) { return new MorphTo( $this->newQuery(), $this, $id, null, $type, $name ); }

// If we are not eager loading the relationship we will essentially treat this // as a belongs-to style relationship since

*/ public static function updating($callback, $priority = 0) { static::registerModelEvent('updating', $callback, $priority); }

/** * Register an updated model event with the dispatcher. * * @param \Closure|string $callback * @param int $priority * @return void */ public static function updated($callback, $priority = 0) { static::registerModelEvent('updated', $callback, $priority); }

/** * Register a creating model event with the dispatcher. * * @param \Closure|string $callback * @param int $priority * @return void */ public static function creating($callback, $priority = 0) { static::registerModelEvent('creating', $callback, $priority); }

/** * Register a created model event with the dispatcher. * * @param \Closure|string $callback * @param int $priority * @return void */ public static function created($callback, $priority = 0) { static::registerModelEvent('created', $callback, $priority); }

/** * Register a deleting model event with the dispatcher. * * @param \Closure|string $callback * @param int $priority * @return void */ public static function deleting($callback, $priority = 0) { static::registerModelEvent('deleting', $callback, $priority); }

/** * Register a deleted model event with the dispatcher. * * @param \Closure|string $callback * @param int $priority * @return void */ public static function deleted($callback, $priority = 0) { static::registerModelEvent('deleted', $callback, $priority); }

/** * Remove all of the event listeners for the model. * * @return void */ public static function flushEventListeners() { if (! isset(static::$dispatcher)) { return; }

$instance = new static;

foreach ($instance->getObservableEvents() as $event) { static::$dispatcher->forget("eloquent.{$event}: ".static::class); } }

/** * Register a model event with the dispatcher. * * @param string $event * @param \Closure|string $callback * @param int $priority * @return void */ protected static function registerModelEvent($event, $callback, $priority = 0) { if (isset(static::$dispatcher)) { $name = static::class;

static::$dispatcher->listen("eloquent.{$event}: {$name}", $callback, $priority); } }

/** * Get the observable event names. * * @return array */ public function getObservableEvents() {

// We will append the names of the class to the event to distinguish it from // other model events that are fired, allowing us to listen on each model // event set individually instead of catching event for all the models. $event = "eloquent.{$event}: ".static::class;

$method = $halt ? 'until' : 'fire';

return static::$dispatcher->$method($event, $this); }

/** * Set the keys for a save update query. * * @param \Illuminate\Database\Eloquent\Builder $query * @return \Illuminate\Database\Eloquent\Builder */ protected function setKeysForSaveQuery(Builder $query) { $query->where($this->getKeyName(), '=', $this->getKeyForSaveQuery());

return $query; }

/** * Get the primary key value for a save query. * * @return mixed */ protected function getKeyForSaveQuery() { if (isset($this->original[$this->getKeyName()])) { return $this->original[$this->getKeyName()]; }

return $this->getAttribute($this->getKeyName()); }

/** * Update the model's update timestamp. * * @return bool */ public function touch() { if (! $this->timestamps) { return false; }

$this->updateTimestamps();

return $this->save(); }

/** * Update the creation and update timestamps. * * @return void */ protected function updateTimestamps() { $time = $this->freshTimestamp();

if (! $this->isDirty(static::UPDATED_AT)) { $this->setUpdatedAt($time); }

if (! $this->exists && ! $this->isDirty(static::CREATED_AT)) { $this->setCreatedAt($time); } }

/** * Set the value of the "created at" attribute. * * @param mixed $value * @return $this */ public function setCreatedAt($value) { $this->{static::CREATED_AT} = $value;

return $this; }

/** * Set the value of the "updated at" attribute. * * @param mixed $value * @return $this */ public function setUpdatedAt($value) { $this->{static::UPDATED_AT} = $value;

return $this; }

/** * Get the name of the "created at" column. * * @return string */ public function getCreatedAtColumn() { return static::CREATED_AT; }

/** * Get the name of the "updated at" column. * * @return string */ public function getUpdatedAtColumn() { return static::UPDATED_AT; }

/**

*/ public function fillable(array $fillable) { $this->fillable = $fillable;

return $this; }

/** * Get the guarded attributes for the model. * * @return array */ public function getGuarded() { return $this->guarded; }

/** * Set the guarded attributes for the model. * * @param array $guarded * @return $this */ public function guard(array $guarded) { $this->guarded = $guarded;

return $this; }

/** * Disable all mass assignable restrictions. * * @param bool $state * @return void */ public static function unguard($state = true) { static::$unguarded = $state; }

/** * Enable the mass assignment restrictions. * * @return void */ public static function reguard() { static::$unguarded = false; }

/** * Determine if current state is "unguarded". * * @return bool */ public static function isUnguarded() { return static::$unguarded; }

/** * Run the given callable while being unguarded. * * @param callable $callback * @return mixed */ public static function unguarded(callable $callback) { if (static::$unguarded) { return $callback(); }

static::unguard();

try { return $callback(); } finally { static::reguard(); } }

/** * Determine if the given attribute may be mass assigned. * * @param string $key * @return bool */ public function isFillable($key) { if (static::$unguarded) { return true; }

// If the key is in the "fillable" array, we can of course assume that it's // a fillable attribute. Otherwise, we will check the guarded array when // we need to determine if the attribute is black-listed on the model. if (in_array($key, $this->getFillable())) { return true; }

if ($this->isGuarded($key)) { return false; }

return empty($this->getFillable()) && ! Str::startsWith($key, '_'); }

/** * Determine if the given key is guarded. * * @param string $key * @return bool */ public function isGuarded($key) { return in_array($key, $this->getGuarded()) || $this->getGuarded() == ['*']; }

/** * Determine if the model is totally guarded. *

And here it is, the source to Model.php, in all it’s 3541 lines of glory.

It’s pretty intimidating the first time you crack it open but we’re determined devs so lets sort this out. Our first requirement is that the default attribute is available after the model is instantiated.

When we access attributes on our Eloquent models we treat them as if they were public properties, but of course they’re not actual public properties so the magic __get method is called, lets start there.

Page 23: Keeping Eloquent Eloquent

}

}

static::$mutatorCache[$class] = $mutatedAttributes;

}

/**

* Dynamically retrieve attributes on the model.

*

* @param string $key

* @return mixed

*/

public function __get($key)

{

return $this->getAttribute($key);

}

/**

* Dynamically set attributes on the model.

*

* @param string $key

* @param mixed $value

* @return void

*/

public function __set($key, $value)

{

$this->setAttribute($key, $value);

$attributes = [‘published’ => false]$key: ‘published’return value: false

The first thing that Eloquent wants to do is take control back from PHP so __get is just a proxy to the getAttribute method and we’re passing it the key we want to access on the model which is ‘published’

Onto getAttribute, the first we’re doing here is checking if the published key exists in the attributes array and it does so we’re going to call ‘getAttributeValue’ passing it the key ‘published’

‘getAttributeValue’ finally resolves a value by calling the ‘getAttributeFromArray’

‘getAttributeFromArray’ does one final check to see if the ‘published’ still exists in the attributes array which it still does so we pull out that value out as false and return it back up the chain to ‘getAttributeValue’

back in ‘getAttributeValue’ we check to see if published has a get mutator which it doesn’t, we check if there is a cast which there isn’t and it’s not a date either so our value ‘false’ is returned back to ‘getAttribute’ which promptly returns to __get and back to our caller.

And sure enough, the default values we set in the `$attributes` array are available as soon as the model is instantiated. That satisfies our first requirement.

Our second requirement was that the default values are persisted to the database when the model is created. In the interest of time, I’ll spare you the gory details and just tell you that it works. You’re just going to have to believe me, or, you could use the source yourself to find out.

Page 24: Keeping Eloquent Eloquent

Use the source yourself to find out.

Page 25: Keeping Eloquent Eloquent

<?php

namespace App;

use Illuminate\Database\Eloquent\Model

class Post extends Model

{

protected $attributes = [

'published' => false,

];

}

But in the end, all we have to do to support default model attributes is set them in the models own $attributes array.

That’s a huge value that goes a long way to eliminating bugs and cleaning up our code.

Page 26: Keeping Eloquent Eloquent

@colindecarlo colindecarlo

Thinking about tweeting?

How about ….

Don’t be afraid to use the source

#laracon

Page 27: Keeping Eloquent Eloquent

Meaningful Relations

@colindecarlo colindecarlo

Quick question.

By a show of hands, how many people here know exactly how Eloquent Relations work?

Ok, now, of the people the that raised their hands, how many of you know exactly how Eloquent Relations work?

If you didn’t see it, Taylor didn’t have his hand up.

Page 28: Keeping Eloquent Eloquent

Relations

• Lazy access

• Eager loading

• Creating

• Saving

@colindecarlo colindecarlo

The reason I asked the question is because, in my opinion, Eloquent Relations are what really unleash the power of the ORM and they do it using such a simple API that it almost seems like magic.

Think about it, Relations either directly enable or assist in most of the things that make Eloquent as good as it is.

Eloquent models have lazy access to their related models which is made possible by the Relation class.

If you’re pulling multiple models out of the database you can eager load their related models avoiding the dreaded N+1 query problem, again, made possible by the Relation class.

Finally, the Relation classes even abstract away the details of linking related models in your database when you’re creating or saving them.

Page 29: Keeping Eloquent Eloquent

JUST WORKS

How many times have you been writing some code dealing with Eloquent Relations and went ‘wow, that just worked’. It’s pretty great feeling but at the same time it’s a double edged sword. Saying “that just worked” is pretty much the same thing as saying “I don’t know how this works” and that’s where we can get into trouble. Because if you don’t know how something works it might as well be magic and we’ve all learned that you don’t mess with magic.

So we train ourselves to write our model relations exactly as how we see them in the documentation. But we’re developers and we don’t have all day to read all the documentation, we’ve got clients breathing down our necks and features to deliver, so we skim.

And we find the first piece of code that looks like what we want and we copy it and we paste it. We change the relation name and the related class name and bingo bango “Wow, that just works!”

And it continues to just work until we get a new requirement from our client that changes how we need to load the related models and we’re back at square one. So we skim, we copy, we paste, it works, amazing!

But is it? Let’s see an example of what I’m talking about and see how we can make it better.

Page 30: Keeping Eloquent Eloquent

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model { public function comments() { return $this->hasMany(Comment::class); } }

Here we are in our Post model. We’ve set up a relation method called ‘comments’ which is also the name of the database table we’re pulling the related data from. The method defines a hasMany relationship between a Post model and many Comment models.

Page 31: Keeping Eloquent Eloquent

<?php

namespace App\Http\Controllers;

use App\Post; use App\Http\Controllers\Controller;

class PostCommentsController extends Controller { public function index($id) { $post = Post::with('comments')->findOrFail($id); return response()->json(['comments' => $post->comments]); } }

Our app serves a RESTful API to our frontend, so here is our PostComments Controller.

Our index action accepts the id of the post whose comments we want and the with method of the Eloquent Builder class makes it really easy to grab those comments by eager loading them with the post itself.

Page 32: Keeping Eloquent Eloquent

<?php

namespace App\Http\Controllers;

use App\Post; use Carbon\Carbon; use App\Http\Controllers\Controller;

class PostRecentCommentsController extends Controller { public function index($id) { $post = Post::with([ 'comments' => function ($query) { return $query->where('created_at', '>', Carbon::yesterday()); } ])->findOrFail($id);

return response()->json(['comments' => $post->comments]); } }

A new requirement comes in from the client. Some Posts are very popular and they generate a lot of comments, so now all we want to do is load the most recent comments for the Post first. We create a new controller to handle these requests and call it the PostRecentComments controller.

We still want to eager load the comments though and when we were skimming the docs we remember seeing that you can constrain eager loads by passing the `with` method an associative array where the key is the relation we want loaded and the value is some anonymous function that accepts the query used to load the relation. We can add conditions to the query which constrain the result set.

Hey, that just works.

Page 33: Keeping Eloquent Eloquent

This is how I feel right now.

I have done something wrong.

I have created something that should never exist.

Page 34: Keeping Eloquent Eloquent

I can do better.

@colindecarlo colindecarlo

I can do better than this.

In my heart of hearts, I know I can make that code beautiful again.

Let’s go.

Page 35: Keeping Eloquent Eloquent

<?php

namespace App\Http\Controllers;

use App\Post; use Carbon\Carbon; use App\Http\Controllers\Controller;

class PostRecentCommentsController extends Controller { public function index($id) { $post = Post::with([ 'comments' => function ($query) { return $query->where('created_at', '>', Carbon::yesterday()); } ])->findOrFail($id);

return response()->json(['comments' => $post->comments]); } }

Page 36: Keeping Eloquent Eloquent

<?php

namespace App;

use Carbon\Carbon; use Illuminate\Database\Eloquent\Model;

class Post extends Model { public function comments() { return $this->hasMany(Comment::class); }

public function recentComments() { return $this->hasMany(Comment::class) ->where('created_at', '>', Carbon::yesterday()); } }

Page 37: Keeping Eloquent Eloquent

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model { public function scopeSince($query, $since) { return $query->where('created_at', '>', $since); } }

Page 38: Keeping Eloquent Eloquent

<?php

namespace App;

use Carbon\Carbon; use Illuminate\Database\Eloquent\Model;

class Post extends Model { public function comments() { return $this->hasMany(Comment::class); }

public function recentComments() { return $this->hasMany(Comment::class) ->since(Carbon::yesterday()); } }

Page 39: Keeping Eloquent Eloquent

<?php

namespace App;

use Carbon\Carbon; use Illuminate\Database\Eloquent\Model;

class Comment extends Model { public function scopeSince($query, $since) { return $query->where('created_at', '>', $since); }

public function scopeSinceYesterday($query) { return $this->scopeSince($query, Carbon::yesterday()); } }

Page 40: Keeping Eloquent Eloquent

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model { public function comments() { return $this->hasMany(Comment::class); }

public function recentComments() { return $this->hasMany(Comment::class)->sinceYesterday(); } }

Page 41: Keeping Eloquent Eloquent

<?php

namespace App\Http\Controllers;

use App\Post; use App\Http\Controllers\Controller;

class PostRecentCommentsController extends Controller { public function index($id) { $post = Post::with('recentComments')->findOrFail($id); return response()->json(['comments' => $post->recentComments]); } }

Page 42: Keeping Eloquent Eloquent

@colindecarlo colindecarlo

Thinking about tweeting?

How about ….

Having more meaningful relations just might change my life.

#laracon

I really want this one

Page 43: Keeping Eloquent Eloquent

Create Using Relations

@colindecarlo colindecarlo

I want to stay on relations for just a few more minutes and talk about simple yet, from what I’ve seen, strangely under used ability of the relation class.

Specifically, what I’m talking about is creating models which “belong to” other models.

Look at this example

Page 44: Keeping Eloquent Eloquent

<?php

namespace App\Http\Controllers;

use App\Post; use App\Comment; use App\Http\Controllers\Controller;

class PostCommentsController extends Controller { public function store(Request $request, Post $post) { $comment = Comment::create([ 'post_id' => $post->id, 'content' => $request->input('comment_content'), 'approved' => false, ]); return response()->json(['id' => $comment->id], 201); } }

Here we are in PostCommentsController creating a comment for a Post.

Eloquent makes this really easy for us, we’ve got the Post given to us by our route model binding and creating the Comment is as simple calling the static create method on Comment with the attributes we want.

Even though it’s really simple to do this, we shouldn’t be creating the comment in this way.

Page 45: Keeping Eloquent Eloquent

<?php

namespace App\Http\Controllers;

use App\Post; use App\Http\Controllers\Controller;

class PostCommentsController extends Controller { public function store(Request $request, Post $post) { $comment = $post->comments()->create([ 'content' => $request->input('comment_content'), 'approved' => false, ]); return response()->json(['id' => $comment->id], 201); } }

We should be doing it like this, by calling the create method on the post models comments relation.

Page 46: Keeping Eloquent Eloquent

Fight!

$comment = $post->comments()->create([ 'content' => $request->input('comment_content'), 'approved' => false, ]);

$comment = Comment::create([ 'post_id' => $post->id, 'content' => $request->input('comment_content'), 'approved' => false, ]);

vs.

So why do I think this difference is so important?

I mean, in the end the result is the same right? And if the result is the same, aren’t I just arguing over preferences?

The reason this difference is so important is because, in the top example, we’re circumventing the abstraction over the data layer that Eloquent is working to provide to us.

It’s happening right here.

We’re referring to the posts internal identity and this isn’t a detail we should be concerned with. We’re thinking past the abstraction to the underlying implementation.

Page 47: Keeping Eloquent Eloquent

Your abstraction is leaking

@colindecarlo colindecarlo

Your abstraction is leaking.

Let’s start treating foreign key values more like toxic entities. We shouldn’t be handling them ourselves, they’re bad for us.

Instead we need to let Eloquent manage these details, after all, that’s what it’s there for.

Page 48: Keeping Eloquent Eloquent

<?php

namespace App\Http\Controllers;

use App\Post; use App\Http\Controllers\Controller;

class PostCommentsController extends Controller { public function store(Request $request, Post $post) { $comment = $post->comments()->create([ 'content' => $request->input('comment_content'), 'approved' => false, ]); return response()->json(['id' => $comment->id], 201); } }

But we’re not done refactoring this controller just yet.

There is another code smell lingering in here, can you spot it?

It’s here.

I’m going to be honest and let you in on a secret because it’s just us here, when I put these slides together, I called this smell Feature Envy and then later changed it to ‘Inappropriate Intimacy’ but after Sandi’s talk yesterday I really think it might be “Message Chains”. Whatever it’s called though I think we can agree that it stinks and we should fix it.

What’s happening is that this controller wants to create a new comment belonging to this post and it knows that there is a ‘create’ method on post’s comments relation that it can use to accomplish that.

The controller is reaching through posts to the comments relation so that it can create the comment itself instead of allowing the post to manage it’s own comments.

Page 49: Keeping Eloquent Eloquent

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model { public function comments() { return $this->hasMany(Comment::class); }

public function newComment($attributes) { return $this->comments()->create($attributes); } }

What we ought to have is a method on out Post model responsible for creating new comments. Since Comments belong to Posts, Posts should have the responsibility of managing them.

Page 50: Keeping Eloquent Eloquent

<?php

namespace App\Http\Controllers;

use App\Post; use App\Http\Controllers\Controller;

class PostCommentsController extends Controller { public function store(Request $request, Post $post) { $comment = $post->newComment([ 'content' => $request->input('comment_content'), 'approved' => false, ]); return response()->json(['id' => $comment->id], 201); } }

Here’s our controller again and now we’re going through the proper channels to create comments.

Everything is good in world.

Page 51: Keeping Eloquent Eloquent

–Everyone

“My use case is different"

@colindecarlo colindecarlo

What’s that, sorry?

Your use case it different. Gotcha.

Page 52: Keeping Eloquent Eloquent

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model { protected $attributes = [ 'approved' => false, ];

public function post() { return $this->belongsTo(Post::class); }

public function author() { return $this->belongsTo(User::class); } }

So comments have authors too and when a comment is made we have to relate the comment to both the post and the author.

I can see how this may present itself as a problem. We’re creating the comment directly with the `create` method on the comments relation and it’s the relation which is setting the post_id on the comment. Now, both the post_id and the author_id on the comment have to be set before the comment is created but we can’t create the comment using both relations.

How do we do it?

Page 53: Keeping Eloquent Eloquent

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model { public function comments() { return $this->hasMany(Comment::class); }

public function newComment($author, $attributes) { $comment = new Comment($attributes); $comment->author()->associate($author);

return $this->comments()->save($comment); } }

If the requirement of our domain is that all comments are associated to Posts and Authors then we’re going to have to change the data that gets passed into our newComment method.

Here you can see we added an ‘author’ parameter which is a User model.

Now, instead of using the `create` method on the comments relation to both instantiate the comment and save it to the database, we’re going to have to create a new instance of Comment ourselves so that we can tell it who the author is.

Once we’ve got the author associated to the comment, we can pass it to the save method of the comments relation which will persist the comment down to the database for us.

This method has a bit of a smell to it now though doesn’t it?

There’s that message chain again.

Page 54: Keeping Eloquent Eloquent

class Comment extends Model { protected $attributes = [ 'approved' => false, ];

public function post() { return $this->belongsTo(Post::class); }

public function author() { return $this->belongsTo(User::class); }

public function byAuthor($author) { return $this->author()->associate($author); } }

Let’s take care of that by adding a ‘byAuthor’ method to our Comment model that the Post can pass the author to.

Page 55: Keeping Eloquent Eloquent

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model { public function comments() { return $this->hasMany(Comment::class); }

public function newComment($author, $attributes) { return $this->comments()->save( (new Comment($attributes))->byAuthor($author) ); } }

Now, back in the newComment method of the Post model we don’t have any smells and everything is in its proper place.

Page 56: Keeping Eloquent Eloquent

@colindecarlo colindecarlo

Thinking about tweeting?

How about ….

Just created a “belongs to” relationship with a Pikachu hanging out on the #Laracon stage.

#gotEm #noRest

Page 57: Keeping Eloquent Eloquent

Custom Collections

@colindecarlo colindecarlo

Page 58: Keeping Eloquent Eloquent

So I’m sure by now you’ve all heard the good news and if you were here for Adam’s talk this morning you might even be more stoked to get back to your project and rid your code of loops and temporary variables through the wonders of Collection pipelines.

I’m with you, I love really enjoy working with collection pipelines.

Page 59: Keeping Eloquent Eloquent

In fact, I even wrote my own collection library.

It’s super popular.

Jokes aside though, I really do think that collection pipelines can improve the way you think about the data in your application.

They’re a great way for a developer to isolate each action that they want to perform on a set of data and then chain those actions together into one complete operation.

If you think about it, these actions linked together to perform a single operation are a lot like a group of sentences, each presenting a complete thought, brought together to form a paragraph.

BUT

Page 60: Keeping Eloquent Eloquent

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;

class PostMetricsEngagementController extends Controller { public function index(Post $post) { $trend = $post->comments->groupBy(function ($comment) use ($post) { return $comment->created_at->diffInWeeks($post->published_at); })->flatMap(function ($group) { return $group->count(); })->eachCons(2)->map(function ($pair) { return $pair->last() - $pair->first(); })->toBase();

return response()->json(['metrics' => $trend]); } }

Have you ever had to open up a controller for one reason or another just to be hit in face with something like

*THIS*

It can be a little jarring to say the least. I wrote this code to prepare for this talk, I knew it was coming in this slide, and even still I want to run away.

What is even happening here? Let’s see.

We’re in the PostMetricsEngagementController so this trend we’re returning is going to be some Post Metrics regarding engagement. We’re taking the post’s comments and grouping them by what looks to be the number of weeks since the post was published that the comment was created.

Then we’re taking those groupings and mapping them into the count of each group. We have flatten that down though so we just have a collection of counts.

And we’re running those counts through eachCons(2), Adam what does ‘eachCons’ do?

Ok, so we’re taking each consecutive pair of weekly comment counts and mapping them into the difference of the latter week with the week previous to it

And finally, just returning it as a Support Collection and not an Eloquent Collection because we don’t have Comments any more we have numbers like +5, -7 and 0.

Wow.

Page 61: Keeping Eloquent Eloquent

“Every developer is a language designer"

– Colin DeCarlo channeling

Dave Thomas by way of

Yukihiro Matsumoto

This is Yukihiro Matsumoto, the creator of the Ruby programming language. I was listening to a podcast recently where matz was being interviewed about how he created Ruby and during that interview he said something that really stuck with me.

“Every developer is a language designer”.

Matz was talking about how as developers, we design the language of our application through the API of it’s components. Our API is a Domain Specific Language. Within any other context it won’t make sense, but inside the context of our application it all adds together. So, if we’re designing the language our application speaks, don’t we owe it to ourselves to make that language as easy and friendly to understand as possible?

Now, that’s actually not a quote in the sense that Matz didn’t actually say those exact words. He was telling a story about how he was at a conference and heard Dave Thomas say something remarkably like that. So it’s more like “Colin DeCarlo channeling Dave Thomas by way of Yukihiro Matsumoto” but it’s still good and the idea is still very much applicable.

Page 62: Keeping Eloquent Eloquent

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;

class PostMetricsEngagementController extends Controller { public function index(Post $post) {

return response()->json(['metrics' => $trend]); } }

$trend = $post->comments->groupBy(function ($comment) use ($post) { return $comment->created_at->diffInWeeks($post->published_at); })->flatMap(function ($group) { return $group->count(); })->eachCons(2)->map(function ($pair) { return $pair->last() - $pair->first(); })->toBase();

$trend = $post->comments->weekOverWeekEngagement(); $trend = $post->weekOverWeekEngagement();

Now keeping that in mind, don’t you think a better language for our application would be this?

Or even better, this?

It’s possible so lets refactor.

Page 63: Keeping Eloquent Eloquent

<?php

namespace App;

use Illuminate\Database\Eloquent\Model; use App\Collections\CommentsCollection;

class Comment extends Model { // …

public function newCollection(array $models = []) { return new CommentsCollection($models); } }

Page 64: Keeping Eloquent Eloquent

<?php

namespace App\Collections;

use Illuminate\Database\Eloquent\Collection;

class CommentsCollection extends Collection { public function weekOverWeekEngagement() { $this->load('post');

return $this->groupBy(function ($comment) { $created = $comment->created_at; return $created->diffInWeeks($comment->post->published_at); })->flatMap(function ($comments) { return $comments->count(); })->eachCons(2)->map(function ($pair) { return $pair->last() - $pair->first(); }); } }

Page 65: Keeping Eloquent Eloquent

<?php

namespace App;

use Illuminate\Database\Eloquent\Model; use App\Collections\CommentsCollection;

class Comment extends Model {

// …

public function getWeeksSincePostAttribute() { return $this->created_at->diffInWeeks($this->post->published_at); }

public function newCollection(array $models = []) { return new CommentsCollection($models); } }

Page 66: Keeping Eloquent Eloquent

<?php

namespace App\Collections;

use Illuminate\Database\Eloquent\Collection;

class CommentsCollection extends Collection { public function weekOverWeekEngagement() { $this->load('post');

return $this->groupBy('weeks_since_post') ->flatMap(function ($comments) { return $comments->count(); })->eachCons(2)->map(function ($pair) { return $pair->last() - $pair->first(); }); } }

Page 67: Keeping Eloquent Eloquent

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model { public function comments() { return $this->hasMany(Comment::class); }

public function weekOverWeekEngagement() { return $this->comments->weekOverWeekEngagement(); }

public function newComment($author, $attributes) { return $this->comments()->save( (new Comment($attributes))->byAuthor($author) ); } }

Page 68: Keeping Eloquent Eloquent

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;

class PostMetricsEngagementController extends Controller { public function index(Post $post) { $trend = $post->weekOverWeekEngagement(); return response()->json(['metrics' => $trend]); } }

Looks good!

Page 69: Keeping Eloquent Eloquent

@colindecarlo colindecarlo

Thinking about tweeting?

How about ….

My parents should have given my college tuition to @colindecarlo

#laracon

Page 70: Keeping Eloquent Eloquent

This is jig. This specific jig is a very fancy version of a type of jig called a box mitre jig. They’re used to hold frames that have been mitred together at 45 degree angle to the table saw blade. The frame and jig are both passed over the blade and a cut is made through the frames mitre joint. You can then glue a mitre joint spline into the cut which reinforces your joint and is also pretty decorative too.

It looks like this.

Page 71: Keeping Eloquent Eloquent
Page 72: Keeping Eloquent Eloquent

Jigs like this are used to guarantee the same cut gets made time and time again.

They are very useful, very customised tools.

Page 73: Keeping Eloquent Eloquent

Reports

@colindecarlo colindecarlo

I have a jig that I like to use for generating Reports in my applications that I’d like to share with you as a final part of the talk.

Honestly, Eloquent, the ORM, doesn’t fit well with generating reports. There’s nothing wrong with that, it’s just not the right tool for the job. The ORMs focus is on the models of your application, your Blogs, Posts and Comments and how they relate to each other whereas a Report puts its focus more on the metadata than the data itself. For instance, the number of Posts or the number of Comments, all the way to the average number of comments that posts from a specific blog get if they are published before noon on a monday vs after noon on a monday.

Reports are probably where I’ve seen the hairiest code of any application but they generally don’t start out that way, they grow into it.

Let’s see an example of what I’m talking about.

Page 74: Keeping Eloquent Eloquent

Your client comes to you and says, we’d like to have a page that we can go to in the app to see a list of all the authors we have writing posts. We want to see how many posts were written and how many comments they’ve gotten to date.

Page 75: Keeping Eloquent Eloquent

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;

class PostsByAuthorController extends Controller { public function index() { $results = DB::table('users') ->select([ 'users.name', DB::raw('COUNT(distinct posts.id) as number_of_posts'), DB::raw('COUNT(comments.id) as total_comments') ]) ->innerJoin('posts', 'users.id', '=', 'posts.author_id') ->leftJoin('comments', 'posts.id', '=', 'comments.post_id') ->groupBy('users.name') ->get();

return response()->json(['report' => $results]); } }

Ok, not too bad.

Here we are in a PostsByAuthor Controller and we’re using the query builder to generate this report by joining together the users, posts and comments database tables to get the results our client is asking for. The query is pretty straight forward and we’re just going to return the results of the query back to the browser as our report.

Page 76: Keeping Eloquent Eloquent

Your client is really happy with this, they’re learning a lot about the data in their application. They’re so happy in fact they want to “drill down” into the data and get a closer look at it.

Now, they want the ability to optionally limit the report to only “active” authors. “active” being that the user has logged into the application within the last 3 months.

Oh, and it seems that the report has been including draft posts all this time. Since drafts don’t get comments it’s kinda throwing the numbers off, so, the report should exclude drafts posts. That is of course unless they want to include draft posts so we need a flag or something to turn that off.

Page 77: Keeping Eloquent Eloquent

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;

class PostsByAuthorController extends Controller { public function index(Request $request) { $query = DB::table('users') ->select([ 'users.name', DB::raw('COUNT(disinct posts.id) as number_of_posts'), DB::raw('COUNT(comments.id) as total_comments') ]) ->innerJoin('posts', 'users.id', '=', 'posts.author_id') ->leftJoin('comments', 'posts.id', '=', 'comments.post_id') ->groupBy('users.name');

if ($request->input('only_active_authors')) { $query->where('users.last_login', '>=', Carbon::now()->subMonths(3)); }

if (!$request->input('include_draft_posts')) { $query->whereNotNull('posts.published_at'); }

$results = $query->get();

return response()->json(['report' => $results]); } }

Oh, ok.

It’s getting a little tight in here, got to shrink the font down a bit but we got it all.

There are our two new options, one for active authors and the other one there to limit the default results to only published posts that is of course unless we want to include them.

Page 78: Keeping Eloquent Eloquent

The client is really happy with these changes.

But they’ve just started tracking backlinks to the posts so it would be really helpful if they could get a report that only included data about posts published after a certain date.

Oh and, it would be really neat to see how posts with backlinks stack up against posts that don’t have backlinks. So there should be an option to filter the results by whether or not a post has backlinks. But backlinks should just be ignored if they haven’t said to include or exclude them from the report.

Page 79: Keeping Eloquent Eloquent

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;

class PostsByAuthorController extends Controller { public function index(Request $request) { $query = DB::table('users') ->select([ 'users.name', DB::raw('COUNT(distinct posts.id) as number_of_posts'), DB::raw('COUNT(comments.id) as total_comments') ]) ->innerJoin('posts', 'users.id', '=', 'posts.author_id') ->leftJoin('comments', 'posts.id', '=', 'comments.post_id') ->groupBy('users.name');

if ($request->input('only_active_authors')) { $query->where('users.last_login', '>=', Carbon::now()->subMonths(3)); }

if (!$request->input('include_draft_posts')) { $query->whereNotNull('posts.published_at'); }

if ($request->input('with_backlinks') !== null) { $query->leftJoin('backlinks', 'posts.id', '=', 'backlinks.post_id'); if ($request->input('with_backlinks') { $query->whereNotNull('backlinks.id'); } else { $query->whereNull('backlinks.id'); } }

if ($since = $request->input('posts_since')) { $query->where('posts.created_at', '>=', Carbon::parse($since)); }

$results = $query->get();

return response()->json(['report' => $results]); } }

Oh man, the client is thrilled!

But, how do you feel?

Page 80: Keeping Eloquent Eloquent

Probably similar to how people feel when they realize that they have this thing in their house.

Terrified.

You don’t know how it got here, you sorta remember talking to someone about a couch but you don’t really remember it being described quite like this.

And you’re afraid to touch it, it’s got so many sharp points on it that it’s just safer to leave it where it is.

Page 81: Keeping Eloquent Eloquent

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;

class PostsByAuthorController extends Controller { public function index(Request $request) { $query = DB::table('users') ->select([ 'users.name', DB::raw('COUNT(distinct posts.id) as number_of_posts'), DB::raw('COUNT(comments.id) as total_comments') ]) ->innerJoin('posts', 'users.id', '=', 'posts.author_id') ->leftJoin('comments', 'posts.id', '=', 'comments.post_id') ->groupBy('users.name');

if ($request->input('only_active_authors')) { $query->where('users.last_login', '>=', Carbon::now()->subMonths(3)); }

if (!$request->input('include_draft_posts')) { $query->whereNotNull('posts.published_at'); }

if ($request->input('with_backlinks') !== null) { $query->leftJoin('backlinks', 'posts.id', '=', 'backlinks.post_id'); if ($request->input('with_backlinks') { $query->whereNotNull('backlinks.id'); } else { $query->whereNull('backlinks.id'); } }

if ($since = $request->input('posts_since')) { $query->where('posts.created_at', '>=', Carbon::parse($since)); }

$results = $query->get();

return response()->json(['report' => $results]); } }

So when I get asked to write a report for an application, how do I manage to keep that thing out of my code?

Page 82: Keeping Eloquent Eloquent

<?php

namespace App\Reports;

use Illuminate\Database\DatabaseManager;

abstract class Report { protected $db; protected $query;

public function __construct(DatabaseManager $db) { $this->db = $db; $this->query = $this->buildBaseQuery(); }

public function get() { return $this->query->get(); }

abstract protected function buildBaseQuery(); }

I reach for my Report jig.

My jig is a simple abstract class called Report. The constructor accepts the active DatabaseManager, which is the same object that the DB Facade resolves to, this way I can start my query at any table in the database.

In the constructor I just assign that to the $db property and then call the abstract protected function buildBaseQuery whose return value I assign to the $query property.

The only other method you see in here is the get method which just executes the query and returns it’s results to the caller.

Page 83: Keeping Eloquent Eloquent

<?php

namespace App\Reports;

class PostsByAuthorReport extends Report { protected function buildBaseQuery() { $query = $this->query->table('users') ->select([ 'users.name', $this->query->raw('COUNT(distinct posts.id) as number_of_posts'), $this->query->raw('COUNT(comments.id) as total_comments') ]) ->innerJoin('posts', 'users.id', '=', 'posts.author_id') ->leftJoin('comments', 'posts.id', '=', 'comments.post_id') ->groupBy('users.name'); } }

Here is the PostsByAuthor Report when it was just a report of all the Authors, the number of Posts they have and the total number of comments they’ve received.

It all happens in the buildBaseQuery method.

Page 84: Keeping Eloquent Eloquent

<?php

namespace App\Http\Controllers;

use App\Reports\PostsByAuthorReport; use App\Http\Controllers\Controller;

class PostsByAuthorController extends Controller { public function index() { $results = app(PostsByAuthorReport::class)->get(); return response()->json(['report' => $results]); } }

And here is how you use the report in the controller. It’s pretty simple and it’s moved all the know how of building the report out of the controller and into your domain.

And when the client comes back to you requesting modifications or improvements to the report all you have to do is add some methods to the report which modify the query.

Page 85: Keeping Eloquent Eloquent

public function filterActiveUsers($yesNo) { return $yesNo ? $this->onlyActiveUsers() : $this; }

public function onlyActiveUsers() { $this->query->where('users.last_login', '>=', Carbon::now()->subMonths(3)); return $this; }

For instance, if they’re filtering for only active users

Page 86: Keeping Eloquent Eloquent

public function includeDrafts($yesNo) { return $yesNo ? $this : $this->onlyPublishedPosts(); }

public function onlyPublishedPosts() { $this->query->whereNotNull('posts.published_at'); return $this; }

Or including draft posts

Page 87: Keeping Eloquent Eloquent

<?php

namespace App\Http\Controllers;

use App\Reports\PostsByAuthorReport; use App\Http\Controllers\Controller;

class PostsByAuthorController extends Controller { public function index(Request $request) { $results = app(PostsByAuthorReport::class) ->filterActiveUsers($request->input('only_active_authors')) ->includeDrafts($request->input('include_draft_posts')) ->havingBacklinks($request->input('with_backlinks')) ->postedSince($request->input('posted_since')) ->get();

return response()->json(['report' => $results]); } }

Anything.

Your controller stays simple and just proxies the options back to the report which does all the work.

So you see here we’ve got the filter active users, whether or not to include draft posts, if we should care about backlinks or not and the publish date cutoff all rolled up into one happy report.

Pretty neat.

Page 88: Keeping Eloquent Eloquent

@colindecarlo colindecarlo

Thinking about tweeting?

How about ….

Saw the perfect couch for my parent’s new condo, thanks @colindecarlo!

#laracon

Page 89: Keeping Eloquent Eloquent

@colindecarlo colindecarlo

Questions?