Pushing Logic to Custom Collections

This is a technique that I recently found useful. Eloquent models allow you to specify a custom collection object to be returned – which sometimes can be a great place to put some business logic.

While refactoring WhichBeach, I used a custom collection to move some logic from the Beach model to a custom BeachCollection. This allowed me to test the logic in isolation.

Creating the Custom Collection

I created a new class extending Laravel’s Eloquent Collection:

<?php

namespace WhichBeach\Beach\Support;

use Illuminate\Database\Eloquent\Collection;

class BeachCollection extends Collection
{

}

In my model, I instructed Laravel to use my custom collection:

<?php

namespace WhichBeach\Beach\Entities;

use WhichBeach\BaseModel;
use WhichBeach\Beach\Support\BeachCollection;

class Beach extends BaseModel
{
    /**
     * Create a new Eloquent Collection instance.
     *
     * @param  array  $models
     * @return \WhichBeach\Beach\Support\BeachCollection
     */
    public function newCollection(array $models = [])
    {
        return new BeachCollection($models);
    }
}

Adding the Logic

Now that I had a place to put my custom code, I could add any number of helper methods to be used throughout my application:

/**
 * Filter the beaches to only contain major beaches
 *
 * @return static
 */
public function major()
{
    return $this->where('is_major', true);
}

/**
 * Filter the beaches to only the ones that have the lowest score
 *
 * @return static
 */
protected function lowestScoring()
{
    return $this->where('score', $this->min('score'));
}

Edit: The above code block has been improved after an excellent suggestion by Joseph Silber in the comments.

This has given me the ability to write more fluent code when dealing with beaches:

$topBeach = $beaches->major()->lowestScoring()->random();

Being able to write the line above is a big plus for me – the less cognitive load required to understand the intended output, the better.

Let me know of any good examples of using custom collections in the comments!

10 comments

  1. We’ve had the where method on the collection object for a while now. This can significantly shorten the above methods:

    
    return $this->where('is_major', true);
    

    and the same for the second method:

    
    return $this->where('score', $this->min('score'));
    
    1. Hi Joseph, you’re right – I’d never used that function before.

      I’ve updated the code to incorporate your suggestion – and I learnt something new. Thanks on both fronts!

    2. Why should the caller know that `is_major` is a boolean (or even that it’s a file/column instead of the product of a calculation, API call, or whatever)? Defining that logic once on the collection (or model) allows your code to be more expressive (expressing intent rather than implementation) and prevents the logic from being repeated (and eventually bifurcated) throughout the codebase.

  2. Is there any reason you put these methods on the Collection class and not as scopes in the Eloquent model? As being in the collection means these filters act on a result set _after_ it’s been fetched from the database, as opposed to filtering the query to fetch specific records.

    1. Hi Martin,

      In this particular case I’ve actually refactored *away* from scopes in favour of filtering the result set, for a number of reasons that aren’t very clear in the small example that I chose to share, here are a few of them:

      – I need to perform some business logic on different sets of filtered results
      – My “lowest scoring” logic is expanded to accept a minimum amount of beaches to return, and I prefer describing this sort of logic in PHP rather than mySQL.
      – I have 50 rows in my database – and this isn’t going to change – so the performance hit is negligible.

      Having said that, my instinct was first to use scopes, so I understand your point.

      Thanks for your feedback!

Leave a comment

Your email address will not be published. Required fields are marked *