You'd have to override Eloquent a little bit. I just ran into something very similar with a BelongsToMany relationship. I was trying to perform a many-to-many query where the relevant local-key was not the primary key. So I extended Eloquent's BelongsToMany a little bit. Start by building an override class for the BelongsToMany relationship class:
namespace App\Overrides\Relations;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany as BaseBelongsToMany;
class BelongsToMany extends BaseBelongsToMany
{
protected $localKey;
/**
* @var array
*/
protected $customConstraints = [];
/**
* BelongsToMany constructor.
* @param Builder $query
* @param Model $parent
* @param string $table
* @param string $foreignKey
* @param string $otherKey
* @param string $relationName
* @param string $localKey
*/
public function __construct(
Builder $query,
Model $parent,
$table,
$foreignKey,
$otherKey,
$relationName = null,
$localKey = null
) {
//The local-key binding, assumed by Eloquent to be the primary key of the model, will have already been set
if ($localKey) { //If it's intended to be overridden, that value in the Query/Builder object needs updating
$this->localKey = $localKey;
$this->setLocalKey($query, $parent, $table, $foreignKey, $localKey);
}
parent::__construct($query, $parent, $table, $foreignKey, $otherKey, $relationName);
}
/**
* If a custom local-key field is defined, don't automatically assume the pivot table's foreign relationship is
* joined to the model's primary key. This method is necessary for lazy-loading.
*
* @param Builder $query
* @param Model $parent
* @param string $table
* @param string $foreignKey
* @param string $localKey
*/
public function setLocalKey(Builder $query, Model $parent, $table, $foreignKey, $localKey)
{
$qualifiedForeignKey = "$table.$foreignKey";
$bindingIndex = null;
//Search for the 'where' value currently linking the pivot table's foreign key to the model's primary key value
$query->getQuery()->wheres = collect($query->getQuery()->wheres)->map(function ($where, $index) use (
$qualifiedForeignKey,
$parent,
&$bindingIndex
) {
//Update the key value, and note the index so the corresponding binding can also be updated
if (array_get($where, 'column', '') == $qualifiedForeignKey) {
$where['value'] = $this->getKey($parent);
$bindingIndex = $index;
}
return $where;
})->toArray();
//If a binding index was discovered, updated it to reflect the value of the custom-defined local key
if (!is_null($bindingIndex)) {
$bindgings = $query->getQuery()->getBindings();
$bindgings[$bindingIndex] = $this->getKey($parent);
$query->getQuery()->setBindings($bindgings);
}
}
/**
* Get all of the primary keys for an array of models.
* Overridden so that the call to $value->getKey() is replaced with $this->getKey()
*
* @param array $models
* @param string $key
* @return array
*/
protected function getKeys(array $models, $key = null)
{
if ($key) {
return parent::getKeys($models, $key);
}
return array_unique(array_values(array_map(function ($value) use ($key) {
return $this->getKey($value);
}, $models)));
}
/**
* If a custom local-key field is defined, don't automatically assume the pivot table's foreign relationship is
* joined to the model's primary key. This method is necessary for eager-loading.
*
* @param Model $model
* @return mixed
*/
protected function getKey(Model $model)
{
return $this->localKey ? $model->getAttribute($this->localKey) : $model->getKey();
}
/**
* Set the where clause for the relation query.
* Overridden so that the call to $this->parent->getKey() is replaced with $this->getKey()
* This method is not necessary if this class is accessed through the typical flow of a Model::belongsToMany() call.
* It is necessary if it's instantiated directly.
*
* @return $this
*/
protected function setWhere()
{
$foreign = $this->getForeignKey();
$this->query->where($foreign, '=', $this->getKey($this->parent));
return $this;
}
}
Next, you'll need to make the Model
class actually use it:
namespace App\Overrides\Traits;
use App\Overrides\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\Relation;
/**
* Intended for use inside classes that extend Illuminate\Database\Eloquent\Model
*
* Class RelationConditions
* @package App\Overrides\Traits
*/
trait CustomConstraints
{
/**
* Intercept the Eloquent Model method and return a custom relation object instead
*
* {@inheritdoc}
*/
public function belongsToMany($related, $table = null, $foreignKey = null, $otherKey = null, $relation = null, $localKey = null)
{
//Avoid having to reproduce parent logic here by asking the returned object for its original parameter values
$base = parent::belongsToMany($related, $table, $foreignKey, $otherKey, $relation);
//The base action will have already applied the appropriate constraints, so don't re-add them here
return Relation::noConstraints(function () use ($base, $localKey) {
//These methods do the same thing, but got renamed
$foreignKeyName = version_compare(app()->version(), '5.3', '>=')
? $base->getQualifiedForeignKeyName()
: $base->getForeignKey();
$relatedKeyName = version_compare(app()->version(), '5.3', '>=')
? $base->getQualifiedRelatedKeyName()
: $base->getOtherKey();
return new BelongsToMany(
$base->getQuery(),
$base->getParent(),
$base->getTable(),
last(explode('.', $foreignKeyName)),
last(explode('.', $relatedKeyName)),
$base->getRelationName(),
$localKey
);
});
}
}
Use this trait inside your model class, and you now have the ability to add a 6th argument that specifies what local-key to use, rather than automatically assume the primary one.