This thread is more an extension of https://laracasts.com/forum/?p=431-eloquent-relation-setup-two-foreign-keys-to-same-table as I have a messages table containing two foreign key sender_user_id and recipient_user_id that both reference the users table. My problem is what if I wish to create an Eloquent Relation on the model so I can take advantage of lazy/eager-loading and re-contextualizing the results? I will be adding a messages() method to the User model below.
class User extends \Illuminate\Database\Eloquent\Model
{
public function messagesSent()
{
return $this->hasMany(Message::class, 'sender_user_id');
}
public function messagesReceived()
{
return $this->hasMany(Message::class, 'recipient_user_id');
}
}
The solution I came up with was to extend \Illuminate\Database\Eloquent\Relations\HasMany to support multiple values in its $foreignKey parameter.
<?php
namespace App\Database\Eloquent\Relations;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Class HasManyWithKeys.
*
* Extend Illuminate\Database\Eloquent\Relations\HasMany so you can reference
* unify HasMany relationships on different foreign keys in the same table.
*
* @package App\Database\Eloquent\Relations
*/
class HasManyWithKeys extends HasMany
{
/**
* @var array
*/
protected $foreignKeys;
/**
* Create a new has one or many relationship instance.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $parent
* @param array $foreignKeys
* @param string $localKey
*
* @return HasManyWithKeys
*/
public function __construct(Builder $query, Model $parent, $foreignKeys, $localKey)
{
$this->foreignKeys = $foreignKeys;
parent::__construct($query, $parent, $foreignKeys[0], $localKey);
}
/**
* Set the base constraints on the relation query.
*/
public function addConstraints()
{
if (static::$constraints) {
$foreignKeys = $this->foreignKeys;
$this->query->where(function ($query) use ($foreignKeys) {
foreach ($foreignKeys as $foreignKey) {
$query->orWhere(function ($query) use ($foreignKey) {
$query->where($foreignKey, '=', $this->getParentKey())
->whereNotNull($this->foreignKey);
});
}
});
}
}
/**
* Set the constraints for an eager load of the relation.
*
* @param array $models
*/
public function addEagerConstraints(array $models)
{
$foreignKeys = $this->foreignKeys;
$this->query->where(function ($query) use ($foreignKeys, $models) {
foreach ($foreignKeys as $foreignKey) {
$query->orWhere(function ($query) use ($foreignKey, $models) {
$$query->whereIn($foreignKey, $this->getKeys($models, $this->localKey));
});
}
});
}
}
Then in the above model definition I could add:
class User extends \Illuminate\Database\Eloquent\Model
{
// ...
public function messages()
{
return $this->hasManyWithKeys(Message::class, ['recipient_user_id', 'sender_user_id']);
}
/**
* Obviously move this to a parent class or Trait.
*
* Define a one-to-many relationship on a table with multiple foreign keys
* referencing the same model.
*
* @param string $related
* @param array|string $foreignKeys
* @param string $localKey
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function hasManyWithKeys($related, $foreignKeys = null, $localKey = null)
{
// Default to Illuminate\Database\Eloquent\Relations\HasMany for less
// than two foreign keys.
if (empty($foreignKeys) || is_string($foreignKeys)) {
return $this->hasMany($related, $foreignKeys, $localKey);
}
if (count($foreignKeys) === 1) {
return $this->hasMany($related, $foreignKeys[0], $localKey);
}
$instance = new $related;
$localKey = $localKey ?: $this->getKeyName();
$foreignKeys = array_map(function ($foreignKey) use ($instance) {
return $instance->getTable() . '.' . $foreignKey;
}, $foreignKeys);
return new HasManyWithKeys($instance->newQuery(), $this, $foreignKeys, $localKey);
}
}
With this you can then use the query builder returned by $user->messages() to retrieve message threads or the unread count for that user.
So did I reinvent the wheel here? Reading through https://laravel.com/docs/5.2/eloquent-relationships and the source of namespace \Illuminate\Database\Eloquent\Relations\*, I was unable to find the capabilities I was looking for. (And yes, that $foreignKeys[0] constructor parameter is cringe-worthy.)