| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646 |
- <?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\Database\Query\Grammars\MySqlGrammar;
- use Illuminate\Database\UniqueConstraintViolationException;
- 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<string|\Illuminate\Contracts\Database\Query\Expression>
- */
- 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|class-string<\Illuminate\Database\Eloquent\Model> $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->whereInEager(
- $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|\Illuminate\Contracts\Database\Query\Expression $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|\Illuminate\Contracts\Database\Query\Expression $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|\Illuminate\Contracts\Database\Query\Expression $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|\Illuminate\Contracts\Database\Query\Expression $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|\Illuminate\Contracts\Database\Query\Expression $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|\Illuminate\Contracts\Database\Query\Expression $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|\Illuminate\Contracts\Database\Query\Expression $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|\Illuminate\Contracts\Database\Query\Expression|array<string, string> $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|\Illuminate\Contracts\Database\Query\Expression $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|\Illuminate\Contracts\Database\Query\Expression $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|\Illuminate\Contracts\Database\Query\Expression $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|\Illuminate\Contracts\Database\Query\Expression $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|\Illuminate\Contracts\Database\Query\Expression $column
- * @return $this
- */
- public function orWherePivotNotNull($column)
- {
- return $this->orWherePivotNull($column, true);
- }
- /**
- * Add an "order by" clause for a pivot table column.
- *
- * @param string|\Illuminate\Contracts\Database\Query\Expression $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 record matching the attributes. If the record is not found, 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->createOrFirst($attributes, $values, $joining, $touch);
- } else {
- try {
- $this->getQuery()->withSavepointIfNeeded(fn () => $this->attach($instance, $joining, $touch));
- } catch (UniqueConstraintViolationException) {
- // Nothing to do, the model was already attached...
- }
- }
- }
- return $instance;
- }
- /**
- * Attempt to create the record. If a unique constraint violation occurs, attempt to find the matching record.
- *
- * @param array $attributes
- * @param array $values
- * @param array $joining
- * @param bool $touch
- * @return \Illuminate\Database\Eloquent\Model
- */
- public function createOrFirst(array $attributes = [], array $values = [], array $joining = [], $touch = true)
- {
- try {
- return $this->getQuery()->withSavePointIfNeeded(fn () => $this->create(array_merge($attributes, $values), $joining, $touch));
- } catch (UniqueConstraintViolationException $e) {
- // ...
- }
- try {
- return tap($this->related->where($attributes)->first() ?? throw $e, function ($instance) use ($joining, $touch) {
- $this->getQuery()->withSavepointIfNeeded(fn () => $this->attach($instance, $joining, $touch));
- });
- } catch (UniqueConstraintViolationException $e) {
- return (clone $this)->useWritePdo()->where($attributes)->first() ?? throw $e;
- }
- }
- /**
- * 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)
- {
- return tap($this->firstOrCreate($attributes, $values, $joining, $touch), function ($instance) use ($values) {
- if (! $instance->wasRecentlyCreated) {
- $instance->fill($values);
- $instance->save(['touch' => false]);
- }
- });
- }
- /**
- * 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|null
- */
- 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 \Illuminate\Database\Eloquent\Model|static|null
- */
- 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->query->applyAfterQueryCallbacks(
- $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)
- {
- return $this->orderedChunkById($count, $callback, $column, $alias);
- }
- /**
- * Chunk the results of a query by comparing IDs in descending order.
- *
- * @param int $count
- * @param callable $callback
- * @param string|null $column
- * @param string|null $alias
- * @return bool
- */
- public function chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
- {
- return $this->orderedChunkById($count, $callback, $column, $alias, descending: true);
- }
- /**
- * Execute a callback over each item while chunking by ID.
- *
- * @param callable $callback
- * @param int $count
- * @param string|null $column
- * @param string|null $alias
- * @return bool
- */
- public function eachById(callable $callback, $count = 1000, $column = null, $alias = null)
- {
- return $this->chunkById($count, function ($results, $page) use ($callback, $count) {
- foreach ($results as $key => $value) {
- if ($callback($value, (($page - 1) * $count) + $key) === false) {
- return false;
- }
- }
- }, $column, $alias);
- }
- /**
- * Chunk the results of a query by comparing IDs in a given order.
- *
- * @param int $count
- * @param callable $callback
- * @param string|null $column
- * @param string|null $alias
- * @param bool $descending
- * @return bool
- */
- public function orderedChunkById($count, callable $callback, $column = null, $alias = null, $descending = false)
- {
- $column ??= $this->getRelated()->qualifyColumn(
- $this->getRelatedKeyName()
- );
- $alias ??= $this->getRelatedKeyName();
- return $this->prepareQueryBuilder()->orderedChunkById($count, function ($results, $page) use ($callback) {
- $this->hydratePivotRelation($results->all());
- return $callback($results, $page);
- }, $column, $alias, $descending);
- }
- /**
- * 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;
- });
- }
- /**
- * Query lazily, by chunking the results of a query by comparing IDs in descending order.
- *
- * @param int $chunkSize
- * @param string|null $column
- * @param string|null $alias
- * @return \Illuminate\Support\LazyCollection
- */
- public function lazyByIdDesc($chunkSize = 1000, $column = null, $alias = null)
- {
- $column ??= $this->getRelated()->qualifyColumn(
- $this->getRelatedKeyName()
- );
- $alias ??= $this->getRelatedKeyName();
- return $this->prepareQueryBuilder()->lazyByIdDesc($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()
- {
- if ($this->related->isIgnoringTouch()) {
- return;
- }
- $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);
- }
- /**
- * Alias to set the "limit" value of the query.
- *
- * @param int $value
- * @return $this
- */
- public function take($value)
- {
- return $this->limit($value);
- }
- /**
- * Set the "limit" value of the query.
- *
- * @param int $value
- * @return $this
- */
- public function limit($value)
- {
- if ($this->parent->exists) {
- $this->query->limit($value);
- } else {
- $column = $this->getExistenceCompareKey();
- $grammar = $this->query->getQuery()->getGrammar();
- if ($grammar instanceof MySqlGrammar && $grammar->useLegacyGroupLimit($this->query->getQuery())) {
- $column = 'pivot_'.last(explode('.', $column));
- }
- $this->query->groupLimit($value, $column);
- }
- return $this;
- }
- /**
- * 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|\Illuminate\Contracts\Database\Query\Expression $column
- * @return string|\Illuminate\Contracts\Database\Query\Expression
- */
- public function qualifyPivotColumn($column)
- {
- if ($this->query->getQuery()->getGrammar()->isExpression($column)) {
- return $column;
- }
- return str_contains($column, '.')
- ? $column
- : $this->table.'.'.$column;
- }
- }
|