From 63916f8ce878f742aaf2a88627092779cddc68ee Mon Sep 17 00:00:00 2001 From: bastien-phi Date: Mon, 17 May 2021 15:31:29 +0200 Subject: [PATCH 01/34] [8.x] Add loadExists on Model and Eloquent Collection (#37388) * Add loadExists on Model and Eloquent Collection * Update Collection.php Co-authored-by: Taylor Otwell --- Eloquent/Collection.php | 11 +++++++++++ Eloquent/Model.php | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/Eloquent/Collection.php b/Eloquent/Collection.php index 05c785eff..e8435ce3d 100755 --- a/Eloquent/Collection.php +++ b/Eloquent/Collection.php @@ -157,6 +157,17 @@ public function loadAvg($relations, $column) return $this->loadAggregate($relations, $column, 'avg'); } + /** + * Load a set of related existences onto the collection. + * + * @param array|string $relations + * @return $this + */ + public function loadExists($relations) + { + return $this->loadAggregate($relations, '*', 'exists'); + } + /** * Load a set of relationships onto the collection if they are not already eager loaded. * diff --git a/Eloquent/Model.php b/Eloquent/Model.php index bc2126514..6005ce5f3 100644 --- a/Eloquent/Model.php +++ b/Eloquent/Model.php @@ -619,6 +619,17 @@ public function loadAvg($relations, $column) return $this->loadAggregate($relations, $column, 'avg'); } + /** + * Eager load related model existence values on the model. + * + * @param array|string $relations + * @return $this + */ + public function loadExists($relations) + { + return $this->loadAggregate($relations, '*', 'exists'); + } + /** * Eager load relationship column aggregation on the polymorphic relation of a model. * From a022beeacf45498afba8b1bf8ad80e1d59f77264 Mon Sep 17 00:00:00 2001 From: Lennart Carstens-Behrens Date: Mon, 17 May 2021 23:13:45 +0200 Subject: [PATCH 02/34] [8.x] Add one-of-many relationship (inner join) (#37362) * added one-of-many to has-one * Apply fixes from StyleCI * fixed getResults * added query methods to forwardToOneOfManyQuery * Apply fixes from StyleCI * improvements & tests * Apply fixes from StyleCI * use where or having * Apply fixes from StyleCI * join * wip * wip * fixes style * updated contract * multiple aggregastes * Apply fixes from StyleCI * formatting * formatting * Apply fixes from StyleCI * formatting * rename class * add file * rename array key * add of-many to morph-one * Apply fixes from StyleCI * fixed pivot test * Apply fixes from StyleCI * fixed return type * formatting * add shortcut methods * move test * multiple columns in shortcut * Apply fixes from StyleCI * add key when missing * Apply fixes from StyleCI * use collections * fail for invalid aggregates * Apply fixes from StyleCI * formatting Co-authored-by: Taylor Otwell --- .../Relations/Concerns/CanBeOneOfMany.php | 237 ++++++++++++++++++ .../Concerns/ComparesRelatedModels.php | 11 +- Eloquent/Relations/HasOne.php | 65 ++++- Eloquent/Relations/MorphOne.php | 59 ++++- 4 files changed, 367 insertions(+), 5 deletions(-) create mode 100644 Eloquent/Relations/Concerns/CanBeOneOfMany.php diff --git a/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/Eloquent/Relations/Concerns/CanBeOneOfMany.php new file mode 100644 index 000000000..ea1afb539 --- /dev/null +++ b/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -0,0 +1,237 @@ +isOneOfMany = true; + + $this->relationName = $relation ?: $this->guessRelationship(); + + $keyName = $this->query->getModel()->getKeyName(); + + $columns = is_string($columns = $column) ? [ + $column => $aggregate, + $keyName => $aggregate, + ] : $column; + + if (! array_key_exists($keyName, $columns)) { + $columns[$keyName] = 'MAX'; + } + + if ($aggregate instanceof Closure) { + $closure = $aggregate; + } + + foreach ($columns as $column => $aggregate) { + if (! in_array(strtolower($aggregate), ['min', 'max'])) { + throw new InvalidArgumentException("Invalid aggregate [{$aggregate}] used within ofMany relation. Available aggregates: MIN, MAX"); + } + + $subQuery = $this->newSubQuery( + isset($previous) ? $previous['column'] : $this->getOneOfManySubQuerySelectColumns(), + $column, $aggregate + ); + + if (isset($previous)) { + $this->addJoinSub($subQuery, $previous['subQuery'], $previous['column']); + } elseif (isset($closure)) { + $closure($subQuery); + } + + if (array_key_last($columns) == $column) { + $this->addJoinSub($this->query, $subQuery, $column); + } + + $previous = [ + 'subQuery' => $subQuery, + 'column' => $column, + ]; + } + + return $this; + } + + /** + * Indicate that the relation is the latest single result of a larger one-to-many relationship. + * + * @param string|array|null $column + * @param string|Closure|null $aggregate + * @param string|null $relation + * @return $this + */ + public function latestOfMany($column = 'id', $relation = null) + { + return $this->ofMany(collect(Arr::wrap($column))->mapWithKeys(function ($column) { + return [$column => 'MAX']; + })->all(), 'MAX', $relation ?: $this->guessRelationship()); + } + + /** + * Indicate that the relation is the oldest single result of a larger one-to-many relationship. + * + * @param string|array|null $column + * @param string|Closure|null $aggregate + * @param string|null $relation + * @return $this + */ + public function oldestOfMany($column = 'id', $relation = null) + { + return $this->ofMany(collect(Arr::wrap($column))->mapWithKeys(function ($column) { + return [$column => 'MIN']; + })->all(), 'MIN', $relation ?: $this->guessRelationship()); + } + + /** + * Get a new query for the related model, grouping the query by the given column, often the foreign key of the relationship. + * + * @param string|array $groupBy + * @param string|null $column + * @param string|null $aggregate + * @return \Illuminate\Database\Eloquent\Builder + */ + protected function newSubQuery($groupBy, $column = null, $aggregate = null) + { + $subQuery = $this->query->getModel() + ->newQuery(); + + foreach (Arr::wrap($groupBy) as $group) { + $subQuery->groupBy($this->qualifyRelatedColumn($group)); + } + + if (! is_null($column)) { + $subQuery->selectRaw($aggregate.'('.$column.') as '.$column); + } + + $this->addOneOfManySubQueryConstraints($subQuery, $groupBy, $column, $aggregate); + + return $subQuery; + } + + /** + * Add the join subquery to the given query on the given column and the relationship's foreign key. + * + * @param \Illuminate\Database\Eloquent\Builder $parent + * @param \Illuminate\Database\Eloquent\Builder $subQuery + * @param string $on + * @return void + */ + protected function addJoinSub(Builder $parent, Builder $subQuery, $on) + { + $parent->joinSub($subQuery, $this->relationName, function ($join) use ($on) { + $join->on($this->qualifySubSelectColumn($on), '=', $this->qualifyRelatedColumn($on)); + + $this->addOneOfManyJoinSubQueryConstraints($join, $on); + }); + } + + /** + * Get the qualified column name for the one-of-many relationship using the subselect join query's alias. + * + * @param string $column + * @return string + */ + public function qualifySubSelectColumn($column) + { + return $this->getRelationName().'.'.last(explode('.', $column)); + } + + /** + * Qualify related column using the related table name if it is not already qualified. + * + * @param string $column + * @return string + */ + protected function qualifyRelatedColumn($column) + { + return Str::contains($column, '.') ? $column : $this->query->getModel()->getTable().'.'.$column; + } + + /** + * Guess the "hasOne" relationship's name via backtrace. + * + * @return string + */ + protected function guessRelationship() + { + return debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['function']; + } + + /** + * Determine whether the relationship is a one-of-many relationship. + * + * @return bool + */ + public function isOneOfMany() + { + return $this->isOneOfMany; + } + + /** + * Get the name of the relationship. + * + * @return string + */ + public function getRelationName() + { + return $this->relationName; + } +} diff --git a/Eloquent/Relations/Concerns/ComparesRelatedModels.php b/Eloquent/Relations/Concerns/ComparesRelatedModels.php index 50ec4f03e..ca0669887 100644 --- a/Eloquent/Relations/Concerns/ComparesRelatedModels.php +++ b/Eloquent/Relations/Concerns/ComparesRelatedModels.php @@ -2,6 +2,7 @@ namespace Illuminate\Database\Eloquent\Relations\Concerns; +use Illuminate\Contracts\Database\Eloquent\SupportsPartialRelations; use Illuminate\Database\Eloquent\Model; trait ComparesRelatedModels @@ -14,10 +15,18 @@ trait ComparesRelatedModels */ public function is($model) { - return ! is_null($model) && + $match = ! is_null($model) && $this->compareKeys($this->getParentKey(), $this->getRelatedKeyFrom($model)) && $this->related->getTable() === $model->getTable() && $this->related->getConnectionName() === $model->getConnectionName(); + + if ($match && $this instanceof SupportsPartialRelations && $this->isOneOfMany()) { + return $this->query + ->whereKey($model->getKey()) + ->exists(); + } + + return $match; } /** diff --git a/Eloquent/Relations/HasOne.php b/Eloquent/Relations/HasOne.php index 81ca9bb44..dc4ee3fd3 100755 --- a/Eloquent/Relations/HasOne.php +++ b/Eloquent/Relations/HasOne.php @@ -2,14 +2,18 @@ namespace Illuminate\Database\Eloquent\Relations; +use Illuminate\Contracts\Database\Eloquent\SupportsPartialRelations; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\Concerns\CanBeOneOfMany; use Illuminate\Database\Eloquent\Relations\Concerns\ComparesRelatedModels; use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels; +use Illuminate\Database\Query\JoinClause; -class HasOne extends HasOneOrMany +class HasOne extends HasOneOrMany implements SupportsPartialRelations { - use ComparesRelatedModels, SupportsDefaultModels; + use ComparesRelatedModels, CanBeOneOfMany, SupportsDefaultModels; /** * Get the results of the relationship. @@ -54,6 +58,63 @@ public function match(array $models, Collection $results, $relation) return $this->matchOne($models, $results, $relation); } + /** + * Add the constraints for an internal relationship existence query. + * + * Essentially, these queries compare on column names like "whereColumn". + * + * @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 (! $this->isOneOfMany()) { + return parent::getRelationExistenceQuery($query, $parentQuery, $columns); + } + + $query->getQuery()->joins = $this->query->getQuery()->joins; + + return $query->select($columns)->whereColumn( + $this->getQualifiedParentKeyName(), '=', $this->getExistenceCompareKey() + ); + } + + /** + * Add constraints for inner join subselect for one of many relationships. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string|null $column + * @param string|null $aggregate + * @return void + */ + public function addOneOfManySubQueryConstraints(Builder $query, $column = null, $aggregate = null) + { + $query->addSelect($this->foreignKey); + } + + /** + * Get the columns that should be selected by the one of many subquery. + * + * @return array|string + */ + public function getOneOfManySubQuerySelectColumns() + { + return $this->foreignKey; + } + + /** + * Add join query constraints for one of many relationships. + * + * @param \Illuminate\Database\Eloquent\JoinClause $join + * @return void + */ + public function addOneOfManyJoinSubQueryConstraints(JoinClause $join) + { + $join->on($this->qualifySubSelectColumn($this->foreignKey), '=', $this->qualifyRelatedColumn($this->foreignKey)); + } + /** * Make a new related instance for the given model. * diff --git a/Eloquent/Relations/MorphOne.php b/Eloquent/Relations/MorphOne.php index a874cdaec..7a3353cbe 100755 --- a/Eloquent/Relations/MorphOne.php +++ b/Eloquent/Relations/MorphOne.php @@ -2,14 +2,18 @@ namespace Illuminate\Database\Eloquent\Relations; +use Illuminate\Contracts\Database\Eloquent\SupportsPartialRelations; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\Concerns\CanBeOneOfMany; use Illuminate\Database\Eloquent\Relations\Concerns\ComparesRelatedModels; use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels; +use Illuminate\Database\Query\JoinClause; -class MorphOne extends MorphOneOrMany +class MorphOne extends MorphOneOrMany implements SupportsPartialRelations { - use ComparesRelatedModels, SupportsDefaultModels; + use CanBeOneOfMany, ComparesRelatedModels, SupportsDefaultModels; /** * Get the results of the relationship. @@ -54,6 +58,57 @@ public function match(array $models, Collection $results, $relation) return $this->matchOne($models, $results, $relation); } + /** + * Get the 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 = ['*']) + { + $query->getQuery()->joins = $this->query->getQuery()->joins; + + return parent::getRelationExistenceQuery($query, $parentQuery, $columns); + } + + /** + * Add constraints for inner join subselect for one of many relationships. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string|null $column + * @param string|null $aggregate + * @return void + */ + public function addOneOfManySubQueryConstraints(Builder $query, $column = null, $aggregate = null) + { + $query->addSelect($this->foreignKey, $this->morphType); + } + + /** + * Get the columns that should be selected by the one of many subquery. + * + * @return array|string + */ + public function getOneOfManySubQuerySelectColumns() + { + return [$this->foreignKey, $this->morphType]; + } + + /** + * Add join query constraints for one of many relationships. + * + * @param \Illuminate\Database\Eloquent\JoinClause $join + * @return void + */ + public function addOneOfManyJoinSubQueryConstraints(JoinClause $join) + { + $join + ->on($this->qualifySubSelectColumn($this->morphType), '=', $this->qualifyRelatedColumn($this->morphType)) + ->on($this->qualifySubSelectColumn($this->foreignKey), '=', $this->qualifyRelatedColumn($this->foreignKey)); + } + /** * Make a new related instance for the given model. * From 40861d878522426fb5acd15c0bdb0fd5249ff9e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=2E=20Nagy=20Gerg=C5=91?= Date: Tue, 18 May 2021 14:49:19 +0200 Subject: [PATCH 03/34] [8.x] Fix docblocks, imports (#37394) * [8.x] Docblocks, imports * spacing * remove unused import --- Concerns/BuildsQueries.php | 6 ++++++ Concerns/ManagesTransactions.php | 2 ++ Console/DbCommand.php | 2 ++ Eloquent/Builder.php | 1 + Eloquent/Concerns/HasAttributes.php | 2 ++ Eloquent/Relations/Concerns/InteractsWithDictionary.php | 2 ++ PDO/Concerns/ConnectsToDatabase.php | 5 ++++- Schema/Builder.php | 6 ++++++ Schema/Grammars/Grammar.php | 8 ++++++-- 9 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Concerns/BuildsQueries.php b/Concerns/BuildsQueries.php index 3bf7dd220..3e97b7d0d 100644 --- a/Concerns/BuildsQueries.php +++ b/Concerns/BuildsQueries.php @@ -81,6 +81,8 @@ public function chunkMap(callable $callback, $count = 1000) * @param callable $callback * @param int $count * @return bool + * + * @throws \RuntimeException */ public function each(callable $callback, $count = 1000) { @@ -172,6 +174,8 @@ public function eachById(callable $callback, $count = 1000, $column = null, $ali * * @param int $chunkSize * @return \Illuminate\Support\LazyCollection + * + * @throws \InvalidArgumentException */ public function lazy($chunkSize = 1000) { @@ -205,6 +209,8 @@ public function lazy($chunkSize = 1000) * @param string|null $column * @param string|null $alias * @return \Illuminate\Support\LazyCollection + * + * @throws \InvalidArgumentException */ public function lazyById($chunkSize = 1000, $column = null, $alias = null) { diff --git a/Concerns/ManagesTransactions.php b/Concerns/ManagesTransactions.php index b4b99c5c4..fac70295d 100644 --- a/Concerns/ManagesTransactions.php +++ b/Concerns/ManagesTransactions.php @@ -320,6 +320,8 @@ public function transactionLevel() * * @param callable $callback * @return void + * + * @throws \RuntimeException */ public function afterCommit($callback) { diff --git a/Console/DbCommand.php b/Console/DbCommand.php index 1bd9f644e..3aee98e2b 100644 --- a/Console/DbCommand.php +++ b/Console/DbCommand.php @@ -47,6 +47,8 @@ public function handle() * Get the database connection configuration. * * @return array + * + * @throws \UnexpectedValueException */ public function getConnection() { diff --git a/Eloquent/Builder.php b/Eloquent/Builder.php index f9415a961..4ea6e1800 100755 --- a/Eloquent/Builder.php +++ b/Eloquent/Builder.php @@ -860,6 +860,7 @@ public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = * * @param bool $shouldReverse * @return \Illuminate\Support\Collection + * * @throws \Illuminate\Pagination\CursorPaginationException */ protected function ensureOrderForCursorPagination($shouldReverse = false) diff --git a/Eloquent/Concerns/HasAttributes.php b/Eloquent/Concerns/HasAttributes.php index af89e47e0..ea4e0f660 100644 --- a/Eloquent/Concerns/HasAttributes.php +++ b/Eloquent/Concerns/HasAttributes.php @@ -1187,6 +1187,8 @@ protected function isEncryptedCastable($key) * * @param string $key * @return bool + * + * @throws \Illuminate\Database\Eloquent\InvalidCastException */ protected function isClassCastable($key) { diff --git a/Eloquent/Relations/Concerns/InteractsWithDictionary.php b/Eloquent/Relations/Concerns/InteractsWithDictionary.php index 9e2186150..abdfdd6a5 100644 --- a/Eloquent/Relations/Concerns/InteractsWithDictionary.php +++ b/Eloquent/Relations/Concerns/InteractsWithDictionary.php @@ -11,6 +11,8 @@ trait InteractsWithDictionary * * @param mixed $attribute * @return mixed + * + * @throws \Doctrine\Instantiator\Exception\InvalidArgumentException */ protected function getDictionaryKey($attribute) { diff --git a/PDO/Concerns/ConnectsToDatabase.php b/PDO/Concerns/ConnectsToDatabase.php index 637c62ce1..84c333801 100644 --- a/PDO/Concerns/ConnectsToDatabase.php +++ b/PDO/Concerns/ConnectsToDatabase.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\PDO\Concerns; use Illuminate\Database\PDO\Connection; +use InvalidArgumentException; use PDO; trait ConnectsToDatabase @@ -12,11 +13,13 @@ trait ConnectsToDatabase * * @param array $params * @return \Illuminate\Database\PDO\Connection + * + * @throws \InvalidArgumentException */ public function connect(array $params) { if (! isset($params['pdo']) || ! $params['pdo'] instanceof PDO) { - throw new \InvalidArgumentException('Laravel requires the "pdo" property to be set and be a PDO instance.'); + throw new InvalidArgumentException('Laravel requires the "pdo" property to be set and be a PDO instance.'); } return new Connection($params['pdo']); diff --git a/Schema/Builder.php b/Schema/Builder.php index 04f96e433..c919d1705 100755 --- a/Schema/Builder.php +++ b/Schema/Builder.php @@ -74,6 +74,8 @@ public static function defaultStringLength($length) * * @param string $type * @return void + * + * @throws \InvalidArgumentException */ public static function defaultMorphKeyType(string $type) { @@ -99,6 +101,8 @@ public static function morphUsingUuids() * * @param string $name * @return bool + * + * @throws \LogicException */ public function createDatabase($name) { @@ -110,6 +114,8 @@ public function createDatabase($name) * * @param string $name * @return bool + * + * @throws \LogicException */ public function dropDatabaseIfExists($name) { diff --git a/Schema/Grammars/Grammar.php b/Schema/Grammars/Grammar.php index 2ca54eecf..2acaa76a8 100755 --- a/Schema/Grammars/Grammar.php +++ b/Schema/Grammars/Grammar.php @@ -33,7 +33,9 @@ abstract class Grammar extends BaseGrammar * * @param string $name * @param \Illuminate\Database\Connection $connection - * @return string + * @return void + * + * @throws \LogicException */ public function compileCreateDatabase($name, $connection) { @@ -44,7 +46,9 @@ public function compileCreateDatabase($name, $connection) * Compile a drop database if exists command. * * @param string $name - * @return string + * @return void + * + * @throws \LogicException */ public function compileDropDatabaseIfExists($name) { From 1f8476e032c45070447a9285635fdce079065e03 Mon Sep 17 00:00:00 2001 From: cbl Date: Wed, 19 May 2021 11:29:12 +0200 Subject: [PATCH 04/34] fixed aggregates (e.g.: withExists) for one of many relationships --- Eloquent/Relations/Concerns/CanBeOneOfMany.php | 13 +++++++++++++ Eloquent/Relations/HasOne.php | 12 ++++-------- Eloquent/Relations/MorphOne.php | 4 +++- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/Eloquent/Relations/Concerns/CanBeOneOfMany.php index ea1afb539..1712dcdb8 100644 --- a/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -183,6 +183,19 @@ protected function addJoinSub(Builder $parent, Builder $subQuery, $on) }); } + /** + * Merge relation ship query joins to the given query builder. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return void + */ + protected function mergeJoinsTo(Builder $query) + { + $query->getQuery()->joins = $this->query->getQuery()->joins; + + $query->addBinding($this->query->getBindings(), 'join'); + } + /** * Get the qualified column name for the one-of-many relationship using the subselect join query's alias. * diff --git a/Eloquent/Relations/HasOne.php b/Eloquent/Relations/HasOne.php index dc4ee3fd3..0964b4f2d 100755 --- a/Eloquent/Relations/HasOne.php +++ b/Eloquent/Relations/HasOne.php @@ -70,15 +70,11 @@ public function match(array $models, Collection $results, $relation) */ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) { - if (! $this->isOneOfMany()) { - return parent::getRelationExistenceQuery($query, $parentQuery, $columns); + if($this->isOneOfMany()) { + $this->mergeJoinsTo($query); } - - $query->getQuery()->joins = $this->query->getQuery()->joins; - - return $query->select($columns)->whereColumn( - $this->getQualifiedParentKeyName(), '=', $this->getExistenceCompareKey() - ); + + return parent::getRelationExistenceQuery($query, $parentQuery, $columns); } /** diff --git a/Eloquent/Relations/MorphOne.php b/Eloquent/Relations/MorphOne.php index 7a3353cbe..dae455e74 100755 --- a/Eloquent/Relations/MorphOne.php +++ b/Eloquent/Relations/MorphOne.php @@ -68,7 +68,9 @@ public function match(array $models, Collection $results, $relation) */ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) { - $query->getQuery()->joins = $this->query->getQuery()->joins; + if($this->isOneOfMany()) { + $this->mergeJoinsTo($query); + } return parent::getRelationExistenceQuery($query, $parentQuery, $columns); } From baf4433fabe22f000e9c596131f6549217fad15d Mon Sep 17 00:00:00 2001 From: cbl Date: Wed, 19 May 2021 11:33:26 +0200 Subject: [PATCH 05/34] Apply fixes from StyleCI --- Eloquent/Relations/HasOne.php | 4 ++-- Eloquent/Relations/MorphOne.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Eloquent/Relations/HasOne.php b/Eloquent/Relations/HasOne.php index 0964b4f2d..4cff251a8 100755 --- a/Eloquent/Relations/HasOne.php +++ b/Eloquent/Relations/HasOne.php @@ -70,10 +70,10 @@ public function match(array $models, Collection $results, $relation) */ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) { - if($this->isOneOfMany()) { + if ($this->isOneOfMany()) { $this->mergeJoinsTo($query); } - + return parent::getRelationExistenceQuery($query, $parentQuery, $columns); } diff --git a/Eloquent/Relations/MorphOne.php b/Eloquent/Relations/MorphOne.php index dae455e74..5afacee6c 100755 --- a/Eloquent/Relations/MorphOne.php +++ b/Eloquent/Relations/MorphOne.php @@ -68,7 +68,7 @@ public function match(array $models, Collection $results, $relation) */ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) { - if($this->isOneOfMany()) { + if ($this->isOneOfMany()) { $this->mergeJoinsTo($query); } From e868e320734c00f803c77ac2757254c305d9b9c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Klabbers?= Date: Wed, 19 May 2021 14:51:41 +0200 Subject: [PATCH 06/34] Allow dabase password to be null (#37418) The current implementation demands the password to be a string, not null. However a password is not required when working on local environments or when using socks. For that reason the password in the schema importing and exporting tooling should also allow for nullable database passwords. --- Schema/MySqlSchemaState.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Schema/MySqlSchemaState.php b/Schema/MySqlSchemaState.php index 56a4ea455..e772fb686 100644 --- a/Schema/MySqlSchemaState.php +++ b/Schema/MySqlSchemaState.php @@ -125,7 +125,7 @@ protected function baseVariables(array $config) 'LARAVEL_LOAD_HOST' => is_array($config['host']) ? $config['host'][0] : $config['host'], 'LARAVEL_LOAD_PORT' => $config['port'] ?? '', 'LARAVEL_LOAD_USER' => $config['username'], - 'LARAVEL_LOAD_PASSWORD' => $config['password'], + 'LARAVEL_LOAD_PASSWORD' => $config['password'] ?? '', 'LARAVEL_LOAD_DATABASE' => $config['database'], ]; } From 88a7b60a17a4c93df4258dc167215384eca3d8bb Mon Sep 17 00:00:00 2001 From: Lennart Carstens-Behrens Date: Wed, 19 May 2021 14:54:16 +0200 Subject: [PATCH 07/34] [8.x] Add default "_of_many" to join alias when relation name is table name (#37411) * improved default alias * Update CanBeOneOfMany.php Co-authored-by: Taylor Otwell --- Eloquent/Relations/Concerns/CanBeOneOfMany.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/Eloquent/Relations/Concerns/CanBeOneOfMany.php index ea1afb539..f02311a77 100644 --- a/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -64,7 +64,9 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) { $this->isOneOfMany = true; - $this->relationName = $relation ?: $this->guessRelationship(); + $this->relationName = $relation ?: $this->getDefaultOneOfManyJoinAlias( + $this->guessRelationship() + ); $keyName = $this->query->getModel()->getKeyName(); @@ -110,6 +112,19 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) return $this; } + /** + * Get the default alias for one of many inner join clause. + * + * @param string $relation + * @return string + */ + protected function getDefaultOneOfManyJoinAlias($relation) + { + return $relation == $this->query->getModel()->getTable() + ? $relation.'_of_many' + : $relation; + } + /** * Indicate that the relation is the latest single result of a larger one-to-many relationship. * From e7c4eac83b029175d7bf87e3933f87db93e4f154 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 19 May 2021 08:01:06 -0500 Subject: [PATCH 08/34] formatting and method naming --- Eloquent/Relations/Concerns/CanBeOneOfMany.php | 14 +++++++------- Eloquent/Relations/HasOne.php | 2 +- Eloquent/Relations/MorphOne.php | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 1712dcdb8..d5d85f0cf 100644 --- a/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -86,19 +86,19 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) throw new InvalidArgumentException("Invalid aggregate [{$aggregate}] used within ofMany relation. Available aggregates: MIN, MAX"); } - $subQuery = $this->newSubQuery( + $subQuery = $this->newOneOfManySubQuery( isset($previous) ? $previous['column'] : $this->getOneOfManySubQuerySelectColumns(), $column, $aggregate ); if (isset($previous)) { - $this->addJoinSub($subQuery, $previous['subQuery'], $previous['column']); + $this->addOneOfManyJoinSubQuery($subQuery, $previous['subQuery'], $previous['column']); } elseif (isset($closure)) { $closure($subQuery); } if (array_key_last($columns) == $column) { - $this->addJoinSub($this->query, $subQuery, $column); + $this->addOneOfManyJoinSubQuery($this->query, $subQuery, $column); } $previous = [ @@ -148,7 +148,7 @@ public function oldestOfMany($column = 'id', $relation = null) * @param string|null $aggregate * @return \Illuminate\Database\Eloquent\Builder */ - protected function newSubQuery($groupBy, $column = null, $aggregate = null) + protected function newOneOfManySubQuery($groupBy, $column = null, $aggregate = null) { $subQuery = $this->query->getModel() ->newQuery(); @@ -174,7 +174,7 @@ protected function newSubQuery($groupBy, $column = null, $aggregate = null) * @param string $on * @return void */ - protected function addJoinSub(Builder $parent, Builder $subQuery, $on) + protected function addOneOfManyJoinSubQuery(Builder $parent, Builder $subQuery, $on) { $parent->joinSub($subQuery, $this->relationName, function ($join) use ($on) { $join->on($this->qualifySubSelectColumn($on), '=', $this->qualifyRelatedColumn($on)); @@ -184,12 +184,12 @@ protected function addJoinSub(Builder $parent, Builder $subQuery, $on) } /** - * Merge relation ship query joins to the given query builder. + * Merge the relationship query joins to the given query builder. * * @param \Illuminate\Database\Eloquent\Builder $builder * @return void */ - protected function mergeJoinsTo(Builder $query) + protected function mergeOneOfManyJoinsTo(Builder $query) { $query->getQuery()->joins = $this->query->getQuery()->joins; diff --git a/Eloquent/Relations/HasOne.php b/Eloquent/Relations/HasOne.php index 4cff251a8..15c735c32 100755 --- a/Eloquent/Relations/HasOne.php +++ b/Eloquent/Relations/HasOne.php @@ -71,7 +71,7 @@ public function match(array $models, Collection $results, $relation) public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) { if ($this->isOneOfMany()) { - $this->mergeJoinsTo($query); + $this->mergeOneOfManyJoinsTo($query); } return parent::getRelationExistenceQuery($query, $parentQuery, $columns); diff --git a/Eloquent/Relations/MorphOne.php b/Eloquent/Relations/MorphOne.php index 5afacee6c..ff526842e 100755 --- a/Eloquent/Relations/MorphOne.php +++ b/Eloquent/Relations/MorphOne.php @@ -69,7 +69,7 @@ public function match(array $models, Collection $results, $relation) public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) { if ($this->isOneOfMany()) { - $this->mergeJoinsTo($query); + $this->mergeOneOfManyJoinsTo($query); } return parent::getRelationExistenceQuery($query, $parentQuery, $columns); From d5bea2ae823dc66f08ea56e09b1bbf7d8b5086c9 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 19 May 2021 08:01:47 -0500 Subject: [PATCH 09/34] formatting --- .../Relations/Concerns/CanBeOneOfMany.php | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/Eloquent/Relations/Concerns/CanBeOneOfMany.php index c0992921a..fc9bbf83e 100644 --- a/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -112,19 +112,6 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) return $this; } - /** - * Get the default alias for one of many inner join clause. - * - * @param string $relation - * @return string - */ - protected function getDefaultOneOfManyJoinAlias($relation) - { - return $relation == $this->query->getModel()->getTable() - ? $relation.'_of_many' - : $relation; - } - /** * Indicate that the relation is the latest single result of a larger one-to-many relationship. * @@ -155,6 +142,19 @@ public function oldestOfMany($column = 'id', $relation = null) })->all(), 'MIN', $relation ?: $this->guessRelationship()); } + /** + * Get the default alias for the one of many inner join clause. + * + * @param string $relation + * @return string + */ + protected function getDefaultOneOfManyJoinAlias($relation) + { + return $relation == $this->query->getModel()->getTable() + ? $relation.'_of_many' + : $relation; + } + /** * Get a new query for the related model, grouping the query by the given column, often the foreign key of the relationship. * From 1975f11630a00f8cc20f36064304be972875cea0 Mon Sep 17 00:00:00 2001 From: Mohamed Said Date: Wed, 19 May 2021 20:38:00 +0200 Subject: [PATCH 10/34] [8.x] Add eloquent strict loading mode (#37363) * add eloquent strict loading mode * stop throwing exceptions on trying to get the value of non loaded attributes * refactor * fix tests * change to public * fix tests * formatting and method naming Co-authored-by: Taylor Otwell --- Eloquent/Builder.php | 7 +++++- Eloquent/Concerns/HasAttributes.php | 27 +++++++++++++++++--- Eloquent/Model.php | 35 ++++++++++++++++++++++++++ LazyLoadingViolationException.php | 39 +++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 LazyLoadingViolationException.php diff --git a/Eloquent/Builder.php b/Eloquent/Builder.php index 4ea6e1800..c966c540e 100755 --- a/Eloquent/Builder.php +++ b/Eloquent/Builder.php @@ -352,7 +352,12 @@ public function hydrate(array $items) $instance = $this->newModelInstance(); return $instance->newCollection(array_map(function ($item) use ($instance) { - return $instance->newFromBuilder($item); + $model = $instance->newFromBuilder($item); + + $model->preventsLazyLoading = Model::preventsLazyLoading(); + + + return $model; }, $items)); } diff --git a/Eloquent/Concerns/HasAttributes.php b/Eloquent/Concerns/HasAttributes.php index ea4e0f660..6e5433422 100644 --- a/Eloquent/Concerns/HasAttributes.php +++ b/Eloquent/Concerns/HasAttributes.php @@ -10,6 +10,8 @@ use Illuminate\Database\Eloquent\InvalidCastException; use Illuminate\Database\Eloquent\JsonEncodingException; use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Database\LazyLoadingViolationException; +use Illuminate\Database\StrictLoadingViolationException; use Illuminate\Support\Arr; use Illuminate\Support\Carbon; use Illuminate\Support\Collection as BaseCollection; @@ -433,13 +435,30 @@ public function getRelationValue($key) return $this->relations[$key]; } + if (! $this->isRelation($key)) { + return; + } + + if ($this->preventsLazyLoading) { + throw new LazyLoadingViolationException($this, $key); + } + // If the "attribute" exists as a method on the model, we will just assume // it is a relationship and will load and return results from the query // and hydrate the relationship's value on the "relationships" array. - if (method_exists($this, $key) || - (static::$relationResolvers[get_class($this)][$key] ?? null)) { - return $this->getRelationshipFromMethod($key); - } + return $this->getRelationshipFromMethod($key); + } + + /** + * Determine if the given key is a relationship method on the model. + * + * @param string $key + * @return bool + */ + public function isRelation($key) + { + return method_exists($this, $key) || + (static::$relationResolvers[get_class($this)][$key] ?? null); } /** diff --git a/Eloquent/Model.php b/Eloquent/Model.php index 6005ce5f3..efbf02130 100644 --- a/Eloquent/Model.php +++ b/Eloquent/Model.php @@ -81,6 +81,13 @@ abstract class Model implements Arrayable, ArrayAccess, Jsonable, JsonSerializab */ protected $withCount = []; + /** + * Indicates whether lazy loading will be prevented on this model. + * + * @var bool + */ + public $preventsLazyLoading = false; + /** * The number of models to return for pagination. * @@ -144,6 +151,13 @@ abstract class Model implements Arrayable, ArrayAccess, Jsonable, JsonSerializab */ protected static $ignoreOnTouch = []; + /** + * Indicates whether lazy loading should be restricted on all models. + * + * @var bool + */ + protected static $modelsShouldPreventLazyLoading = false; + /** * The name of the "created at" column. * @@ -333,6 +347,17 @@ public static function isIgnoringTouch($class = null) return false; } + /** + * Prevent model relationships from being lazy loaded. + * + * @param bool $value + * @return void + */ + public static function preventLazyLoading($value = true) + { + static::$modelsShouldPreventLazyLoading = $value; + } + /** * Fill the model with an array of attributes. * @@ -1807,6 +1832,16 @@ public function setPerPage($perPage) return $this; } + /** + * Determine if lazy loading is disabled. + * + * @return bool + */ + public static function preventsLazyLoading() + { + return static::$modelsShouldPreventLazyLoading; + } + /** * Dynamically retrieve attributes on the model. * diff --git a/LazyLoadingViolationException.php b/LazyLoadingViolationException.php new file mode 100644 index 000000000..1bcd40c95 --- /dev/null +++ b/LazyLoadingViolationException.php @@ -0,0 +1,39 @@ +model = $class; + $this->relation = $relation; + } +} From 8b0de57873225b717d3614898406307dec7d5b62 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 19 May 2021 13:38:27 -0500 Subject: [PATCH 11/34] Apply fixes from StyleCI (#37427) --- Eloquent/Builder.php | 1 - Eloquent/Concerns/HasAttributes.php | 1 - 2 files changed, 2 deletions(-) diff --git a/Eloquent/Builder.php b/Eloquent/Builder.php index c966c540e..b56fa4072 100755 --- a/Eloquent/Builder.php +++ b/Eloquent/Builder.php @@ -356,7 +356,6 @@ public function hydrate(array $items) $model->preventsLazyLoading = Model::preventsLazyLoading(); - return $model; }, $items)); } diff --git a/Eloquent/Concerns/HasAttributes.php b/Eloquent/Concerns/HasAttributes.php index 6e5433422..6156bde25 100644 --- a/Eloquent/Concerns/HasAttributes.php +++ b/Eloquent/Concerns/HasAttributes.php @@ -11,7 +11,6 @@ use Illuminate\Database\Eloquent\JsonEncodingException; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\LazyLoadingViolationException; -use Illuminate\Database\StrictLoadingViolationException; use Illuminate\Support\Arr; use Illuminate\Support\Carbon; use Illuminate\Support\Collection as BaseCollection; From dcfbfad8c3a33905c66d2a0b7c67bfa3f759c6de Mon Sep 17 00:00:00 2001 From: Lennart Carstens-Behrens Date: Thu, 20 May 2021 19:45:06 +0200 Subject: [PATCH 12/34] [8.x] Fix eager loading one-of-many relationships with multiple aggregates (#37436) * fix eager loading with multiple aggregates * Apply fixes from StyleCI --- Eloquent/Relations/Concerns/CanBeOneOfMany.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/Eloquent/Relations/Concerns/CanBeOneOfMany.php index fc9bbf83e..36c2956cf 100644 --- a/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -89,7 +89,7 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) } $subQuery = $this->newOneOfManySubQuery( - isset($previous) ? $previous['column'] : $this->getOneOfManySubQuerySelectColumns(), + $this->getOneOfManySubQuerySelectColumns(), $column, $aggregate ); From 5d7518c7ec83d5ad958acfbc5479de90be9bdbe4 Mon Sep 17 00:00:00 2001 From: Lennart Carstens-Behrens Date: Fri, 21 May 2021 14:42:35 +0200 Subject: [PATCH 13/34] [8.x] Add beforeQuery to base query builder (#37431) * add preserve to query builder * Apply fixes from StyleCI * formatting' Co-authored-by: Taylor Otwell --- Query/Builder.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/Query/Builder.php b/Query/Builder.php index 9f4e83a28..9df7d46ee 100755 --- a/Query/Builder.php +++ b/Query/Builder.php @@ -182,6 +182,13 @@ class Builder */ public $lock; + /** + * The callbacks that should be invoked before the query is executed. + * + * @var array + */ + public $beforeQueryCallbacks = []; + /** * All of the available clause operators. * @@ -2256,6 +2263,33 @@ public function sharedLock() return $this->lock(false); } + /** + * Register a closure to be invoked before the query is executed. + * + * @param callable $callback + * @return $this + */ + public function beforeQuery(callable $callback) + { + $this->beforeQueryCallbacks[] = $callback; + + return $this; + } + + /** + * Invoke the "before query" modification callbacks. + * + * @return void + */ + public function applyBeforeQueryCallbacks() + { + foreach ($this->beforeQueryCallbacks as $callback) { + $callback($this); + } + + $this->beforeQueryCallbacks = []; + } + /** * Get the SQL representation of the query. * @@ -2263,6 +2297,8 @@ public function sharedLock() */ public function toSql() { + $this->applyBeforeQueryCallbacks(); + return $this->grammar->compileSelect($this); } @@ -2663,6 +2699,8 @@ public function implode($column, $glue = '') */ public function exists() { + $this->applyBeforeQueryCallbacks(); + $results = $this->connection->select( $this->grammar->compileExists($this), $this->getBindings(), ! $this->useWritePdo ); @@ -2901,6 +2939,8 @@ public function insert(array $values) } } + $this->applyBeforeQueryCallbacks(); + // Finally, we will run this query against the database connection and return // the results. We will need to also flatten these bindings before running // the query so they are all in one huge, flattened array for execution. @@ -2931,6 +2971,8 @@ public function insertOrIgnore(array $values) } } + $this->applyBeforeQueryCallbacks(); + return $this->connection->affectingStatement( $this->grammar->compileInsertOrIgnore($this, $values), $this->cleanBindings(Arr::flatten($values, 1)) @@ -2946,6 +2988,8 @@ public function insertOrIgnore(array $values) */ public function insertGetId(array $values, $sequence = null) { + $this->applyBeforeQueryCallbacks(); + $sql = $this->grammar->compileInsertGetId($this, $values, $sequence); $values = $this->cleanBindings($values); @@ -2962,6 +3006,8 @@ public function insertGetId(array $values, $sequence = null) */ public function insertUsing(array $columns, $query) { + $this->applyBeforeQueryCallbacks(); + [$sql, $bindings] = $this->createSub($query); return $this->connection->affectingStatement( @@ -2978,6 +3024,8 @@ public function insertUsing(array $columns, $query) */ public function update(array $values) { + $this->applyBeforeQueryCallbacks(); + $sql = $this->grammar->compileUpdate($this, $values); return $this->connection->update($sql, $this->cleanBindings( @@ -3035,6 +3083,8 @@ public function upsert(array $values, $uniqueBy, $update = null) $update = array_keys(reset($values)); } + $this->applyBeforeQueryCallbacks(); + $bindings = $this->cleanBindings(array_merge( Arr::flatten($values, 1), collect($update)->reject(function ($value, $key) { @@ -3109,6 +3159,8 @@ public function delete($id = null) $this->where($this->from.'.id', '=', $id); } + $this->applyBeforeQueryCallbacks(); + return $this->connection->delete( $this->grammar->compileDelete($this), $this->cleanBindings( $this->grammar->prepareBindingsForDelete($this->bindings) @@ -3123,6 +3175,8 @@ public function delete($id = null) */ public function truncate() { + $this->applyBeforeQueryCallbacks(); + foreach ($this->grammar->compileTruncate($this) as $sql => $bindings) { $this->connection->statement($sql, $bindings); } From ea2510bb6e2d50a52c4769c7ffe3987b0b145220 Mon Sep 17 00:00:00 2001 From: Lennart Carstens-Behrens Date: Fri, 21 May 2021 21:16:29 +0200 Subject: [PATCH 14/34] [8.x] Improve one-of-many performance (#37451) * add preserve to query builder * Apply fixes from StyleCI * improved one-of-many performance * add constraints to one-of-many subquery * Apply fixes from StyleCI * formatting * formatting Co-authored-by: Taylor Otwell --- .../Relations/Concerns/CanBeOneOfMany.php | 47 +++++++++++++++++-- Eloquent/Relations/HasOneOrMany.php | 8 ++-- Eloquent/Relations/MorphOneOrMany.php | 4 +- Eloquent/Relations/Relation.php | 10 ++++ 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 36c2956cf..0a2c6fac7 100644 --- a/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -25,6 +25,13 @@ trait CanBeOneOfMany */ protected $relationName; + /** + * The one of many inner join subselect query builder instance. + * + * @var \Illuminate\Database\Eloquent\Builder|null + */ + protected $oneOfManySubQuery; + /** * Add constraints for inner join subselect for one of many relationships. * @@ -99,6 +106,10 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) $closure($subQuery); } + if (! isset($previous)) { + $this->oneOfManySubQuery = $subQuery; + } + if (array_key_last($columns) == $column) { $this->addOneOfManyJoinSubQuery($this->query, $subQuery, $column); } @@ -109,6 +120,8 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) ]; } + $this->addConstraints(); + return $this; } @@ -191,10 +204,12 @@ protected function newOneOfManySubQuery($groupBy, $column = null, $aggregate = n */ protected function addOneOfManyJoinSubQuery(Builder $parent, Builder $subQuery, $on) { - $parent->joinSub($subQuery, $this->relationName, function ($join) use ($on) { - $join->on($this->qualifySubSelectColumn($on), '=', $this->qualifyRelatedColumn($on)); + $parent->beforeQuery(function ($parent) use ($subQuery, $on) { + $parent->joinSub($subQuery, $this->relationName, function ($join) use ($on) { + $join->on($this->qualifySubSelectColumn($on), '=', $this->qualifyRelatedColumn($on)); - $this->addOneOfManyJoinSubQueryConstraints($join, $on); + $this->addOneOfManyJoinSubQueryConstraints($join, $on); + }); }); } @@ -206,9 +221,31 @@ protected function addOneOfManyJoinSubQuery(Builder $parent, Builder $subQuery, */ protected function mergeOneOfManyJoinsTo(Builder $query) { - $query->getQuery()->joins = $this->query->getQuery()->joins; + $query->getQuery()->beforeQueryCallbacks = $this->query->getQuery()->beforeQueryCallbacks; + + $query->applyBeforeQueryCallbacks(); + } + + /** + * Get the query builder that will contain the relationship constraints. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + protected function getRelationQuery() + { + return $this->isOneOfMany() + ? $this->oneOfManySubQuery + : $this->query; + } - $query->addBinding($this->query->getBindings(), 'join'); + /** + * Get the one of many inner join subselect builder instance. + * + * @return \Illuminate\Database\Eloquent\Builder|void + */ + public function getOneOfManySubQuery() + { + return $this->oneOfManySubQuery; } /** diff --git a/Eloquent/Relations/HasOneOrMany.php b/Eloquent/Relations/HasOneOrMany.php index 18b0f8fc9..f3a50501f 100755 --- a/Eloquent/Relations/HasOneOrMany.php +++ b/Eloquent/Relations/HasOneOrMany.php @@ -80,9 +80,11 @@ public function makeMany($records) public function addConstraints() { if (static::$constraints) { - $this->query->where($this->foreignKey, '=', $this->getParentKey()); + $query = $this->getRelationQuery(); - $this->query->whereNotNull($this->foreignKey); + $query->where($this->foreignKey, '=', $this->getParentKey()); + + $query->whereNotNull($this->foreignKey); } } @@ -96,7 +98,7 @@ public function addEagerConstraints(array $models) { $whereIn = $this->whereInMethod($this->parent, $this->localKey); - $this->query->{$whereIn}( + $this->getRelationQuery()->{$whereIn}( $this->foreignKey, $this->getKeys($models, $this->localKey) ); } diff --git a/Eloquent/Relations/MorphOneOrMany.php b/Eloquent/Relations/MorphOneOrMany.php index 887ebe247..ff58ef972 100755 --- a/Eloquent/Relations/MorphOneOrMany.php +++ b/Eloquent/Relations/MorphOneOrMany.php @@ -50,7 +50,7 @@ public function addConstraints() if (static::$constraints) { parent::addConstraints(); - $this->query->where($this->morphType, $this->morphClass); + $this->getRelationQuery()->where($this->morphType, $this->morphClass); } } @@ -64,7 +64,7 @@ public function addEagerConstraints(array $models) { parent::addEagerConstraints($models); - $this->query->where($this->morphType, $this->morphClass); + $this->getRelationQuery()->where($this->morphType, $this->morphClass); } /** diff --git a/Eloquent/Relations/Relation.php b/Eloquent/Relations/Relation.php index 29131b275..7fe9f3e9f 100755 --- a/Eloquent/Relations/Relation.php +++ b/Eloquent/Relations/Relation.php @@ -271,6 +271,16 @@ protected function getKeys(array $models, $key = null) })->values()->unique(null, true)->sort()->all(); } + /** + * Get the query builder that will contain the relationship constraints. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + protected function getRelationQuery() + { + return $this->query; + } + /** * Get the underlying query for the relation. * From aa416bead3d5298b09edeb16a8d9c8fdb664cb9f Mon Sep 17 00:00:00 2001 From: Mohamed Said Date: Tue, 25 May 2021 12:05:27 +0200 Subject: [PATCH 15/34] reconnect the correct connection when using read or write connections --- Connection.php | 30 ++++++++++++++++++++++++++++++ DatabaseManager.php | 10 +++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/Connection.php b/Connection.php index b2ded4c9a..0dda1a622 100755 --- a/Connection.php +++ b/Connection.php @@ -161,6 +161,13 @@ class Connection implements ConnectionInterface */ protected static $resolvers = []; + /** + * The type of the connection. + * + * @var string|null + */ + protected $type; + /** * Create a new database connection instance. * @@ -1045,6 +1052,16 @@ public function getName() return $this->getConfig('name'); } + /** + * Get the database connection full name. + * + * @return string|null + */ + public function getFullName() + { + return $this->getName().($this->type ? '::'.$this->type : ''); + } + /** * Get an option from the configuration options. * @@ -1274,6 +1291,19 @@ public function setDatabaseName($database) return $this; } + /** + * Set the type of the connection. + * + * @param string $type + * @return $this + */ + public function setType($type) + { + $this->type = $type; + + return $this; + } + /** * Get the table prefix for the connection. * diff --git a/DatabaseManager.php b/DatabaseManager.php index 05fd454df..3f426c216 100755 --- a/DatabaseManager.php +++ b/DatabaseManager.php @@ -62,7 +62,7 @@ public function __construct($app, ConnectionFactory $factory) $this->factory = $factory; $this->reconnector = function ($connection) { - $this->reconnect($connection->getName()); + $this->reconnect($connection->getFullName()); }; } @@ -165,7 +165,7 @@ protected function configuration($name) */ protected function configure(Connection $connection, $type) { - $connection = $this->setPdoForType($connection, $type); + $connection = $this->setPdoForType($connection, $type)->setType($type); // First we'll set the fetch mode and a few other dependencies of the database // connection. This method basically just configures and prepares it to get @@ -275,7 +275,11 @@ public function usingConnection($name, callable $callback) */ protected function refreshPdoConnections($name) { - $fresh = $this->makeConnection($name); + [$database, $type] = $this->parseConnectionName($name); + + $fresh = $this->configure( + $this->makeConnection($database), $type + ); return $this->connections[$name] ->setPdo($fresh->getRawPdo()) From 3fac0ae25c92fa4f7ab605f01a58598e442f76ec Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 25 May 2021 07:40:17 -0500 Subject: [PATCH 16/34] formatting' --- Connection.php | 26 +++++++++++++------------- DatabaseManager.php | 8 ++++---- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Connection.php b/Connection.php index 0dda1a622..71efec09d 100755 --- a/Connection.php +++ b/Connection.php @@ -49,6 +49,13 @@ class Connection implements ConnectionInterface */ protected $database; + /** + * The type of the connection. + * + * @var string|null + */ + protected $type; + /** * The table prefix for the connection. * @@ -161,13 +168,6 @@ class Connection implements ConnectionInterface */ protected static $resolvers = []; - /** - * The type of the connection. - * - * @var string|null - */ - protected $type; - /** * Create a new database connection instance. * @@ -1057,9 +1057,9 @@ public function getName() * * @return string|null */ - public function getFullName() + public function getNameWithReadWriteType() { - return $this->getName().($this->type ? '::'.$this->type : ''); + return $this->getName().($this->readWriteType ? '::'.$this->readWriteType : ''); } /** @@ -1292,14 +1292,14 @@ public function setDatabaseName($database) } /** - * Set the type of the connection. + * Set the read / write type of the connection. * - * @param string $type + * @param string|null $readWriteType * @return $this */ - public function setType($type) + public function setReadWriteType($readWriteType) { - $this->type = $type; + $this->readWriteType = $readWriteType; return $this; } diff --git a/DatabaseManager.php b/DatabaseManager.php index 3f426c216..5d2f7cdcb 100755 --- a/DatabaseManager.php +++ b/DatabaseManager.php @@ -62,7 +62,7 @@ public function __construct($app, ConnectionFactory $factory) $this->factory = $factory; $this->reconnector = function ($connection) { - $this->reconnect($connection->getFullName()); + $this->reconnect($connection->getNameWithReadWriteType()); }; } @@ -165,7 +165,7 @@ protected function configuration($name) */ protected function configure(Connection $connection, $type) { - $connection = $this->setPdoForType($connection, $type)->setType($type); + $connection = $this->setPdoForType($connection, $type)->setReadWriteType($type); // First we'll set the fetch mode and a few other dependencies of the database // connection. This method basically just configures and prepares it to get @@ -282,8 +282,8 @@ protected function refreshPdoConnections($name) ); return $this->connections[$name] - ->setPdo($fresh->getRawPdo()) - ->setReadPdo($fresh->getRawReadPdo()); + ->setPdo($fresh->getRawPdo()) + ->setReadPdo($fresh->getRawReadPdo()); } /** From 675014812fcff1678cf8de2155666d8d2fe2d2ea Mon Sep 17 00:00:00 2001 From: Roy de Vos Burchart Date: Tue, 25 May 2021 17:12:23 +0200 Subject: [PATCH 17/34] Add violatedLazyLoading handler --- Eloquent/Concerns/HasAttributes.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Eloquent/Concerns/HasAttributes.php b/Eloquent/Concerns/HasAttributes.php index 6156bde25..deedfff05 100644 --- a/Eloquent/Concerns/HasAttributes.php +++ b/Eloquent/Concerns/HasAttributes.php @@ -439,7 +439,7 @@ public function getRelationValue($key) } if ($this->preventsLazyLoading) { - throw new LazyLoadingViolationException($this, $key); + $this->violatedLazyLoading($key); } // If the "attribute" exists as a method on the model, we will just assume @@ -460,6 +460,17 @@ public function isRelation($key) (static::$relationResolvers[get_class($this)][$key] ?? null); } + /** + * Handle a lazy loading violation, for example by throwing an exception. + * + * @param string $key + * @return void + */ + protected function violatedLazyLoading($key) + { + throw new LazyLoadingViolationException($this, $key); + } + /** * Get a relationship value from a method. * From af35405edd787c209d2752daa81daeeee661f8d4 Mon Sep 17 00:00:00 2001 From: Roy de Vos Burchart Date: Wed, 26 May 2021 12:13:27 +0200 Subject: [PATCH 18/34] Add global callback handler Adds a way to call `Model::handleLazyLoadingViolationUsing()` from a service provider to add a way to handle violations for all models instead of overriding `violatedLazyLoading` on the models themselves. --- Eloquent/Concerns/HasAttributes.php | 5 +++++ Eloquent/Model.php | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/Eloquent/Concerns/HasAttributes.php b/Eloquent/Concerns/HasAttributes.php index deedfff05..a5f58062e 100644 --- a/Eloquent/Concerns/HasAttributes.php +++ b/Eloquent/Concerns/HasAttributes.php @@ -468,6 +468,11 @@ public function isRelation($key) */ protected function violatedLazyLoading($key) { + if (isset(static::$violatedLazyLoadingCallback)) { + call_user_func(static::$violatedLazyLoadingCallback, $this, $key); + return; + } + throw new LazyLoadingViolationException($this, $key); } diff --git a/Eloquent/Model.php b/Eloquent/Model.php index efbf02130..679515018 100644 --- a/Eloquent/Model.php +++ b/Eloquent/Model.php @@ -158,6 +158,13 @@ abstract class Model implements Arrayable, ArrayAccess, Jsonable, JsonSerializab */ protected static $modelsShouldPreventLazyLoading = false; + /** + * The callback that is responsible for handing lazy loading violations. + * + * @var callable|null + */ + protected static $violatedLazyLoadingCallback; + /** * The name of the "created at" column. * @@ -358,6 +365,16 @@ public static function preventLazyLoading($value = true) static::$modelsShouldPreventLazyLoading = $value; } + /** + * Register a callback that is responsible for handling lazy loading violations. + * + * @param callable $callback + */ + public static function handleLazyLoadingViolationUsing(callable $callback) + { + static::$violatedLazyLoadingCallback = $callback; + } + /** * Fill the model with an array of attributes. * From cb3fd5f70f365f457b7496c71db3dfd8b8f45430 Mon Sep 17 00:00:00 2001 From: Roy de Vos Burchart Date: Wed, 26 May 2021 12:30:59 +0200 Subject: [PATCH 19/34] style --- Eloquent/Concerns/HasAttributes.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Eloquent/Concerns/HasAttributes.php b/Eloquent/Concerns/HasAttributes.php index a5f58062e..e9570f9a5 100644 --- a/Eloquent/Concerns/HasAttributes.php +++ b/Eloquent/Concerns/HasAttributes.php @@ -470,6 +470,7 @@ protected function violatedLazyLoading($key) { if (isset(static::$violatedLazyLoadingCallback)) { call_user_func(static::$violatedLazyLoadingCallback, $this, $key); + return; } From 4764e7ad64b673cfc9f3ebe50a66b878d6f5e829 Mon Sep 17 00:00:00 2001 From: Adrien Foulon <6115458+Tofandel@users.noreply.github.com> Date: Wed, 26 May 2021 14:29:00 +0200 Subject: [PATCH 20/34] fix #37483 (aggregates with having) (#37487) --- Query/Builder.php | 4 ++-- Query/Grammars/Grammar.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Query/Builder.php b/Query/Builder.php index 9df7d46ee..0ef10ac1c 100755 --- a/Query/Builder.php +++ b/Query/Builder.php @@ -2826,8 +2826,8 @@ public function average($column) */ public function aggregate($function, $columns = ['*']) { - $results = $this->cloneWithout($this->unions ? [] : ['columns']) - ->cloneWithoutBindings($this->unions ? [] : ['select']) + $results = $this->cloneWithout($this->unions || $this->havings ? [] : ['columns']) + ->cloneWithoutBindings($this->unions || $this->havings ? [] : ['select']) ->setAggregate($function, $columns) ->get($columns); diff --git a/Query/Grammars/Grammar.php b/Query/Grammars/Grammar.php index b7305e8ea..d7bc534da 100755 --- a/Query/Grammars/Grammar.php +++ b/Query/Grammars/Grammar.php @@ -45,7 +45,7 @@ class Grammar extends BaseGrammar */ public function compileSelect(Builder $query) { - if ($query->unions && $query->aggregate) { + if (($query->unions || $query->havings) && $query->aggregate) { return $this->compileUnionAggregate($query); } From 4880351240379f2b3cb91f6b62ea824c3a449ff2 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 26 May 2021 07:37:37 -0500 Subject: [PATCH 21/34] formatting --- Eloquent/Concerns/HasAttributes.php | 14 ++++++-------- Eloquent/Model.php | 9 +++++---- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/Eloquent/Concerns/HasAttributes.php b/Eloquent/Concerns/HasAttributes.php index e9570f9a5..56b52971a 100644 --- a/Eloquent/Concerns/HasAttributes.php +++ b/Eloquent/Concerns/HasAttributes.php @@ -439,7 +439,7 @@ public function getRelationValue($key) } if ($this->preventsLazyLoading) { - $this->violatedLazyLoading($key); + $this->handleLazyLoadingViolation($key); } // If the "attribute" exists as a method on the model, we will just assume @@ -461,17 +461,15 @@ public function isRelation($key) } /** - * Handle a lazy loading violation, for example by throwing an exception. + * Handle a lazy loading violation. * * @param string $key - * @return void + * @return mixed */ - protected function violatedLazyLoading($key) + protected function handleLazyLoadingViolation($key) { - if (isset(static::$violatedLazyLoadingCallback)) { - call_user_func(static::$violatedLazyLoadingCallback, $this, $key); - - return; + if (isset(static::$lazyLoadingViolationCallback)) { + return call_user_func(static::$lazyLoadingViolationCallback, $this, $key); } throw new LazyLoadingViolationException($this, $key); diff --git a/Eloquent/Model.php b/Eloquent/Model.php index 679515018..e8844d8d7 100644 --- a/Eloquent/Model.php +++ b/Eloquent/Model.php @@ -159,11 +159,11 @@ abstract class Model implements Arrayable, ArrayAccess, Jsonable, JsonSerializab protected static $modelsShouldPreventLazyLoading = false; /** - * The callback that is responsible for handing lazy loading violations. + * The callback that is responsible for handling lazy loading violations. * * @var callable|null */ - protected static $violatedLazyLoadingCallback; + protected static $lazyLoadingViolationCallback; /** * The name of the "created at" column. @@ -368,11 +368,12 @@ public static function preventLazyLoading($value = true) /** * Register a callback that is responsible for handling lazy loading violations. * - * @param callable $callback + * @param callable $callback + * @return void */ public static function handleLazyLoadingViolationUsing(callable $callback) { - static::$violatedLazyLoadingCallback = $callback; + static::$lazyLoadingViolationCallback = $callback; } /** From 75f0427e32976ac0eded60389ae91aa99d4e8115 Mon Sep 17 00:00:00 2001 From: Mohammad ALTAWEEL Date: Thu, 27 May 2021 15:34:20 +0300 Subject: [PATCH 22/34] [8.x] Init the traits when the model is being unserialized (#37492) * Init the traits when the model is being unserialized * Add tests for trait boot and initialization --- Eloquent/Model.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Eloquent/Model.php b/Eloquent/Model.php index e8844d8d7..fdcceedac 100644 --- a/Eloquent/Model.php +++ b/Eloquent/Model.php @@ -2014,5 +2014,7 @@ public function __sleep() public function __wakeup() { $this->bootIfNotBooted(); + + $this->initializeTraits(); } } From c8a1d6c4bd636779cdf3c3b93d0b149472278070 Mon Sep 17 00:00:00 2001 From: Mohamed Said Date: Thu, 27 May 2021 17:21:03 +0200 Subject: [PATCH 23/34] relax the lazy loading restrictions (#37503) --- Eloquent/Builder.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Eloquent/Builder.php b/Eloquent/Builder.php index b56fa4072..49df237b5 100755 --- a/Eloquent/Builder.php +++ b/Eloquent/Builder.php @@ -351,10 +351,12 @@ public function hydrate(array $items) { $instance = $this->newModelInstance(); - return $instance->newCollection(array_map(function ($item) use ($instance) { + return $instance->newCollection(array_map(function ($item) use ($items, $instance) { $model = $instance->newFromBuilder($item); - $model->preventsLazyLoading = Model::preventsLazyLoading(); + if (count($items) > 1) { + $model->preventsLazyLoading = Model::preventsLazyLoading(); + } return $model; }, $items)); From 75d981e33af8f143887076da09f9aadb0045ce64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stan=20Dani=C3=ABls?= <1199737+standaniels@users.noreply.github.com> Date: Tue, 1 Jun 2021 16:30:27 +0200 Subject: [PATCH 24/34] [8.x] Get queueable relationship when collection has non-numeric keys (#37556) * Add test to show bug * Remove any keys from queueable relations array --- Eloquent/Collection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Eloquent/Collection.php b/Eloquent/Collection.php index e8435ce3d..c1323f881 100755 --- a/Eloquent/Collection.php +++ b/Eloquent/Collection.php @@ -692,7 +692,7 @@ public function getQueueableRelations() } elseif (count($relations) === 1) { return reset($relations); } else { - return array_intersect(...$relations); + return array_intersect(...array_values($relations)); } } From 665f041fba11f5a25a8034e41d4778d89f6138cc Mon Sep 17 00:00:00 2001 From: Jav Date: Tue, 1 Jun 2021 16:52:52 +0200 Subject: [PATCH 25/34] [8.x] Columns in the order by list must be unique (#37550) * Columns in the order by list must be unique MySql is ok with it, but SqlServer error out. See a similar issue here: https://github.com/laravel/nova-issues/issues/1621 * override with next call * Update Builder.php Co-authored-by: Taylor Otwell --- Query/Builder.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Query/Builder.php b/Query/Builder.php index 0ef10ac1c..e21f6d965 100755 --- a/Query/Builder.php +++ b/Query/Builder.php @@ -1983,6 +1983,16 @@ public function orderBy($column, $direction = 'asc') throw new InvalidArgumentException('Order direction must be "asc" or "desc".'); } + if (is_array($this->{$this->unions ? 'unionOrders' : 'orders'})) { + foreach ($this->{$this->unions ? 'unionOrders' : 'orders'} as $key => $value) { + if ($value['column'] === $column) { + $this->{$this->unions ? 'unionOrders' : 'orders'}[$key]['direction'] = $direction; + + return $this; + } + } + } + $this->{$this->unions ? 'unionOrders' : 'orders'}[] = [ 'column' => $column, 'direction' => $direction, From 261ef9af00cb8756a0669c25c5f89c66c75573d6 Mon Sep 17 00:00:00 2001 From: Mohamed Said Date: Tue, 1 Jun 2021 16:53:10 +0200 Subject: [PATCH 26/34] Allow connecting to read or write connections with the db command (#37548) --- Console/DbCommand.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Console/DbCommand.php b/Console/DbCommand.php index 3aee98e2b..a5c627a01 100644 --- a/Console/DbCommand.php +++ b/Console/DbCommand.php @@ -14,7 +14,9 @@ class DbCommand extends Command * * @var string */ - protected $signature = 'db {connection? : The database connection that should be used}'; + protected $signature = 'db {connection? : The database connection that should be used} + {--read : Connect to the read connection} + {--write : Connect to the write connection}'; /** * The console command description. @@ -64,6 +66,12 @@ public function getConnection() $connection = (new ConfigurationUrlParser)->parseConfiguration($connection); } + if ($this->option('read')) { + $connection = array_merge($connection, $connection['read']); + } elseif ($this->option('write')) { + $connection = array_merge($connection, $connection['write']); + } + return $connection; } From 08d58d68eae7c8b18ac8028cd4fd72606d6ad0c5 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 1 Jun 2021 09:53:38 -0500 Subject: [PATCH 27/34] [8.x] More Convenient Model Broadcasting (#37491) * Initial commit of model broadcasting conveniences * allow null return from broadcaston * add broadcast methods * Allow HasBroadcastChannel instances in routes This allows HasBroadcastChannel instances to be passed to broadcast routes. * Rename method * Do not broadcast if no channels for model event * add trait * use model basename * allow manual override of channels * add test * add test * allow specification of connection, queue, afterCommit * wip --- Eloquent/BroadcastableModelEventOccurred.php | 100 ++++++++++ Eloquent/BroadcastsEvents.php | 182 +++++++++++++++++++ Eloquent/Model.php | 23 ++- 3 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 Eloquent/BroadcastableModelEventOccurred.php create mode 100644 Eloquent/BroadcastsEvents.php diff --git a/Eloquent/BroadcastableModelEventOccurred.php b/Eloquent/BroadcastableModelEventOccurred.php new file mode 100644 index 000000000..4fc016028 --- /dev/null +++ b/Eloquent/BroadcastableModelEventOccurred.php @@ -0,0 +1,100 @@ +model = $model; + $this->event = $event; + } + + /** + * The channels the event should broadcast on. + * + * @return array + */ + public function broadcastOn() + { + $channels = empty($this->channels) + ? ($this->model->broadcastOn($this->event) ?: []) + : $this->channels; + + return collect($channels)->map(function ($channel) { + return $channel instanceof Model ? new PrivateChannel($channel) : $channel; + })->all(); + } + + /** + * The name the event should broadcast as. + * + * @return string + */ + public function broadcastAs() + { + return class_basename($this->model).ucfirst($this->event); + } + + /** + * Manually specify the channels the event should broadcast on. + * + * @param array $channels + * @return $this + */ + public function onChannels(array $channels) + { + $this->channels = $channels; + + return $this; + } +} diff --git a/Eloquent/BroadcastsEvents.php b/Eloquent/BroadcastsEvents.php new file mode 100644 index 000000000..969298cfe --- /dev/null +++ b/Eloquent/BroadcastsEvents.php @@ -0,0 +1,182 @@ +broadcastCreated(); + }); + + static::updated(function ($model) { + $model->broadcastUpdated(); + }); + + if (method_exists(static::class, 'bootSoftDeletes')) { + static::trashed(function ($model) { + $model->broadcastTrashed(); + }); + + static::restored(function ($model) { + $model->broadcastRestored(); + }); + } + + static::deleted(function ($model) { + $model->broadcastDeleted(); + }); + } + + /** + * Broadcast that the model was created. + * + * @param \Illuminate\Broadcasting\Channel|\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|array|null $channels + * @return \Illuminate\Broadcasting\PendingBroadcast + */ + public function broadcastCreated($channels = null) + { + return $this->broadcastIfBroadcastChannelsExistForEvent( + $this->newBroadcastableModelEvent('created'), 'created', $channels + ); + } + + /** + * Broadcast that the model was updated. + * + * @param \Illuminate\Broadcasting\Channel|\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|array|null $channels + * @return \Illuminate\Broadcasting\PendingBroadcast + */ + public function broadcastUpdated($channels = null) + { + return $this->broadcastIfBroadcastChannelsExistForEvent( + $this->newBroadcastableModelEvent('updated'), 'updated', $channels + ); + } + + /** + * Broadcast that the model was trashed. + * + * @param \Illuminate\Broadcasting\Channel|\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|array|null $channels + * @return \Illuminate\Broadcasting\PendingBroadcast + */ + public function broadcastTrashed($channels = null) + { + return $this->broadcastIfBroadcastChannelsExistForEvent( + $this->newBroadcastableModelEvent('trashed'), 'trashed', $channels + ); + } + + /** + * Broadcast that the model was restored. + * + * @param \Illuminate\Broadcasting\Channel|\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|array|null $channels + * @return \Illuminate\Broadcasting\PendingBroadcast + */ + public function broadcastRestored($channels = null) + { + return $this->broadcastIfBroadcastChannelsExistForEvent( + $this->newBroadcastableModelEvent('restored'), 'restored', $channels + ); + } + + /** + * Broadcast that the model was deleted. + * + * @param \Illuminate\Broadcasting\Channel|\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|array|null $channels + * @return \Illuminate\Broadcasting\PendingBroadcast + */ + public function broadcastDeleted($channels = null) + { + return $this->broadcastIfBroadcastChannelsExistForEvent( + $this->newBroadcastableModelEvent('deleted'), 'deleted', $channels + ); + } + + /** + * Broadcast the given event instance if channels are configured for the model event. + * + * @param mixed $instance + * @param string $event + * @param mixed $channels + * @return \Illuminate\Broadcasting\PendingBroadcast|null + */ + protected function broadcastIfBroadcastChannelsExistForEvent($instance, $event, $channels = null) + { + if (! empty($this->broadcastOn($event)) || ! empty($channels)) { + return broadcast($instance->onChannels(Arr::wrap($channels))); + } + } + + /** + * Create a new broadcastable model event event. + * + * @param string $event + * @return mixed + */ + public function newBroadcastableModelEvent($event) + { + return tap(new BroadcastableModelEventOccurred($this, $event), function ($event) { + $event->connection = property_exists($this, 'broadcastConnection') + ? $this->broadcastConnection + : $this->broadcastConnection(); + + $event->queue = property_exists($this, 'broadcastQueue') + ? $this->broadcastQueue + : $this->broadcastQueue(); + + $event->afterCommit = property_exists($this, 'broadcastAfterCommit') + ? $this->broadcastAfterCommit + : $this->broadcastAfterCommit(); + }); + } + + /** + * Get the channels that model events should broadcast on. + * + * @param string $event + * @return \Illuminate\Broadcasting\Channel|array + */ + public function broadcastOn($event) + { + return [$this]; + } + + /** + * Get the queue connection that should be used to broadcast model events. + * + * @return string|null + */ + public function broadcastConnection() + { + // + } + + /** + * Get the queue that should be used to broadcast model events. + * + * @return string|null + */ + public function broadcastQueue() + { + // + } + + /** + * Determine if the model event broadcast queued job should be dispatched after all transactions are committed. + * + * @return bool + */ + public function broadcastAfterCommit() + { + return false; + } +} diff --git a/Eloquent/Model.php b/Eloquent/Model.php index fdcceedac..67423f437 100644 --- a/Eloquent/Model.php +++ b/Eloquent/Model.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Eloquent; use ArrayAccess; +use Illuminate\Contracts\Broadcasting\HasBroadcastChannel; use Illuminate\Contracts\Queue\QueueableCollection; use Illuminate\Contracts\Queue\QueueableEntity; use Illuminate\Contracts\Routing\UrlRoutable; @@ -21,7 +22,7 @@ use JsonSerializable; use LogicException; -abstract class Model implements Arrayable, ArrayAccess, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable +abstract class Model implements Arrayable, ArrayAccess, HasBroadcastChannel, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable { use Concerns\HasAttributes, Concerns\HasEvents, @@ -1860,6 +1861,26 @@ public static function preventsLazyLoading() return static::$modelsShouldPreventLazyLoading; } + /** + * Get the broadcast channel route definition that is associated with the given entity. + * + * @return string + */ + public function broadcastChannelRoute() + { + return str_replace('\\', '.', get_class($this)).'.{'.Str::camel(class_basename($this)).'}'; + } + + /** + * Get the broadcast channel name that is associated with the given entity. + * + * @return string + */ + public function broadcastChannel() + { + return str_replace('\\', '.', get_class($this)).'.'.$this->getKey(); + } + /** * Dynamically retrieve attributes on the model. * From 3c053ab5a774155077de6693e23e2598b97891ed Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Tue, 1 Jun 2021 15:27:52 -0400 Subject: [PATCH 28/34] [8.x] Use "Conditionable" in existing classes that implement when() (#37561) * [8.x] Use "Conditionable" in existing classes that implement when() * Add full implementation of unless() * Reorder traits and add tests * StyleCI --- Concerns/BuildsQueries.php | 41 +++----------------------------------- 1 file changed, 3 insertions(+), 38 deletions(-) diff --git a/Concerns/BuildsQueries.php b/Concerns/BuildsQueries.php index 3e97b7d0d..461ef7115 100644 --- a/Concerns/BuildsQueries.php +++ b/Concerns/BuildsQueries.php @@ -10,11 +10,14 @@ use Illuminate\Pagination\Paginator; use Illuminate\Support\Collection; use Illuminate\Support\LazyCollection; +use Illuminate\Support\Traits\Conditionable; use InvalidArgumentException; use RuntimeException; trait BuildsQueries { + use Conditionable; + /** * Chunk the results of the query. * @@ -278,25 +281,6 @@ public function sole($columns = ['*']) return $result->first(); } - /** - * Apply the callback's query changes if the given "value" is true. - * - * @param mixed $value - * @param callable $callback - * @param callable|null $default - * @return mixed|$this - */ - public function when($value, $callback, $default = null) - { - if ($value) { - return $callback($this, $value) ?: $this; - } elseif ($default) { - return $default($this, $value) ?: $this; - } - - return $this; - } - /** * Pass the query to a given callback. * @@ -308,25 +292,6 @@ public function tap($callback) return $this->when(true, $callback); } - /** - * Apply the callback's query changes if the given "value" is false. - * - * @param mixed $value - * @param callable $callback - * @param callable|null $default - * @return mixed|$this - */ - public function unless($value, $callback, $default = null) - { - if (! $value) { - return $callback($this, $value) ?: $this; - } elseif ($default) { - return $default($this, $value) ?: $this; - } - - return $this; - } - /** * Create a new length-aware paginator instance. * From 905886ead0b052878608cbe5f3fb0e38ff687cf4 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 3 Jun 2021 11:39:17 -0500 Subject: [PATCH 29/34] revert change --- Query/Builder.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Query/Builder.php b/Query/Builder.php index e21f6d965..0ef10ac1c 100755 --- a/Query/Builder.php +++ b/Query/Builder.php @@ -1983,16 +1983,6 @@ public function orderBy($column, $direction = 'asc') throw new InvalidArgumentException('Order direction must be "asc" or "desc".'); } - if (is_array($this->{$this->unions ? 'unionOrders' : 'orders'})) { - foreach ($this->{$this->unions ? 'unionOrders' : 'orders'} as $key => $value) { - if ($value['column'] === $column) { - $this->{$this->unions ? 'unionOrders' : 'orders'}[$key]['direction'] = $direction; - - return $this; - } - } - } - $this->{$this->unions ? 'unionOrders' : 'orders'}[] = [ 'column' => $column, 'direction' => $direction, From bfc46054653c6b35144b091a87d449fedc99c9de Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 3 Jun 2021 14:54:13 -0500 Subject: [PATCH 30/34] add methods for indicating the write connection should be used --- Connection.php | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/Connection.php b/Connection.php index 71efec09d..24bd64e37 100755 --- a/Connection.php +++ b/Connection.php @@ -129,10 +129,17 @@ class Connection implements ConnectionInterface /** * Indicates if changes have been made to the database. * - * @var int + * @var bool */ protected $recordsModified = false; + /** + * Indicates if the connection should use the "write" PDO connection. + * + * @var bool + */ + protected $readOnWriteConnection = false; + /** * All of the queries run against the connection. * @@ -861,6 +868,16 @@ public function raw($value) return new Expression($value); } + /** + * Determine if the database connection has modified any database records. + * + * @return bool + */ + public function hasModifiedRecords() + { + return $this->recordsModified; + } + /** * Indicate if any records have been modified. * @@ -884,6 +901,19 @@ public function forgetRecordModificationState() $this->recordsModified = false; } + /** + * Indicate that the connection should use the write PDO connection for reads. + * + * @param bool $value + * @return $this + */ + public function useWriteConnectionWhenReading($value = true) + { + $this->readOnWriteConnection = $value; + + return $this; + } + /** * Is Doctrine available? * @@ -980,7 +1010,8 @@ public function getReadPdo() return $this->getPdo(); } - if ($this->recordsModified && $this->getConfig('sticky')) { + if ($this->readOnWriteConnection || + ($this->recordsModified && $this->getConfig('sticky'))) { return $this->getPdo(); } From c75abb585c57cee9e116cbf856aa34e4410546b4 Mon Sep 17 00:00:00 2001 From: Jelle Spekken Date: Fri, 4 Jun 2021 15:27:09 +0200 Subject: [PATCH 31/34] Fixes correct return type (#37592) This commit fixes the correct return type of the cursorPaginate method from `\Illuminate\Contracts\Pagination\Paginator` (which it doens't return) to `\Illuminate\Contracts\Pagination\CursorPaginator` --- Eloquent/Builder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Eloquent/Builder.php b/Eloquent/Builder.php index 49df237b5..a829069f8 100755 --- a/Eloquent/Builder.php +++ b/Eloquent/Builder.php @@ -827,7 +827,7 @@ public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'p * @param array $columns * @param string $cursorName * @param string|null $cursor - * @return \Illuminate\Contracts\Pagination\Paginator + * @return \Illuminate\Contracts\Pagination\CursorPaginator * @throws \Illuminate\Pagination\CursorPaginationException */ public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null) From 1e06ebe09d6d801ede9e36e5f2a298ca85b11fe3 Mon Sep 17 00:00:00 2001 From: Jav Date: Fri, 4 Jun 2021 15:52:23 +0200 Subject: [PATCH 32/34] [8.x] Columns in the order by list must be unique (#37582) * Columns in the order by list must be unique MySql is ok with it, but SqlServer error out. See a similar issue here: https://github.com/laravel/nova-issues/issues/1621 * override with next call * Update Builder.php * fix: "undefined index: column" from #37581 * add a regression test Co-authored-by: Taylor Otwell --- Query/Builder.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Query/Builder.php b/Query/Builder.php index 0ef10ac1c..acfc9e444 100755 --- a/Query/Builder.php +++ b/Query/Builder.php @@ -1983,6 +1983,16 @@ public function orderBy($column, $direction = 'asc') throw new InvalidArgumentException('Order direction must be "asc" or "desc".'); } + if (is_array($this->{$this->unions ? 'unionOrders' : 'orders'})) { + foreach ($this->{$this->unions ? 'unionOrders' : 'orders'} as $key => $value) { + if (isset($value['column']) && $value['column'] === $column) { + $this->{$this->unions ? 'unionOrders' : 'orders'}[$key]['direction'] = $direction; + + return $this; + } + } + } + $this->{$this->unions ? 'unionOrders' : 'orders'}[] = [ 'column' => $column, 'direction' => $direction, From 002595d4de68a4e946c2e5769a51e7dfdacd6ce6 Mon Sep 17 00:00:00 2001 From: Mohamed Said Date: Mon, 7 Jun 2021 15:32:55 +0200 Subject: [PATCH 33/34] fire a trashed event on soft deleting and listen to it for model broadcasting (#37618) --- Eloquent/BroadcastableModelEventOccurred.php | 10 ++++++++++ Eloquent/BroadcastsEvents.php | 2 +- Eloquent/SoftDeletes.php | 13 +++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Eloquent/BroadcastableModelEventOccurred.php b/Eloquent/BroadcastableModelEventOccurred.php index 4fc016028..a27e8ed2a 100644 --- a/Eloquent/BroadcastableModelEventOccurred.php +++ b/Eloquent/BroadcastableModelEventOccurred.php @@ -97,4 +97,14 @@ public function onChannels(array $channels) return $this; } + + /** + * Get the event name. + * + * @return string + */ + public function event() + { + return $this->event; + } } diff --git a/Eloquent/BroadcastsEvents.php b/Eloquent/BroadcastsEvents.php index 969298cfe..a2c55bed2 100644 --- a/Eloquent/BroadcastsEvents.php +++ b/Eloquent/BroadcastsEvents.php @@ -22,7 +22,7 @@ public static function bootBroadcastsEvents() }); if (method_exists(static::class, 'bootSoftDeletes')) { - static::trashed(function ($model) { + static::softDeleted(function ($model) { $model->broadcastTrashed(); }); diff --git a/Eloquent/SoftDeletes.php b/Eloquent/SoftDeletes.php index c1a3b04c7..cac971ccd 100644 --- a/Eloquent/SoftDeletes.php +++ b/Eloquent/SoftDeletes.php @@ -96,6 +96,8 @@ protected function runSoftDelete() $query->update($columns); $this->syncOriginalAttributes(array_keys($columns)); + + $this->fireModelEvent('trashed', false); } /** @@ -136,6 +138,17 @@ public function trashed() return ! is_null($this->{$this->getDeletedAtColumn()}); } + /** + * Register a "softDeleted" model event callback with the dispatcher. + * + * @param \Closure|string $callback + * @return void + */ + public static function softDeleted($callback) + { + static::registerModelEvent('trashed', $callback); + } + /** * Register a "restoring" model event callback with the dispatcher. * From ece544c68a9ccc575ddd5694bf628d85c5832607 Mon Sep 17 00:00:00 2001 From: Lennart Carstens-Behrens Date: Mon, 7 Jun 2021 15:33:24 +0200 Subject: [PATCH 34/34] [8.x] Fix one-of-many bindings (#37616) * fix * test * formatting * formatting * formatting --- Eloquent/Relations/Concerns/CanBeOneOfMany.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 0a2c6fac7..e30bf0413 100644 --- a/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -205,6 +205,8 @@ protected function newOneOfManySubQuery($groupBy, $column = null, $aggregate = n protected function addOneOfManyJoinSubQuery(Builder $parent, Builder $subQuery, $on) { $parent->beforeQuery(function ($parent) use ($subQuery, $on) { + $subQuery->applyBeforeQueryCallbacks(); + $parent->joinSub($subQuery, $this->relationName, function ($join) use ($on) { $join->on($this->qualifySubSelectColumn($on), '=', $this->qualifyRelatedColumn($on));