1504 lines
40 KiB
PHP
Executable File
1504 lines
40 KiB
PHP
Executable File
<?php
|
|
|
|
namespace Illuminate\Database\Eloquent\Relations;
|
|
|
|
use Closure;
|
|
use Illuminate\Contracts\Support\Arrayable;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
use Illuminate\Database\Eloquent\Relations\Concerns\AsPivot;
|
|
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
|
|
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithPivotTable;
|
|
use Illuminate\Support\Str;
|
|
use InvalidArgumentException;
|
|
|
|
class BelongsToMany extends Relation
|
|
{
|
|
use InteractsWithDictionary, InteractsWithPivotTable;
|
|
|
|
/**
|
|
* The intermediate table for the relation.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $table;
|
|
|
|
/**
|
|
* The foreign key of the parent model.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $foreignPivotKey;
|
|
|
|
/**
|
|
* The associated key of the relation.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $relatedPivotKey;
|
|
|
|
/**
|
|
* The key name of the parent model.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $parentKey;
|
|
|
|
/**
|
|
* The key name of the related model.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $relatedKey;
|
|
|
|
/**
|
|
* The "name" of the relationship.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $relationName;
|
|
|
|
/**
|
|
* The pivot table columns to retrieve.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $pivotColumns = [];
|
|
|
|
/**
|
|
* Any pivot table restrictions for where clauses.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $pivotWheres = [];
|
|
|
|
/**
|
|
* Any pivot table restrictions for whereIn clauses.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $pivotWhereIns = [];
|
|
|
|
/**
|
|
* Any pivot table restrictions for whereNull clauses.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $pivotWhereNulls = [];
|
|
|
|
/**
|
|
* The default values for the pivot columns.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $pivotValues = [];
|
|
|
|
/**
|
|
* Indicates if timestamps are available on the pivot table.
|
|
*
|
|
* @var bool
|
|
*/
|
|
public $withTimestamps = false;
|
|
|
|
/**
|
|
* The custom pivot table column for the created_at timestamp.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $pivotCreatedAt;
|
|
|
|
/**
|
|
* The custom pivot table column for the updated_at timestamp.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $pivotUpdatedAt;
|
|
|
|
/**
|
|
* The class name of the custom pivot model to use for the relationship.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $using;
|
|
|
|
/**
|
|
* The name of the accessor to use for the "pivot" relationship.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $accessor = 'pivot';
|
|
|
|
/**
|
|
* Create a new belongs to many relationship instance.
|
|
*
|
|
* @param \Illuminate\Database\Eloquent\Builder $query
|
|
* @param \Illuminate\Database\Eloquent\Model $parent
|
|
* @param string $table
|
|
* @param string $foreignPivotKey
|
|
* @param string $relatedPivotKey
|
|
* @param string $parentKey
|
|
* @param string $relatedKey
|
|
* @param string|null $relationName
|
|
* @return void
|
|
*/
|
|
public function __construct(Builder $query, Model $parent, $table, $foreignPivotKey,
|
|
$relatedPivotKey, $parentKey, $relatedKey, $relationName = null)
|
|
{
|
|
$this->parentKey = $parentKey;
|
|
$this->relatedKey = $relatedKey;
|
|
$this->relationName = $relationName;
|
|
$this->relatedPivotKey = $relatedPivotKey;
|
|
$this->foreignPivotKey = $foreignPivotKey;
|
|
$this->table = $this->resolveTableName($table);
|
|
|
|
parent::__construct($query, $parent);
|
|
}
|
|
|
|
/**
|
|
* Attempt to resolve the intermediate table name from the given string.
|
|
*
|
|
* @param string $table
|
|
* @return string
|
|
*/
|
|
protected function resolveTableName($table)
|
|
{
|
|
if (! str_contains($table, '\\') || ! class_exists($table)) {
|
|
return $table;
|
|
}
|
|
|
|
$model = new $table;
|
|
|
|
if (! $model instanceof Model) {
|
|
return $table;
|
|
}
|
|
|
|
if (in_array(AsPivot::class, class_uses_recursive($model))) {
|
|
$this->using($table);
|
|
}
|
|
|
|
return $model->getTable();
|
|
}
|
|
|
|
/**
|
|
* Set the base constraints on the relation query.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function addConstraints()
|
|
{
|
|
$this->performJoin();
|
|
|
|
if (static::$constraints) {
|
|
$this->addWhereConstraints();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the join clause for the relation query.
|
|
*
|
|
* @param \Illuminate\Database\Eloquent\Builder|null $query
|
|
* @return $this
|
|
*/
|
|
protected function performJoin($query = null)
|
|
{
|
|
$query = $query ?: $this->query;
|
|
|
|
// We need to join to the intermediate table on the related model's primary
|
|
// key column with the intermediate table's foreign key for the related
|
|
// model instance. Then we can set the "where" for the parent models.
|
|
$query->join(
|
|
$this->table,
|
|
$this->getQualifiedRelatedKeyName(),
|
|
'=',
|
|
$this->getQualifiedRelatedPivotKeyName()
|
|
);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set the where clause for the relation query.
|
|
*
|
|
* @return $this
|
|
*/
|
|
protected function addWhereConstraints()
|
|
{
|
|
$this->query->where(
|
|
$this->getQualifiedForeignPivotKeyName(), '=', $this->parent->{$this->parentKey}
|
|
);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set the constraints for an eager load of the relation.
|
|
*
|
|
* @param array $models
|
|
* @return void
|
|
*/
|
|
public function addEagerConstraints(array $models)
|
|
{
|
|
$whereIn = $this->whereInMethod($this->parent, $this->parentKey);
|
|
|
|
$this->query->{$whereIn}(
|
|
$this->getQualifiedForeignPivotKeyName(),
|
|
$this->getKeys($models, $this->parentKey)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Initialize the relation on a set of models.
|
|
*
|
|
* @param array $models
|
|
* @param string $relation
|
|
* @return array
|
|
*/
|
|
public function initRelation(array $models, $relation)
|
|
{
|
|
foreach ($models as $model) {
|
|
$model->setRelation($relation, $this->related->newCollection());
|
|
}
|
|
|
|
return $models;
|
|
}
|
|
|
|
/**
|
|
* Match the eagerly loaded results to their parents.
|
|
*
|
|
* @param array $models
|
|
* @param \Illuminate\Database\Eloquent\Collection $results
|
|
* @param string $relation
|
|
* @return array
|
|
*/
|
|
public function match(array $models, Collection $results, $relation)
|
|
{
|
|
$dictionary = $this->buildDictionary($results);
|
|
|
|
// Once we have an array dictionary of child objects we can easily match the
|
|
// children back to their parent using the dictionary and the keys on the
|
|
// parent models. Then we should return these hydrated models back out.
|
|
foreach ($models as $model) {
|
|
$key = $this->getDictionaryKey($model->{$this->parentKey});
|
|
|
|
if (isset($dictionary[$key])) {
|
|
$model->setRelation(
|
|
$relation, $this->related->newCollection($dictionary[$key])
|
|
);
|
|
}
|
|
}
|
|
|
|
return $models;
|
|
}
|
|
|
|
/**
|
|
* Build model dictionary keyed by the relation's foreign key.
|
|
*
|
|
* @param \Illuminate\Database\Eloquent\Collection $results
|
|
* @return array
|
|
*/
|
|
protected function buildDictionary(Collection $results)
|
|
{
|
|
// First we'll build a dictionary of child models keyed by the foreign key
|
|
// of the relation so that we will easily and quickly match them to the
|
|
// parents without having a possibly slow inner loop for every model.
|
|
$dictionary = [];
|
|
|
|
foreach ($results as $result) {
|
|
$value = $this->getDictionaryKey($result->{$this->accessor}->{$this->foreignPivotKey});
|
|
|
|
$dictionary[$value][] = $result;
|
|
}
|
|
|
|
return $dictionary;
|
|
}
|
|
|
|
/**
|
|
* Get the class being used for pivot models.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getPivotClass()
|
|
{
|
|
return $this->using ?? Pivot::class;
|
|
}
|
|
|
|
/**
|
|
* Specify the custom pivot model to use for the relationship.
|
|
*
|
|
* @param string $class
|
|
* @return $this
|
|
*/
|
|
public function using($class)
|
|
{
|
|
$this->using = $class;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Specify the custom pivot accessor to use for the relationship.
|
|
*
|
|
* @param string $accessor
|
|
* @return $this
|
|
*/
|
|
public function as($accessor)
|
|
{
|
|
$this->accessor = $accessor;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set a where clause for a pivot table column.
|
|
*
|
|
* @param string $column
|
|
* @param mixed $operator
|
|
* @param mixed $value
|
|
* @param string $boolean
|
|
* @return $this
|
|
*/
|
|
public function wherePivot($column, $operator = null, $value = null, $boolean = 'and')
|
|
{
|
|
$this->pivotWheres[] = func_get_args();
|
|
|
|
return $this->where($this->qualifyPivotColumn($column), $operator, $value, $boolean);
|
|
}
|
|
|
|
/**
|
|
* Set a "where between" clause for a pivot table column.
|
|
*
|
|
* @param string $column
|
|
* @param array $values
|
|
* @param string $boolean
|
|
* @param bool $not
|
|
* @return $this
|
|
*/
|
|
public function wherePivotBetween($column, array $values, $boolean = 'and', $not = false)
|
|
{
|
|
return $this->whereBetween($this->qualifyPivotColumn($column), $values, $boolean, $not);
|
|
}
|
|
|
|
/**
|
|
* Set a "or where between" clause for a pivot table column.
|
|
*
|
|
* @param string $column
|
|
* @param array $values
|
|
* @return $this
|
|
*/
|
|
public function orWherePivotBetween($column, array $values)
|
|
{
|
|
return $this->wherePivotBetween($column, $values, 'or');
|
|
}
|
|
|
|
/**
|
|
* Set a "where pivot not between" clause for a pivot table column.
|
|
*
|
|
* @param string $column
|
|
* @param array $values
|
|
* @param string $boolean
|
|
* @return $this
|
|
*/
|
|
public function wherePivotNotBetween($column, array $values, $boolean = 'and')
|
|
{
|
|
return $this->wherePivotBetween($column, $values, $boolean, true);
|
|
}
|
|
|
|
/**
|
|
* Set a "or where not between" clause for a pivot table column.
|
|
*
|
|
* @param string $column
|
|
* @param array $values
|
|
* @return $this
|
|
*/
|
|
public function orWherePivotNotBetween($column, array $values)
|
|
{
|
|
return $this->wherePivotBetween($column, $values, 'or', true);
|
|
}
|
|
|
|
/**
|
|
* Set a "where in" clause for a pivot table column.
|
|
*
|
|
* @param string $column
|
|
* @param mixed $values
|
|
* @param string $boolean
|
|
* @param bool $not
|
|
* @return $this
|
|
*/
|
|
public function wherePivotIn($column, $values, $boolean = 'and', $not = false)
|
|
{
|
|
$this->pivotWhereIns[] = func_get_args();
|
|
|
|
return $this->whereIn($this->qualifyPivotColumn($column), $values, $boolean, $not);
|
|
}
|
|
|
|
/**
|
|
* Set an "or where" clause for a pivot table column.
|
|
*
|
|
* @param string $column
|
|
* @param mixed $operator
|
|
* @param mixed $value
|
|
* @return $this
|
|
*/
|
|
public function orWherePivot($column, $operator = null, $value = null)
|
|
{
|
|
return $this->wherePivot($column, $operator, $value, 'or');
|
|
}
|
|
|
|
/**
|
|
* Set a where clause for a pivot table column.
|
|
*
|
|
* In addition, new pivot records will receive this value.
|
|
*
|
|
* @param string|array $column
|
|
* @param mixed $value
|
|
* @return $this
|
|
*
|
|
* @throws \InvalidArgumentException
|
|
*/
|
|
public function withPivotValue($column, $value = null)
|
|
{
|
|
if (is_array($column)) {
|
|
foreach ($column as $name => $value) {
|
|
$this->withPivotValue($name, $value);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
if (is_null($value)) {
|
|
throw new InvalidArgumentException('The provided value may not be null.');
|
|
}
|
|
|
|
$this->pivotValues[] = compact('column', 'value');
|
|
|
|
return $this->wherePivot($column, '=', $value);
|
|
}
|
|
|
|
/**
|
|
* Set an "or where in" clause for a pivot table column.
|
|
*
|
|
* @param string $column
|
|
* @param mixed $values
|
|
* @return $this
|
|
*/
|
|
public function orWherePivotIn($column, $values)
|
|
{
|
|
return $this->wherePivotIn($column, $values, 'or');
|
|
}
|
|
|
|
/**
|
|
* Set a "where not in" clause for a pivot table column.
|
|
*
|
|
* @param string $column
|
|
* @param mixed $values
|
|
* @param string $boolean
|
|
* @return $this
|
|
*/
|
|
public function wherePivotNotIn($column, $values, $boolean = 'and')
|
|
{
|
|
return $this->wherePivotIn($column, $values, $boolean, true);
|
|
}
|
|
|
|
/**
|
|
* Set an "or where not in" clause for a pivot table column.
|
|
*
|
|
* @param string $column
|
|
* @param mixed $values
|
|
* @return $this
|
|
*/
|
|
public function orWherePivotNotIn($column, $values)
|
|
{
|
|
return $this->wherePivotNotIn($column, $values, 'or');
|
|
}
|
|
|
|
/**
|
|
* Set a "where null" clause for a pivot table column.
|
|
*
|
|
* @param string $column
|
|
* @param string $boolean
|
|
* @param bool $not
|
|
* @return $this
|
|
*/
|
|
public function wherePivotNull($column, $boolean = 'and', $not = false)
|
|
{
|
|
$this->pivotWhereNulls[] = func_get_args();
|
|
|
|
return $this->whereNull($this->qualifyPivotColumn($column), $boolean, $not);
|
|
}
|
|
|
|
/**
|
|
* Set a "where not null" clause for a pivot table column.
|
|
*
|
|
* @param string $column
|
|
* @param string $boolean
|
|
* @return $this
|
|
*/
|
|
public function wherePivotNotNull($column, $boolean = 'and')
|
|
{
|
|
return $this->wherePivotNull($column, $boolean, true);
|
|
}
|
|
|
|
/**
|
|
* Set a "or where null" clause for a pivot table column.
|
|
*
|
|
* @param string $column
|
|
* @param bool $not
|
|
* @return $this
|
|
*/
|
|
public function orWherePivotNull($column, $not = false)
|
|
{
|
|
return $this->wherePivotNull($column, 'or', $not);
|
|
}
|
|
|
|
/**
|
|
* Set a "or where not null" clause for a pivot table column.
|
|
*
|
|
* @param string $column
|
|
* @return $this
|
|
*/
|
|
public function orWherePivotNotNull($column)
|
|
{
|
|
return $this->orWherePivotNull($column, true);
|
|
}
|
|
|
|
/**
|
|
* Add an "order by" clause for a pivot table column.
|
|
*
|
|
* @param string $column
|
|
* @param string $direction
|
|
* @return $this
|
|
*/
|
|
public function orderByPivot($column, $direction = 'asc')
|
|
{
|
|
return $this->orderBy($this->qualifyPivotColumn($column), $direction);
|
|
}
|
|
|
|
/**
|
|
* Find a related model by its primary key or return a new instance of the related model.
|
|
*
|
|
* @param mixed $id
|
|
* @param array $columns
|
|
* @return \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model
|
|
*/
|
|
public function findOrNew($id, $columns = ['*'])
|
|
{
|
|
if (is_null($instance = $this->find($id, $columns))) {
|
|
$instance = $this->related->newInstance();
|
|
}
|
|
|
|
return $instance;
|
|
}
|
|
|
|
/**
|
|
* Get the first related model record matching the attributes or instantiate it.
|
|
*
|
|
* @param array $attributes
|
|
* @param array $values
|
|
* @return \Illuminate\Database\Eloquent\Model
|
|
*/
|
|
public function firstOrNew(array $attributes = [], array $values = [])
|
|
{
|
|
if (is_null($instance = $this->related->where($attributes)->first())) {
|
|
$instance = $this->related->newInstance(array_merge($attributes, $values));
|
|
}
|
|
|
|
return $instance;
|
|
}
|
|
|
|
/**
|
|
* Get the first related record matching the attributes or create it.
|
|
*
|
|
* @param array $attributes
|
|
* @param array $values
|
|
* @param array $joining
|
|
* @param bool $touch
|
|
* @return \Illuminate\Database\Eloquent\Model
|
|
*/
|
|
public function firstOrCreate(array $attributes = [], array $values = [], array $joining = [], $touch = true)
|
|
{
|
|
if (is_null($instance = (clone $this)->where($attributes)->first())) {
|
|
if (is_null($instance = $this->related->where($attributes)->first())) {
|
|
$instance = $this->create(array_merge($attributes, $values), $joining, $touch);
|
|
} else {
|
|
$this->attach($instance, $joining, $touch);
|
|
}
|
|
}
|
|
|
|
return $instance;
|
|
}
|
|
|
|
/**
|
|
* Create or update a related record matching the attributes, and fill it with values.
|
|
*
|
|
* @param array $attributes
|
|
* @param array $values
|
|
* @param array $joining
|
|
* @param bool $touch
|
|
* @return \Illuminate\Database\Eloquent\Model
|
|
*/
|
|
public function updateOrCreate(array $attributes, array $values = [], array $joining = [], $touch = true)
|
|
{
|
|
if (is_null($instance = (clone $this)->where($attributes)->first())) {
|
|
if (is_null($instance = $this->related->where($attributes)->first())) {
|
|
return $this->create(array_merge($attributes, $values), $joining, $touch);
|
|
} else {
|
|
$this->attach($instance, $joining, $touch);
|
|
}
|
|
}
|
|
|
|
$instance->fill($values);
|
|
|
|
$instance->save(['touch' => false]);
|
|
|
|
return $instance;
|
|
}
|
|
|
|
/**
|
|
* Find a related model by its primary key.
|
|
*
|
|
* @param mixed $id
|
|
* @param array $columns
|
|
* @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|null
|
|
*/
|
|
public function find($id, $columns = ['*'])
|
|
{
|
|
if (! $id instanceof Model && (is_array($id) || $id instanceof Arrayable)) {
|
|
return $this->findMany($id, $columns);
|
|
}
|
|
|
|
return $this->where(
|
|
$this->getRelated()->getQualifiedKeyName(), '=', $this->parseId($id)
|
|
)->first($columns);
|
|
}
|
|
|
|
/**
|
|
* Find multiple related models by their primary keys.
|
|
*
|
|
* @param \Illuminate\Contracts\Support\Arrayable|array $ids
|
|
* @param array $columns
|
|
* @return \Illuminate\Database\Eloquent\Collection
|
|
*/
|
|
public function findMany($ids, $columns = ['*'])
|
|
{
|
|
$ids = $ids instanceof Arrayable ? $ids->toArray() : $ids;
|
|
|
|
if (empty($ids)) {
|
|
return $this->getRelated()->newCollection();
|
|
}
|
|
|
|
return $this->whereKey(
|
|
$this->parseIds($ids)
|
|
)->get($columns);
|
|
}
|
|
|
|
/**
|
|
* Find a related model by its primary key or throw an exception.
|
|
*
|
|
* @param mixed $id
|
|
* @param array $columns
|
|
* @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection
|
|
*
|
|
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException<\Illuminate\Database\Eloquent\Model>
|
|
*/
|
|
public function findOrFail($id, $columns = ['*'])
|
|
{
|
|
$result = $this->find($id, $columns);
|
|
|
|
$id = $id instanceof Arrayable ? $id->toArray() : $id;
|
|
|
|
if (is_array($id)) {
|
|
if (count($result) === count(array_unique($id))) {
|
|
return $result;
|
|
}
|
|
} elseif (! is_null($result)) {
|
|
return $result;
|
|
}
|
|
|
|
throw (new ModelNotFoundException)->setModel(get_class($this->related), $id);
|
|
}
|
|
|
|
/**
|
|
* Find a related model by its primary key or call a callback.
|
|
*
|
|
* @param mixed $id
|
|
* @param \Closure|array $columns
|
|
* @param \Closure|null $callback
|
|
* @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|mixed
|
|
*/
|
|
public function findOr($id, $columns = ['*'], Closure $callback = null)
|
|
{
|
|
if ($columns instanceof Closure) {
|
|
$callback = $columns;
|
|
|
|
$columns = ['*'];
|
|
}
|
|
|
|
$result = $this->find($id, $columns);
|
|
|
|
$id = $id instanceof Arrayable ? $id->toArray() : $id;
|
|
|
|
if (is_array($id)) {
|
|
if (count($result) === count(array_unique($id))) {
|
|
return $result;
|
|
}
|
|
} elseif (! is_null($result)) {
|
|
return $result;
|
|
}
|
|
|
|
return $callback();
|
|
}
|
|
|
|
/**
|
|
* Add a basic where clause to the query, and return the first result.
|
|
*
|
|
* @param \Closure|string|array $column
|
|
* @param mixed $operator
|
|
* @param mixed $value
|
|
* @param string $boolean
|
|
* @return \Illuminate\Database\Eloquent\Model|static
|
|
*/
|
|
public function firstWhere($column, $operator = null, $value = null, $boolean = 'and')
|
|
{
|
|
return $this->where($column, $operator, $value, $boolean)->first();
|
|
}
|
|
|
|
/**
|
|
* Execute the query and get the first result.
|
|
*
|
|
* @param array $columns
|
|
* @return mixed
|
|
*/
|
|
public function first($columns = ['*'])
|
|
{
|
|
$results = $this->take(1)->get($columns);
|
|
|
|
return count($results) > 0 ? $results->first() : null;
|
|
}
|
|
|
|
/**
|
|
* Execute the query and get the first result or throw an exception.
|
|
*
|
|
* @param array $columns
|
|
* @return \Illuminate\Database\Eloquent\Model|static
|
|
*
|
|
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException<\Illuminate\Database\Eloquent\Model>
|
|
*/
|
|
public function firstOrFail($columns = ['*'])
|
|
{
|
|
if (! is_null($model = $this->first($columns))) {
|
|
return $model;
|
|
}
|
|
|
|
throw (new ModelNotFoundException)->setModel(get_class($this->related));
|
|
}
|
|
|
|
/**
|
|
* Execute the query and get the first result or call a callback.
|
|
*
|
|
* @param \Closure|array $columns
|
|
* @param \Closure|null $callback
|
|
* @return \Illuminate\Database\Eloquent\Model|static|mixed
|
|
*/
|
|
public function firstOr($columns = ['*'], Closure $callback = null)
|
|
{
|
|
if ($columns instanceof Closure) {
|
|
$callback = $columns;
|
|
|
|
$columns = ['*'];
|
|
}
|
|
|
|
if (! is_null($model = $this->first($columns))) {
|
|
return $model;
|
|
}
|
|
|
|
return $callback();
|
|
}
|
|
|
|
/**
|
|
* Get the results of the relationship.
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function getResults()
|
|
{
|
|
return ! is_null($this->parent->{$this->parentKey})
|
|
? $this->get()
|
|
: $this->related->newCollection();
|
|
}
|
|
|
|
/**
|
|
* Execute the query as a "select" statement.
|
|
*
|
|
* @param array $columns
|
|
* @return \Illuminate\Database\Eloquent\Collection
|
|
*/
|
|
public function get($columns = ['*'])
|
|
{
|
|
// First we'll add the proper select columns onto the query so it is run with
|
|
// the proper columns. Then, we will get the results and hydrate our pivot
|
|
// models with the result of those columns as a separate model relation.
|
|
$builder = $this->query->applyScopes();
|
|
|
|
$columns = $builder->getQuery()->columns ? [] : $columns;
|
|
|
|
$models = $builder->addSelect(
|
|
$this->shouldSelect($columns)
|
|
)->getModels();
|
|
|
|
$this->hydratePivotRelation($models);
|
|
|
|
// If we actually found models we will also eager load any relationships that
|
|
// have been specified as needing to be eager loaded. This will solve the
|
|
// n + 1 query problem for the developer and also increase performance.
|
|
if (count($models) > 0) {
|
|
$models = $builder->eagerLoadRelations($models);
|
|
}
|
|
|
|
return $this->related->newCollection($models);
|
|
}
|
|
|
|
/**
|
|
* Get the select columns for the relation query.
|
|
*
|
|
* @param array $columns
|
|
* @return array
|
|
*/
|
|
protected function shouldSelect(array $columns = ['*'])
|
|
{
|
|
if ($columns == ['*']) {
|
|
$columns = [$this->related->getTable().'.*'];
|
|
}
|
|
|
|
return array_merge($columns, $this->aliasedPivotColumns());
|
|
}
|
|
|
|
/**
|
|
* Get the pivot columns for the relation.
|
|
*
|
|
* "pivot_" is prefixed at each column for easy removal later.
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function aliasedPivotColumns()
|
|
{
|
|
$defaults = [$this->foreignPivotKey, $this->relatedPivotKey];
|
|
|
|
return collect(array_merge($defaults, $this->pivotColumns))->map(function ($column) {
|
|
return $this->qualifyPivotColumn($column).' as pivot_'.$column;
|
|
})->unique()->all();
|
|
}
|
|
|
|
/**
|
|
* Get a paginator for the "select" statement.
|
|
*
|
|
* @param int|null $perPage
|
|
* @param array $columns
|
|
* @param string $pageName
|
|
* @param int|null $page
|
|
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
|
*/
|
|
public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
|
|
{
|
|
$this->query->addSelect($this->shouldSelect($columns));
|
|
|
|
return tap($this->query->paginate($perPage, $columns, $pageName, $page), function ($paginator) {
|
|
$this->hydratePivotRelation($paginator->items());
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Paginate the given query into a simple paginator.
|
|
*
|
|
* @param int|null $perPage
|
|
* @param array $columns
|
|
* @param string $pageName
|
|
* @param int|null $page
|
|
* @return \Illuminate\Contracts\Pagination\Paginator
|
|
*/
|
|
public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
|
|
{
|
|
$this->query->addSelect($this->shouldSelect($columns));
|
|
|
|
return tap($this->query->simplePaginate($perPage, $columns, $pageName, $page), function ($paginator) {
|
|
$this->hydratePivotRelation($paginator->items());
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Paginate the given query into a cursor paginator.
|
|
*
|
|
* @param int|null $perPage
|
|
* @param array $columns
|
|
* @param string $cursorName
|
|
* @param string|null $cursor
|
|
* @return \Illuminate\Contracts\Pagination\CursorPaginator
|
|
*/
|
|
public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
|
|
{
|
|
$this->query->addSelect($this->shouldSelect($columns));
|
|
|
|
return tap($this->query->cursorPaginate($perPage, $columns, $cursorName, $cursor), function ($paginator) {
|
|
$this->hydratePivotRelation($paginator->items());
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Chunk the results of the query.
|
|
*
|
|
* @param int $count
|
|
* @param callable $callback
|
|
* @return bool
|
|
*/
|
|
public function chunk($count, callable $callback)
|
|
{
|
|
return $this->prepareQueryBuilder()->chunk($count, function ($results, $page) use ($callback) {
|
|
$this->hydratePivotRelation($results->all());
|
|
|
|
return $callback($results, $page);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Chunk the results of a query by comparing numeric IDs.
|
|
*
|
|
* @param int $count
|
|
* @param callable $callback
|
|
* @param string|null $column
|
|
* @param string|null $alias
|
|
* @return bool
|
|
*/
|
|
public function chunkById($count, callable $callback, $column = null, $alias = null)
|
|
{
|
|
$this->prepareQueryBuilder();
|
|
|
|
$column ??= $this->getRelated()->qualifyColumn(
|
|
$this->getRelatedKeyName()
|
|
);
|
|
|
|
$alias ??= $this->getRelatedKeyName();
|
|
|
|
return $this->query->chunkById($count, function ($results) use ($callback) {
|
|
$this->hydratePivotRelation($results->all());
|
|
|
|
return $callback($results);
|
|
}, $column, $alias);
|
|
}
|
|
|
|
/**
|
|
* Execute a callback over each item while chunking.
|
|
*
|
|
* @param callable $callback
|
|
* @param int $count
|
|
* @return bool
|
|
*/
|
|
public function each(callable $callback, $count = 1000)
|
|
{
|
|
return $this->chunk($count, function ($results) use ($callback) {
|
|
foreach ($results as $key => $value) {
|
|
if ($callback($value, $key) === false) {
|
|
return false;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Query lazily, by chunks of the given size.
|
|
*
|
|
* @param int $chunkSize
|
|
* @return \Illuminate\Support\LazyCollection
|
|
*/
|
|
public function lazy($chunkSize = 1000)
|
|
{
|
|
return $this->prepareQueryBuilder()->lazy($chunkSize)->map(function ($model) {
|
|
$this->hydratePivotRelation([$model]);
|
|
|
|
return $model;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Query lazily, by chunking the results of a query by comparing IDs.
|
|
*
|
|
* @param int $chunkSize
|
|
* @param string|null $column
|
|
* @param string|null $alias
|
|
* @return \Illuminate\Support\LazyCollection
|
|
*/
|
|
public function lazyById($chunkSize = 1000, $column = null, $alias = null)
|
|
{
|
|
$column ??= $this->getRelated()->qualifyColumn(
|
|
$this->getRelatedKeyName()
|
|
);
|
|
|
|
$alias ??= $this->getRelatedKeyName();
|
|
|
|
return $this->prepareQueryBuilder()->lazyById($chunkSize, $column, $alias)->map(function ($model) {
|
|
$this->hydratePivotRelation([$model]);
|
|
|
|
return $model;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get a lazy collection for the given query.
|
|
*
|
|
* @return \Illuminate\Support\LazyCollection
|
|
*/
|
|
public function cursor()
|
|
{
|
|
return $this->prepareQueryBuilder()->cursor()->map(function ($model) {
|
|
$this->hydratePivotRelation([$model]);
|
|
|
|
return $model;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Prepare the query builder for query execution.
|
|
*
|
|
* @return \Illuminate\Database\Eloquent\Builder
|
|
*/
|
|
protected function prepareQueryBuilder()
|
|
{
|
|
return $this->query->addSelect($this->shouldSelect());
|
|
}
|
|
|
|
/**
|
|
* Hydrate the pivot table relationship on the models.
|
|
*
|
|
* @param array $models
|
|
* @return void
|
|
*/
|
|
protected function hydratePivotRelation(array $models)
|
|
{
|
|
// To hydrate the pivot relationship, we will just gather the pivot attributes
|
|
// and create a new Pivot model, which is basically a dynamic model that we
|
|
// will set the attributes, table, and connections on it so it will work.
|
|
foreach ($models as $model) {
|
|
$model->setRelation($this->accessor, $this->newExistingPivot(
|
|
$this->migratePivotAttributes($model)
|
|
));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the pivot attributes from a model.
|
|
*
|
|
* @param \Illuminate\Database\Eloquent\Model $model
|
|
* @return array
|
|
*/
|
|
protected function migratePivotAttributes(Model $model)
|
|
{
|
|
$values = [];
|
|
|
|
foreach ($model->getAttributes() as $key => $value) {
|
|
// To get the pivots attributes we will just take any of the attributes which
|
|
// begin with "pivot_" and add those to this arrays, as well as unsetting
|
|
// them from the parent's models since they exist in a different table.
|
|
if (str_starts_with($key, 'pivot_')) {
|
|
$values[substr($key, 6)] = $value;
|
|
|
|
unset($model->$key);
|
|
}
|
|
}
|
|
|
|
return $values;
|
|
}
|
|
|
|
/**
|
|
* If we're touching the parent model, touch.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function touchIfTouching()
|
|
{
|
|
if ($this->touchingParent()) {
|
|
$this->getParent()->touch();
|
|
}
|
|
|
|
if ($this->getParent()->touches($this->relationName)) {
|
|
$this->touch();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine if we should touch the parent on sync.
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function touchingParent()
|
|
{
|
|
return $this->getRelated()->touches($this->guessInverseRelation());
|
|
}
|
|
|
|
/**
|
|
* Attempt to guess the name of the inverse of the relation.
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function guessInverseRelation()
|
|
{
|
|
return Str::camel(Str::pluralStudly(class_basename($this->getParent())));
|
|
}
|
|
|
|
/**
|
|
* Touch all of the related models for the relationship.
|
|
*
|
|
* E.g.: Touch all roles associated with this user.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function touch()
|
|
{
|
|
$columns = [
|
|
$this->related->getUpdatedAtColumn() => $this->related->freshTimestampString(),
|
|
];
|
|
|
|
// If we actually have IDs for the relation, we will run the query to update all
|
|
// the related model's timestamps, to make sure these all reflect the changes
|
|
// to the parent models. This will help us keep any caching synced up here.
|
|
if (count($ids = $this->allRelatedIds()) > 0) {
|
|
$this->getRelated()->newQueryWithoutRelationships()->whereKey($ids)->update($columns);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all of the IDs for the related models.
|
|
*
|
|
* @return \Illuminate\Support\Collection
|
|
*/
|
|
public function allRelatedIds()
|
|
{
|
|
return $this->newPivotQuery()->pluck($this->relatedPivotKey);
|
|
}
|
|
|
|
/**
|
|
* Save a new model and attach it to the parent model.
|
|
*
|
|
* @param \Illuminate\Database\Eloquent\Model $model
|
|
* @param array $pivotAttributes
|
|
* @param bool $touch
|
|
* @return \Illuminate\Database\Eloquent\Model
|
|
*/
|
|
public function save(Model $model, array $pivotAttributes = [], $touch = true)
|
|
{
|
|
$model->save(['touch' => false]);
|
|
|
|
$this->attach($model, $pivotAttributes, $touch);
|
|
|
|
return $model;
|
|
}
|
|
|
|
/**
|
|
* Save a new model without raising any events and attach it to the parent model.
|
|
*
|
|
* @param \Illuminate\Database\Eloquent\Model $model
|
|
* @param array $pivotAttributes
|
|
* @param bool $touch
|
|
* @return \Illuminate\Database\Eloquent\Model
|
|
*/
|
|
public function saveQuietly(Model $model, array $pivotAttributes = [], $touch = true)
|
|
{
|
|
return Model::withoutEvents(function () use ($model, $pivotAttributes, $touch) {
|
|
return $this->save($model, $pivotAttributes, $touch);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Save an array of new models and attach them to the parent model.
|
|
*
|
|
* @param \Illuminate\Support\Collection|array $models
|
|
* @param array $pivotAttributes
|
|
* @return array
|
|
*/
|
|
public function saveMany($models, array $pivotAttributes = [])
|
|
{
|
|
foreach ($models as $key => $model) {
|
|
$this->save($model, (array) ($pivotAttributes[$key] ?? []), false);
|
|
}
|
|
|
|
$this->touchIfTouching();
|
|
|
|
return $models;
|
|
}
|
|
|
|
/**
|
|
* Save an array of new models without raising any events and attach them to the parent model.
|
|
*
|
|
* @param \Illuminate\Support\Collection|array $models
|
|
* @param array $pivotAttributes
|
|
* @return array
|
|
*/
|
|
public function saveManyQuietly($models, array $pivotAttributes = [])
|
|
{
|
|
return Model::withoutEvents(function () use ($models, $pivotAttributes) {
|
|
return $this->saveMany($models, $pivotAttributes);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a new instance of the related model.
|
|
*
|
|
* @param array $attributes
|
|
* @param array $joining
|
|
* @param bool $touch
|
|
* @return \Illuminate\Database\Eloquent\Model
|
|
*/
|
|
public function create(array $attributes = [], array $joining = [], $touch = true)
|
|
{
|
|
$instance = $this->related->newInstance($attributes);
|
|
|
|
// Once we save the related model, we need to attach it to the base model via
|
|
// through intermediate table so we'll use the existing "attach" method to
|
|
// accomplish this which will insert the record and any more attributes.
|
|
$instance->save(['touch' => false]);
|
|
|
|
$this->attach($instance, $joining, $touch);
|
|
|
|
return $instance;
|
|
}
|
|
|
|
/**
|
|
* Create an array of new instances of the related models.
|
|
*
|
|
* @param iterable $records
|
|
* @param array $joinings
|
|
* @return array
|
|
*/
|
|
public function createMany(iterable $records, array $joinings = [])
|
|
{
|
|
$instances = [];
|
|
|
|
foreach ($records as $key => $record) {
|
|
$instances[] = $this->create($record, (array) ($joinings[$key] ?? []), false);
|
|
}
|
|
|
|
$this->touchIfTouching();
|
|
|
|
return $instances;
|
|
}
|
|
|
|
/**
|
|
* Add the constraints for a relationship query.
|
|
*
|
|
* @param \Illuminate\Database\Eloquent\Builder $query
|
|
* @param \Illuminate\Database\Eloquent\Builder $parentQuery
|
|
* @param array|mixed $columns
|
|
* @return \Illuminate\Database\Eloquent\Builder
|
|
*/
|
|
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
|
|
{
|
|
if ($parentQuery->getQuery()->from == $query->getQuery()->from) {
|
|
return $this->getRelationExistenceQueryForSelfJoin($query, $parentQuery, $columns);
|
|
}
|
|
|
|
$this->performJoin($query);
|
|
|
|
return parent::getRelationExistenceQuery($query, $parentQuery, $columns);
|
|
}
|
|
|
|
/**
|
|
* Add the constraints for a relationship query on the same table.
|
|
*
|
|
* @param \Illuminate\Database\Eloquent\Builder $query
|
|
* @param \Illuminate\Database\Eloquent\Builder $parentQuery
|
|
* @param array|mixed $columns
|
|
* @return \Illuminate\Database\Eloquent\Builder
|
|
*/
|
|
public function getRelationExistenceQueryForSelfJoin(Builder $query, Builder $parentQuery, $columns = ['*'])
|
|
{
|
|
$query->select($columns);
|
|
|
|
$query->from($this->related->getTable().' as '.$hash = $this->getRelationCountHash());
|
|
|
|
$this->related->setTable($hash);
|
|
|
|
$this->performJoin($query);
|
|
|
|
return parent::getRelationExistenceQuery($query, $parentQuery, $columns);
|
|
}
|
|
|
|
/**
|
|
* Get the key for comparing against the parent key in "has" query.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getExistenceCompareKey()
|
|
{
|
|
return $this->getQualifiedForeignPivotKeyName();
|
|
}
|
|
|
|
/**
|
|
* Specify that the pivot table has creation and update timestamps.
|
|
*
|
|
* @param mixed $createdAt
|
|
* @param mixed $updatedAt
|
|
* @return $this
|
|
*/
|
|
public function withTimestamps($createdAt = null, $updatedAt = null)
|
|
{
|
|
$this->withTimestamps = true;
|
|
|
|
$this->pivotCreatedAt = $createdAt;
|
|
$this->pivotUpdatedAt = $updatedAt;
|
|
|
|
return $this->withPivot($this->createdAt(), $this->updatedAt());
|
|
}
|
|
|
|
/**
|
|
* Get the name of the "created at" column.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function createdAt()
|
|
{
|
|
return $this->pivotCreatedAt ?: $this->parent->getCreatedAtColumn();
|
|
}
|
|
|
|
/**
|
|
* Get the name of the "updated at" column.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function updatedAt()
|
|
{
|
|
return $this->pivotUpdatedAt ?: $this->parent->getUpdatedAtColumn();
|
|
}
|
|
|
|
/**
|
|
* Get the foreign key for the relation.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getForeignPivotKeyName()
|
|
{
|
|
return $this->foreignPivotKey;
|
|
}
|
|
|
|
/**
|
|
* Get the fully qualified foreign key for the relation.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getQualifiedForeignPivotKeyName()
|
|
{
|
|
return $this->qualifyPivotColumn($this->foreignPivotKey);
|
|
}
|
|
|
|
/**
|
|
* Get the "related key" for the relation.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getRelatedPivotKeyName()
|
|
{
|
|
return $this->relatedPivotKey;
|
|
}
|
|
|
|
/**
|
|
* Get the fully qualified "related key" for the relation.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getQualifiedRelatedPivotKeyName()
|
|
{
|
|
return $this->qualifyPivotColumn($this->relatedPivotKey);
|
|
}
|
|
|
|
/**
|
|
* Get the parent key for the relationship.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getParentKeyName()
|
|
{
|
|
return $this->parentKey;
|
|
}
|
|
|
|
/**
|
|
* Get the fully qualified parent key name for the relation.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getQualifiedParentKeyName()
|
|
{
|
|
return $this->parent->qualifyColumn($this->parentKey);
|
|
}
|
|
|
|
/**
|
|
* Get the related key for the relationship.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getRelatedKeyName()
|
|
{
|
|
return $this->relatedKey;
|
|
}
|
|
|
|
/**
|
|
* Get the fully qualified related key name for the relation.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getQualifiedRelatedKeyName()
|
|
{
|
|
return $this->related->qualifyColumn($this->relatedKey);
|
|
}
|
|
|
|
/**
|
|
* Get the intermediate table for the relationship.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getTable()
|
|
{
|
|
return $this->table;
|
|
}
|
|
|
|
/**
|
|
* Get the relationship name for the relationship.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getRelationName()
|
|
{
|
|
return $this->relationName;
|
|
}
|
|
|
|
/**
|
|
* Get the name of the pivot accessor for this relationship.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getPivotAccessor()
|
|
{
|
|
return $this->accessor;
|
|
}
|
|
|
|
/**
|
|
* Get the pivot columns for this relationship.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getPivotColumns()
|
|
{
|
|
return $this->pivotColumns;
|
|
}
|
|
|
|
/**
|
|
* Qualify the given column name by the pivot table.
|
|
*
|
|
* @param string $column
|
|
* @return string
|
|
*/
|
|
public function qualifyPivotColumn($column)
|
|
{
|
|
return str_contains($column, '.')
|
|
? $column
|
|
: $this->table.'.'.$column;
|
|
}
|
|
}
|