Skip to content

Instantly share code, notes, and snippets.

@pippinsmith
Last active October 10, 2019 15:23
Show Gist options
  • Save pippinsmith/46423ede829c0165b36322b749ad986b to your computer and use it in GitHub Desktop.
Save pippinsmith/46423ede829c0165b36322b749ad986b to your computer and use it in GitHub Desktop.
Eloquent Builder that allows for eager loading relationships with parameters

Allowed uses:

Model::with('method1' => ['params' => [$param, $param2]]);

Model::with('method1' => ['as' => 'alias', 'params' => [$param, $param2]]);

Model::with('method1' => ['params' => [$param], 'constraints' => function ($query) use ($param3) {
  $query->where('column', $param3);
}]);

Model::with('method1' => [
                              ['as' => 'alias', 'params' => [$param],
                              ['as' => 'alias2', 'params' => [$param3]
                          ]);
                               
Model::with(['method1' => ['params' => $param], 'method2' => ['params' => [$param2]]);

(combined derivatives of these functionalities also work)

<?php
namespace App;
use BadMethodCallException;
use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\RelationNotFoundException;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Str;
/**
* Class CustomEloquentBuilder
*
* Allows for calling params through a with on relationship methods
*
* @see Builder
*/
class CustomEloquentBuilder extends Builder
{
/**
* Eager load the relationships for the models.
*
* @param array $models
* @return array
*/
public function eagerLoadRelations(array $models)
{
foreach ($this->eagerLoad as $name => $constraints) {
// For nested eager loads we'll skip loading them here and they will be set as an
// eager load on the query to retrieve the relation so that they will be eager
// loaded on that query, because that is where they get hydrated as models.
if (mb_strpos($name, '.') === false) {
if (is_array($constraints)) {
$models = $this->eagerLoadRelationCustom($models, $name, $constraints);
} else {
$models = $this->eagerLoadRelation($models, $name, $constraints);
}
}
}
return $models;
}
/**
* Get the relation instance for the given relation name.
*
* @param string $name
* @param array $params
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function getRelationCustom($name, array $params)
{
// We want to run a relationship query without any constrains so that we will
// not have to remove these where clauses manually which gets really hacky
// and error prone. We don't want constraints because we add eager ones.
$relation = Relation::noConstraints(function () use ($name, $params) {
try {
return $this->getModel()->newInstance()->$name(...$params);
} catch (BadMethodCallException $e) {
throw RelationNotFoundException::make($this->getModel(), $name);
}
});
$nested = $this->relationsNestedUnder($name);
// If there are nested relationships set on the query, we will put those onto
// the query instances so that they can be handled after this relationship
// is loaded. In this way they will all trickle down as they are loaded.
if (count($nested) > 0) {
$relation->getQuery()->with($nested);
}
return $relation;
}
/**
* Eagerly load the relationship on a set of models.
*
* @param array $models
* @param string $name
* @param \Closure $constraints
* @return array
*/
protected function eagerLoadRelation(array $models, $name, Closure $constraints)
{
// First we will "back up" the existing where conditions on the query so we can
// add our eager constraints. Then we will merge the wheres that were on the
// query back to it in order that any where conditions might be specified.
// We'll also catch the RelationNotFoundException and just return the models
try {
$relation = $this->getRelation($name);
} catch (RelationNotFoundException $e) {
return $models;
}
$relation->addEagerConstraints($models);
$constraints($relation);
// Once we have the results, we just match those back up to their parent models
// using the relationship instance. Then we just return the finished arrays
// of models which have been eagerly hydrated and are readied for return.
return $relation->match(
$relation->initRelation($models, $name),
$relation->getEager(),
$name
);
}
/**
* Eagerly load the relationship on a set of models.
*
* @param array $models
* @param string $name
* @param array $constraints
* @return array
*/
protected function eagerLoadRelationCustom(array $models, $name, $constraints)
{
// First we will shave off the extra digit and pipe symbol
if (mb_strpos($name, '|') !== false) {
$name = mb_substr($name, 0, mb_strpos($name, '|'));
}
// Then we will separate the custom params from the constraint closure
if (isset($constraints['params'])) {
$params = $constraints['params'];
}
if (isset($constraints['as'])) {
$as = $constraints['as'];
}
if (isset($constraints['constraints'])) {
$constraints = $constraints['constraints'];
} else {
$constraints = function ($query) {
};
}
// Then we will "back up" the existing where conditions on the query so we can
// add our eager constraints.
// We will call either the custom getRelation function which accepts
// parameters for the relationship function on the model.
if (isset($params)) {
try {
$relation = $this->getRelationCustom($name, $params);
} catch (RelationNotFoundException $e) {
return $models;
}
}
// Or the original getRelation function
if (!isset($relation)) {
try {
$relation = $this->getRelation($name);
} catch (RelationNotFoundException $e) {
return $models;
}
}
// Then we will merge the wheres that were on the
// query back to it in order that any where conditions might be specified.
$relation->addEagerConstraints($models);
$constraints($relation);
// Once we have the results, we just match those back up to their parent models
// using the relationship instance. Then we just return the finished arrays
// of models which have been eagerly hydrated and are readied for return.
return $relation->match(
$relation->initRelation($models, $as ?? $name),
$relation->getEager(),
$as ?? $name
);
}
/**
* Parse a list of relations into individuals.
*
* @param array $relations
* @return array
*/
protected function parseWithRelations(array $relations)
{
$results = [];
foreach ($relations as $name => $constraints) {
// If the "name" value is a numeric key, we can assume that no
// constraints have been specified. We'll just put an empty
// Closure there, so that we can treat them all the same.
if (is_numeric($name)) {
$name = $constraints;
list($name, $constraints) = Str::contains($name, ':')
? $this->createSelectWithConstraint($name)
: [$name, function () {
}];
}
if (is_array($constraints) && count($constraints) > 0 && is_numeric(key($constraints))) {
// Here we assume there are no nested withs in withs that have parameters supplied
// We check if there are multiple param arrays in the same constraint and split them accordingly
$i = 0;
foreach ($constraints as $constraint) {
$results[$name . '|' . $i++] = $constraint;
}
} elseif (is_array($constraints) && count($constraints) > 0 && !is_numeric(key($constraints))) {
// Here we assume the same thing, but we don't split the array since it only contains one constraint
$results[$name] = $constraints;
} else {
// We need to separate out any nested includes, which allows the developers
// to load deep relationships using "dots" without stating each level of
// the relationship with its own key in the array of eager-load names.
$results = $this->addNestedWiths($name, $results);
$results[$name] = $constraints;
}
}
return $results;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment