diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 7e54581c2a..0000000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -/.github export-ignore -.gitattributes export-ignore diff --git a/.github/workflows/close-pull-request.yml b/.github/workflows/close-pull-request.yml deleted file mode 100644 index 6cbfcf0671..0000000000 --- a/.github/workflows/close-pull-request.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Close Pull Request - -on: - pull_request_target: - types: [opened] - -jobs: - run: - runs-on: ubuntu-24.04 - steps: - - uses: superbrothers/close-pull-request@v3 - with: - comment: "Thank you for your pull request. However, you have submitted this PR on the Illuminate organization which is a read-only sub split of `laravel/framework`. Please submit your PR on the https://github.com/laravel/framework repository.

Thanks!" diff --git a/Capsule/Manager.php b/Capsule/Manager.php index ddcc85dcf7..1a14401973 100755 --- a/Capsule/Manager.php +++ b/Capsule/Manager.php @@ -2,13 +2,13 @@ namespace Illuminate\Database\Capsule; +use PDO; use Illuminate\Container\Container; -use Illuminate\Contracts\Events\Dispatcher; -use Illuminate\Database\Connectors\ConnectionFactory; use Illuminate\Database\DatabaseManager; -use Illuminate\Database\Eloquent\Model as Eloquent; +use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Support\Traits\CapsuleManagerTrait; -use PDO; +use Illuminate\Database\Eloquent\Model as Eloquent; +use Illuminate\Database\Connectors\ConnectionFactory; class Manager { @@ -25,14 +25,15 @@ class Manager * Create a new database capsule manager. * * @param \Illuminate\Container\Container|null $container + * @return void */ - public function __construct(?Container $container = null) + public function __construct(Container $container = null) { $this->setupContainer($container ?: new Container); // Once we have the container setup, we will setup the default configuration // options in the container "config" binding. This will make the database - // manager work correctly out of the box without extreme configuration. + // manager behave correctly since all the correct binding are in place. $this->setupDefaultConfiguration(); $this->setupManager(); @@ -65,7 +66,7 @@ protected function setupManager() /** * Get a connection instance from the global manager. * - * @param string|null $connection + * @param string $connection * @return \Illuminate\Database\Connection */ public static function connection($connection = null) @@ -76,20 +77,19 @@ public static function connection($connection = null) /** * Get a fluent query builder instance. * - * @param \Closure|\Illuminate\Database\Query\Builder|string $table - * @param string|null $as - * @param string|null $connection + * @param string $table + * @param string $connection * @return \Illuminate\Database\Query\Builder */ - public static function table($table, $as = null, $connection = null) + public static function table($table, $connection = null) { - return static::$instance->connection($connection)->table($table, $as); + return static::$instance->connection($connection)->table($table); } /** * Get a schema builder instance. * - * @param string|null $connection + * @param string $connection * @return \Illuminate\Database\Schema\Builder */ public static function schema($connection = null) @@ -100,7 +100,7 @@ public static function schema($connection = null) /** * Get a registered connection instance. * - * @param string|null $name + * @param string $name * @return \Illuminate\Database\Connection */ public function getConnection($name = null) @@ -111,7 +111,7 @@ public function getConnection($name = null) /** * Register a connection with the manager. * - * @param array $config + * @param array $config * @param string $name * @return void */ @@ -135,7 +135,7 @@ public function bootEloquent() // If we have an event dispatcher instance, we will go ahead and register it // with the Eloquent ORM, allowing for model callbacks while creating and - // updating "model" instances; however, it is not necessary to operate. + // updating "model" instances; however, if it not necessary to operate. if ($dispatcher = $this->getEventDispatcher()) { Eloquent::setEventDispatcher($dispatcher); } @@ -191,11 +191,11 @@ public function setEventDispatcher(Dispatcher $dispatcher) * Dynamically pass methods to the default connection. * * @param string $method - * @param array $parameters + * @param array $parameters * @return mixed */ public static function __callStatic($method, $parameters) { - return static::connection()->$method(...$parameters); + return call_user_func_array([static::connection(), $method], $parameters); } } diff --git a/ClassMorphViolationException.php b/ClassMorphViolationException.php deleted file mode 100644 index 6594d2d902..0000000000 --- a/ClassMorphViolationException.php +++ /dev/null @@ -1,29 +0,0 @@ -model = $class; - } -} diff --git a/Concerns/BuildsQueries.php b/Concerns/BuildsQueries.php deleted file mode 100644 index 537594e085..0000000000 --- a/Concerns/BuildsQueries.php +++ /dev/null @@ -1,615 +0,0 @@ -, int): mixed $callback - * @return bool - */ - public function chunk($count, callable $callback) - { - $this->enforceOrderBy(); - - $skip = $this->getOffset(); - $remaining = $this->getLimit(); - - $page = 1; - - do { - $offset = (($page - 1) * $count) + (int) $skip; - - $limit = is_null($remaining) ? $count : min($count, $remaining); - - if ($limit == 0) { - break; - } - - $results = $this->offset($offset)->limit($limit)->get(); - - $countResults = $results->count(); - - if ($countResults == 0) { - break; - } - - if (! is_null($remaining)) { - $remaining = max($remaining - $countResults, 0); - } - - if ($callback($results, $page) === false) { - return false; - } - - unset($results); - - $page++; - } while ($countResults == $count); - - return true; - } - - /** - * Run a map over each item while chunking. - * - * @template TReturn - * - * @param callable(TValue): TReturn $callback - * @param int $count - * @return \Illuminate\Support\Collection - */ - public function chunkMap(callable $callback, $count = 1000) - { - $collection = new Collection; - - $this->chunk($count, function ($items) use ($collection, $callback) { - $items->each(function ($item) use ($collection, $callback) { - $collection->push($callback($item)); - }); - }); - - return $collection; - } - - /** - * Execute a callback over each item while chunking. - * - * @param callable(TValue, int): mixed $callback - * @param int $count - * @return bool - * - * @throws \RuntimeException - */ - 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; - } - } - }); - } - - /** - * Chunk the results of a query by comparing IDs. - * - * @param int $count - * @param callable(\Illuminate\Support\Collection, int): mixed $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(\Illuminate\Support\Collection, int): mixed $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); - } - - /** - * Chunk the results of a query by comparing IDs in a given order. - * - * @param int $count - * @param callable(\Illuminate\Support\Collection, int): mixed $callback - * @param string|null $column - * @param string|null $alias - * @param bool $descending - * @return bool - * - * @throws \RuntimeException - */ - public function orderedChunkById($count, callable $callback, $column = null, $alias = null, $descending = false) - { - $column ??= $this->defaultKeyName(); - $alias ??= $column; - $lastId = null; - $skip = $this->getOffset(); - $remaining = $this->getLimit(); - - $page = 1; - - do { - $clone = clone $this; - - if ($skip && $page > 1) { - $clone->offset(0); - } - - $limit = is_null($remaining) ? $count : min($count, $remaining); - - if ($limit == 0) { - break; - } - - // We'll execute the query for the given page and get the results. If there are - // no results we can just break and return from here. When there are results - // we will call the callback with the current chunk of these results here. - if ($descending) { - $results = $clone->forPageBeforeId($limit, $lastId, $column)->get(); - } else { - $results = $clone->forPageAfterId($limit, $lastId, $column)->get(); - } - - $countResults = $results->count(); - - if ($countResults == 0) { - break; - } - - if (! is_null($remaining)) { - $remaining = max($remaining - $countResults, 0); - } - - // On each chunk result set, we will pass them to the callback and then let the - // developer take care of everything within the callback, which allows us to - // keep the memory low for spinning through large result sets for working. - if ($callback($results, $page) === false) { - return false; - } - - $lastId = data_get($results->last(), $alias); - - if ($lastId === null) { - throw new RuntimeException("The chunkById operation was aborted because the [{$alias}] column is not present in the query result."); - } - - unset($results); - - $page++; - } while ($countResults == $count); - - return true; - } - - /** - * Execute a callback over each item while chunking by ID. - * - * @param callable(TValue, int): mixed $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); - } - - /** - * Query lazily, by chunks of the given size. - * - * @param int $chunkSize - * @return \Illuminate\Support\LazyCollection - * - * @throws \InvalidArgumentException - */ - public function lazy($chunkSize = 1000) - { - if ($chunkSize < 1) { - throw new InvalidArgumentException('The chunk size should be at least 1'); - } - - $this->enforceOrderBy(); - - return new LazyCollection(function () use ($chunkSize) { - $page = 1; - - while (true) { - $results = $this->forPage($page++, $chunkSize)->get(); - - foreach ($results as $result) { - yield $result; - } - - if ($results->count() < $chunkSize) { - return; - } - } - }); - } - - /** - * 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 - * - * @throws \InvalidArgumentException - */ - public function lazyById($chunkSize = 1000, $column = null, $alias = null) - { - return $this->orderedLazyById($chunkSize, $column, $alias); - } - - /** - * 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 - * - * @throws \InvalidArgumentException - */ - public function lazyByIdDesc($chunkSize = 1000, $column = null, $alias = null) - { - return $this->orderedLazyById($chunkSize, $column, $alias, true); - } - - /** - * Query lazily, by chunking the results of a query by comparing IDs in a given order. - * - * @param int $chunkSize - * @param string|null $column - * @param string|null $alias - * @param bool $descending - * @return \Illuminate\Support\LazyCollection - * - * @throws \InvalidArgumentException - * @throws \RuntimeException - */ - protected function orderedLazyById($chunkSize = 1000, $column = null, $alias = null, $descending = false) - { - if ($chunkSize < 1) { - throw new InvalidArgumentException('The chunk size should be at least 1'); - } - - $column ??= $this->defaultKeyName(); - - $alias ??= $column; - - return new LazyCollection(function () use ($chunkSize, $column, $alias, $descending) { - $lastId = null; - - while (true) { - $clone = clone $this; - - if ($descending) { - $results = $clone->forPageBeforeId($chunkSize, $lastId, $column)->get(); - } else { - $results = $clone->forPageAfterId($chunkSize, $lastId, $column)->get(); - } - - foreach ($results as $result) { - yield $result; - } - - if ($results->count() < $chunkSize) { - return; - } - - $lastId = $results->last()->{$alias}; - - if ($lastId === null) { - throw new RuntimeException("The lazyById operation was aborted because the [{$alias}] column is not present in the query result."); - } - } - }); - } - - /** - * Execute the query and get the first result. - * - * @param array|string $columns - * @return TValue|null - */ - public function first($columns = ['*']) - { - return $this->limit(1)->get($columns)->first(); - } - - /** - * Execute the query and get the first result or throw an exception. - * - * @param array|string $columns - * @param string|null $message - * @return TValue - * - * @throws \Illuminate\Database\RecordNotFoundException - */ - public function firstOrFail($columns = ['*'], $message = null) - { - if (! is_null($result = $this->first($columns))) { - return $result; - } - - throw new RecordNotFoundException($message ?: 'No record found for the given query.'); - } - - /** - * Execute the query and get the first result if it's the sole matching record. - * - * @param array|string $columns - * @return TValue - * - * @throws \Illuminate\Database\RecordsNotFoundException - * @throws \Illuminate\Database\MultipleRecordsFoundException - */ - public function sole($columns = ['*']) - { - $result = $this->limit(2)->get($columns); - - $count = $result->count(); - - if ($count === 0) { - throw new RecordsNotFoundException; - } - - if ($count > 1) { - throw new MultipleRecordsFoundException($count); - } - - return $result->first(); - } - - /** - * Paginate the given query using a cursor paginator. - * - * @param int $perPage - * @param array|string $columns - * @param string $cursorName - * @param \Illuminate\Pagination\Cursor|string|null $cursor - * @return \Illuminate\Contracts\Pagination\CursorPaginator - */ - protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName = 'cursor', $cursor = null) - { - if (! $cursor instanceof Cursor) { - $cursor = is_string($cursor) - ? Cursor::fromEncoded($cursor) - : CursorPaginator::resolveCurrentCursor($cursorName, $cursor); - } - - $orders = $this->ensureOrderForCursorPagination(! is_null($cursor) && $cursor->pointsToPreviousItems()); - - if (! is_null($cursor)) { - // Reset the union bindings so we can add the cursor where in the correct position... - $this->setBindings([], 'union'); - - $addCursorConditions = function (self $builder, $previousColumn, $originalColumn, $i) use (&$addCursorConditions, $cursor, $orders) { - $unionBuilders = $builder->getUnionBuilders(); - - if (! is_null($previousColumn)) { - $originalColumn ??= $this->getOriginalColumnNameForCursorPagination($this, $previousColumn); - - $builder->where( - Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn, - '=', - $cursor->parameter($previousColumn) - ); - - $unionBuilders->each(function ($unionBuilder) use ($previousColumn, $cursor) { - $unionBuilder->where( - $this->getOriginalColumnNameForCursorPagination($unionBuilder, $previousColumn), - '=', - $cursor->parameter($previousColumn) - ); - - $this->addBinding($unionBuilder->getRawBindings()['where'], 'union'); - }); - } - - $builder->where(function (self $secondBuilder) use ($addCursorConditions, $cursor, $orders, $i, $unionBuilders) { - ['column' => $column, 'direction' => $direction] = $orders[$i]; - - $originalColumn = $this->getOriginalColumnNameForCursorPagination($this, $column); - - $secondBuilder->where( - Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn, - $direction === 'asc' ? '>' : '<', - $cursor->parameter($column) - ); - - if ($i < $orders->count() - 1) { - $secondBuilder->orWhere(function (self $thirdBuilder) use ($addCursorConditions, $column, $originalColumn, $i) { - $addCursorConditions($thirdBuilder, $column, $originalColumn, $i + 1); - }); - } - - $unionBuilders->each(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions) { - $unionWheres = $unionBuilder->getRawBindings()['where']; - - $originalColumn = $this->getOriginalColumnNameForCursorPagination($unionBuilder, $column); - $unionBuilder->where(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions, $originalColumn, $unionWheres) { - $unionBuilder->where( - $originalColumn, - $direction === 'asc' ? '>' : '<', - $cursor->parameter($column) - ); - - if ($i < $orders->count() - 1) { - $unionBuilder->orWhere(function (self $fourthBuilder) use ($addCursorConditions, $column, $originalColumn, $i) { - $addCursorConditions($fourthBuilder, $column, $originalColumn, $i + 1); - }); - } - - $this->addBinding($unionWheres, 'union'); - $this->addBinding($unionBuilder->getRawBindings()['where'], 'union'); - }); - }); - }); - }; - - $addCursorConditions($this, null, null, 0); - } - - $this->limit($perPage + 1); - - return $this->cursorPaginator($this->get($columns), $perPage, $cursor, [ - 'path' => Paginator::resolveCurrentPath(), - 'cursorName' => $cursorName, - 'parameters' => $orders->pluck('column')->toArray(), - ]); - } - - /** - * Get the original column name of the given column, without any aliasing. - * - * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*> $builder - * @param string $parameter - * @return string - */ - protected function getOriginalColumnNameForCursorPagination($builder, string $parameter) - { - $columns = $builder instanceof Builder ? $builder->getQuery()->getColumns() : $builder->getColumns(); - - if (! is_null($columns)) { - foreach ($columns as $column) { - if (($position = strripos($column, ' as ')) !== false) { - $original = substr($column, 0, $position); - - $alias = substr($column, $position + 4); - - if ($parameter === $alias || $builder->getGrammar()->wrap($parameter) === $alias) { - return $original; - } - } - } - } - - return $parameter; - } - - /** - * Create a new length-aware paginator instance. - * - * @param \Illuminate\Support\Collection $items - * @param int $total - * @param int $perPage - * @param int $currentPage - * @param array $options - * @return \Illuminate\Pagination\LengthAwarePaginator - */ - protected function paginator($items, $total, $perPage, $currentPage, $options) - { - return Container::getInstance()->makeWith(LengthAwarePaginator::class, compact( - 'items', 'total', 'perPage', 'currentPage', 'options' - )); - } - - /** - * Create a new simple paginator instance. - * - * @param \Illuminate\Support\Collection $items - * @param int $perPage - * @param int $currentPage - * @param array $options - * @return \Illuminate\Pagination\Paginator - */ - protected function simplePaginator($items, $perPage, $currentPage, $options) - { - return Container::getInstance()->makeWith(Paginator::class, compact( - 'items', 'perPage', 'currentPage', 'options' - )); - } - - /** - * Create a new cursor paginator instance. - * - * @param \Illuminate\Support\Collection $items - * @param int $perPage - * @param \Illuminate\Pagination\Cursor $cursor - * @param array $options - * @return \Illuminate\Pagination\CursorPaginator - */ - protected function cursorPaginator($items, $perPage, $cursor, $options) - { - return Container::getInstance()->makeWith(CursorPaginator::class, compact( - 'items', 'perPage', 'cursor', 'options' - )); - } - - /** - * Pass the query to a given callback and then return it. - * - * @param callable($this): mixed $callback - * @return $this - */ - public function tap($callback) - { - $callback($this); - - return $this; - } - - /** - * Pass the query to a given callback and return the result. - * - * @template TReturn - * - * @param (callable($this): TReturn) $callback - * @return (TReturn is null|void ? $this : TReturn) - */ - public function pipe($callback) - { - return $callback($this) ?? $this; - } -} diff --git a/Concerns/BuildsWhereDateClauses.php b/Concerns/BuildsWhereDateClauses.php deleted file mode 100644 index 06da844273..0000000000 --- a/Concerns/BuildsWhereDateClauses.php +++ /dev/null @@ -1,249 +0,0 @@ -wherePastOrFuture($columns, '<', 'and'); - } - - /** - * Add a where clause to determine if a "date" column is in the past or now to the query. - * - * @param array|string $columns - * @return $this - */ - public function whereNowOrPast($columns) - { - return $this->wherePastOrFuture($columns, '<=', 'and'); - } - - /** - * Add an "or where" clause to determine if a "date" column is in the past to the query. - * - * @param array|string $columns - * @return $this - */ - public function orWherePast($columns) - { - return $this->wherePastOrFuture($columns, '<', 'or'); - } - - /** - * Add a where clause to determine if a "date" column is in the past or now to the query. - * - * @param array|string $columns - * @return $this - */ - public function orWhereNowOrPast($columns) - { - return $this->wherePastOrFuture($columns, '<=', 'or'); - } - - /** - * Add a where clause to determine if a "date" column is in the future to the query. - * - * @param array|string $columns - * @return $this - */ - public function whereFuture($columns) - { - return $this->wherePastOrFuture($columns, '>', 'and'); - } - - /** - * Add a where clause to determine if a "date" column is in the future or now to the query. - * - * @param array|string $columns - * @return $this - */ - public function whereNowOrFuture($columns) - { - return $this->wherePastOrFuture($columns, '>=', 'and'); - } - - /** - * Add an "or where" clause to determine if a "date" column is in the future to the query. - * - * @param array|string $columns - * @return $this - */ - public function orWhereFuture($columns) - { - return $this->wherePastOrFuture($columns, '>', 'or'); - } - - /** - * Add an "or where" clause to determine if a "date" column is in the future or now to the query. - * - * @param array|string $columns - * @return $this - */ - public function orWhereNowOrFuture($columns) - { - return $this->wherePastOrFuture($columns, '>=', 'or'); - } - - /** - * Add an "where" clause to determine if a "date" column is in the past or future. - * - * @param array|string $columns - * @param string $operator - * @param string $boolean - * @return $this - */ - protected function wherePastOrFuture($columns, $operator, $boolean) - { - $type = 'Basic'; - $value = Carbon::now(); - - foreach (Arr::wrap($columns) as $column) { - $this->wheres[] = compact('type', 'column', 'boolean', 'operator', 'value'); - - $this->addBinding($value); - } - - return $this; - } - - /** - * Add a "where date" clause to determine if a "date" column is today to the query. - * - * @param array|string $columns - * @param string $boolean - * @return $this - */ - public function whereToday($columns, $boolean = 'and') - { - return $this->whereTodayBeforeOrAfter($columns, '=', $boolean); - } - - /** - * Add a "where date" clause to determine if a "date" column is before today. - * - * @param array|string $columns - * @return $this - */ - public function whereBeforeToday($columns) - { - return $this->whereTodayBeforeOrAfter($columns, '<', 'and'); - } - - /** - * Add a "where date" clause to determine if a "date" column is today or before to the query. - * - * @param array|string $columns - * @return $this - */ - public function whereTodayOrBefore($columns) - { - return $this->whereTodayBeforeOrAfter($columns, '<=', 'and'); - } - - /** - * Add a "where date" clause to determine if a "date" column is after today. - * - * @param array|string $columns - * @return $this - */ - public function whereAfterToday($columns) - { - return $this->whereTodayBeforeOrAfter($columns, '>', 'and'); - } - - /** - * Add a "where date" clause to determine if a "date" column is today or after to the query. - * - * @param array|string $columns - * @return $this - */ - public function whereTodayOrAfter($columns) - { - return $this->whereTodayBeforeOrAfter($columns, '>=', 'and'); - } - - /** - * Add an "or where date" clause to determine if a "date" column is today to the query. - * - * @param array|string $columns - * @return $this - */ - public function orWhereToday($columns) - { - return $this->whereToday($columns, 'or'); - } - - /** - * Add an "or where date" clause to determine if a "date" column is before today. - * - * @param array|string $columns - * @return $this - */ - public function orWhereBeforeToday($columns) - { - return $this->whereTodayBeforeOrAfter($columns, '<', 'or'); - } - - /** - * Add an "or where date" clause to determine if a "date" column is today or before to the query. - * - * @param array|string $columns - * @return $this - */ - public function orWhereTodayOrBefore($columns) - { - return $this->whereTodayBeforeOrAfter($columns, '<=', 'or'); - } - - /** - * Add an "or where date" clause to determine if a "date" column is after today. - * - * @param array|string $columns - * @return $this - */ - public function orWhereAfterToday($columns) - { - return $this->whereTodayBeforeOrAfter($columns, '>', 'or'); - } - - /** - * Add an "or where date" clause to determine if a "date" column is today or after to the query. - * - * @param array|string $columns - * @return $this - */ - public function orWhereTodayOrAfter($columns) - { - return $this->whereTodayBeforeOrAfter($columns, '>=', 'or'); - } - - /** - * Add a "where date" clause to determine if a "date" column is today or after to the query. - * - * @param array|string $columns - * @param string $operator - * @param string $boolean - * @return $this - */ - protected function whereTodayBeforeOrAfter($columns, $operator, $boolean) - { - $value = Carbon::today()->format('Y-m-d'); - - foreach (Arr::wrap($columns) as $column) { - $this->addDateBasedWhere('Date', $column, $operator, $value, $boolean); - } - - return $this; - } -} diff --git a/Concerns/CompilesJsonPaths.php b/Concerns/CompilesJsonPaths.php deleted file mode 100644 index fb62914374..0000000000 --- a/Concerns/CompilesJsonPaths.php +++ /dev/null @@ -1,65 +0,0 @@ -', $column, 2); - - $field = $this->wrap($parts[0]); - - $path = count($parts) > 1 ? ', '.$this->wrapJsonPath($parts[1], '->') : ''; - - return [$field, $path]; - } - - /** - * Wrap the given JSON path. - * - * @param string $value - * @param string $delimiter - * @return string - */ - protected function wrapJsonPath($value, $delimiter = '->') - { - $value = preg_replace("/([\\\\]+)?\\'/", "''", $value); - - $jsonPath = (new Collection(explode($delimiter, $value))) - ->map(fn ($segment) => $this->wrapJsonPathSegment($segment)) - ->join('.'); - - return "'$".(str_starts_with($jsonPath, '[') ? '' : '.').$jsonPath."'"; - } - - /** - * Wrap the given JSON path segment. - * - * @param string $segment - * @return string - */ - protected function wrapJsonPathSegment($segment) - { - if (preg_match('/(\[[^\]]+\])+$/', $segment, $parts)) { - $key = Str::beforeLast($segment, $parts[0]); - - if (! empty($key)) { - return '"'.$key.'"'.$parts[0]; - } - - return $parts[0]; - } - - return '"'.$segment.'"'; - } -} diff --git a/Concerns/ExplainsQueries.php b/Concerns/ExplainsQueries.php deleted file mode 100644 index 7168de1e55..0000000000 --- a/Concerns/ExplainsQueries.php +++ /dev/null @@ -1,24 +0,0 @@ -toSql(); - - $bindings = $this->getBindings(); - - $explanation = $this->getConnection()->select('EXPLAIN '.$sql, $bindings); - - return new Collection($explanation); - } -} diff --git a/Concerns/ManagesTransactions.php b/Concerns/ManagesTransactions.php deleted file mode 100644 index 9874727d26..0000000000 --- a/Concerns/ManagesTransactions.php +++ /dev/null @@ -1,373 +0,0 @@ -beginTransaction(); - - // We'll simply execute the given callback within a try / catch block and if we - // catch any exception we can rollback this transaction so that none of this - // gets actually persisted to a database or stored in a permanent fashion. - try { - $callbackResult = $callback($this); - } - - // If we catch an exception we'll rollback this transaction and try again if we - // are not out of attempts. If we are out of attempts we will just throw the - // exception back out, and let the developer handle an uncaught exception. - catch (Throwable $e) { - $this->handleTransactionException( - $e, $currentAttempt, $attempts - ); - - continue; - } - - $levelBeingCommitted = $this->transactions; - - try { - if ($this->transactions == 1) { - $this->fireConnectionEvent('committing'); - $this->getPdo()->commit(); - } - - $this->transactions = max(0, $this->transactions - 1); - } catch (Throwable $e) { - $this->handleCommitTransactionException( - $e, $currentAttempt, $attempts - ); - - continue; - } - - $this->transactionsManager?->commit( - $this->getName(), - $levelBeingCommitted, - $this->transactions - ); - - $this->fireConnectionEvent('committed'); - - return $callbackResult; - } - } - - /** - * Handle an exception encountered when running a transacted statement. - * - * @param \Throwable $e - * @param int $currentAttempt - * @param int $maxAttempts - * @return void - * - * @throws \Throwable - */ - protected function handleTransactionException(Throwable $e, $currentAttempt, $maxAttempts) - { - // On a deadlock, MySQL rolls back the entire transaction so we can't just - // retry the query. We have to throw this exception all the way out and - // let the developer handle it in another way. We will decrement too. - if ($this->causedByConcurrencyError($e) && - $this->transactions > 1) { - $this->transactions--; - - $this->transactionsManager?->rollback( - $this->getName(), $this->transactions - ); - - throw new DeadlockException($e->getMessage(), is_int($e->getCode()) ? $e->getCode() : 0, $e); - } - - // If there was an exception we will rollback this transaction and then we - // can check if we have exceeded the maximum attempt count for this and - // if we haven't we will return and try this query again in our loop. - $this->rollBack(); - - if ($this->causedByConcurrencyError($e) && - $currentAttempt < $maxAttempts) { - return; - } - - throw $e; - } - - /** - * Start a new database transaction. - * - * @return void - * - * @throws \Throwable - */ - public function beginTransaction() - { - foreach ($this->beforeStartingTransaction as $callback) { - $callback($this); - } - - $this->createTransaction(); - - $this->transactions++; - - $this->transactionsManager?->begin( - $this->getName(), $this->transactions - ); - - $this->fireConnectionEvent('beganTransaction'); - } - - /** - * Create a transaction within the database. - * - * @return void - * - * @throws \Throwable - */ - protected function createTransaction() - { - if ($this->transactions == 0) { - $this->reconnectIfMissingConnection(); - - try { - $this->executeBeginTransactionStatement(); - } catch (Throwable $e) { - $this->handleBeginTransactionException($e); - } - } elseif ($this->transactions >= 1 && $this->queryGrammar->supportsSavepoints()) { - $this->createSavepoint(); - } - } - - /** - * Create a save point within the database. - * - * @return void - * - * @throws \Throwable - */ - protected function createSavepoint() - { - $this->getPdo()->exec( - $this->queryGrammar->compileSavepoint('trans'.($this->transactions + 1)) - ); - } - - /** - * Handle an exception from a transaction beginning. - * - * @param \Throwable $e - * @return void - * - * @throws \Throwable - */ - protected function handleBeginTransactionException(Throwable $e) - { - if ($this->causedByLostConnection($e)) { - $this->reconnect(); - - $this->executeBeginTransactionStatement(); - } else { - throw $e; - } - } - - /** - * Commit the active database transaction. - * - * @return void - * - * @throws \Throwable - */ - public function commit() - { - if ($this->transactionLevel() == 1) { - $this->fireConnectionEvent('committing'); - $this->getPdo()->commit(); - } - - [$levelBeingCommitted, $this->transactions] = [ - $this->transactions, - max(0, $this->transactions - 1), - ]; - - $this->transactionsManager?->commit( - $this->getName(), $levelBeingCommitted, $this->transactions - ); - - $this->fireConnectionEvent('committed'); - } - - /** - * Handle an exception encountered when committing a transaction. - * - * @param \Throwable $e - * @param int $currentAttempt - * @param int $maxAttempts - * @return void - * - * @throws \Throwable - */ - protected function handleCommitTransactionException(Throwable $e, $currentAttempt, $maxAttempts) - { - $this->transactions = max(0, $this->transactions - 1); - - if ($this->causedByConcurrencyError($e) && $currentAttempt < $maxAttempts) { - return; - } - - if ($this->causedByLostConnection($e)) { - $this->transactions = 0; - } - - throw $e; - } - - /** - * Rollback the active database transaction. - * - * @param int|null $toLevel - * @return void - * - * @throws \Throwable - */ - public function rollBack($toLevel = null) - { - // We allow developers to rollback to a certain transaction level. We will verify - // that this given transaction level is valid before attempting to rollback to - // that level. If it's not we will just return out and not attempt anything. - $toLevel = is_null($toLevel) - ? $this->transactions - 1 - : $toLevel; - - if ($toLevel < 0 || $toLevel >= $this->transactions) { - return; - } - - // Next, we will actually perform this rollback within this database and fire the - // rollback event. We will also set the current transaction level to the given - // level that was passed into this method so it will be right from here out. - try { - $this->performRollBack($toLevel); - } catch (Throwable $e) { - $this->handleRollBackException($e); - } - - $this->transactions = $toLevel; - - $this->transactionsManager?->rollback( - $this->getName(), $this->transactions - ); - - $this->fireConnectionEvent('rollingBack'); - } - - /** - * Perform a rollback within the database. - * - * @param int $toLevel - * @return void - * - * @throws \Throwable - */ - protected function performRollBack($toLevel) - { - if ($toLevel == 0) { - $pdo = $this->getPdo(); - - if ($pdo->inTransaction()) { - $pdo->rollBack(); - } - } elseif ($this->queryGrammar->supportsSavepoints()) { - $this->getPdo()->exec( - $this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1)) - ); - } - } - - /** - * Handle an exception from a rollback. - * - * @param \Throwable $e - * @return void - * - * @throws \Throwable - */ - protected function handleRollBackException(Throwable $e) - { - if ($this->causedByLostConnection($e)) { - $this->transactions = 0; - - $this->transactionsManager?->rollback( - $this->getName(), $this->transactions - ); - } - - throw $e; - } - - /** - * Get the number of active transactions. - * - * @return int - */ - public function transactionLevel() - { - return $this->transactions; - } - - /** - * Execute the callback after a transaction commits. - * - * @param callable $callback - * @return void - * - * @throws \RuntimeException - */ - public function afterCommit($callback) - { - if ($this->transactionsManager) { - return $this->transactionsManager->addCallback($callback); - } - - throw new RuntimeException('Transactions Manager has not been set.'); - } - - /** - * Execute the callback after a transaction rolls back. - * - * @param callable $callback - * @return void - * - * @throws \RuntimeException - */ - public function afterRollBack($callback) - { - if ($this->transactionsManager) { - return $this->transactionsManager->addCallbackForRollback($callback); - } - - throw new RuntimeException('Transactions Manager has not been set.'); - } -} diff --git a/Concerns/ParsesSearchPath.php b/Concerns/ParsesSearchPath.php deleted file mode 100644 index e822c722b7..0000000000 --- a/Concerns/ParsesSearchPath.php +++ /dev/null @@ -1,25 +0,0 @@ -getCode() === 40001 || $e->getCode() === '40001')) { - return true; - } - - $message = $e->getMessage(); - - return Str::contains($message, [ - 'Deadlock found when trying to get lock', - 'deadlock detected', - 'The database file is locked', - 'database is locked', - 'database table is locked', - 'A table in the database is locked', - 'has been chosen as the deadlock victim', - 'Lock wait timeout exceeded; try restarting transaction', - 'WSREP detected deadlock/conflict and aborted the transaction. Try restarting the transaction', - 'Record has changed since last read in table', - ]); - } -} diff --git a/ConfigurationUrlParser.php b/ConfigurationUrlParser.php deleted file mode 100644 index bc7c624a28..0000000000 --- a/ConfigurationUrlParser.php +++ /dev/null @@ -1,10 +0,0 @@ -): mixed)}[] - */ - protected $queryDurationHandlers = []; - /** * Indicates if the connection is in a "dry run". * @@ -190,40 +121,41 @@ class Connection implements ConnectionInterface protected $pretending = false; /** - * All of the callbacks that should be invoked before a transaction is started. + * The name of the connected database. * - * @var \Closure[] + * @var string */ - protected $beforeStartingTransaction = []; + protected $database; /** - * All of the callbacks that should be invoked before a query is executed. + * The instance of Doctrine connection. * - * @var (\Closure(string, array, \Illuminate\Database\Connection): mixed)[] + * @var \Doctrine\DBAL\Connection */ - protected $beforeExecutingCallbacks = []; + protected $doctrineConnection; /** - * The connection resolvers. + * The table prefix for the connection. * - * @var \Closure[] + * @var string */ - protected static $resolvers = []; + protected $tablePrefix = ''; /** - * The last retrieved PDO read / write type. + * The database connection configuration options. * - * @var null|'read'|'write' + * @var array */ - protected $latestPdoTypeRetrieved = null; + protected $config = []; /** * Create a new database connection instance. * - * @param \PDO|(\Closure(): \PDO) $pdo - * @param string $database - * @param string $tablePrefix - * @param array $config + * @param \PDO|\Closure $pdo + * @param string $database + * @param string $tablePrefix + * @param array $config + * @return void */ public function __construct($pdo, $database = '', $tablePrefix = '', array $config = []) { @@ -263,7 +195,7 @@ public function useDefaultQueryGrammar() */ protected function getDefaultQueryGrammar() { - return new QueryGrammar($this); + return new QueryGrammar; } /** @@ -279,7 +211,7 @@ public function useDefaultSchemaGrammar() /** * Get the default schema grammar instance. * - * @return \Illuminate\Database\Schema\Grammars\Grammar|null + * @return \Illuminate\Database\Schema\Grammars\Grammar */ protected function getDefaultSchemaGrammar() { @@ -323,13 +255,12 @@ public function getSchemaBuilder() /** * Begin a fluent query against a database table. * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Contracts\Database\Query\Expression|\UnitEnum|string $table - * @param string|null $as + * @param string $table * @return \Illuminate\Database\Query\Builder */ - public function table($table, $as = null) + public function table($table) { - return $this->query()->from(enum_value($table), $as); + return $this->query()->from($table); } /** @@ -345,52 +276,35 @@ public function query() } /** - * Run a select statement and return a single result. + * Get a new raw query expression. * - * @param string $query - * @param array $bindings - * @param bool $useReadPdo - * @return mixed + * @param mixed $value + * @return \Illuminate\Database\Query\Expression */ - public function selectOne($query, $bindings = [], $useReadPdo = true) + public function raw($value) { - $records = $this->select($query, $bindings, $useReadPdo); - - return array_shift($records); + return new Expression($value); } /** - * Run a select statement and return the first column of the first row. + * Run a select statement and return a single result. * * @param string $query - * @param array $bindings - * @param bool $useReadPdo + * @param array $bindings * @return mixed - * - * @throws \Illuminate\Database\MultipleColumnsSelectedException */ - public function scalar($query, $bindings = [], $useReadPdo = true) + public function selectOne($query, $bindings = []) { - $record = $this->selectOne($query, $bindings, $useReadPdo); - - if (is_null($record)) { - return null; - } - - $record = (array) $record; + $records = $this->select($query, $bindings); - if (count($record) > 1) { - throw new MultipleColumnsSelectedException; - } - - return array_first($record); + return count($records) > 0 ? reset($records) : null; } /** * Run a select statement against the database. * * @param string $query - * @param array $bindings + * @param array $bindings * @return array */ public function selectFromWriteConnection($query, $bindings = []) @@ -404,119 +318,49 @@ public function selectFromWriteConnection($query, $bindings = []) * @param string $query * @param array $bindings * @param bool $useReadPdo - * @param array $fetchUsing * @return array */ - public function select($query, $bindings = [], $useReadPdo = true, array $fetchUsing = []) + public function select($query, $bindings = [], $useReadPdo = true) { - return $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo, $fetchUsing) { - if ($this->pretending()) { + return $this->run($query, $bindings, function ($me, $query, $bindings) use ($useReadPdo) { + if ($me->pretending()) { return []; } // For select statements, we'll simply execute the query and return an array // of the database result set. Each element in the array will be a single // row from the database table, and will either be an array or objects. - $statement = $this->prepared( - $this->getPdoForSelect($useReadPdo)->prepare($query) - ); - - $this->bindValues($statement, $this->prepareBindings($bindings)); - - $statement->execute(); - - return $statement->fetchAll(...$fetchUsing); - }); - } - - /** - * Run a select statement against the database and returns all of the result sets. - * - * @param string $query - * @param array $bindings - * @param bool $useReadPdo - * @param array $fetchUsing - * @return array - */ - public function selectResultSets($query, $bindings = [], $useReadPdo = true, array $fetchUsing = []) - { - return $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo, $fetchUsing) { - if ($this->pretending()) { - return []; - } - - $statement = $this->prepared( - $this->getPdoForSelect($useReadPdo)->prepare($query) - ); + $statement = $this->getPdoForSelect($useReadPdo)->prepare($query); - $this->bindValues($statement, $this->prepareBindings($bindings)); + $this->bindValues($statement, $me->prepareBindings($bindings)); $statement->execute(); - $sets = []; - - do { - $sets[] = $statement->fetchAll(...$fetchUsing); - } while ($statement->nextRowset()); + $fetchArgument = $me->getFetchArgument(); - return $sets; + return isset($fetchArgument) + ? $statement->fetchAll($me->getFetchMode(), $fetchArgument, $me->getFetchConstructorArgument()) + : $statement->fetchAll($me->getFetchMode()); }); } /** - * Run a select statement against the database and returns a generator. + * Bind values to their parameters in the given statement. * - * @param string $query + * @param \PDOStatement $statement * @param array $bindings - * @param bool $useReadPdo - * @param array $fetchUsing - * @return \Generator + * @return void */ - public function cursor($query, $bindings = [], $useReadPdo = true, array $fetchUsing = []) + public function bindValues($statement, $bindings) { - $statement = $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo) { - if ($this->pretending()) { - return []; - } - - // First we will create a statement for the query. Then, we will set the fetch - // mode and prepare the bindings for the query. Once that's done we will be - // ready to execute the query against the database and return the cursor. - $statement = $this->prepared($this->getPdoForSelect($useReadPdo) - ->prepare($query)); - - $this->bindValues( - $statement, $this->prepareBindings($bindings) + foreach ($bindings as $key => $value) { + $statement->bindValue( + is_string($key) ? $key : $key + 1, $value, + filter_var($value, FILTER_VALIDATE_FLOAT) !== false ? PDO::PARAM_INT : PDO::PARAM_STR ); - - // Next, we'll execute the query against the database and return the statement - // so we can return the cursor. The cursor will use a PHP generator to give - // back one row at a time without using a bunch of memory to render them. - $statement->execute(); - - return $statement; - }); - - while ($record = $statement->fetch(...$fetchUsing)) { - yield $record; } } - /** - * Configure the PDO prepared statement. - * - * @param \PDOStatement $statement - * @return \PDOStatement - */ - protected function prepared(PDOStatement $statement) - { - $statement->setFetchMode($this->fetchMode); - - $this->event(new StatementPrepared($this, $statement)); - - return $statement; - } - /** * Get the PDO connection to use for a select query. * @@ -532,7 +376,7 @@ protected function getPdoForSelect($useReadPdo = true) * Run an insert statement against the database. * * @param string $query - * @param array $bindings + * @param array $bindings * @return bool */ public function insert($query, $bindings = []) @@ -544,7 +388,7 @@ public function insert($query, $bindings = []) * Run an update statement against the database. * * @param string $query - * @param array $bindings + * @param array $bindings * @return int */ public function update($query, $bindings = []) @@ -556,7 +400,7 @@ public function update($query, $bindings = []) * Run a delete statement against the database. * * @param string $query - * @param array $bindings + * @param array $bindings * @return int */ public function delete($query, $bindings = []) @@ -568,21 +412,19 @@ public function delete($query, $bindings = []) * Execute an SQL statement and return the boolean result. * * @param string $query - * @param array $bindings + * @param array $bindings * @return bool */ public function statement($query, $bindings = []) { - return $this->run($query, $bindings, function ($query, $bindings) { - if ($this->pretending()) { + return $this->run($query, $bindings, function ($me, $query, $bindings) { + if ($me->pretending()) { return true; } $statement = $this->getPdo()->prepare($query); - $this->bindValues($statement, $this->prepareBindings($bindings)); - - $this->recordsHaveBeenModified(); + $this->bindValues($statement, $me->prepareBindings($bindings)); return $statement->execute(); }); @@ -592,30 +434,26 @@ public function statement($query, $bindings = []) * Run an SQL statement and get the number of rows affected. * * @param string $query - * @param array $bindings + * @param array $bindings * @return int */ public function affectingStatement($query, $bindings = []) { - return $this->run($query, $bindings, function ($query, $bindings) { - if ($this->pretending()) { + return $this->run($query, $bindings, function ($me, $query, $bindings) { + if ($me->pretending()) { return 0; } // For update or delete statements, we want to get the number of rows affected // by the statement and return that back to the developer. We'll first need // to execute the statement and then we'll use PDO to fetch the affected. - $statement = $this->getPdo()->prepare($query); + $statement = $me->getPdo()->prepare($query); - $this->bindValues($statement, $this->prepareBindings($bindings)); + $this->bindValues($statement, $me->prepareBindings($bindings)); $statement->execute(); - $this->recordsHaveBeenModified( - ($count = $statement->rowCount()) > 0 - ); - - return $count; + return $statement->rowCount(); }); } @@ -627,154 +465,175 @@ public function affectingStatement($query, $bindings = []) */ public function unprepared($query) { - return $this->run($query, [], function ($query) { - if ($this->pretending()) { + return $this->run($query, [], function ($me, $query) { + if ($me->pretending()) { return true; } - $this->recordsHaveBeenModified( - $change = $this->getPdo()->exec($query) !== false - ); - - return $change; + return (bool) $me->getPdo()->exec($query); }); } /** - * Get the number of open connections for the database. - * - * @return int|null - */ - public function threadCount() - { - $query = $this->getQueryGrammar()->compileThreadCount(); - - return $query ? $this->scalar($query) : null; - } - - /** - * Execute the given callback in "dry run" mode. + * Prepare the query bindings for execution. * - * @param (\Closure(\Illuminate\Database\Connection): mixed) $callback - * @return array{query: string, bindings: array, time: float|null}[] + * @param array $bindings + * @return array */ - public function pretend(Closure $callback) + public function prepareBindings(array $bindings) { - return $this->withFreshQueryLog(function () use ($callback) { - $this->pretending = true; - - try { - // Basically to make the database connection "pretend", we will just return - // the default values for all the query methods, then we will return an - // array of queries that were "executed" within the Closure callback. - $callback($this); + $grammar = $this->getQueryGrammar(); - return $this->queryLog; - } finally { - $this->pretending = false; + foreach ($bindings as $key => $value) { + // We need to transform all instances of DateTimeInterface into the actual + // date string. Each query grammar maintains its own date string format + // so we'll just ask the grammar for the format to get from the date. + if ($value instanceof DateTimeInterface) { + $bindings[$key] = $value->format($grammar->getDateFormat()); + } elseif ($value === false) { + $bindings[$key] = 0; } - }); + } + + return $bindings; } /** - * Execute the given callback without "pretending". + * Execute a Closure within a transaction. * * @param \Closure $callback * @return mixed + * + * @throws \Exception|\Throwable */ - public function withoutPretending(Closure $callback) + public function transaction(Closure $callback) { - if (! $this->pretending) { - return $callback(); + $this->beginTransaction(); + + // We'll simply execute the given callback within a try / catch block + // and if we catch any exception we can rollback the transaction + // so that none of the changes are persisted to the database. + try { + $result = $callback($this); + + $this->commit(); } - $this->pretending = false; + // If we catch an exception, we will roll back so nothing gets messed + // up in the database. Then we'll re-throw the exception so it can + // be handled how the developer sees fit for their applications. + catch (Exception $e) { + $this->rollBack(); - try { - return $callback(); - } finally { - $this->pretending = true; + throw $e; + } catch (Throwable $e) { + $this->rollBack(); + + throw $e; } + + return $result; } /** - * Execute the given callback in "dry run" mode. + * Start a new database transaction. * - * @param (\Closure(): array{query: string, bindings: array, time: float|null}[]) $callback - * @return array{query: string, bindings: array, time: float|null}[] + * @return void */ - protected function withFreshQueryLog($callback) + public function beginTransaction() { - $loggingQueries = $this->loggingQueries; + ++$this->transactions; - // First we will back up the value of the logging queries property and then - // we'll be ready to run callbacks. This query log will also get cleared - // so we will have a new log of all the queries that are executed now. - $this->enableQueryLog(); + if ($this->transactions == 1) { + $this->getPdo()->beginTransaction(); + } elseif ($this->transactions > 1 && $this->queryGrammar->supportsSavepoints()) { + $this->getPdo()->exec( + $this->queryGrammar->compileSavepoint('trans'.$this->transactions) + ); + } - $this->queryLog = []; + $this->fireConnectionEvent('beganTransaction'); + } - // Now we'll execute this callback and capture the result. Once it has been - // executed we will restore the value of query logging and give back the - // value of the callback so the original callers can have the results. - $result = $callback(); + /** + * Commit the active database transaction. + * + * @return void + */ + public function commit() + { + if ($this->transactions == 1) { + $this->getPdo()->commit(); + } - $this->loggingQueries = $loggingQueries; + --$this->transactions; - return $result; + $this->fireConnectionEvent('committed'); } /** - * Bind values to their parameters in the given statement. + * Rollback the active database transaction. * - * @param \PDOStatement $statement - * @param array $bindings * @return void */ - public function bindValues($statement, $bindings) + public function rollBack() { - foreach ($bindings as $key => $value) { - $statement->bindValue( - is_string($key) ? $key : $key + 1, - $value, - match (true) { - is_int($value) => PDO::PARAM_INT, - is_resource($value) => PDO::PARAM_LOB, - default => PDO::PARAM_STR - }, + if ($this->transactions == 1) { + $this->getPdo()->rollBack(); + } elseif ($this->transactions > 1 && $this->queryGrammar->supportsSavepoints()) { + $this->getPdo()->exec( + $this->queryGrammar->compileSavepointRollBack('trans'.$this->transactions) ); } + + $this->transactions = max(0, $this->transactions - 1); + + $this->fireConnectionEvent('rollingBack'); } /** - * Prepare the query bindings for execution. + * Get the number of active transactions. * - * @param array $bindings + * @return int + */ + public function transactionLevel() + { + return $this->transactions; + } + + /** + * Execute the given callback in "dry run" mode. + * + * @param \Closure $callback * @return array */ - public function prepareBindings(array $bindings) + public function pretend(Closure $callback) { - $grammar = $this->getQueryGrammar(); + $loggingQueries = $this->loggingQueries; - foreach ($bindings as $key => $value) { - // We need to transform all instances of DateTimeInterface into the actual - // date string. Each query grammar maintains its own date string format - // so we'll just ask the grammar for the format to get from the date. - if ($value instanceof DateTimeInterface) { - $bindings[$key] = $value->format($grammar->getDateFormat()); - } elseif (is_bool($value)) { - $bindings[$key] = (int) $value; - } - } + $this->enableQueryLog(); - return $bindings; + $this->pretending = true; + + $this->queryLog = []; + + // Basically to make the database connection "pretend", we will just return + // the default values for all the query methods, then we will return an + // array of queries that were "executed" within the Closure callback. + $callback($this); + + $this->pretending = false; + + $this->loggingQueries = $loggingQueries; + + return $this->queryLog; } /** * Run a SQL statement and log its execution context. * - * @param string $query - * @param array $bindings + * @param string $query + * @param array $bindings * @param \Closure $callback * @return mixed * @@ -782,10 +641,6 @@ public function prepareBindings(array $bindings) */ protected function run($query, $bindings, Closure $callback) { - foreach ($this->beforeExecutingCallbacks as $beforeExecutingCallback) { - $beforeExecutingCallback($query, $bindings, $this); - } - $this->reconnectIfMissingConnection(); $start = microtime(true); @@ -796,7 +651,11 @@ protected function run($query, $bindings, Closure $callback) try { $result = $this->runQueryCallback($query, $bindings, $callback); } catch (QueryException $e) { - $result = $this->handleQueryException( + if ($this->transactions >= 1) { + throw $e; + } + + $result = $this->tryAgainIfCausedByLostConnection( $e, $query, $bindings, $callback ); } @@ -804,9 +663,9 @@ protected function run($query, $bindings, Closure $callback) // Once we have run the query we will calculate the time that it took to run and // then log the query, bindings, and execution time so we will report them on // the event that the developer needs them. We'll log time in milliseconds. - $this->logQuery( - $query, $bindings, $this->getElapsedTime($start) - ); + $time = $this->getElapsedTime($start); + + $this->logQuery($query, $bindings, $time); return $result; } @@ -814,8 +673,8 @@ protected function run($query, $bindings, Closure $callback) /** * Run a SQL statement. * - * @param string $query - * @param array $bindings + * @param string $query + * @param array $bindings * @param \Closure $callback * @return mixed * @@ -827,168 +686,27 @@ protected function runQueryCallback($query, $bindings, Closure $callback) // run the SQL against the PDO connection. Then we can calculate the time it // took to execute and log the query SQL, bindings and time in our memory. try { - return $callback($query, $bindings); + $result = $callback($this, $query, $bindings); } // If an exception occurs when attempting to run a query, we'll format the error // message to include the bindings with SQL, which will make this exception a // lot more helpful to the developer instead of just the database's errors. catch (Exception $e) { - $exceptionType = $this->isUniqueConstraintError($e) - ? UniqueConstraintViolationException::class - : QueryException::class; - - throw new $exceptionType( - $this->getNameWithReadWriteType(), - $query, - $this->prepareBindings($bindings), - $e, - $this->getConnectionDetails(), - $this->latestReadWriteTypeUsed(), + throw new QueryException( + $query, $this->prepareBindings($bindings), $e ); } - } - - /** - * Determine if the given database exception was caused by a unique constraint violation. - * - * @param \Exception $exception - * @return bool - */ - protected function isUniqueConstraintError(Exception $exception) - { - return false; - } - - /** - * Log a query in the connection's query log. - * - * @param string $query - * @param array $bindings - * @param float|null $time - * @return void - */ - public function logQuery($query, $bindings, $time = null) - { - $this->totalQueryDuration += $time ?? 0.0; - - $readWriteType = $this->latestReadWriteTypeUsed(); - - $this->event(new QueryExecuted($query, $bindings, $time, $this, $readWriteType)); - - $query = $this->pretending === true - ? $this->queryGrammar?->substituteBindingsIntoRawSql($query, $bindings) ?? $query - : $query; - - if ($this->loggingQueries) { - $this->queryLog[] = compact('query', 'bindings', 'time', 'readWriteType'); - } - } - - /** - * Get the elapsed time in milliseconds since a given starting point. - * - * @param float $start - * @return float - */ - protected function getElapsedTime($start) - { - return round((microtime(true) - $start) * 1000, 2); - } - - /** - * Register a callback to be invoked when the connection queries for longer than a given amount of time. - * - * @param \DateTimeInterface|\Carbon\CarbonInterval|float|int $threshold - * @param (callable(\Illuminate\Database\Connection, \Illuminate\Database\Events\QueryExecuted): mixed) $handler - * @return void - */ - public function whenQueryingForLongerThan($threshold, $handler) - { - $threshold = $threshold instanceof DateTimeInterface - ? $this->secondsUntil($threshold) * 1000 - : $threshold; - - $threshold = $threshold instanceof CarbonInterval - ? $threshold->totalMilliseconds - : $threshold; - - $this->queryDurationHandlers[] = [ - 'has_run' => false, - 'handler' => $handler, - ]; - - $key = count($this->queryDurationHandlers) - 1; - - $this->listen(function ($event) use ($threshold, $handler, $key) { - if (! $this->queryDurationHandlers[$key]['has_run'] && $this->totalQueryDuration() > $threshold) { - $handler($this, $event); - - $this->queryDurationHandlers[$key]['has_run'] = true; - } - }); - } - - /** - * Allow all the query duration handlers to run again, even if they have already run. - * - * @return void - */ - public function allowQueryDurationHandlersToRunAgain() - { - foreach ($this->queryDurationHandlers as $key => $queryDurationHandler) { - $this->queryDurationHandlers[$key]['has_run'] = false; - } - } - - /** - * Get the duration of all run queries in milliseconds. - * - * @return float - */ - public function totalQueryDuration() - { - return $this->totalQueryDuration; - } - - /** - * Reset the duration of all run queries. - * - * @return void - */ - public function resetTotalQueryDuration() - { - $this->totalQueryDuration = 0.0; - } - - /** - * Handle a query exception. - * - * @param \Illuminate\Database\QueryException $e - * @param string $query - * @param array $bindings - * @param \Closure $callback - * @return mixed - * - * @throws \Illuminate\Database\QueryException - */ - protected function handleQueryException(QueryException $e, $query, $bindings, Closure $callback) - { - if ($this->transactions >= 1) { - throw $e; - } - return $this->tryAgainIfCausedByLostConnection( - $e, $query, $bindings, $callback - ); + return $result; } /** * Handle a query exception that occurred during query execution. * * @param \Illuminate\Database\QueryException $e - * @param string $query - * @param array $bindings + * @param string $query + * @param array $bindings * @param \Closure $callback * @return mixed * @@ -1005,12 +723,22 @@ protected function tryAgainIfCausedByLostConnection(QueryException $e, $query, $ throw $e; } + /** + * Disconnect from the underlying PDO connection. + * + * @return void + */ + public function disconnect() + { + $this->setPdo(null)->setReadPdo(null); + } + /** * Reconnect to the database. * - * @return mixed|false + * @return void * - * @throws \Illuminate\Database\LostConnectionException + * @throws \LogicException */ public function reconnect() { @@ -1018,7 +746,7 @@ public function reconnect() return call_user_func($this->reconnector, $this); } - throw new LostConnectionException('Lost connection and no reconnector available.'); + throw new LogicException('Lost connection and no reconnector available.'); } /** @@ -1026,225 +754,130 @@ public function reconnect() * * @return void */ - public function reconnectIfMissingConnection() + protected function reconnectIfMissingConnection() { - if (is_null($this->pdo)) { + if (is_null($this->getPdo()) || is_null($this->getReadPdo())) { $this->reconnect(); } } /** - * Disconnect from the underlying PDO connection. + * Log a query in the connection's query log. * + * @param string $query + * @param array $bindings + * @param float|null $time * @return void */ - public function disconnect() - { - $this->setPdo(null)->setReadPdo(null); - } - - /** - * Register a hook to be run just before a database transaction is started. - * - * @param \Closure $callback - * @return $this - */ - public function beforeStartingTransaction(Closure $callback) - { - $this->beforeStartingTransaction[] = $callback; - - return $this; - } - - /** - * Register a hook to be run just before a database query is executed. - * - * @param \Closure $callback - * @return $this - */ - public function beforeExecuting(Closure $callback) + public function logQuery($query, $bindings, $time = null) { - $this->beforeExecutingCallbacks[] = $callback; + if (isset($this->events)) { + $this->events->fire(new Events\QueryExecuted( + $query, $bindings, $time, $this + )); + } - return $this; + if ($this->loggingQueries) { + $this->queryLog[] = compact('query', 'bindings', 'time'); + } } /** * Register a database query listener with the connection. * - * @param \Closure(\Illuminate\Database\Events\QueryExecuted) $callback + * @param \Closure $callback * @return void */ public function listen(Closure $callback) { - $this->events?->listen(Events\QueryExecuted::class, $callback); + if (isset($this->events)) { + $this->events->listen(Events\QueryExecuted::class, $callback); + } } /** * Fire an event for this connection. * * @param string $event - * @return array|null - */ - protected function fireConnectionEvent($event) - { - return $this->events?->dispatch(match ($event) { - 'beganTransaction' => new TransactionBeginning($this), - 'committed' => new TransactionCommitted($this), - 'committing' => new TransactionCommitting($this), - 'rollingBack' => new TransactionRolledBack($this), - default => null, - }); - } - - /** - * Fire the given event if possible. - * - * @param mixed $event * @return void */ - protected function event($event) - { - $this->events?->dispatch($event); - } - - /** - * Get a new raw query expression. - * - * @param mixed $value - * @return \Illuminate\Contracts\Database\Query\Expression - */ - public function raw($value) + protected function fireConnectionEvent($event) { - return new Expression($value); - } - - /** - * Escape a value for safe SQL embedding. - * - * @param string|float|int|bool|null $value - * @param bool $binary - * @return string - * - * @throws \RuntimeException - */ - public function escape($value, $binary = false) - { - if ($value === null) { - return 'null'; - } elseif ($binary) { - return $this->escapeBinary($value); - } elseif (is_int($value) || is_float($value)) { - return (string) $value; - } elseif (is_bool($value)) { - return $this->escapeBool($value); - } elseif (is_array($value)) { - throw new RuntimeException('The database connection does not support escaping arrays.'); - } else { - if (str_contains($value, "\00")) { - throw new RuntimeException('Strings with null bytes cannot be escaped. Use the binary escape option.'); - } - - if (preg_match('//u', $value) === false) { - throw new RuntimeException('Strings with invalid UTF-8 byte sequences cannot be escaped.'); - } + if (! isset($this->events)) { + return; + } - return $this->escapeString($value); + switch ($event) { + case 'beganTransaction': + return $this->events->fire(new Events\TransactionBeginning($this)); + case 'committed': + return $this->events->fire(new Events\TransactionCommitted($this)); + case 'rollingBack': + return $this->events->fire(new Events\TransactionRolledBack($this)); } } /** - * Escape a string value for safe SQL embedding. + * Get the elapsed time since a given starting point. * - * @param string $value - * @return string + * @param int $start + * @return float */ - protected function escapeString($value) + protected function getElapsedTime($start) { - return $this->getReadPdo()->quote($value); + return round((microtime(true) - $start) * 1000, 2); } /** - * Escape a boolean value for safe SQL embedding. + * Is Doctrine available? * - * @param bool $value - * @return string + * @return bool */ - protected function escapeBool($value) + public function isDoctrineAvailable() { - return $value ? '1' : '0'; + return class_exists('Doctrine\DBAL\Connection'); } /** - * Escape a binary value for safe SQL embedding. + * Get a Doctrine Schema Column instance. * - * @param string $value - * @return string - * - * @throws \RuntimeException + * @param string $table + * @param string $column + * @return \Doctrine\DBAL\Schema\Column */ - protected function escapeBinary($value) + public function getDoctrineColumn($table, $column) { - throw new RuntimeException('The database connection does not support escaping binary values.'); - } + $schema = $this->getDoctrineSchemaManager(); - /** - * Determine if the database connection has modified any database records. - * - * @return bool - */ - public function hasModifiedRecords() - { - return $this->recordsModified; + return $schema->listTableDetails($table)->getColumn($column); } /** - * Indicate if any records have been modified. + * Get the Doctrine DBAL schema manager for the connection. * - * @param bool $value - * @return void + * @return \Doctrine\DBAL\Schema\AbstractSchemaManager */ - public function recordsHaveBeenModified($value = true) + public function getDoctrineSchemaManager() { - if (! $this->recordsModified) { - $this->recordsModified = $value; - } + return $this->getDoctrineDriver()->getSchemaManager($this->getDoctrineConnection()); } /** - * Set the record modification state. + * Get the Doctrine DBAL database connection instance. * - * @param bool $value - * @return $this + * @return \Doctrine\DBAL\Connection */ - public function setRecordModificationState(bool $value) + public function getDoctrineConnection() { - $this->recordsModified = $value; + if (is_null($this->doctrineConnection)) { + $driver = $this->getDoctrineDriver(); - return $this; - } + $data = ['pdo' => $this->getPdo(), 'dbname' => $this->getConfig('database')]; - /** - * Reset the record modification state. - * - * @return void - */ - 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; + $this->doctrineConnection = new DoctrineConnection($data, $driver); + } - return $this; + return $this->doctrineConnection; } /** @@ -1254,8 +887,6 @@ public function useWriteConnectionWhenReading($value = true) */ public function getPdo() { - $this->latestPdoTypeRetrieved = 'write'; - if ($this->pdo instanceof Closure) { return $this->pdo = call_user_func($this->pdo); } @@ -1263,16 +894,6 @@ public function getPdo() return $this->pdo; } - /** - * Get the current PDO connection parameter without executing any reconnect logic. - * - * @return \PDO|\Closure|null - */ - public function getRawPdo() - { - return $this->pdo; - } - /** * Get the current PDO connection used for reading. * @@ -1280,43 +901,26 @@ public function getRawPdo() */ public function getReadPdo() { - if ($this->transactions > 0) { - return $this->getPdo(); - } - - if ($this->readOnWriteConnection || - ($this->recordsModified && $this->getConfig('sticky'))) { + if ($this->transactions >= 1) { return $this->getPdo(); } - $this->latestPdoTypeRetrieved = 'read'; - - if ($this->readPdo instanceof Closure) { - return $this->readPdo = call_user_func($this->readPdo); - } - return $this->readPdo ?: $this->getPdo(); } - /** - * Get the current read PDO connection parameter without executing any reconnect logic. - * - * @return \PDO|\Closure|null - */ - public function getRawReadPdo() - { - return $this->readPdo; - } - /** * Set the PDO connection. * - * @param \PDO|\Closure|null $pdo + * @param \PDO|null $pdo * @return $this + * + * @throws \RuntimeException */ public function setPdo($pdo) { - $this->transactions = 0; + if ($this->transactions >= 1) { + throw new RuntimeException("Can't swap PDO instance while within transaction."); + } $this->pdo = $pdo; @@ -1326,7 +930,7 @@ public function setPdo($pdo) /** * Set the PDO connection used for reading. * - * @param \PDO|\Closure|null $pdo + * @param \PDO|null $pdo * @return $this */ public function setReadPdo($pdo) @@ -1336,23 +940,10 @@ public function setReadPdo($pdo) return $this; } - /** - * Set the read PDO connection configuration. - * - * @param array $config - * @return $this - */ - public function setReadPdoConfig(array $config) - { - $this->readPdoConfig = $config; - - return $this; - } - /** * Set the reconnect instance on the connection. * - * @param (callable(\Illuminate\Database\Connection): mixed) $reconnector + * @param callable $reconnector * @return $this */ public function setReconnector(callable $reconnector) @@ -1372,50 +963,17 @@ public function getName() return $this->getConfig('name'); } - /** - * Get the database connection with its read / write type. - * - * @return string|null - */ - public function getNameWithReadWriteType() - { - $name = $this->getName().($this->readWriteType ? '::'.$this->readWriteType : ''); - - return empty($name) ? null : $name; - } - /** * Get an option from the configuration options. * - * @param string|null $option + * @param string $option * @return mixed */ - public function getConfig($option = null) + public function getConfig($option) { return Arr::get($this->config, $option); } - /** - * Get the basic connection information as an array for debugging. - * - * @return array - */ - protected function getConnectionDetails() - { - $config = $this->latestReadWriteTypeUsed() === 'read' - ? $this->readPdoConfig - : $this->config; - - return [ - 'driver' => $this->getDriverName(), - 'name' => $this->getNameWithReadWriteType(), - 'host' => $config['host'] ?? null, - 'port' => $config['port'] ?? null, - 'database' => $config['database'] ?? null, - 'unix_socket' => $config['unix_socket'] ?? null, - ]; - } - /** * Get the PDO driver name. * @@ -1426,16 +984,6 @@ public function getDriverName() return $this->getConfig('driver'); } - /** - * Get a human-readable name for the given connection driver. - * - * @return string - */ - public function getDriverTitle() - { - return $this->getDriverName(); - } - /** * Get the query grammar used by the connection. * @@ -1450,13 +998,11 @@ public function getQueryGrammar() * Set the query grammar used by the connection. * * @param \Illuminate\Database\Query\Grammars\Grammar $grammar - * @return $this + * @return void */ public function setQueryGrammar(Query\Grammars\Grammar $grammar) { $this->queryGrammar = $grammar; - - return $this; } /** @@ -1473,13 +1019,11 @@ public function getSchemaGrammar() * Set the schema grammar used by the connection. * * @param \Illuminate\Database\Schema\Grammars\Grammar $grammar - * @return $this + * @return void */ public function setSchemaGrammar(Schema\Grammars\Grammar $grammar) { $this->schemaGrammar = $grammar; - - return $this; } /** @@ -1496,19 +1040,17 @@ public function getPostProcessor() * Set the query post processor used by the connection. * * @param \Illuminate\Database\Query\Processors\Processor $processor - * @return $this + * @return void */ public function setPostProcessor(Processor $processor) { $this->postProcessor = $processor; - - return $this; } /** * Get the event dispatcher used by the connection. * - * @return \Illuminate\Contracts\Events\Dispatcher|null + * @return \Illuminate\Contracts\Events\Dispatcher */ public function getEventDispatcher() { @@ -1519,94 +1061,78 @@ public function getEventDispatcher() * Set the event dispatcher instance on the connection. * * @param \Illuminate\Contracts\Events\Dispatcher $events - * @return $this + * @return void */ public function setEventDispatcher(Dispatcher $events) { $this->events = $events; - - return $this; } /** - * Unset the event dispatcher for this connection. + * Determine if the connection in a "dry run". * - * @return void + * @return bool */ - public function unsetEventDispatcher() + public function pretending() { - $this->events = null; + return $this->pretending === true; } /** - * Run the statement to start a new transaction. + * Get the default fetch mode for the connection. * - * @return void + * @return int */ - protected function executeBeginTransactionStatement() + public function getFetchMode() { - $this->getPdo()->beginTransaction(); + return $this->fetchMode; } /** - * Set the transaction manager instance on the connection. + * Get the fetch argument to be applied when selecting. * - * @param \Illuminate\Database\DatabaseTransactionsManager $manager - * @return $this + * @return mixed */ - public function setTransactionManager($manager) + public function getFetchArgument() { - $this->transactionsManager = $manager; - - return $this; + return $this->fetchArgument; } /** - * Unset the transaction manager for this connection. + * Get custom constructor arguments for the PDO::FETCH_CLASS fetch mode. * - * @return void + * @return array */ - public function unsetTransactionManager() + public function getFetchConstructorArgument() { - $this->transactionsManager = null; + return $this->fetchConstructorArgument; } /** - * Determine if the connection is in a "dry run". + * Set the default fetch mode for the connection, and optional arguments for the given fetch mode. * - * @return bool + * @param int $fetchMode + * @param mixed $fetchArgument + * @param array $fetchConstructorArgument + * @return int */ - public function pretending() + public function setFetchMode($fetchMode, $fetchArgument = null, array $fetchConstructorArgument = []) { - return $this->pretending === true; + $this->fetchMode = $fetchMode; + $this->fetchArgument = $fetchArgument; + $this->fetchConstructorArgument = $fetchConstructorArgument; } /** * Get the connection query log. * - * @return array{query: string, bindings: array, time: float|null}[] + * @return array */ public function getQueryLog() { return $this->queryLog; } - /** - * Get the connection query log with embedded bindings. - * - * @return array - */ - public function getRawQueryLog() - { - return array_map(fn (array $log) => [ - 'raw_query' => $this->queryGrammar->substituteBindingsIntoRawSql( - $log['query'], - $this->prepareBindings($log['bindings']) - ), - 'time' => $log['time'], - ], $this->getQueryLog()); - } - /** * Clear the query log. * @@ -1661,36 +1187,11 @@ public function getDatabaseName() * Set the name of the connected database. * * @param string $database - * @return $this + * @return string */ public function setDatabaseName($database) { $this->database = $database; - - return $this; - } - - /** - * Set the read / write type of the connection. - * - * @param string|null $readWriteType - * @return $this - */ - public function setReadWriteType($readWriteType) - { - $this->readWriteType = $readWriteType; - - return $this; - } - - /** - * Retrieve the latest read / write type used. - * - * @return 'read'|'write'|null - */ - protected function latestReadWriteTypeUsed() - { - return $this->readWriteType ?? $this->latestPdoTypeRetrieved; } /** @@ -1707,79 +1208,25 @@ public function getTablePrefix() * Set the table prefix in use by the connection. * * @param string $prefix - * @return $this + * @return void */ public function setTablePrefix($prefix) { $this->tablePrefix = $prefix; - return $this; - } - - /** - * Execute the given callback without table prefix. - * - * @param \Closure $callback - * @return mixed - */ - public function withoutTablePrefix(Closure $callback): mixed - { - $tablePrefix = $this->getTablePrefix(); - - $this->setTablePrefix(''); - - try { - return $callback($this); - } finally { - $this->setTablePrefix($tablePrefix); - } - } - - /** - * Get the server version for the connection. - * - * @return string - */ - public function getServerVersion(): string - { - return $this->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION); - } - - /** - * Register a connection resolver. - * - * @param string $driver - * @param \Closure $callback - * @return void - */ - public static function resolverFor($driver, Closure $callback) - { - static::$resolvers[$driver] = $callback; - } - - /** - * Get the connection resolver for the given driver. - * - * @param string $driver - * @return \Closure|null - */ - public static function getResolver($driver) - { - return static::$resolvers[$driver] ?? null; + $this->getQueryGrammar()->setTablePrefix($prefix); } /** - * Prepare the instance for cloning. + * Set the table prefix and return the grammar. * - * @return void + * @param \Illuminate\Database\Grammar $grammar + * @return \Illuminate\Database\Grammar */ - public function __clone() + public function withTablePrefix(Grammar $grammar) { - // When cloning, re-initialize grammars to reference cloned connection... - $this->useDefaultQueryGrammar(); + $grammar->setTablePrefix($this->tablePrefix); - if (! is_null($this->schemaGrammar)) { - $this->useDefaultSchemaGrammar(); - } + return $grammar; } } diff --git a/ConnectionInterface.php b/ConnectionInterface.php index 69e1fafa38..16eb667502 100755 --- a/ConnectionInterface.php +++ b/ConnectionInterface.php @@ -9,17 +9,16 @@ interface ConnectionInterface /** * Begin a fluent query against a database table. * - * @param \Closure|\Illuminate\Database\Query\Builder|\UnitEnum|string $table - * @param string|null $as + * @param string $table * @return \Illuminate\Database\Query\Builder */ - public function table($table, $as = null); + public function table($table); /** * Get a new raw query expression. * * @param mixed $value - * @return \Illuminate\Contracts\Database\Query\Expression + * @return \Illuminate\Database\Query\Expression */ public function raw($value); @@ -27,51 +26,25 @@ public function raw($value); * Run a select statement and return a single result. * * @param string $query - * @param array $bindings - * @param bool $useReadPdo + * @param array $bindings * @return mixed */ - public function selectOne($query, $bindings = [], $useReadPdo = true); - - /** - * Run a select statement and return the first column of the first row. - * - * @param string $query - * @param array $bindings - * @param bool $useReadPdo - * @return mixed - * - * @throws \Illuminate\Database\MultipleColumnsSelectedException - */ - public function scalar($query, $bindings = [], $useReadPdo = true); + public function selectOne($query, $bindings = []); /** * Run a select statement against the database. * * @param string $query - * @param array $bindings - * @param bool $useReadPdo - * @param array $fetchUsing + * @param array $bindings * @return array */ - public function select($query, $bindings = [], $useReadPdo = true, array $fetchUsing = []); - - /** - * Run a select statement against the database and returns a generator. - * - * @param string $query - * @param array $bindings - * @param bool $useReadPdo - * @param array $fetchUsing - * @return \Generator - */ - public function cursor($query, $bindings = [], $useReadPdo = true, array $fetchUsing = []); + public function select($query, $bindings = []); /** * Run an insert statement against the database. * * @param string $query - * @param array $bindings + * @param array $bindings * @return bool */ public function insert($query, $bindings = []); @@ -80,7 +53,7 @@ public function insert($query, $bindings = []); * Run an update statement against the database. * * @param string $query - * @param array $bindings + * @param array $bindings * @return int */ public function update($query, $bindings = []); @@ -89,7 +62,7 @@ public function update($query, $bindings = []); * Run a delete statement against the database. * * @param string $query - * @param array $bindings + * @param array $bindings * @return int */ public function delete($query, $bindings = []); @@ -98,7 +71,7 @@ public function delete($query, $bindings = []); * Execute an SQL statement and return the boolean result. * * @param string $query - * @param array $bindings + * @param array $bindings * @return bool */ public function statement($query, $bindings = []); @@ -107,7 +80,7 @@ public function statement($query, $bindings = []); * Run an SQL statement and get the number of rows affected. * * @param string $query - * @param array $bindings + * @param array $bindings * @return int */ public function affectingStatement($query, $bindings = []); @@ -132,12 +105,11 @@ public function prepareBindings(array $bindings); * Execute a Closure within a transaction. * * @param \Closure $callback - * @param int $attempts * @return mixed * * @throws \Throwable */ - public function transaction(Closure $callback, $attempts = 1); + public function transaction(Closure $callback); /** * Start a new database transaction. @@ -174,11 +146,4 @@ public function transactionLevel(); * @return array */ public function pretend(Closure $callback); - - /** - * Get the name of the connected database. - * - * @return string - */ - public function getDatabaseName(); } diff --git a/ConnectionResolver.php b/ConnectionResolver.php index b7b6279e1f..425ab6bce6 100755 --- a/ConnectionResolver.php +++ b/ConnectionResolver.php @@ -7,7 +7,7 @@ class ConnectionResolver implements ConnectionResolverInterface /** * All of the registered connections. * - * @var \Illuminate\Database\ConnectionInterface[] + * @var array */ protected $connections = []; @@ -21,7 +21,8 @@ class ConnectionResolver implements ConnectionResolverInterface /** * Create a new connection resolver instance. * - * @param array $connections + * @param array $connections + * @return void */ public function __construct(array $connections = []) { @@ -33,7 +34,7 @@ public function __construct(array $connections = []) /** * Get a database connection instance. * - * @param string|null $name + * @param string $name * @return \Illuminate\Database\ConnectionInterface */ public function connection($name = null) diff --git a/ConnectionResolverInterface.php b/ConnectionResolverInterface.php index 47161d37d6..eb0397a5d7 100755 --- a/ConnectionResolverInterface.php +++ b/ConnectionResolverInterface.php @@ -7,7 +7,7 @@ interface ConnectionResolverInterface /** * Get a database connection instance. * - * @param \UnitEnum|string|null $name + * @param string $name * @return \Illuminate\Database\ConnectionInterface */ public function connection($name = null); diff --git a/Connectors/ConnectionFactory.php b/Connectors/ConnectionFactory.php index 7017c0aa5c..184789970e 100755 --- a/Connectors/ConnectionFactory.php +++ b/Connectors/ConnectionFactory.php @@ -2,16 +2,14 @@ namespace Illuminate\Database\Connectors; -use Illuminate\Contracts\Container\Container; -use Illuminate\Database\Connection; -use Illuminate\Database\MariaDbConnection; +use PDO; +use Illuminate\Support\Arr; +use InvalidArgumentException; use Illuminate\Database\MySqlConnection; -use Illuminate\Database\PostgresConnection; use Illuminate\Database\SQLiteConnection; +use Illuminate\Database\PostgresConnection; use Illuminate\Database\SqlServerConnection; -use Illuminate\Support\Arr; -use InvalidArgumentException; -use PDOException; +use Illuminate\Contracts\Container\Container; class ConnectionFactory { @@ -26,6 +24,7 @@ class ConnectionFactory * Create a new connection factory instance. * * @param \Illuminate\Contracts\Container\Container $container + * @return void */ public function __construct(Container $container) { @@ -35,8 +34,8 @@ public function __construct(Container $container) /** * Establish a PDO connection based on the configuration. * - * @param array $config - * @param string|null $name + * @param array $config + * @param string $name * @return \Illuminate\Database\Connection */ public function make(array $config, $name = null) @@ -50,18 +49,6 @@ public function make(array $config, $name = null) return $this->createSingleConnection($config); } - /** - * Parse and prepare the database configuration. - * - * @param array $config - * @param string $name - * @return array - */ - protected function parseConfig(array $config, $name) - { - return Arr::add(Arr::add($config, 'prefix', ''), 'name', $name); - } - /** * Create a single database connection instance. * @@ -70,15 +57,15 @@ protected function parseConfig(array $config, $name) */ protected function createSingleConnection(array $config) { - $pdo = $this->createPdoResolver($config); + $pdo = function () use ($config) { + return $this->createConnector($config)->connect($config); + }; - return $this->createConnection( - $config['driver'], $pdo, $config['database'], $config['prefix'], $config - ); + return $this->createConnection($config['driver'], $pdo, $config['database'], $config['prefix'], $config); } /** - * Create a read / write database connection instance. + * Create a single database connection instance. * * @param array $config * @return \Illuminate\Database\Connection @@ -87,20 +74,20 @@ protected function createReadWriteConnection(array $config) { $connection = $this->createSingleConnection($this->getWriteConfig($config)); - return $connection - ->setReadPdo($this->createReadPdo($config)) - ->setReadPdoConfig($this->getReadConfig($config)); + return $connection->setReadPdo($this->createReadPdo($config)); } /** * Create a new PDO instance for reading. * * @param array $config - * @return \Closure + * @return \PDO */ protected function createReadPdo(array $config) { - return $this->createPdoResolver($this->getReadConfig($config)); + $readConfig = $this->getReadConfig($config); + + return $this->createConnector($readConfig)->connect($readConfig); } /** @@ -111,36 +98,44 @@ protected function createReadPdo(array $config) */ protected function getReadConfig(array $config) { - return $this->mergeReadWriteConfig( - $config, $this->getReadWriteConfig($config, 'read') - ); + $readConfig = $this->getReadWriteConfig($config, 'read'); + + if (isset($readConfig['host']) && is_array($readConfig['host'])) { + $readConfig['host'] = count($readConfig['host']) > 1 + ? $readConfig['host'][array_rand($readConfig['host'])] + : $readConfig['host'][0]; + } + + return $this->mergeReadWriteConfig($config, $readConfig); } /** - * Get the write configuration for a read / write connection. + * Get the read configuration for a read / write connection. * * @param array $config * @return array */ protected function getWriteConfig(array $config) { - return $this->mergeReadWriteConfig( - $config, $this->getReadWriteConfig($config, 'write') - ); + $writeConfig = $this->getReadWriteConfig($config, 'write'); + + return $this->mergeReadWriteConfig($config, $writeConfig); } /** * Get a read / write level configuration. * - * @param array $config + * @param array $config * @param string $type * @return array */ protected function getReadWriteConfig(array $config, $type) { - return isset($config[$type][0]) - ? Arr::random($config[$type]) - : $config[$type]; + if (isset($config[$type][0])) { + return $config[$type][array_rand($config[$type])]; + } + + return $config[$type]; } /** @@ -156,73 +151,15 @@ protected function mergeReadWriteConfig(array $config, array $merge) } /** - * Create a new Closure that resolves to a PDO instance. - * - * @param array $config - * @return \Closure - */ - protected function createPdoResolver(array $config) - { - return array_key_exists('host', $config) - ? $this->createPdoResolverWithHosts($config) - : $this->createPdoResolverWithoutHosts($config); - } - - /** - * Create a new Closure that resolves to a PDO instance with a specific host or an array of hosts. - * - * @param array $config - * @return \Closure - * - * @throws \PDOException - */ - protected function createPdoResolverWithHosts(array $config) - { - return function () use ($config) { - foreach (Arr::shuffle($this->parseHosts($config)) as $host) { - $config['host'] = $host; - - try { - return $this->createConnector($config)->connect($config); - } catch (PDOException $e) { - continue; - } - } - - if (isset($e)) { - throw $e; - } - }; - } - - /** - * Parse the hosts configuration item into an array. + * Parse and prepare the database configuration. * - * @param array $config + * @param array $config + * @param string $name * @return array - * - * @throws \InvalidArgumentException */ - protected function parseHosts(array $config) - { - $hosts = Arr::wrap($config['host']); - - if (empty($hosts)) { - throw new InvalidArgumentException('Database hosts array is empty.'); - } - - return $hosts; - } - - /** - * Create a new Closure that resolves to a PDO instance where there is no configured host. - * - * @param array $config - * @return \Closure - */ - protected function createPdoResolverWithoutHosts(array $config) + protected function parseConfig(array $config, $name) { - return fn () => $this->createConnector($config)->connect($config); + return Arr::add(Arr::add($config, 'prefix', ''), 'name', $name); } /** @@ -243,41 +180,55 @@ public function createConnector(array $config) return $this->container->make($key); } - return match ($config['driver']) { - 'mysql' => new MySqlConnector, - 'mariadb' => new MariaDbConnector, - 'pgsql' => new PostgresConnector, - 'sqlite' => new SQLiteConnector, - 'sqlsrv' => new SqlServerConnector, - default => throw new InvalidArgumentException("Unsupported driver [{$config['driver']}]."), - }; + switch ($config['driver']) { + case 'mysql': + return new MySqlConnector; + + case 'pgsql': + return new PostgresConnector; + + case 'sqlite': + return new SQLiteConnector; + + case 'sqlsrv': + return new SqlServerConnector; + } + + throw new InvalidArgumentException("Unsupported driver [{$config['driver']}]"); } /** * Create a new connection instance. * - * @param string $driver - * @param \PDO|\Closure $connection - * @param string $database - * @param string $prefix - * @param array $config + * @param string $driver + * @param \PDO|\Closure $connection + * @param string $database + * @param string $prefix + * @param array $config * @return \Illuminate\Database\Connection * * @throws \InvalidArgumentException */ protected function createConnection($driver, $connection, $database, $prefix = '', array $config = []) { - if ($resolver = Connection::getResolver($driver)) { - return $resolver($connection, $database, $prefix, $config); + if ($this->container->bound($key = "db.connection.{$driver}")) { + return $this->container->make($key, [$connection, $database, $prefix, $config]); } - return match ($driver) { - 'mysql' => new MySqlConnection($connection, $database, $prefix, $config), - 'mariadb' => new MariaDbConnection($connection, $database, $prefix, $config), - 'pgsql' => new PostgresConnection($connection, $database, $prefix, $config), - 'sqlite' => new SQLiteConnection($connection, $database, $prefix, $config), - 'sqlsrv' => new SqlServerConnection($connection, $database, $prefix, $config), - default => throw new InvalidArgumentException("Unsupported driver [{$driver}]."), - }; + switch ($driver) { + case 'mysql': + return new MySqlConnection($connection, $database, $prefix, $config); + + case 'pgsql': + return new PostgresConnection($connection, $database, $prefix, $config); + + case 'sqlite': + return new SQLiteConnection($connection, $database, $prefix, $config); + + case 'sqlsrv': + return new SqlServerConnection($connection, $database, $prefix, $config); + } + + throw new InvalidArgumentException("Unsupported driver [$driver]"); } } diff --git a/Connectors/Connector.php b/Connectors/Connector.php index 8d0af49a9a..ea2637c7e5 100755 --- a/Connectors/Connector.php +++ b/Connectors/Connector.php @@ -2,10 +2,10 @@ namespace Illuminate\Database\Connectors; +use PDO; use Exception; +use Illuminate\Support\Arr; use Illuminate\Database\DetectsLostConnections; -use PDO; -use Throwable; class Connector { @@ -25,80 +25,41 @@ class Connector ]; /** - * Create a new PDO connection. + * Get the PDO options based on the configuration. * - * @param string $dsn * @param array $config - * @param array $options - * @return \PDO - * - * @throws \Exception + * @return array */ - public function createConnection($dsn, array $config, array $options) + public function getOptions(array $config) { - [$username, $password] = [ - $config['username'] ?? null, $config['password'] ?? null, - ]; + $options = Arr::get($config, 'options', []); - try { - return $this->createPdoConnection( - $dsn, $username, $password, $options - ); - } catch (Exception $e) { - return $this->tryAgainIfCausedByLostConnection( - $e, $dsn, $username, $password, $options - ); - } - } - - /** - * Create a new PDO connection instance. - * - * @param string $dsn - * @param string $username - * @param string $password - * @param array $options - * @return \PDO - */ - protected function createPdoConnection($dsn, $username, #[\SensitiveParameter] $password, $options) - { - return version_compare(PHP_VERSION, '8.4.0', '<') - ? new PDO($dsn, $username, $password, $options) - : PDO::connect($dsn, $username, $password, $options); /** @phpstan-ignore staticMethod.notFound (PHP 8.4) */ + return array_diff_key($this->options, $options) + $options; } /** - * Handle an exception that occurred during connect execution. + * Create a new PDO connection. * - * @param \Throwable $e * @param string $dsn - * @param string $username - * @param string $password - * @param array $options + * @param array $config + * @param array $options * @return \PDO - * - * @throws \Throwable */ - protected function tryAgainIfCausedByLostConnection(Throwable $e, $dsn, $username, #[\SensitiveParameter] $password, $options) + public function createConnection($dsn, array $config, array $options) { - if ($this->causedByLostConnection($e)) { - return $this->createPdoConnection($dsn, $username, $password, $options); - } + $username = Arr::get($config, 'username'); - throw $e; - } + $password = Arr::get($config, 'password'); - /** - * Get the PDO options based on the configuration. - * - * @param array $config - * @return array - */ - public function getOptions(array $config) - { - $options = $config['options'] ?? []; + try { + $pdo = new PDO($dsn, $username, $password, $options); + } catch (Exception $e) { + $pdo = $this->tryAgainIfCausedByLostConnection( + $e, $dsn, $username, $password, $options + ); + } - return array_diff_key($this->options, $options) + $options; + return $pdo; } /** @@ -121,4 +82,25 @@ public function setDefaultOptions(array $options) { $this->options = $options; } + + /** + * Handle a exception that occurred during connect execution. + * + * @param \Exception $e + * @param string $dsn + * @param string $username + * @param string $password + * @param array $options + * @return \PDO + * + * @throws \Exception + */ + protected function tryAgainIfCausedByLostConnection(Exception $e, $dsn, $username, $password, $options) + { + if ($this->causedByLostConnection($e)) { + return new PDO($dsn, $username, $password, $options); + } + + throw $e; + } } diff --git a/Connectors/MariaDbConnector.php b/Connectors/MariaDbConnector.php deleted file mode 100755 index b7203f87ae..0000000000 --- a/Connectors/MariaDbConnector.php +++ /dev/null @@ -1,32 +0,0 @@ -createConnection($dsn, $config, $options); - if (! empty($config['database']) && - (! isset($config['use_db_after_connecting']) || - $config['use_db_after_connecting'])) { + if (isset($config['unix_socket'])) { $connection->exec("use `{$config['database']}`;"); } - $this->configureConnection($connection, $config); + $collation = $config['collation']; + + // Next we will set the "names" and "collation" on the clients connections so + // a correct character set will be used by this client. The collation also + // is set on the server but needs to be set here on this client objects. + $charset = $config['charset']; + + $names = "set names '$charset'". + (! is_null($collation) ? " collate '$collation'" : ''); + + $connection->prepare($names)->execute(); + + // Next, we will check to see if a timezone has been specified in this config + // and if it has we will issue a statement to modify the timezone with the + // database. Setting this DB timezone is an optional configuration item. + if (isset($config['timezone'])) { + $connection->prepare( + 'set time_zone="'.$config['timezone'].'"' + )->execute(); + } + + $this->setModes($connection, $config); return $connection; } @@ -39,14 +58,12 @@ public function connect(array $config) * * Chooses socket or host/port based on the 'unix_socket' config value. * - * @param array $config + * @param array $config * @return string */ protected function getDsn(array $config) { - return $this->hasSocket($config) - ? $this->getSocketDsn($config) - : $this->getHostDsn($config); + return $this->configHasSocket($config) ? $this->getSocketDsn($config) : $this->getHostDsn($config); } /** @@ -55,7 +72,7 @@ protected function getDsn(array $config) * @param array $config * @return bool */ - protected function hasSocket(array $config) + protected function configHasSocket(array $config) { return isset($config['unix_socket']) && ! empty($config['unix_socket']); } @@ -79,76 +96,32 @@ protected function getSocketDsn(array $config) */ protected function getHostDsn(array $config) { - return isset($config['port']) - ? "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']}" - : "mysql:host={$config['host']};dbname={$config['database']}"; + extract($config, EXTR_SKIP); + + return isset($port) + ? "mysql:host={$host};port={$port};dbname={$database}" + : "mysql:host={$host};dbname={$database}"; } /** - * Configure the given PDO connection. + * Set the modes for the connection. * * @param \PDO $connection * @param array $config * @return void */ - protected function configureConnection(PDO $connection, array $config) + protected function setModes(PDO $connection, array $config) { - if (isset($config['isolation_level'])) { - $connection->exec(sprintf('SET SESSION TRANSACTION ISOLATION LEVEL %s;', $config['isolation_level'])); - } - - $statements = []; + if (isset($config['modes'])) { + $modes = implode(',', $config['modes']); - if (isset($config['charset'])) { - if (isset($config['collation'])) { - $statements[] = sprintf("NAMES '%s' COLLATE '%s'", $config['charset'], $config['collation']); + $connection->prepare("set session sql_mode='".$modes."'")->execute(); + } elseif (isset($config['strict'])) { + if ($config['strict']) { + $connection->prepare("set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'")->execute(); } else { - $statements[] = sprintf("NAMES '%s'", $config['charset']); + $connection->prepare("set session sql_mode='NO_ENGINE_SUBSTITUTION'")->execute(); } } - - if (isset($config['timezone'])) { - $statements[] = sprintf("time_zone='%s'", $config['timezone']); - } - - $sqlMode = $this->getSqlMode($connection, $config); - - if ($sqlMode !== null) { - $statements[] = sprintf("SESSION sql_mode='%s'", $sqlMode); - } - - if ($statements !== []) { - $connection->exec(sprintf('SET %s;', implode(', ', $statements))); - } - } - - /** - * Get the sql_mode value. - * - * @param \PDO $connection - * @param array $config - * @return string|null - */ - protected function getSqlMode(PDO $connection, array $config) - { - if (isset($config['modes'])) { - return implode(',', $config['modes']); - } - - if (! isset($config['strict'])) { - return null; - } - - if (! $config['strict']) { - return 'NO_ENGINE_SUBSTITUTION'; - } - - $version = $config['version'] ?? $connection->getAttribute(PDO::ATTR_SERVER_VERSION); - - if (version_compare($version, '8.0.11', '>=')) { - return 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'; - } - - return 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'; } } diff --git a/Connectors/PostgresConnector.php b/Connectors/PostgresConnector.php index 31d2ff4732..9371c8df4f 100755 --- a/Connectors/PostgresConnector.php +++ b/Connectors/PostgresConnector.php @@ -2,13 +2,10 @@ namespace Illuminate\Database\Connectors; -use Illuminate\Database\Concerns\ParsesSearchPath; use PDO; class PostgresConnector extends Connector implements ConnectorInterface { - use ParsesSearchPath; - /** * The default PDO connection options. * @@ -32,20 +29,42 @@ public function connect(array $config) // First we'll create the basic DSN and connection instance connecting to the // using the configuration option specified by the developer. We will also // set the default character set on the connections to UTF-8 by default. - $connection = $this->createConnection( - $this->getDsn($config), $config, $this->getOptions($config) - ); + $dsn = $this->getDsn($config); + + $options = $this->getOptions($config); + + $connection = $this->createConnection($dsn, $config, $options); - $this->configureIsolationLevel($connection, $config); + $charset = $config['charset']; + + $connection->prepare("set names '$charset'")->execute(); // Next, we will check to see if a timezone has been specified in this config // and if it has we will issue a statement to modify the timezone with the // database. Setting this DB timezone is an optional configuration item. - $this->configureTimezone($connection, $config); + if (isset($config['timezone'])) { + $timezone = $config['timezone']; + + $connection->prepare("set time zone '$timezone'")->execute(); + } - $this->configureSearchPath($connection, $config); + // Unlike MySQL, Postgres allows the concept of "schema" and a default schema + // may have been specified on the connections. If that is the case we will + // set the default schema search paths to the specified database schema. + if (isset($config['schema'])) { + $schema = $this->formatSchema($config['schema']); - $this->configureSynchronousCommit($connection, $config); + $connection->prepare("set search_path to {$schema}")->execute(); + } + + // Postgres allows an application_name to be set by the user and this name is + // used to when monitoring the application with pg_stat_activity. So we'll + // determine if the option has been specified and run a statement if so. + if (isset($config['application_name'])) { + $applicationName = $config['application_name']; + + $connection->prepare("set application_name to '$applicationName'")->execute(); + } return $connection; } @@ -53,7 +72,7 @@ public function connect(array $config) /** * Create a DSN string from a configuration. * - * @param array $config + * @param array $config * @return string */ protected function getDsn(array $config) @@ -65,123 +84,34 @@ protected function getDsn(array $config) $host = isset($host) ? "host={$host};" : ''; - // Sometimes - users may need to connect to a database that has a different - // name than the database used for "information_schema" queries. This is - // typically the case if using "pgbouncer" type software when pooling. - $database = $connect_via_database ?? $database ?? null; - $port = $connect_via_port ?? $port ?? null; - - $dsn = "pgsql:{$host}dbname='{$database}'"; + $dsn = "pgsql:{$host}dbname={$database}"; // If a port was specified, we will add it to this Postgres DSN connections // format. Once we have done that we are ready to return this connection // string back out for usage, as this has been fully constructed here. - if (! is_null($port)) { + if (isset($config['port'])) { $dsn .= ";port={$port}"; } - if (isset($charset)) { - $dsn .= ";client_encoding='{$charset}'"; - } - - // Postgres allows an application_name to be set by the user and this name is - // used to when monitoring the application with pg_stat_activity. So we'll - // determine if the option has been specified and run a statement if so. - if (isset($application_name)) { - $dsn .= ";application_name='".str_replace("'", "\'", $application_name)."'"; - } - - return $this->addSslOptions($dsn, $config); - } - - /** - * Add the SSL options to the DSN. - * - * @param string $dsn - * @param array $config - * @return string - */ - protected function addSslOptions($dsn, array $config) - { - foreach (['sslmode', 'sslcert', 'sslkey', 'sslrootcert'] as $option) { - if (isset($config[$option])) { - $dsn .= ";{$option}={$config[$option]}"; - } + if (isset($config['sslmode'])) { + $dsn .= ";sslmode={$sslmode}"; } return $dsn; } /** - * Set the connection transaction isolation level. + * Format the schema for the DSN. * - * @param \PDO $connection - * @param array $config - * @return void - */ - protected function configureIsolationLevel($connection, array $config) - { - if (isset($config['isolation_level'])) { - $connection->prepare("set session characteristics as transaction isolation level {$config['isolation_level']}")->execute(); - } - } - - /** - * Set the timezone on the connection. - * - * @param \PDO $connection - * @param array $config - * @return void - */ - protected function configureTimezone($connection, array $config) - { - if (isset($config['timezone'])) { - $timezone = $config['timezone']; - - $connection->prepare("set time zone '{$timezone}'")->execute(); - } - } - - /** - * Set the "search_path" on the database connection. - * - * @param \PDO $connection - * @param array $config - * @return void - */ - protected function configureSearchPath($connection, $config) - { - if (isset($config['search_path']) || isset($config['schema'])) { - $searchPath = $this->quoteSearchPath( - $this->parseSearchPath($config['search_path'] ?? $config['schema']) - ); - - $connection->prepare("set search_path to {$searchPath}")->execute(); - } - } - - /** - * Format the search path for the DSN. - * - * @param array $searchPath + * @param array|string $schema * @return string */ - protected function quoteSearchPath($searchPath) - { - return count($searchPath) === 1 ? '"'.$searchPath[0].'"' : '"'.implode('", "', $searchPath).'"'; - } - - /** - * Configure the synchronous_commit setting. - * - * @param \PDO $connection - * @param array $config - * @return void - */ - protected function configureSynchronousCommit($connection, array $config) + protected function formatSchema($schema) { - if (isset($config['synchronous_commit'])) { - $connection->prepare("set synchronous_commit to '{$config['synchronous_commit']}'")->execute(); + if (is_array($schema)) { + return '"'.implode('", "', $schema).'"'; + } else { + return '"'.$schema.'"'; } } } diff --git a/Connectors/SQLiteConnector.php b/Connectors/SQLiteConnector.php index 858549ec55..28f90915b5 100755 --- a/Connectors/SQLiteConnector.php +++ b/Connectors/SQLiteConnector.php @@ -2,7 +2,7 @@ namespace Illuminate\Database\Connectors; -use Illuminate\Database\SQLiteDatabaseDoesNotExistException; +use InvalidArgumentException; class SQLiteConnector extends Connector implements ConnectorInterface { @@ -11,139 +11,29 @@ class SQLiteConnector extends Connector implements ConnectorInterface * * @param array $config * @return \PDO + * + * @throws \InvalidArgumentException */ public function connect(array $config) { $options = $this->getOptions($config); - $path = $this->parseDatabasePath($config['database']); - - $connection = $this->createConnection("sqlite:{$path}", $config, $options); - - $this->configurePragmas($connection, $config); - $this->configureForeignKeyConstraints($connection, $config); - $this->configureBusyTimeout($connection, $config); - $this->configureJournalMode($connection, $config); - $this->configureSynchronous($connection, $config); - - return $connection; - } - - /** - * Get the absolute database path. - * - * @param string $path - * @return string - * - * @throws \Illuminate\Database\SQLiteDatabaseDoesNotExistException - */ - protected function parseDatabasePath(string $path): string - { - $database = $path; - // SQLite supports "in-memory" databases that only last as long as the owning // connection does. These are useful for tests or for short lifetime store - // querying. In-memory databases shall be anonymous (:memory:) or named. - if ($path === ':memory:' || - str_contains($path, '?mode=memory') || - str_contains($path, '&mode=memory') - ) { - return $path; + // querying. In-memory databases may only have a single open connection. + if ($config['database'] == ':memory:') { + return $this->createConnection('sqlite::memory:', $config, $options); } - $path = realpath($path) ?: realpath(base_path($path)); + $path = realpath($config['database']); // Here we'll verify that the SQLite database exists before going any further // as the developer probably wants to know if the database exists and this // SQLite driver will not throw any exception if it does not by default. if ($path === false) { - throw new SQLiteDatabaseDoesNotExistException($database); - } - - return $path; - } - - /** - * Set miscellaneous user-configured pragmas. - * - * @param \PDO $connection - * @param array $config - * @return void - */ - protected function configurePragmas($connection, array $config): void - { - if (! isset($config['pragmas'])) { - return; - } - - foreach ($config['pragmas'] as $pragma => $value) { - $connection->prepare("pragma {$pragma} = {$value}")->execute(); - } - } - - /** - * Enable or disable foreign key constraints if configured. - * - * @param \PDO $connection - * @param array $config - * @return void - */ - protected function configureForeignKeyConstraints($connection, array $config): void - { - if (! isset($config['foreign_key_constraints'])) { - return; - } - - $foreignKeys = $config['foreign_key_constraints'] ? 1 : 0; - - $connection->prepare("pragma foreign_keys = {$foreignKeys}")->execute(); - } - - /** - * Set the busy timeout if configured. - * - * @param \PDO $connection - * @param array $config - * @return void - */ - protected function configureBusyTimeout($connection, array $config): void - { - if (! isset($config['busy_timeout'])) { - return; - } - - $connection->prepare("pragma busy_timeout = {$config['busy_timeout']}")->execute(); - } - - /** - * Set the journal mode if configured. - * - * @param \PDO $connection - * @param array $config - * @return void - */ - protected function configureJournalMode($connection, array $config): void - { - if (! isset($config['journal_mode'])) { - return; - } - - $connection->prepare("pragma journal_mode = {$config['journal_mode']}")->execute(); - } - - /** - * Set the synchronous mode if configured. - * - * @param \PDO $connection - * @param array $config - * @return void - */ - protected function configureSynchronous($connection, array $config): void - { - if (! isset($config['synchronous'])) { - return; + throw new InvalidArgumentException("Database (${config['database']}) does not exist."); } - $connection->prepare("pragma synchronous = {$config['synchronous']}")->execute(); + return $this->createConnection("sqlite:{$path}", $config, $options); } } diff --git a/Connectors/SqlServerConnector.php b/Connectors/SqlServerConnector.php index 14cb72dbbf..7a2a6c8146 100755 --- a/Connectors/SqlServerConnector.php +++ b/Connectors/SqlServerConnector.php @@ -2,8 +2,8 @@ namespace Illuminate\Database\Connectors; -use Illuminate\Support\Arr; use PDO; +use Illuminate\Support\Arr; class SqlServerConnector extends Connector implements ConnectorInterface { @@ -29,37 +29,13 @@ public function connect(array $config) { $options = $this->getOptions($config); - $connection = $this->createConnection($this->getDsn($config), $config, $options); - - $this->configureIsolationLevel($connection, $config); - - return $connection; - } - - /** - * Set the connection transaction isolation level. - * - * https://learn.microsoft.com/en-us/sql/t-sql/statements/set-transaction-isolation-level-transact-sql - * - * @param \PDO $connection - * @param array $config - * @return void - */ - protected function configureIsolationLevel($connection, array $config) - { - if (! isset($config['isolation_level'])) { - return; - } - - $connection->prepare( - "SET TRANSACTION ISOLATION LEVEL {$config['isolation_level']}" - )->execute(); + return $this->createConnection($this->getDsn($config), $config, $options); } /** * Create a DSN string from a configuration. * - * @param array $config + * @param array $config * @return string */ protected function getDsn(array $config) @@ -67,41 +43,45 @@ protected function getDsn(array $config) // First we will create the basic DSN setup as well as the port if it is in // in the configuration options. This will give us the basic DSN we will // need to establish the PDO connections and return them back for use. - if ($this->prefersOdbc($config)) { + if (in_array('dblib', $this->getAvailableDrivers())) { + return $this->getDblibDsn($config); + } elseif ($this->prefersOdbc($config)) { return $this->getOdbcDsn($config); - } - - if (in_array('sqlsrv', $this->getAvailableDrivers())) { - return $this->getSqlSrvDsn($config); } else { - return $this->getDblibDsn($config); + return $this->getSqlSrvDsn($config); } } /** - * Determine if the database configuration prefers ODBC. + * Get the DSN string for a DbLib connection. * * @param array $config - * @return bool + * @return string */ - protected function prefersOdbc(array $config) + protected function getDblibDsn(array $config) { - return in_array('odbc', $this->getAvailableDrivers()) && - ($config['odbc'] ?? null) === true; + $arguments = [ + 'host' => $this->buildHostString($config, ':'), + 'dbname' => $config['database'], + ]; + + $arguments = array_merge( + $arguments, Arr::only($config, ['appname', 'charset']) + ); + + return $this->buildConnectString('dblib', $arguments); } /** - * Get the DSN string for a DbLib connection. + * Determine if the database configuration prefers ODBC. * * @param array $config - * @return string + * @return bool */ - protected function getDblibDsn(array $config) + protected function prefersOdbc(array $config) { - return $this->buildConnectString('dblib', array_merge([ - 'host' => $this->buildHostString($config, ':'), - 'dbname' => $config['database'], - ], Arr::only($config, ['appname', 'charset', 'version']))); + return in_array('odbc', $this->getAvailableDrivers()) && + array_get($config, 'odbc') === true; } /** @@ -112,9 +92,11 @@ protected function getDblibDsn(array $config) */ protected function getOdbcDsn(array $config) { - return isset($config['odbc_datasource_name']) - ? 'odbc:'.$config['odbc_datasource_name'] - : ''; + if (isset($config['odbc_datasource_name'])) { + return 'odbc:'.$config['odbc_datasource_name']; + } + + return ''; } /** @@ -133,60 +115,16 @@ protected function getSqlSrvDsn(array $config) $arguments['Database'] = $config['database']; } - if (isset($config['readonly'])) { - $arguments['ApplicationIntent'] = 'ReadOnly'; - } - - if (isset($config['pooling']) && $config['pooling'] === false) { - $arguments['ConnectionPooling'] = '0'; - } - if (isset($config['appname'])) { $arguments['APP'] = $config['appname']; } - if (isset($config['encrypt'])) { - $arguments['Encrypt'] = $config['encrypt']; - } - - if (isset($config['trust_server_certificate'])) { - $arguments['TrustServerCertificate'] = $config['trust_server_certificate']; - } - - if (isset($config['multiple_active_result_sets']) && $config['multiple_active_result_sets'] === false) { - $arguments['MultipleActiveResultSets'] = 'false'; - } - - if (isset($config['transaction_isolation'])) { - $arguments['TransactionIsolation'] = $config['transaction_isolation']; - } - - if (isset($config['multi_subnet_failover'])) { - $arguments['MultiSubnetFailover'] = $config['multi_subnet_failover']; - } - - if (isset($config['column_encryption'])) { - $arguments['ColumnEncryption'] = $config['column_encryption']; - } - - if (isset($config['key_store_authentication'])) { - $arguments['KeyStoreAuthentication'] = $config['key_store_authentication']; - } - - if (isset($config['key_store_principal_id'])) { - $arguments['KeyStorePrincipalId'] = $config['key_store_principal_id']; - } - - if (isset($config['key_store_secret'])) { - $arguments['KeyStoreSecret'] = $config['key_store_secret']; - } - - if (isset($config['login_timeout'])) { - $arguments['LoginTimeout'] = $config['login_timeout']; + if (isset($config['readonly'])) { + $arguments['ApplicationIntent'] = 'ReadOnly'; } - if (isset($config['authentication'])) { - $arguments['Authentication'] = $config['authentication']; + if (isset($config['pooling']) && $config['pooling'] === false) { + $arguments['ConnectionPooling'] = '0'; } return $this->buildConnectString('sqlsrv', $arguments); @@ -201,9 +139,11 @@ protected function getSqlSrvDsn(array $config) */ protected function buildConnectString($driver, array $arguments) { - return $driver.':'.implode(';', array_map(function ($key) use ($arguments) { + $options = array_map(function ($key) use ($arguments) { return sprintf('%s=%s', $key, $arguments[$key]); - }, array_keys($arguments))); + }, array_keys($arguments)); + + return $driver.':'.implode(';', $options); } /** @@ -215,11 +155,11 @@ protected function buildConnectString($driver, array $arguments) */ protected function buildHostString(array $config, $separator) { - if (empty($config['port'])) { + if (isset($config['port'])) { + return $config['host'].$separator.$config['port']; + } else { return $config['host']; } - - return $config['host'].$separator.$config['port']; } /** diff --git a/Console/DatabaseInspectionCommand.php b/Console/DatabaseInspectionCommand.php deleted file mode 100644 index 8faab04147..0000000000 --- a/Console/DatabaseInspectionCommand.php +++ /dev/null @@ -1,50 +0,0 @@ -getDriverTitle(); - } - - /** - * Get the number of open connections for a database. - * - * @param \Illuminate\Database\ConnectionInterface $connection - * @return int|null - * - * @deprecated - */ - protected function getConnectionCount(ConnectionInterface $connection) - { - return $connection->threadCount(); - } - - /** - * Get the connection configuration details for the given connection. - * - * @param string|null $database - * @return array - */ - protected function getConfigFromDatabase($database) - { - $database ??= config('database.default'); - - return Arr::except(config('database.connections.'.$database), ['password']); - } -} diff --git a/Console/DbCommand.php b/Console/DbCommand.php deleted file mode 100644 index 3017607355..0000000000 --- a/Console/DbCommand.php +++ /dev/null @@ -1,257 +0,0 @@ -getConnection(); - - if (! isset($connection['host']) && $connection['driver'] !== 'sqlite') { - $this->components->error('No host specified for this database connection.'); - $this->line(' Use the [--read] and [--write] options to specify a read or write connection.'); - $this->newLine(); - - return Command::FAILURE; - } - - try { - (new Process( - array_merge([$command = $this->getCommand($connection)], $this->commandArguments($connection)), - null, - $this->commandEnvironment($connection) - ))->setTimeout(null)->setTty(true)->mustRun(function ($type, $buffer) { - $this->output->write($buffer); - }); - } catch (ProcessFailedException $e) { - throw_unless($e->getProcess()->getExitCode() === 127, $e); - - $this->error("{$command} not found in path."); - - return Command::FAILURE; - } - - return 0; - } - - /** - * Get the database connection configuration. - * - * @return array - * - * @throws \UnexpectedValueException - */ - public function getConnection() - { - $connection = $this->laravel['config']['database.connections.'. - (($db = $this->argument('connection')) ?? $this->laravel['config']['database.default']) - ]; - - if (empty($connection)) { - throw new UnexpectedValueException("Invalid database connection [{$db}]."); - } - - if (! empty($connection['url'])) { - $connection = (new ConfigurationUrlParser)->parseConfiguration($connection); - } - - if ($this->option('read')) { - if (is_array($connection['read']['host'])) { - $connection['read']['host'] = $connection['read']['host'][0]; - } - - $connection = array_merge($connection, $connection['read']); - } elseif ($this->option('write')) { - if (is_array($connection['write']['host'])) { - $connection['write']['host'] = $connection['write']['host'][0]; - } - - $connection = array_merge($connection, $connection['write']); - } - - return $connection; - } - - /** - * Get the arguments for the database client command. - * - * @param array $connection - * @return array - */ - public function commandArguments(array $connection) - { - $driver = ucfirst($connection['driver']); - - return $this->{"get{$driver}Arguments"}($connection); - } - - /** - * Get the environment variables for the database client command. - * - * @param array $connection - * @return array|null - */ - public function commandEnvironment(array $connection) - { - $driver = ucfirst($connection['driver']); - - if (method_exists($this, "get{$driver}Environment")) { - return $this->{"get{$driver}Environment"}($connection); - } - - return null; - } - - /** - * Get the database client command to run. - * - * @param array $connection - * @return string - */ - public function getCommand(array $connection) - { - return [ - 'mysql' => 'mysql', - 'mariadb' => 'mariadb', - 'pgsql' => 'psql', - 'sqlite' => 'sqlite3', - 'sqlsrv' => 'sqlcmd', - ][$connection['driver']]; - } - - /** - * Get the arguments for the MySQL CLI. - * - * @param array $connection - * @return array - */ - protected function getMysqlArguments(array $connection) - { - $optionalArguments = [ - 'password' => '--password='.$connection['password'], - 'unix_socket' => '--socket='.($connection['unix_socket'] ?? ''), - 'charset' => '--default-character-set='.($connection['charset'] ?? ''), - ]; - - if (! $connection['password']) { - unset($optionalArguments['password']); - } - - return array_merge([ - '--host='.$connection['host'], - '--port='.$connection['port'], - '--user='.$connection['username'], - ], $this->getOptionalArguments($optionalArguments, $connection), [$connection['database']]); - } - - /** - * Get the arguments for the MariaDB CLI. - * - * @param array $connection - * @return array - */ - protected function getMariaDbArguments(array $connection) - { - return $this->getMysqlArguments($connection); - } - - /** - * Get the arguments for the Postgres CLI. - * - * @param array $connection - * @return array - */ - protected function getPgsqlArguments(array $connection) - { - return [$connection['database']]; - } - - /** - * Get the arguments for the SQLite CLI. - * - * @param array $connection - * @return array - */ - protected function getSqliteArguments(array $connection) - { - return [$connection['database']]; - } - - /** - * Get the arguments for the SQL Server CLI. - * - * @param array $connection - * @return array - */ - protected function getSqlsrvArguments(array $connection) - { - return array_merge(...$this->getOptionalArguments([ - 'database' => ['-d', $connection['database']], - 'username' => ['-U', $connection['username']], - 'password' => ['-P', $connection['password']], - 'host' => ['-S', 'tcp:'.$connection['host'] - .($connection['port'] ? ','.$connection['port'] : ''), ], - 'trust_server_certificate' => ['-C'], - ], $connection)); - } - - /** - * Get the environment variables for the Postgres CLI. - * - * @param array $connection - * @return array|null - */ - protected function getPgsqlEnvironment(array $connection) - { - return array_merge(...$this->getOptionalArguments([ - 'username' => ['PGUSER' => $connection['username']], - 'host' => ['PGHOST' => $connection['host']], - 'port' => ['PGPORT' => $connection['port']], - 'password' => ['PGPASSWORD' => $connection['password']], - ], $connection)); - } - - /** - * Get the optional arguments based on the connection configuration. - * - * @param array $args - * @param array $connection - * @return array - */ - protected function getOptionalArguments(array $args, array $connection) - { - return array_values(array_filter($args, function ($key) use ($connection) { - return ! empty($connection[$key]); - }, ARRAY_FILTER_USE_KEY)); - } -} diff --git a/Console/DumpCommand.php b/Console/DumpCommand.php deleted file mode 100644 index fea8fc0554..0000000000 --- a/Console/DumpCommand.php +++ /dev/null @@ -1,104 +0,0 @@ -isProhibited()) { - return Command::FAILURE; - } - - $connection = $connections->connection($database = $this->input->getOption('database')); - - $this->schemaState($connection)->dump( - $connection, $path = $this->path($connection) - ); - - $dispatcher->dispatch(new SchemaDumped($connection, $path)); - - $info = 'Database schema dumped'; - - if ($this->option('prune')) { - (new Filesystem)->deleteDirectory( - $path = database_path('migrations'), preserve: false - ); - - $info .= ' and pruned'; - - $dispatcher->dispatch(new MigrationsPruned($connection, $path)); - } - - $this->components->info($info.' successfully.'); - } - - /** - * Create a schema state instance for the given connection. - * - * @param \Illuminate\Database\Connection $connection - * @return mixed - */ - protected function schemaState(Connection $connection) - { - $migrations = Config::get('database.migrations', 'migrations'); - - $migrationTable = is_array($migrations) ? ($migrations['table'] ?? 'migrations') : $migrations; - - return $connection->getSchemaState() - ->withMigrationTable($migrationTable) - ->handleOutputUsing(function ($type, $buffer) { - $this->output->write($buffer); - }); - } - - /** - * Get the path that the dump should be written to. - * - * @param \Illuminate\Database\Connection $connection - */ - protected function path(Connection $connection) - { - return tap($this->option('path') ?: database_path('schema/'.$connection->getName().'-schema.sql'), function ($path) { - (new Filesystem)->ensureDirectoryExists(dirname($path)); - }); - } -} diff --git a/Console/Factories/FactoryMakeCommand.php b/Console/Factories/FactoryMakeCommand.php deleted file mode 100644 index 6d080a1439..0000000000 --- a/Console/Factories/FactoryMakeCommand.php +++ /dev/null @@ -1,144 +0,0 @@ -resolveStubPath('/stubs/factory.stub'); - } - - /** - * Resolve the fully-qualified path to the stub. - * - * @param string $stub - * @return string - */ - protected function resolveStubPath($stub) - { - return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) - ? $customPath - : __DIR__.$stub; - } - - /** - * Build the class with the given name. - * - * @param string $name - * @return string - */ - protected function buildClass($name) - { - $factory = class_basename(Str::ucfirst(str_replace('Factory', '', $name))); - - $namespaceModel = $this->option('model') - ? $this->qualifyModel($this->option('model')) - : $this->qualifyModel($this->guessModelName($name)); - - $model = class_basename($namespaceModel); - - $namespace = $this->getNamespace( - Str::replaceFirst($this->rootNamespace(), 'Database\\Factories\\', $this->qualifyClass($this->getNameInput())) - ); - - $replace = [ - '{{ factoryNamespace }}' => $namespace, - 'NamespacedDummyModel' => $namespaceModel, - '{{ namespacedModel }}' => $namespaceModel, - '{{namespacedModel}}' => $namespaceModel, - 'DummyModel' => $model, - '{{ model }}' => $model, - '{{model}}' => $model, - '{{ factory }}' => $factory, - '{{factory}}' => $factory, - ]; - - return str_replace( - array_keys($replace), array_values($replace), parent::buildClass($name) - ); - } - - /** - * Get the destination class path. - * - * @param string $name - * @return string - */ - protected function getPath($name) - { - $name = (new Stringable($name))->replaceFirst($this->rootNamespace(), '')->finish('Factory')->value(); - - return $this->laravel->databasePath().'/factories/'.str_replace('\\', '/', $name).'.php'; - } - - /** - * Guess the model name from the Factory name or return a default model name. - * - * @param string $name - * @return string - */ - protected function guessModelName($name) - { - if (str_ends_with($name, 'Factory')) { - $name = substr($name, 0, -7); - } - - $modelName = $this->qualifyModel(Str::after($name, $this->rootNamespace())); - - if (class_exists($modelName)) { - return $modelName; - } - - if (is_dir(app_path('Models/'))) { - return $this->rootNamespace().'Models\Model'; - } - - return $this->rootNamespace().'Model'; - } - - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return [ - ['model', 'm', InputOption::VALUE_OPTIONAL, 'The name of the model'], - ]; - } -} diff --git a/Console/Factories/stubs/factory.stub b/Console/Factories/stubs/factory.stub deleted file mode 100644 index f931493a27..0000000000 --- a/Console/Factories/stubs/factory.stub +++ /dev/null @@ -1,23 +0,0 @@ - - */ -class {{ factory }}Factory extends Factory -{ - /** - * Define the model's default state. - * - * @return array - */ - public function definition(): array - { - return [ - // - ]; - } -} diff --git a/Console/Migrations/BaseCommand.php b/Console/Migrations/BaseCommand.php index a250d2945f..5fca1144a8 100755 --- a/Console/Migrations/BaseCommand.php +++ b/Console/Migrations/BaseCommand.php @@ -3,43 +3,9 @@ namespace Illuminate\Database\Console\Migrations; use Illuminate\Console\Command; -use Illuminate\Support\Collection; class BaseCommand extends Command { - /** - * Get all of the migration paths. - * - * @return string[] - */ - protected function getMigrationPaths() - { - // Here, we will check to see if a path option has been defined. If it has we will - // use the path relative to the root of the installation folder so our database - // migrations may be run for any customized path from within the application. - if ($this->input->hasOption('path') && $this->option('path')) { - return (new Collection($this->option('path')))->map(function ($path) { - return ! $this->usingRealPath() - ? $this->laravel->basePath().'/'.$path - : $path; - })->all(); - } - - return array_merge( - $this->migrator->paths(), [$this->getMigrationPath()] - ); - } - - /** - * Determine if the given path(s) are pre-resolved "real" paths. - * - * @return bool - */ - protected function usingRealPath() - { - return $this->input->hasOption('realpath') && $this->option('realpath'); - } - /** * Get the path to the migration directory. * diff --git a/Console/Migrations/FreshCommand.php b/Console/Migrations/FreshCommand.php deleted file mode 100644 index 723d3c2298..0000000000 --- a/Console/Migrations/FreshCommand.php +++ /dev/null @@ -1,148 +0,0 @@ -migrator = $migrator; - } - - /** - * Execute the console command. - * - * @return int - */ - public function handle() - { - if ($this->isProhibited() || - ! $this->confirmToProceed()) { - return Command::FAILURE; - } - - $database = $this->input->getOption('database'); - - $this->migrator->usingConnection($database, function () use ($database) { - if ($this->migrator->repositoryExists()) { - $this->newLine(); - - $this->components->task('Dropping all tables', fn () => $this->callSilent('db:wipe', array_filter([ - '--database' => $database, - '--drop-views' => $this->option('drop-views'), - '--drop-types' => $this->option('drop-types'), - '--force' => true, - ])) == 0); - } - }); - - $this->newLine(); - - $this->call('migrate', array_filter([ - '--database' => $database, - '--path' => $this->input->getOption('path'), - '--realpath' => $this->input->getOption('realpath'), - '--schema-path' => $this->input->getOption('schema-path'), - '--force' => true, - '--step' => $this->option('step'), - ])); - - if ($this->laravel->bound(Dispatcher::class)) { - $this->laravel[Dispatcher::class]->dispatch( - new DatabaseRefreshed($database, $this->needsSeeding()) - ); - } - - if ($this->needsSeeding()) { - $this->runSeeder($database); - } - - return 0; - } - - /** - * Determine if the developer has requested database seeding. - * - * @return bool - */ - protected function needsSeeding() - { - return $this->option('seed') || $this->option('seeder'); - } - - /** - * Run the database seeder command. - * - * @param string $database - * @return void - */ - protected function runSeeder($database) - { - $this->call('db:seed', array_filter([ - '--database' => $database, - '--class' => $this->option('seeder') ?: 'Database\\Seeders\\DatabaseSeeder', - '--force' => true, - ])); - } - - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return [ - ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'], - ['drop-views', null, InputOption::VALUE_NONE, 'Drop all tables and views'], - ['drop-types', null, InputOption::VALUE_NONE, 'Drop all tables and types (Postgres only)'], - ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'], - ['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to the migrations files to be executed'], - ['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'], - ['schema-path', null, InputOption::VALUE_OPTIONAL, 'The path to a schema dump file'], - ['seed', null, InputOption::VALUE_NONE, 'Indicates if the seed task should be re-run'], - ['seeder', null, InputOption::VALUE_OPTIONAL, 'The class name of the root seeder'], - ['step', null, InputOption::VALUE_NONE, 'Force the migrations to be run so they can be rolled back individually'], - ]; - } -} diff --git a/Console/Migrations/InstallCommand.php b/Console/Migrations/InstallCommand.php index b89cd4b4e8..103dcaa928 100755 --- a/Console/Migrations/InstallCommand.php +++ b/Console/Migrations/InstallCommand.php @@ -3,11 +3,9 @@ namespace Illuminate\Database\Console\Migrations; use Illuminate\Console\Command; -use Illuminate\Database\Migrations\MigrationRepositoryInterface; -use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputOption; +use Illuminate\Database\Migrations\MigrationRepositoryInterface; -#[AsCommand(name: 'migrate:install')] class InstallCommand extends Command { /** @@ -35,6 +33,7 @@ class InstallCommand extends Command * Create a new migration install command instance. * * @param \Illuminate\Database\Migrations\MigrationRepositoryInterface $repository + * @return void */ public function __construct(MigrationRepositoryInterface $repository) { @@ -48,15 +47,13 @@ public function __construct(MigrationRepositoryInterface $repository) * * @return void */ - public function handle() + public function fire() { $this->repository->setSource($this->input->getOption('database')); - if (! $this->repository->repositoryExists()) { - $this->repository->createRepository(); - } + $this->repository->createRepository(); - $this->components->info('Migration table created successfully.'); + $this->info('Migration table created successfully.'); } /** @@ -67,7 +64,7 @@ public function handle() protected function getOptions() { return [ - ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'], + ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use.'], ]; } } diff --git a/Console/Migrations/MigrateCommand.php b/Console/Migrations/MigrateCommand.php index e3116ca3d1..38c51b66e4 100755 --- a/Console/Migrations/MigrateCommand.php +++ b/Console/Migrations/MigrateCommand.php @@ -3,40 +3,19 @@ namespace Illuminate\Database\Console\Migrations; use Illuminate\Console\ConfirmableTrait; -use Illuminate\Contracts\Console\Isolatable; -use Illuminate\Contracts\Events\Dispatcher; -use Illuminate\Database\Events\SchemaLoaded; use Illuminate\Database\Migrations\Migrator; -use Illuminate\Database\SQLiteDatabaseDoesNotExistException; -use Illuminate\Database\SqlServerConnection; -use Illuminate\Support\Str; -use PDOException; -use RuntimeException; -use Symfony\Component\Console\Attribute\AsCommand; -use Throwable; +use Symfony\Component\Console\Input\InputOption; -use function Laravel\Prompts\confirm; - -#[AsCommand(name: 'migrate')] -class MigrateCommand extends BaseCommand implements Isolatable +class MigrateCommand extends BaseCommand { use ConfirmableTrait; /** - * The name and signature of the console command. + * The console command name. * * @var string */ - protected $signature = 'migrate {--database= : The database connection to use} - {--force : Force the operation to run when in production} - {--path=* : The path(s) to the migrations files to be executed} - {--realpath : Indicate any provided migration file paths are pre-resolved absolute paths} - {--schema-path= : The path to a schema dump file} - {--pretend : Dump the SQL queries that would be run} - {--seed : Indicates if the seed task should be re-run} - {--seeder= : The class name of the root seeder} - {--step : Force the migrations to be run so they can be rolled back individually} - {--graceful : Return a successful exit code even if an error occurs}'; + protected $name = 'migrate'; /** * The console command description. @@ -52,84 +31,64 @@ class MigrateCommand extends BaseCommand implements Isolatable */ protected $migrator; - /** - * The event dispatcher instance. - * - * @var \Illuminate\Contracts\Events\Dispatcher - */ - protected $dispatcher; - /** * Create a new migration command instance. * * @param \Illuminate\Database\Migrations\Migrator $migrator - * @param \Illuminate\Contracts\Events\Dispatcher $dispatcher + * @return void */ - public function __construct(Migrator $migrator, Dispatcher $dispatcher) + public function __construct(Migrator $migrator) { parent::__construct(); $this->migrator = $migrator; - $this->dispatcher = $dispatcher; } /** * Execute the console command. * - * @return int - * - * @throws \Throwable + * @return void */ - public function handle() + public function fire() { if (! $this->confirmToProceed()) { - return 1; + return; } - try { - $this->runMigrations(); - } catch (Throwable $e) { - if ($this->option('graceful')) { - $this->components->warn($e->getMessage()); + $this->prepareDatabase(); - return 0; - } + // The pretend option can be used for "simulating" the migration and grabbing + // the SQL queries that would fire if the migration were to be run against + // a database for real, which is helpful for double checking migrations. + $pretend = $this->input->getOption('pretend'); - throw $e; + // Next, we will check to see if a path option has been defined. If it has + // we will use the path relative to the root of this installation folder + // so that migrations may be run for any path within the applications. + if (! is_null($path = $this->input->getOption('path'))) { + $path = $this->laravel->basePath().'/'.$path; + } else { + $path = $this->getMigrationPath(); } - return 0; - } - - /** - * Run the pending migrations. - * - * @return void - */ - protected function runMigrations() - { - $this->migrator->usingConnection($this->option('database'), function () { - $this->prepareDatabase(); + $this->migrator->run($path, [ + 'pretend' => $pretend, + 'step' => $this->input->getOption('step'), + ]); - // Next, we will check to see if a path option has been defined. If it has - // we will use the path relative to the root of this installation folder - // so that migrations may be run for any path within the applications. - $this->migrator->setOutput($this->output) - ->run($this->getMigrationPaths(), [ - 'pretend' => $this->option('pretend'), - 'step' => $this->option('step'), - ]); + // Once the migrator has run we will grab the note output and send it out to + // the console screen, since the migrator itself functions without having + // any instances of the OutputInterface contract passed into the class. + foreach ($this->migrator->getNotes() as $note) { + $this->output->writeln($note); + } - // Finally, if the "seed" option has been given, we will re-run the database - // seed task to re-populate the database, which is convenient when adding - // a migration and a seed at the same time, as it is only this command. - if ($this->option('seed') && ! $this->option('pretend')) { - $this->call('db:seed', [ - '--class' => $this->option('seeder') ?: 'Database\\Seeders\\DatabaseSeeder', - '--force' => true, - ]); - } - }); + // Finally, if the "seed" option has been given, we will re-run the database + // seed task to re-populate the database, which is convenient when adding + // a migration and a seed at the same time, as it is only this command. + if ($this->input->getOption('seed')) { + $this->call('db:seed', ['--force' => true]); + } } /** @@ -139,205 +98,34 @@ protected function runMigrations() */ protected function prepareDatabase() { - if (! $this->repositoryExists()) { - $this->components->info('Preparing database.'); - - $this->components->task('Creating migration table', function () { - return $this->callSilent('migrate:install', array_filter([ - '--database' => $this->option('database'), - ])) == 0; - }); - - $this->newLine(); - } - - if (! $this->migrator->hasRunAnyMigrations() && ! $this->option('pretend')) { - $this->loadSchemaState(); - } - } - - /** - * Determine if the migrator repository exists. - * - * @return bool - */ - protected function repositoryExists() - { - return retry(2, fn () => $this->migrator->repositoryExists(), 0, function ($e) { - try { - return $this->handleMissingDatabase($e->getPrevious()); - } catch (Throwable) { - return false; - } - }); - } - - /** - * Attempt to create the database if it is missing. - * - * @param \Throwable $e - * @return bool - */ - protected function handleMissingDatabase(Throwable $e) - { - if ($e instanceof SQLiteDatabaseDoesNotExistException) { - return $this->createMissingSqliteDatabase($e->path); - } - - $connection = $this->migrator->resolveConnection($this->option('database')); - - if (! $e instanceof PDOException) { - return false; - } - - if (($e->getCode() === 1049 && in_array($connection->getDriverName(), ['mysql', 'mariadb'])) || - (($e->errorInfo[0] ?? null) == '08006' && - $connection->getDriverName() == 'pgsql' && - Str::contains($e->getMessage(), '"'.$connection->getDatabaseName().'"'))) { - return $this->createMissingMySqlOrPgsqlDatabase($connection); - } - - return false; - } - - /** - * Create a missing SQLite database. - * - * @param string $path - * @return bool - * - * @throws \RuntimeException - */ - protected function createMissingSqliteDatabase($path) - { - if ($this->option('force')) { - return touch($path); - } - - if ($this->option('no-interaction')) { - return false; - } + $this->migrator->setConnection($this->input->getOption('database')); - $this->components->warn('The SQLite database configured for this application does not exist: '.$path); + if (! $this->migrator->repositoryExists()) { + $options = ['--database' => $this->input->getOption('database')]; - if (! confirm('Would you like to create it?', default: true)) { - $this->components->info('Operation cancelled. No database was created.'); - - throw new RuntimeException('Database was not created. Aborting migration.'); + $this->call('migrate:install', $options); } - - return touch($path); } /** - * Create a missing MySQL or Postgres database. + * Get the console command options. * - * @param \Illuminate\Database\Connection $connection - * @return bool - * - * @throws \RuntimeException + * @return array */ - protected function createMissingMySqlOrPgsqlDatabase($connection) + protected function getOptions() { - if ($this->laravel['config']->get("database.connections.{$connection->getName()}.database") !== $connection->getDatabaseName()) { - return false; - } - - if (! $this->option('force') && $this->option('no-interaction')) { - return false; - } - - if (! $this->option('force') && ! $this->option('no-interaction')) { - $this->components->warn("The database '{$connection->getDatabaseName()}' does not exist on the '{$connection->getName()}' connection."); + return [ + ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use.'], - if (! confirm('Would you like to create it?', default: true)) { - $this->components->info('Operation cancelled. No database was created.'); + ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production.'], - throw new RuntimeException('Database was not created. Aborting migration.'); - } - } - try { - $this->laravel['config']->set( - "database.connections.{$connection->getName()}.database", - match ($connection->getDriverName()) { - 'mysql', 'mariadb' => null, - 'pgsql' => 'postgres', - }, - ); + ['path', null, InputOption::VALUE_OPTIONAL, 'The path of migrations files to be executed.'], - $this->laravel['db']->purge(); + ['pretend', null, InputOption::VALUE_NONE, 'Dump the SQL queries that would be run.'], - $freshConnection = $this->migrator->resolveConnection($this->option('database')); - - return tap($freshConnection->unprepared( - match ($connection->getDriverName()) { - 'mysql', 'mariadb' => "CREATE DATABASE IF NOT EXISTS `{$connection->getDatabaseName()}`", - 'pgsql' => 'CREATE DATABASE "'.$connection->getDatabaseName().'"', - } - ), function () { - $this->laravel['db']->purge(); - }); - } finally { - $this->laravel['config']->set("database.connections.{$connection->getName()}.database", $connection->getDatabaseName()); - } - } - - /** - * Load the schema state to seed the initial database schema structure. - * - * @return void - */ - protected function loadSchemaState() - { - $connection = $this->migrator->resolveConnection($this->option('database')); - - // First, we will make sure that the connection supports schema loading and that - // the schema file exists before we proceed any further. If not, we will just - // continue with the standard migration operation as normal without errors. - if ($connection instanceof SqlServerConnection || - ! is_file($path = $this->schemaPath($connection))) { - return; - } - - $this->components->info('Loading stored database schemas.'); - - $this->components->task($path, function () use ($connection, $path) { - // Since the schema file will create the "migrations" table and reload it to its - // proper state, we need to delete it here so we don't get an error that this - // table already exists when the stored database schema file gets executed. - $this->migrator->deleteRepository(); - - $connection->getSchemaState()->handleOutputUsing(function ($type, $buffer) { - $this->output->write($buffer); - })->load($path); - }); - - $this->newLine(); - - // Finally, we will fire an event that this schema has been loaded so developers - // can perform any post schema load tasks that are necessary in listeners for - // this event, which may seed the database tables with some necessary data. - $this->dispatcher->dispatch( - new SchemaLoaded($connection, $path) - ); - } - - /** - * Get the path to the stored schema for the given connection. - * - * @param \Illuminate\Database\Connection $connection - * @return string - */ - protected function schemaPath($connection) - { - if ($this->option('schema-path')) { - return $this->option('schema-path'); - } - - if (file_exists($path = database_path('schema/'.$connection->getName().'-schema.dump'))) { - return $path; - } + ['seed', null, InputOption::VALUE_NONE, 'Indicates if the seed task should be re-run.'], - return database_path('schema/'.$connection->getName().'-schema.sql'); + ['step', null, InputOption::VALUE_NONE, 'Force the migrations to be run so they can be rolled back individually.'], + ]; } } diff --git a/Console/Migrations/MigrateMakeCommand.php b/Console/Migrations/MigrateMakeCommand.php index ac5077f58d..9858fa5684 100644 --- a/Console/Migrations/MigrateMakeCommand.php +++ b/Console/Migrations/MigrateMakeCommand.php @@ -2,26 +2,20 @@ namespace Illuminate\Database\Console\Migrations; -use Illuminate\Contracts\Console\PromptsForMissingInput; -use Illuminate\Database\Migrations\MigrationCreator; use Illuminate\Support\Composer; -use Illuminate\Support\Str; -use Symfony\Component\Console\Attribute\AsCommand; +use Illuminate\Database\Migrations\MigrationCreator; -#[AsCommand(name: 'make:migration')] -class MigrateMakeCommand extends BaseCommand implements PromptsForMissingInput +class MigrateMakeCommand extends BaseCommand { /** * The console command signature. * * @var string */ - protected $signature = 'make:migration {name : The name of the migration} - {--create= : The table to be created} - {--table= : The table to migrate} - {--path= : The location where the migration file should be created} - {--realpath : Indicate any provided migration file paths are pre-resolved absolute paths} - {--fullpath : Output the full path of the migration (Deprecated)}'; + protected $signature = 'make:migration {name : The name of the migration.} + {--create= : The table to be created.} + {--table= : The table to migrate.} + {--path= : The location where the migration file should be created.}'; /** * The console command description. @@ -41,8 +35,6 @@ class MigrateMakeCommand extends BaseCommand implements PromptsForMissingInput * The Composer instance. * * @var \Illuminate\Support\Composer - * - * @deprecated Will be removed in a future Laravel version. */ protected $composer; @@ -51,6 +43,7 @@ class MigrateMakeCommand extends BaseCommand implements PromptsForMissingInput * * @param \Illuminate\Database\Migrations\MigrationCreator $creator * @param \Illuminate\Support\Composer $composer + * @return void */ public function __construct(MigrationCreator $creator, Composer $composer) { @@ -65,37 +58,29 @@ public function __construct(MigrationCreator $creator, Composer $composer) * * @return void */ - public function handle() + public function fire() { // It's possible for the developer to specify the tables to modify in this // schema operation. The developer may also specify if this table needs // to be freshly created so we can create the appropriate migrations. - $name = Str::snake(trim($this->input->getArgument('name'))); + $name = trim($this->input->getArgument('name')); $table = $this->input->getOption('table'); $create = $this->input->getOption('create') ?: false; - // If no table was given as an option but a create option is given then we - // will use the "create" option as the table name. This allows the devs - // to pass a table name into this option as a short-cut for creating. if (! $table && is_string($create)) { $table = $create; $create = true; } - // Next, we will attempt to guess the table name if this the migration has - // "create" in the name. This will allow us to provide a convenient way - // of creating migrations that create new tables for the application. - if (! $table) { - [$table, $create] = TableGuesser::guess($name); - } - // Now we are ready to write the migration out to disk. Once we've written // the migration out, we will dump-autoload for the entire framework to // make sure that the migrations are registered by the class loaders. $this->writeMigration($name, $table, $create); + + $this->composer->dumpAutoloads(); } /** @@ -103,20 +88,16 @@ public function handle() * * @param string $name * @param string $table - * @param bool $create - * @return void + * @param bool $create + * @return string */ protected function writeMigration($name, $table, $create) { - $file = $this->creator->create( - $name, $this->getMigrationPath(), $table, $create - ); + $path = $this->getMigrationPath(); - if (windows_os()) { - $file = str_replace('/', '\\', $file); - } + $file = pathinfo($this->creator->create($name, $path, $table, $create), PATHINFO_FILENAME); - $this->components->info(sprintf('Migration [%s] created successfully.', $file)); + $this->line("Created Migration: $file"); } /** @@ -127,23 +108,9 @@ protected function writeMigration($name, $table, $create) protected function getMigrationPath() { if (! is_null($targetPath = $this->input->getOption('path'))) { - return ! $this->usingRealPath() - ? $this->laravel->basePath().'/'.$targetPath - : $targetPath; + return $this->laravel->basePath().'/'.$targetPath; } return parent::getMigrationPath(); } - - /** - * Prompt for missing input arguments using the returned questions. - * - * @return array - */ - protected function promptForMissingArgumentsUsing() - { - return [ - 'name' => ['What should the migration be named?', 'E.g. create_flights_table'], - ]; - } } diff --git a/Console/Migrations/RefreshCommand.php b/Console/Migrations/RefreshCommand.php index 7d74f5b38c..ce4e8f1938 100755 --- a/Console/Migrations/RefreshCommand.php +++ b/Console/Migrations/RefreshCommand.php @@ -4,16 +4,11 @@ use Illuminate\Console\Command; use Illuminate\Console\ConfirmableTrait; -use Illuminate\Console\Prohibitable; -use Illuminate\Contracts\Events\Dispatcher; -use Illuminate\Database\Events\DatabaseRefreshed; -use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputOption; -#[AsCommand(name: 'migrate:refresh')] class RefreshCommand extends Command { - use ConfirmableTrait, Prohibitable; + use ConfirmableTrait; /** * The console command name. @@ -32,90 +27,36 @@ class RefreshCommand extends Command /** * Execute the console command. * - * @return int + * @return void */ - public function handle() + public function fire() { - if ($this->isProhibited() || - ! $this->confirmToProceed()) { - return Command::FAILURE; + if (! $this->confirmToProceed()) { + return; } - // Next we'll gather some of the options so that we can have the right options - // to pass to the commands. This includes options such as which database to - // use and the path to use for the migration. Then we'll run the command. $database = $this->input->getOption('database'); + $force = $this->input->getOption('force'); + $path = $this->input->getOption('path'); - // If the "step" option is specified it means we only want to rollback a small - // number of migrations before migrating again. For example, the user might - // only rollback and remigrate the latest four migrations instead of all. - $step = $this->input->getOption('step') ?: 0; - - if ($step > 0) { - $this->runRollback($database, $path, $step); - } else { - $this->runReset($database, $path); - } + $this->call('migrate:reset', [ + '--database' => $database, '--force' => $force, + ]); // The refresh command is essentially just a brief aggregate of a few other of // the migration commands and just provides a convenient wrapper to execute // them in succession. We'll also see if we need to re-seed the database. - $this->call('migrate', array_filter([ + $this->call('migrate', [ '--database' => $database, + '--force' => $force, '--path' => $path, - '--realpath' => $this->input->getOption('realpath'), - '--force' => true, - ])); - - if ($this->laravel->bound(Dispatcher::class)) { - $this->laravel[Dispatcher::class]->dispatch( - new DatabaseRefreshed($database, $this->needsSeeding()) - ); - } + ]); if ($this->needsSeeding()) { $this->runSeeder($database); } - - return 0; - } - - /** - * Run the rollback command. - * - * @param string $database - * @param string $path - * @param int $step - * @return void - */ - protected function runRollback($database, $path, $step) - { - $this->call('migrate:rollback', array_filter([ - '--database' => $database, - '--path' => $path, - '--realpath' => $this->input->getOption('realpath'), - '--step' => $step, - '--force' => true, - ])); - } - - /** - * Run the reset command. - * - * @param string $database - * @param string $path - * @return void - */ - protected function runReset($database, $path) - { - $this->call('migrate:reset', array_filter([ - '--database' => $database, - '--path' => $path, - '--realpath' => $this->input->getOption('realpath'), - '--force' => true, - ])); } /** @@ -136,11 +77,13 @@ protected function needsSeeding() */ protected function runSeeder($database) { - $this->call('db:seed', array_filter([ - '--database' => $database, - '--class' => $this->option('seeder') ?: 'Database\\Seeders\\DatabaseSeeder', - '--force' => true, - ])); + $class = $this->option('seeder') ?: 'DatabaseSeeder'; + + $force = $this->input->getOption('force'); + + $this->call('db:seed', [ + '--database' => $database, '--class' => $class, '--force' => $force, + ]); } /** @@ -151,13 +94,15 @@ protected function runSeeder($database) protected function getOptions() { return [ - ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'], - ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'], - ['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to the migrations files to be executed'], - ['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'], - ['seed', null, InputOption::VALUE_NONE, 'Indicates if the seed task should be re-run'], - ['seeder', null, InputOption::VALUE_OPTIONAL, 'The class name of the root seeder'], - ['step', null, InputOption::VALUE_OPTIONAL, 'The number of migrations to be reverted & re-run'], + ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use.'], + + ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production.'], + + ['path', null, InputOption::VALUE_OPTIONAL, 'The path of migrations files to be executed.'], + + ['seed', null, InputOption::VALUE_NONE, 'Indicates if the seed task should be re-run.'], + + ['seeder', null, InputOption::VALUE_OPTIONAL, 'The class name of the root seeder.'], ]; } } diff --git a/Console/Migrations/ResetCommand.php b/Console/Migrations/ResetCommand.php index 787801bab2..8871d3d02b 100755 --- a/Console/Migrations/ResetCommand.php +++ b/Console/Migrations/ResetCommand.php @@ -4,15 +4,12 @@ use Illuminate\Console\Command; use Illuminate\Console\ConfirmableTrait; -use Illuminate\Console\Prohibitable; use Illuminate\Database\Migrations\Migrator; -use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputOption; -#[AsCommand(name: 'migrate:reset')] -class ResetCommand extends BaseCommand +class ResetCommand extends Command { - use ConfirmableTrait, Prohibitable; + use ConfirmableTrait; /** * The console command name. @@ -39,6 +36,7 @@ class ResetCommand extends BaseCommand * Create a new migration rollback command instance. * * @param \Illuminate\Database\Migrations\Migrator $migrator + * @return void */ public function __construct(Migrator $migrator) { @@ -50,27 +48,32 @@ public function __construct(Migrator $migrator) /** * Execute the console command. * - * @return int + * @return void */ - public function handle() + public function fire() { - if ($this->isProhibited() || - ! $this->confirmToProceed()) { - return Command::FAILURE; + if (! $this->confirmToProceed()) { + return; } - return $this->migrator->usingConnection($this->option('database'), function () { - // First, we'll make sure that the migration table actually exists before we - // start trying to rollback and re-run all of the migrations. If it's not - // present we'll just bail out with an info message for the developers. - if (! $this->migrator->repositoryExists()) { - return $this->components->warn('Migration table not found.'); - } - - $this->migrator->setOutput($this->output)->reset( - $this->getMigrationPaths(), $this->option('pretend') - ); - }); + $this->migrator->setConnection($this->input->getOption('database')); + + if (! $this->migrator->repositoryExists()) { + $this->output->writeln('Migration table not found.'); + + return; + } + + $pretend = $this->input->getOption('pretend'); + + $this->migrator->reset($pretend); + + // Once the migrator has run we will grab the note output and send it out to + // the console screen, since the migrator itself functions without having + // any instances of the OutputInterface contract passed into the class. + foreach ($this->migrator->getNotes() as $note) { + $this->output->writeln($note); + } } /** @@ -81,15 +84,11 @@ public function handle() protected function getOptions() { return [ - ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'], - - ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'], - - ['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to the migrations files to be executed'], + ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use.'], - ['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'], + ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production.'], - ['pretend', null, InputOption::VALUE_NONE, 'Dump the SQL queries that would be run'], + ['pretend', null, InputOption::VALUE_NONE, 'Dump the SQL queries that would be run.'], ]; } } diff --git a/Console/Migrations/RollbackCommand.php b/Console/Migrations/RollbackCommand.php index 9c3543ec5b..a341b4fecb 100755 --- a/Console/Migrations/RollbackCommand.php +++ b/Console/Migrations/RollbackCommand.php @@ -4,15 +4,12 @@ use Illuminate\Console\Command; use Illuminate\Console\ConfirmableTrait; -use Illuminate\Console\Prohibitable; use Illuminate\Database\Migrations\Migrator; -use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputOption; -#[AsCommand('migrate:rollback')] -class RollbackCommand extends BaseCommand +class RollbackCommand extends Command { - use ConfirmableTrait, Prohibitable; + use ConfirmableTrait; /** * The console command name. @@ -39,6 +36,7 @@ class RollbackCommand extends BaseCommand * Create a new migration rollback command instance. * * @param \Illuminate\Database\Migrations\Migrator $migrator + * @return void */ public function __construct(Migrator $migrator) { @@ -50,26 +48,26 @@ public function __construct(Migrator $migrator) /** * Execute the console command. * - * @return int + * @return void */ - public function handle() + public function fire() { - if ($this->isProhibited() || - ! $this->confirmToProceed()) { - return Command::FAILURE; + if (! $this->confirmToProceed()) { + return; } - $this->migrator->usingConnection($this->option('database'), function () { - $this->migrator->setOutput($this->output)->rollback( - $this->getMigrationPaths(), [ - 'pretend' => $this->option('pretend'), - 'step' => (int) $this->option('step'), - 'batch' => (int) $this->option('batch'), - ] - ); - }); - - return 0; + $this->migrator->setConnection($this->input->getOption('database')); + + $pretend = $this->input->getOption('pretend'); + + $this->migrator->rollback($pretend); + + // Once the migrator has run we will grab the note output and send it out to + // the console screen, since the migrator itself functions without having + // any instances of the OutputInterface contract passed into the class. + foreach ($this->migrator->getNotes() as $note) { + $this->output->writeln($note); + } } /** @@ -80,13 +78,11 @@ public function handle() protected function getOptions() { return [ - ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'], - ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'], - ['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to the migrations files to be executed'], - ['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'], - ['pretend', null, InputOption::VALUE_NONE, 'Dump the SQL queries that would be run'], - ['step', null, InputOption::VALUE_OPTIONAL, 'The number of migrations to be reverted'], - ['batch', null, InputOption::VALUE_REQUIRED, 'The batch of migrations (identified by their batch number) to be reverted'], + ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use.'], + + ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production.'], + + ['pretend', null, InputOption::VALUE_NONE, 'Dump the SQL queries that would be run.'], ]; } } diff --git a/Console/Migrations/StatusCommand.php b/Console/Migrations/StatusCommand.php index cbb16a133c..aba7acf036 100644 --- a/Console/Migrations/StatusCommand.php +++ b/Console/Migrations/StatusCommand.php @@ -3,12 +3,8 @@ namespace Illuminate\Database\Console\Migrations; use Illuminate\Database\Migrations\Migrator; -use Illuminate\Support\Collection; -use Illuminate\Support\Stringable; -use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputOption; -#[AsCommand(name: 'migrate:status')] class StatusCommand extends BaseCommand { /** @@ -35,7 +31,8 @@ class StatusCommand extends BaseCommand /** * Create a new migration rollback command instance. * - * @param \Illuminate\Database\Migrations\Migrator $migrator + * @param \Illuminate\Database\Migrations\Migrator $migrator + * @return \Illuminate\Database\Console\Migrations\StatusCommand */ public function __construct(Migrator $migrator) { @@ -47,82 +44,46 @@ public function __construct(Migrator $migrator) /** * Execute the console command. * - * @return int|null + * @return void */ - public function handle() + public function fire() { - return $this->migrator->usingConnection($this->option('database'), function () { - if (! $this->migrator->repositoryExists()) { - $this->components->error('Migration table not found.'); + if (! $this->migrator->repositoryExists()) { + return $this->error('No migrations found.'); + } - return 1; - } + $this->migrator->setConnection($this->input->getOption('database')); - $ran = $this->migrator->getRepository()->getRan(); + if (! is_null($path = $this->input->getOption('path'))) { + $path = $this->laravel->basePath().'/'.$path; + } else { + $path = $this->getMigrationPath(); + } - $batches = $this->migrator->getRepository()->getMigrationBatches(); + $ran = $this->migrator->getRepository()->getRan(); - $migrations = $this->getStatusFor($ran, $batches) - ->when($this->option('pending') !== false, fn ($collection) => $collection->filter(function ($migration) { - return (new Stringable($migration[1]))->contains('Pending'); - })); + $migrations = []; - if (count($migrations) > 0) { - $this->newLine(); + foreach ($this->getAllMigrationFiles($path) as $migration) { + $migrations[] = in_array($migration, $ran) ? ['Y', $migration] : ['N', $migration]; + } - $this->components->twoColumnDetail('Migration name', 'Batch / Status'); - - $migrations - ->each( - fn ($migration) => $this->components->twoColumnDetail($migration[0], $migration[1]) - ); - - $this->newLine(); - } elseif ($this->option('pending') !== false) { - $this->components->info('No pending migrations'); - } else { - $this->components->info('No migrations found'); - } - - if ($this->option('pending') && $migrations->some(fn ($m) => (new Stringable($m[1]))->contains('Pending'))) { - return $this->option('pending'); - } - }); + if (count($migrations) > 0) { + $this->table(['Ran?', 'Migration'], $migrations); + } else { + $this->error('No migrations found'); + } } /** - * Get the status for the given run migrations. - * - * @param array $ran - * @param array $batches - * @return \Illuminate\Support\Collection - */ - protected function getStatusFor(array $ran, array $batches) - { - return (new Collection($this->getAllMigrationFiles())) - ->map(function ($migration) use ($ran, $batches) { - $migrationName = $this->migrator->getMigrationName($migration); - - $status = in_array($migrationName, $ran) - ? 'Ran' - : 'Pending'; - - if (in_array($migrationName, $ran)) { - $status = '['.$batches[$migrationName].'] '.$status; - } - - return [$migrationName, $status]; - }); - } - - /** - * Get an array of all of the migration files. + * Get all of the migration files. * + * @param string $path * @return array */ - protected function getAllMigrationFiles() + protected function getAllMigrationFiles($path) { - return $this->migrator->getMigrationFiles($this->getMigrationPaths()); + return $this->migrator->getMigrationFiles($path); } /** @@ -133,10 +94,9 @@ protected function getAllMigrationFiles() protected function getOptions() { return [ - ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'], - ['pending', null, InputOption::VALUE_OPTIONAL, 'Only list pending migrations', false], - ['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to the migrations files to use'], - ['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'], + ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use.'], + + ['path', null, InputOption::VALUE_OPTIONAL, 'The path of migrations files to use.'], ]; } } diff --git a/Console/Migrations/TableGuesser.php b/Console/Migrations/TableGuesser.php deleted file mode 100644 index baf19485ee..0000000000 --- a/Console/Migrations/TableGuesser.php +++ /dev/null @@ -1,37 +0,0 @@ -connection = $connection; - $this->events = $events; - } - - /** - * Execute the console command. - * - * @return void - */ - public function handle() - { - $databases = $this->parseDatabases($this->option('databases')); - - $this->displayConnections($databases); - - if ($this->option('max')) { - $this->dispatchEvents($databases); - } - } - - /** - * Parse the database into an array of the connections. - * - * @param string $databases - * @return \Illuminate\Support\Collection - */ - protected function parseDatabases($databases) - { - return (new Collection(explode(',', $databases)))->map(function ($database) { - if (! $database) { - $database = $this->laravel['config']['database.default']; - } - - $maxConnections = $this->option('max'); - - $connections = $this->connection->connection($database)->threadCount(); - - return [ - 'database' => $database, - 'connections' => $connections, - 'status' => $maxConnections && $connections >= $maxConnections ? 'ALERT' : 'OK', - ]; - }); - } - - /** - * Display the databases and their connection counts in the console. - * - * @param \Illuminate\Support\Collection $databases - * @return void - */ - protected function displayConnections($databases) - { - $this->newLine(); - - $this->components->twoColumnDetail('Database name', 'Connections'); - - $databases->each(function ($database) { - $status = '['.$database['connections'].'] '.$database['status']; - - $this->components->twoColumnDetail($database['database'], $status); - }); - - $this->newLine(); - } - - /** - * Dispatch the database monitoring events. - * - * @param \Illuminate\Support\Collection $databases - * @return void - */ - protected function dispatchEvents($databases) - { - $databases->each(function ($database) { - if ($database['status'] === 'OK') { - return; - } - - $this->events->dispatch( - new DatabaseBusy( - $database['database'], - $database['connections'] - ) - ); - }); - } -} diff --git a/Console/PruneCommand.php b/Console/PruneCommand.php deleted file mode 100644 index 527ab70c6f..0000000000 --- a/Console/PruneCommand.php +++ /dev/null @@ -1,199 +0,0 @@ -models(); - - if ($models->isEmpty()) { - $this->components->info('No prunable models found.'); - - return; - } - - if ($this->option('pretend')) { - $models->each(function ($model) { - $this->pretendToPrune($model); - }); - - return; - } - - $pruning = []; - - $events->listen(ModelsPruned::class, function ($event) use (&$pruning) { - if (! in_array($event->model, $pruning)) { - $pruning[] = $event->model; - - $this->newLine(); - - $this->components->info(sprintf('Pruning [%s] records.', $event->model)); - } - - $this->components->twoColumnDetail($event->model, "{$event->count} records"); - }); - - $events->dispatch(new ModelPruningStarting($models->all())); - - $models->each(function ($model) { - $this->pruneModel($model); - }); - - $events->dispatch(new ModelPruningFinished($models->all())); - - $events->forget(ModelsPruned::class); - } - - /** - * Prune the given model. - * - * @param string $model - * @return void - */ - protected function pruneModel(string $model) - { - $instance = new $model; - - $chunkSize = property_exists($instance, 'prunableChunkSize') - ? $instance->prunableChunkSize - : $this->option('chunk'); - - $total = $model::isPrunable() - ? $instance->pruneAll($chunkSize) - : 0; - - if ($total == 0) { - $this->components->info("No prunable [$model] records found."); - } - } - - /** - * Determine the models that should be pruned. - * - * @return \Illuminate\Support\Collection - * - * @throws \InvalidArgumentException - */ - protected function models() - { - $models = $this->option('model'); - $except = $this->option('except'); - - if ($models && $except) { - throw new InvalidArgumentException('The --models and --except options cannot be combined.'); - } - - if ($models) { - return (new Collection($models)) - ->filter(static fn (string $model) => class_exists($model)) - ->values(); - } - - return (new Collection(Finder::create()->in($this->getPath())->files()->name('*.php'))) - ->map(function ($model) { - $namespace = $this->laravel->getNamespace(); - - return $namespace.str_replace( - ['/', '.php'], - ['\\', ''], - Str::after($model->getRealPath(), realpath(app_path()).DIRECTORY_SEPARATOR) - ); - }) - ->when(! empty($except), fn ($models) => $models->reject(fn ($model) => in_array($model, $except))) - ->filter(fn ($model) => $this->isPrunable($model)) - ->values(); - } - - /** - * Get the path where models are located. - * - * @return string[]|string - */ - protected function getPath() - { - if (! empty($path = $this->option('path'))) { - return (new Collection($path)) - ->map(fn ($path) => base_path($path)) - ->all(); - } - - return app_path('Models'); - } - - /** - * Display how many models will be pruned. - * - * @param class-string $model - * @return void - */ - protected function pretendToPrune($model) - { - $instance = new $model; - - $count = $instance->prunable() - ->when($model::isSoftDeletable(), function ($query) { - $query->withTrashed(); - })->count(); - - if ($count === 0) { - $this->components->info("No prunable [$model] records found."); - } else { - $this->components->info("{$count} [{$model}] records will be pruned."); - } - } - - /** - * Determine if the given model is prunable. - * - * @param string $model - * @return bool - */ - protected function isPrunable(string $model) - { - return class_exists($model) - && is_a($model, Model::class, true) - && ! (new \ReflectionClass($model))->isAbstract() - && $model::isPrunable(); - } -} diff --git a/Console/Seeds/SeedCommand.php b/Console/Seeds/SeedCommand.php index 515ff410b3..a505de06b2 100644 --- a/Console/Seeds/SeedCommand.php +++ b/Console/Seeds/SeedCommand.php @@ -3,18 +3,14 @@ namespace Illuminate\Database\Console\Seeds; use Illuminate\Console\Command; -use Illuminate\Console\ConfirmableTrait; -use Illuminate\Console\Prohibitable; -use Illuminate\Database\ConnectionResolverInterface as Resolver; use Illuminate\Database\Eloquent\Model; -use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Console\Input\InputArgument; +use Illuminate\Console\ConfirmableTrait; use Symfony\Component\Console\Input\InputOption; +use Illuminate\Database\ConnectionResolverInterface as Resolver; -#[AsCommand(name: 'db:seed')] class SeedCommand extends Command { - use ConfirmableTrait, Prohibitable; + use ConfirmableTrait; /** * The console command name. @@ -41,6 +37,7 @@ class SeedCommand extends Command * Create a new database seed command instance. * * @param \Illuminate\Database\ConnectionResolverInterface $resolver + * @return void */ public function __construct(Resolver $resolver) { @@ -52,30 +49,19 @@ public function __construct(Resolver $resolver) /** * Execute the console command. * - * @return int + * @return void */ - public function handle() + public function fire() { - if ($this->isProhibited() || - ! $this->confirmToProceed()) { - return Command::FAILURE; + if (! $this->confirmToProceed()) { + return; } - $this->components->info('Seeding database.'); - - $previousConnection = $this->resolver->getDefaultConnection(); - $this->resolver->setDefaultConnection($this->getDatabase()); Model::unguarded(function () { - $this->getSeeder()->__invoke(); + $this->getSeeder()->run(); }); - - if ($previousConnection) { - $this->resolver->setDefaultConnection($previousConnection); - } - - return 0; } /** @@ -85,20 +71,9 @@ public function handle() */ protected function getSeeder() { - $class = $this->input->getArgument('class') ?? $this->input->getOption('class'); - - if (! str_contains($class, '\\')) { - $class = 'Database\\Seeders\\'.$class; - } - - if ($class === 'Database\\Seeders\\DatabaseSeeder' && - ! class_exists($class)) { - $class = 'DatabaseSeeder'; - } + $class = $this->laravel->make($this->input->getOption('class')); - return $this->laravel->make($class) - ->setContainer($this->laravel) - ->setCommand($this); + return $class->setContainer($this->laravel)->setCommand($this); } /** @@ -113,18 +88,6 @@ protected function getDatabase() return $database ?: $this->laravel['config']['database.default']; } - /** - * Get the console command arguments. - * - * @return array - */ - protected function getArguments() - { - return [ - ['class', InputArgument::OPTIONAL, 'The class name of the root seeder', null], - ]; - } - /** * Get the console command options. * @@ -133,9 +96,11 @@ protected function getArguments() protected function getOptions() { return [ - ['class', null, InputOption::VALUE_OPTIONAL, 'The class name of the root seeder', 'Database\\Seeders\\DatabaseSeeder'], + ['class', null, InputOption::VALUE_OPTIONAL, 'The class name of the root seeder', 'DatabaseSeeder'], + ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to seed'], - ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'], + + ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production.'], ]; } } diff --git a/Console/Seeds/SeederMakeCommand.php b/Console/Seeds/SeederMakeCommand.php index c021bbbe54..3db79f1241 100644 --- a/Console/Seeds/SeederMakeCommand.php +++ b/Console/Seeds/SeederMakeCommand.php @@ -2,11 +2,10 @@ namespace Illuminate\Database\Console\Seeds; +use Illuminate\Support\Composer; +use Illuminate\Filesystem\Filesystem; use Illuminate\Console\GeneratorCommand; -use Illuminate\Support\Str; -use Symfony\Component\Console\Attribute\AsCommand; -#[AsCommand(name: 'make:seeder')] class SeederMakeCommand extends GeneratorCommand { /** @@ -31,36 +30,46 @@ class SeederMakeCommand extends GeneratorCommand protected $type = 'Seeder'; /** - * Execute the console command. + * The Composer instance. + * + * @var \Illuminate\Support\Composer + */ + protected $composer; + + /** + * Create a new command instance. * + * @param \Illuminate\Filesystem\Filesystem $files + * @param \Illuminate\Support\Composer $composer * @return void */ - public function handle() + public function __construct(Filesystem $files, Composer $composer) { - parent::handle(); + parent::__construct($files); + + $this->composer = $composer; } /** - * Get the stub file for the generator. + * Execute the console command. * - * @return string + * @return void */ - protected function getStub() + public function fire() { - return $this->resolveStubPath('/stubs/seeder.stub'); + parent::fire(); + + $this->composer->dumpAutoloads(); } /** - * Resolve the fully-qualified path to the stub. + * Get the stub file for the generator. * - * @param string $stub * @return string */ - protected function resolveStubPath($stub) + protected function getStub() { - return is_file($customPath = $this->laravel->basePath(trim($stub, '/'))) - ? $customPath - : __DIR__.$stub; + return __DIR__.'/stubs/seeder.stub'; } /** @@ -71,22 +80,17 @@ protected function resolveStubPath($stub) */ protected function getPath($name) { - $name = str_replace('\\', '/', Str::replaceFirst($this->rootNamespace(), '', $name)); - - if (is_dir($this->laravel->databasePath().'/seeds')) { - return $this->laravel->databasePath().'/seeds/'.$name.'.php'; - } - - return $this->laravel->databasePath().'/seeders/'.$name.'.php'; + return $this->laravel->databasePath().'/seeds/'.$name.'.php'; } /** - * Get the root namespace for the class. + * Parse the name and format according to the root namespace. * + * @param string $name * @return string */ - protected function rootNamespace() + protected function parseName($name) { - return 'Database\Seeders\\'; + return $name; } } diff --git a/Console/Seeds/WithoutModelEvents.php b/Console/Seeds/WithoutModelEvents.php deleted file mode 100644 index acd9ec3f20..0000000000 --- a/Console/Seeds/WithoutModelEvents.php +++ /dev/null @@ -1,19 +0,0 @@ - Model::withoutEvents($callback); - } -} diff --git a/Console/Seeds/stubs/seeder.stub b/Console/Seeds/stubs/seeder.stub index 8b5403f38d..4aa3845422 100644 --- a/Console/Seeds/stubs/seeder.stub +++ b/Console/Seeds/stubs/seeder.stub @@ -1,16 +1,15 @@ Note: This can be slow on large databases } - {--views : Show the database views Note: This can be slow on large databases } - {--types : Show the user defined types}'; - - /** - * The console command description. - * - * @var string - */ - protected $description = 'Display information about the given database'; - - /** - * Execute the console command. - * - * @param \Illuminate\Database\ConnectionResolverInterface $connections - * @return int - */ - public function handle(ConnectionResolverInterface $connections) - { - $connection = $connections->connection($database = $this->input->getOption('database')); - - $schema = $connection->getSchemaBuilder(); - - $data = [ - 'platform' => [ - 'config' => $this->getConfigFromDatabase($database), - 'name' => $connection->getDriverTitle(), - 'connection' => $connection->getName(), - 'version' => $connection->getServerVersion(), - 'open_connections' => $connection->threadCount(), - ], - 'tables' => $this->tables($connection, $schema), - ]; - - if ($this->option('views')) { - $data['views'] = $this->views($connection, $schema); - } - - if ($this->option('types')) { - $data['types'] = $this->types($connection, $schema); - } - - $this->display($data); - - return 0; - } - - /** - * Get information regarding the tables within the database. - * - * @param \Illuminate\Database\ConnectionInterface $connection - * @param \Illuminate\Database\Schema\Builder $schema - * @return \Illuminate\Support\Collection - */ - protected function tables(ConnectionInterface $connection, Builder $schema) - { - return (new Collection($schema->getTables()))->map(fn ($table) => [ - 'table' => $table['name'], - 'schema' => $table['schema'], - 'schema_qualified_name' => $table['schema_qualified_name'], - 'size' => $table['size'], - 'rows' => $this->option('counts') - ? $connection->withoutTablePrefix(fn ($connection) => $connection->table($table['schema_qualified_name'])->count()) - : null, - 'engine' => $table['engine'], - 'collation' => $table['collation'], - 'comment' => $table['comment'], - ]); - } - - /** - * Get information regarding the views within the database. - * - * @param \Illuminate\Database\ConnectionInterface $connection - * @param \Illuminate\Database\Schema\Builder $schema - * @return \Illuminate\Support\Collection - */ - protected function views(ConnectionInterface $connection, Builder $schema) - { - return (new Collection($schema->getViews())) - ->map(fn ($view) => [ - 'view' => $view['name'], - 'schema' => $view['schema'], - 'rows' => $connection->withoutTablePrefix(fn ($connection) => $connection->table($view['schema_qualified_name'])->count()), - ]); - } - - /** - * Get information regarding the user-defined types within the database. - * - * @param \Illuminate\Database\ConnectionInterface $connection - * @param \Illuminate\Database\Schema\Builder $schema - * @return \Illuminate\Support\Collection - */ - protected function types(ConnectionInterface $connection, Builder $schema) - { - return (new Collection($schema->getTypes())) - ->map(fn ($type) => [ - 'name' => $type['name'], - 'schema' => $type['schema'], - 'type' => $type['type'], - 'category' => $type['category'], - ]); - } - - /** - * Render the database information. - * - * @param array $data - * @return void - */ - protected function display(array $data) - { - $this->option('json') ? $this->displayJson($data) : $this->displayForCli($data); - } - - /** - * Render the database information as JSON. - * - * @param array $data - * @return void - */ - protected function displayJson(array $data) - { - $this->output->writeln(json_encode($data)); - } - - /** - * Render the database information formatted for the CLI. - * - * @param array $data - * @return void - */ - protected function displayForCli(array $data) - { - $platform = $data['platform']; - $tables = $data['tables']; - $views = $data['views'] ?? null; - $types = $data['types'] ?? null; - - $this->newLine(); - - $this->components->twoColumnDetail(''.$platform['name'].'', $platform['version']); - $this->components->twoColumnDetail('Connection', $platform['connection']); - $this->components->twoColumnDetail('Database', Arr::get($platform['config'], 'database')); - $this->components->twoColumnDetail('Host', Arr::get($platform['config'], 'host')); - $this->components->twoColumnDetail('Port', Arr::get($platform['config'], 'port')); - $this->components->twoColumnDetail('Username', Arr::get($platform['config'], 'username')); - $this->components->twoColumnDetail('URL', Arr::get($platform['config'], 'url')); - $this->components->twoColumnDetail('Open Connections', $platform['open_connections']); - $this->components->twoColumnDetail('Tables', $tables->count()); - - if ($tableSizeSum = $tables->sum('size')) { - $this->components->twoColumnDetail('Total Size', Number::fileSize($tableSizeSum, 2)); - } - - $this->newLine(); - - if ($tables->isNotEmpty()) { - $hasSchema = ! is_null($tables->first()['schema']); - - $this->components->twoColumnDetail( - ($hasSchema ? 'Schema / ' : '').'Table', - 'Size'.($this->option('counts') ? ' / Rows' : '') - ); - - $tables->each(function ($table) { - $tableSize = is_null($table['size']) ? null : Number::fileSize($table['size'], 2); - - $this->components->twoColumnDetail( - ($table['schema'] ? $table['schema'].' / ' : '').$table['table'].($this->output->isVerbose() ? ' '.$table['engine'].'' : null), - ($tableSize ?? '—').($this->option('counts') ? ' / '.Number::format($table['rows']).'' : '') - ); - - if ($this->output->isVerbose()) { - if ($table['comment']) { - $this->components->bulletList([ - $table['comment'], - ]); - } - } - }); - - $this->newLine(); - } - - if ($views && $views->isNotEmpty()) { - $hasSchema = ! is_null($views->first()['schema']); - - $this->components->twoColumnDetail( - ($hasSchema ? 'Schema / ' : '').'View', - 'Rows' - ); - - $views->each(fn ($view) => $this->components->twoColumnDetail( - ($view['schema'] ? $view['schema'].' / ' : '').$view['view'], - Number::format($view['rows']) - )); - - $this->newLine(); - } - - if ($types && $types->isNotEmpty()) { - $hasSchema = ! is_null($types->first()['schema']); - - $this->components->twoColumnDetail( - ($hasSchema ? 'Schema / ' : '').'Type', - 'Type / Category' - ); - - $types->each(fn ($type) => $this->components->twoColumnDetail( - ($type['schema'] ? $type['schema'].' / ' : '').$type['name'], - $type['type'].' / '.$type['category'] - )); - - $this->newLine(); - } - } -} diff --git a/Console/ShowModelCommand.php b/Console/ShowModelCommand.php deleted file mode 100644 index 463b0b73e6..0000000000 --- a/Console/ShowModelCommand.php +++ /dev/null @@ -1,191 +0,0 @@ -inspect( - $this->argument('model'), - $this->option('database') - ); - } catch (BindingResolutionException $e) { - $this->components->error($e->getMessage()); - - return 1; - } - - $this->display($info); - - return 0; - } - - /** - * Render the model information. - * - * @return void - */ - protected function display(ModelInfo $modelData) - { - $this->option('json') - ? $this->displayJson($modelData) - : $this->displayCli($modelData); - } - - /** - * Render the model information as JSON. - * - * @return void - */ - protected function displayJson(ModelInfo $modelData) - { - $this->output->writeln( - (new Collection($modelData))->toJson() - ); - } - - /** - * Render the model information for the CLI. - * - * @return void - */ - protected function displayCli(ModelInfo $modelData) - { - $this->newLine(); - - $this->components->twoColumnDetail(''.$modelData->class.''); - $this->components->twoColumnDetail('Database', $modelData->database); - $this->components->twoColumnDetail('Table', $modelData->table); - - if ($policy = $modelData->policy ?? false) { - $this->components->twoColumnDetail('Policy', $policy); - } - - $this->newLine(); - - $this->components->twoColumnDetail( - 'Attributes', - 'type / cast', - ); - - foreach ($modelData->attributes as $attribute) { - $first = trim(sprintf( - '%s %s', - $attribute['name'], - (new Collection(['increments', 'unique', 'nullable', 'fillable', 'hidden', 'appended'])) - ->filter(fn ($property) => $attribute[$property]) - ->map(fn ($property) => sprintf('%s', $property)) - ->implode(', ') - )); - - $second = (new Collection([ - $attribute['type'], - $attribute['cast'] ? ''.$attribute['cast'].'' : null, - ]))->filter()->implode(' / '); - - $this->components->twoColumnDetail($first, $second); - - if ($attribute['default'] !== null) { - $this->components->bulletList( - [sprintf('default: %s', $attribute['default'])], - OutputInterface::VERBOSITY_VERBOSE - ); - } - } - - $this->newLine(); - - $this->components->twoColumnDetail('Relations'); - - foreach ($modelData->relations as $relation) { - $this->components->twoColumnDetail( - sprintf('%s %s', $relation['name'], $relation['type']), - $relation['related'] - ); - } - - $this->newLine(); - - $this->components->twoColumnDetail('Events'); - - if ($modelData->events->count()) { - foreach ($modelData->events as $event) { - $this->components->twoColumnDetail( - sprintf('%s', $event['event']), - sprintf('%s', $event['class']), - ); - } - } - - $this->newLine(); - - $this->components->twoColumnDetail('Observers'); - - if ($modelData->observers->count()) { - foreach ($modelData->observers as $observer) { - $this->components->twoColumnDetail( - sprintf('%s', $observer['event']), - implode(', ', $observer['observer']) - ); - } - } - - $this->newLine(); - } - - /** - * Prompt for missing input arguments using the returned questions. - * - * @return array - */ - protected function promptForMissingArgumentsUsing(): array - { - return [ - 'model' => fn (): string => suggest('Which model would you like to show?', $this->findAvailableModels()), - ]; - } -} diff --git a/Console/TableCommand.php b/Console/TableCommand.php deleted file mode 100644 index ecfa00a9e1..0000000000 --- a/Console/TableCommand.php +++ /dev/null @@ -1,283 +0,0 @@ -connection($this->input->getOption('database')); - $tables = (new Collection($connection->getSchemaBuilder()->getTables())) - ->keyBy('schema_qualified_name')->all(); - - $tableNames = (new Collection($tables))->keys(); - - $tableName = $this->argument('table') ?: search( - 'Which table would you like to inspect?', - fn (string $query) => $tableNames - ->filter(fn ($table) => str_contains(strtolower($table), strtolower($query))) - ->values() - ->all() - ); - - $table = $tables[$tableName] ?? (new Collection($tables))->when( - Arr::wrap($connection->getSchemaBuilder()->getCurrentSchemaListing() - ?? $connection->getSchemaBuilder()->getCurrentSchemaName()), - fn (Collection $collection, array $currentSchemas) => $collection->sortBy( - function (array $table) use ($currentSchemas) { - $index = array_search($table['schema'], $currentSchemas); - - return $index === false ? PHP_INT_MAX : $index; - } - ) - )->firstWhere('name', $tableName); - - if (! $table) { - $this->components->warn("Table [{$tableName}] doesn't exist."); - - return 1; - } - - [$columns, $indexes, $foreignKeys] = $connection->withoutTablePrefix(function ($connection) use ($table) { - $schema = $connection->getSchemaBuilder(); - $tableName = $table['schema_qualified_name']; - - return [ - $this->columns($schema, $tableName), - $this->indexes($schema, $tableName), - $this->foreignKeys($schema, $tableName), - ]; - }); - - $data = [ - 'table' => [ - 'schema' => $table['schema'], - 'name' => $table['name'], - 'schema_qualified_name' => $table['schema_qualified_name'], - 'columns' => count($columns), - 'size' => $table['size'], - 'comment' => $table['comment'], - 'collation' => $table['collation'], - 'engine' => $table['engine'], - ], - 'columns' => $columns, - 'indexes' => $indexes, - 'foreign_keys' => $foreignKeys, - ]; - - $this->display($data); - - return 0; - } - - /** - * Get the information regarding the table's columns. - * - * @param \Illuminate\Database\Schema\Builder $schema - * @param string $table - * @return \Illuminate\Support\Collection - */ - protected function columns(Builder $schema, string $table) - { - return (new Collection($schema->getColumns($table)))->map(fn ($column) => [ - 'column' => $column['name'], - 'attributes' => $this->getAttributesForColumn($column), - 'default' => $column['default'], - 'type' => $column['type'], - ]); - } - - /** - * Get the attributes for a table column. - * - * @param array $column - * @return \Illuminate\Support\Collection - */ - protected function getAttributesForColumn($column) - { - return (new Collection([ - $column['type_name'], - $column['generation'] ? $column['generation']['type'] : null, - $column['auto_increment'] ? 'autoincrement' : null, - $column['nullable'] ? 'nullable' : null, - $column['collation'], - ]))->filter(); - } - - /** - * Get the information regarding the table's indexes. - * - * @param \Illuminate\Database\Schema\Builder $schema - * @param string $table - * @return \Illuminate\Support\Collection - */ - protected function indexes(Builder $schema, string $table) - { - return (new Collection($schema->getIndexes($table)))->map(fn ($index) => [ - 'name' => $index['name'], - 'columns' => new Collection($index['columns']), - 'attributes' => $this->getAttributesForIndex($index), - ]); - } - - /** - * Get the attributes for a table index. - * - * @param array $index - * @return \Illuminate\Support\Collection - */ - protected function getAttributesForIndex($index) - { - return (new Collection([ - $index['type'], - count($index['columns']) > 1 ? 'compound' : null, - $index['unique'] && ! $index['primary'] ? 'unique' : null, - $index['primary'] ? 'primary' : null, - ]))->filter(); - } - - /** - * Get the information regarding the table's foreign keys. - * - * @param \Illuminate\Database\Schema\Builder $schema - * @param string $table - * @return \Illuminate\Support\Collection - */ - protected function foreignKeys(Builder $schema, string $table) - { - return (new Collection($schema->getForeignKeys($table)))->map(fn ($foreignKey) => [ - 'name' => $foreignKey['name'], - 'columns' => new Collection($foreignKey['columns']), - 'foreign_schema' => $foreignKey['foreign_schema'], - 'foreign_table' => $foreignKey['foreign_table'], - 'foreign_columns' => new Collection($foreignKey['foreign_columns']), - 'on_update' => $foreignKey['on_update'], - 'on_delete' => $foreignKey['on_delete'], - ]); - } - - /** - * Render the table information. - * - * @param array $data - * @return void - */ - protected function display(array $data) - { - $this->option('json') ? $this->displayJson($data) : $this->displayForCli($data); - } - - /** - * Render the table information as JSON. - * - * @param array $data - * @return void - */ - protected function displayJson(array $data) - { - $this->output->writeln(json_encode($data)); - } - - /** - * Render the table information formatted for the CLI. - * - * @param array $data - * @return void - */ - protected function displayForCli(array $data) - { - [$table, $columns, $indexes, $foreignKeys] = [ - $data['table'], $data['columns'], $data['indexes'], $data['foreign_keys'], - ]; - - $this->newLine(); - - $this->components->twoColumnDetail(''.$table['schema_qualified_name'].'', $table['comment'] ? ''.$table['comment'].'' : null); - $this->components->twoColumnDetail('Columns', $table['columns']); - - if (! is_null($table['size'])) { - $this->components->twoColumnDetail('Size', Number::fileSize($table['size'], 2)); - } - - if ($table['engine']) { - $this->components->twoColumnDetail('Engine', $table['engine']); - } - - if ($table['collation']) { - $this->components->twoColumnDetail('Collation', $table['collation']); - } - - $this->newLine(); - - if ($columns->isNotEmpty()) { - $this->components->twoColumnDetail('Column', 'Type'); - - $columns->each(function ($column) { - $this->components->twoColumnDetail( - $column['column'].' '.$column['attributes']->implode(', ').'', - (! is_null($column['default']) ? ''.$column['default'].' ' : '').$column['type'] - ); - }); - - $this->newLine(); - } - - if ($indexes->isNotEmpty()) { - $this->components->twoColumnDetail('Index'); - - $indexes->each(function ($index) { - $this->components->twoColumnDetail( - $index['name'].' '.$index['columns']->implode(', ').'', - $index['attributes']->implode(', ') - ); - }); - - $this->newLine(); - } - - if ($foreignKeys->isNotEmpty()) { - $this->components->twoColumnDetail('Foreign Key', 'On Update / On Delete'); - - $foreignKeys->each(function ($foreignKey) { - $this->components->twoColumnDetail( - $foreignKey['name'].' '.$foreignKey['columns']->implode(', ').' references '.$foreignKey['foreign_columns']->implode(', ').' on '.$foreignKey['foreign_table'].'', - $foreignKey['on_update'].' / '.$foreignKey['on_delete'], - ); - }); - - $this->newLine(); - } - } -} diff --git a/Console/WipeCommand.php b/Console/WipeCommand.php deleted file mode 100644 index d638db41d0..0000000000 --- a/Console/WipeCommand.php +++ /dev/null @@ -1,129 +0,0 @@ -isProhibited() || - ! $this->confirmToProceed()) { - return Command::FAILURE; - } - - $database = $this->input->getOption('database'); - - if ($this->option('drop-views')) { - $this->dropAllViews($database); - - $this->components->info('Dropped all views successfully.'); - } - - $this->dropAllTables($database); - - $this->components->info('Dropped all tables successfully.'); - - if ($this->option('drop-types')) { - $this->dropAllTypes($database); - - $this->components->info('Dropped all types successfully.'); - } - - $this->flushDatabaseConnection($database); - - return 0; - } - - /** - * Drop all of the database tables. - * - * @param string $database - * @return void - */ - protected function dropAllTables($database) - { - $this->laravel['db']->connection($database) - ->getSchemaBuilder() - ->dropAllTables(); - } - - /** - * Drop all of the database views. - * - * @param string $database - * @return void - */ - protected function dropAllViews($database) - { - $this->laravel['db']->connection($database) - ->getSchemaBuilder() - ->dropAllViews(); - } - - /** - * Drop all of the database types. - * - * @param string $database - * @return void - */ - protected function dropAllTypes($database) - { - $this->laravel['db']->connection($database) - ->getSchemaBuilder() - ->dropAllTypes(); - } - - /** - * Flush the given database connection. - * - * @param string $database - * @return void - */ - protected function flushDatabaseConnection($database) - { - $this->laravel['db']->connection($database)->disconnect(); - } - - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return [ - ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'], - ['drop-views', null, InputOption::VALUE_NONE, 'Drop all tables and views'], - ['drop-types', null, InputOption::VALUE_NONE, 'Drop all tables and types (Postgres only)'], - ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'], - ]; - } -} diff --git a/DatabaseManager.php b/DatabaseManager.php index 2d8edbc533..17f8bf676f 100755 --- a/DatabaseManager.php +++ b/DatabaseManager.php @@ -2,32 +2,18 @@ namespace Illuminate\Database; -use Illuminate\Database\Connectors\ConnectionFactory; -use Illuminate\Database\Events\ConnectionEstablished; +use PDO; use Illuminate\Support\Arr; -use Illuminate\Support\Collection; -use Illuminate\Support\ConfigurationUrlParser; use Illuminate\Support\Str; -use Illuminate\Support\Traits\Macroable; use InvalidArgumentException; -use PDO; -use RuntimeException; - -use function Illuminate\Support\enum_value; +use Illuminate\Database\Connectors\ConnectionFactory; -/** - * @mixin \Illuminate\Database\Connection - */ class DatabaseManager implements ConnectionResolverInterface { - use Macroable { - __call as macroCall; - } - /** * The application instance. * - * @var \Illuminate\Contracts\Foundation\Application + * @var \Illuminate\Foundation\Application */ protected $app; @@ -41,141 +27,124 @@ class DatabaseManager implements ConnectionResolverInterface /** * The active connection instances. * - * @var array + * @var array */ protected $connections = []; - /** - * The dynamically configured (DB::build) connection configurations. - * - * @var array - */ - protected $dynamicConnectionConfigurations = []; - /** * The custom connection resolvers. * - * @var array + * @var array */ protected $extensions = []; - /** - * The callback to be executed to reconnect to a database. - * - * @var callable - */ - protected $reconnector; - /** * Create a new database manager instance. * - * @param \Illuminate\Contracts\Foundation\Application $app + * @param \Illuminate\Foundation\Application $app * @param \Illuminate\Database\Connectors\ConnectionFactory $factory + * @return void */ public function __construct($app, ConnectionFactory $factory) { $this->app = $app; $this->factory = $factory; - - $this->reconnector = function ($connection) { - $connection->setPdo( - $this->reconnect($connection->getNameWithReadWriteType())->getRawPdo() - ); - }; } /** * Get a database connection instance. * - * @param \UnitEnum|string|null $name + * @param string $name * @return \Illuminate\Database\Connection */ public function connection($name = null) { - [$database, $type] = $this->parseConnectionName($name = enum_value($name) ?: $this->getDefaultConnection()); + list($name, $type) = $this->parseConnectionName($name); // If we haven't created this connection, we'll create it based on the config // provided in the application. Once we've created the connections we will // set the "fetch mode" for PDO which determines the query return types. if (! isset($this->connections[$name])) { - $this->connections[$name] = $this->configure( - $this->makeConnection($database), $type - ); + $connection = $this->makeConnection($name); + + $this->setPdoForType($connection, $type); - $this->dispatchConnectionEstablishedEvent($this->connections[$name]); + $this->connections[$name] = $this->prepare($connection); } return $this->connections[$name]; } /** - * Build a database connection instance from the given configuration. + * Parse the connection into an array of the name and read / write type. * - * @param array $config - * @return \Illuminate\Database\ConnectionInterface + * @param string $name + * @return array */ - public function build(array $config) + protected function parseConnectionName($name) { - $config['name'] ??= static::calculateDynamicConnectionName($config); - - $this->dynamicConnectionConfigurations[$config['name']] = $config; + $name = $name ?: $this->getDefaultConnection(); - return $this->connectUsing($config['name'], $config, true); + return Str::endsWith($name, ['::read', '::write']) + ? explode('::', $name, 2) : [$name, null]; } /** - * Calculate the dynamic connection name for an on-demand connection based on its configuration. + * Disconnect from the given database and remove from local cache. * - * @param array $config - * @return string + * @param string $name + * @return void */ - public static function calculateDynamicConnectionName(array $config) + public function purge($name = null) { - return 'dynamic_'.md5((new Collection($config))->map(function ($value, $key) { - return $key.(is_string($value) || is_int($value) ? $value : ''); - })->implode('')); + $this->disconnect($name); + + unset($this->connections[$name]); } /** - * Get a database connection instance from the given configuration. - * - * @param \UnitEnum|string $name - * @param array $config - * @param bool $force - * @return \Illuminate\Database\ConnectionInterface + * Disconnect from the given database. * - * @throws \RuntimeException + * @param string $name + * @return void */ - public function connectUsing(string $name, array $config, bool $force = false) + public function disconnect($name = null) { - if ($force) { - $this->purge($name = enum_value($name)); - } - - if (isset($this->connections[$name])) { - throw new RuntimeException("Cannot establish connection [$name] because another connection with that name already exists."); + if (isset($this->connections[$name = $name ?: $this->getDefaultConnection()])) { + $this->connections[$name]->disconnect(); } + } - $connection = $this->configure( - $this->factory->make($config, $name), null - ); + /** + * Reconnect to the given database. + * + * @param string $name + * @return \Illuminate\Database\Connection + */ + public function reconnect($name = null) + { + $this->disconnect($name = $name ?: $this->getDefaultConnection()); - $this->dispatchConnectionEstablishedEvent($connection); + if (! isset($this->connections[$name])) { + return $this->connection($name); + } - return tap($connection, fn ($connection) => $this->connections[$name] = $connection); + return $this->refreshPdoConnections($name); } /** - * Parse the connection into an array of the name and read / write type. + * Refresh the PDO connections on a given connection. * * @param string $name - * @return array + * @return \Illuminate\Database\Connection */ - protected function parseConnectionName($name) + protected function refreshPdoConnections($name) { - return Str::endsWith($name, ['::read', '::write']) - ? explode('::', $name, 2) - : [$name, null]; + $fresh = $this->makeConnection($name); + + return $this->connections[$name] + ->setPdo($fresh->getPdo()) + ->setReadPdo($fresh->getReadPdo()); } /** @@ -186,7 +155,7 @@ protected function parseConnectionName($name) */ protected function makeConnection($name) { - $config = $this->configuration($name); + $config = $this->getConfig($name); // First we will check by the connection name to see if an extension has been // registered specifically for that connection. If it has we will call the @@ -195,97 +164,54 @@ protected function makeConnection($name) return call_user_func($this->extensions[$name], $config, $name); } + $driver = $config['driver']; + // Next we will check to see if an extension has been registered for a driver // and will call the Closure if so, which allows us to have a more generic // resolver for the drivers themselves which applies to all connections. - if (isset($this->extensions[$driver = $config['driver']])) { + if (isset($this->extensions[$driver])) { return call_user_func($this->extensions[$driver], $config, $name); } return $this->factory->make($config, $name); } - /** - * Get the configuration for a connection. - * - * @param string $name - * @return array - * - * @throws \InvalidArgumentException - */ - protected function configuration($name) - { - $connections = $this->app['config']['database.connections']; - - $config = $this->dynamicConnectionConfigurations[$name] ?? Arr::get($connections, $name); - - if (is_null($config)) { - throw new InvalidArgumentException("Database connection [{$name}] not configured."); - } - - return (new ConfigurationUrlParser) - ->parseConfiguration($config); - } - /** * Prepare the database connection instance. * * @param \Illuminate\Database\Connection $connection - * @param string $type * @return \Illuminate\Database\Connection */ - protected function configure(Connection $connection, $type) + protected function prepare(Connection $connection) { - $connection = $this->setPdoForType($connection, $type)->setReadWriteType($type); + $connection->setFetchMode($this->app['config']['database.fetch']); - // 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 - // used by the application. Once we're finished we'll return it back out. if ($this->app->bound('events')) { $connection->setEventDispatcher($this->app['events']); } - if ($this->app->bound('db.transactions')) { - $connection->setTransactionManager($this->app['db.transactions']); - } - // Here we'll set a reconnector callback. This reconnector can be any callable // so we will set a Closure to reconnect from this manager with the name of // the connection, which will allow us to reconnect from the connections. - $connection->setReconnector($this->reconnector); + $connection->setReconnector(function ($connection) { + $this->reconnect($connection->getName()); + }); return $connection; } /** - * Dispatch the ConnectionEstablished event if the event dispatcher is available. - * - * @param \Illuminate\Database\Connection $connection - * @return void - */ - protected function dispatchConnectionEstablishedEvent(Connection $connection) - { - if (! $this->app->bound('events')) { - return; - } - - $this->app['events']->dispatch( - new ConnectionEstablished($connection) - ); - } - - /** - * Prepare the read / write mode for database connection instance. + * Prepare the read write mode for database connection instance. * * @param \Illuminate\Database\Connection $connection - * @param string|null $type + * @param string $type * @return \Illuminate\Database\Connection */ protected function setPdoForType(Connection $connection, $type = null) { - if ($type === 'read') { + if ($type == 'read') { $connection->setPdo($connection->getReadPdo()); - } elseif ($type === 'write') { + } elseif ($type == 'write') { $connection->setReadPdo($connection->getPdo()); } @@ -293,87 +219,27 @@ protected function setPdoForType(Connection $connection, $type = null) } /** - * Disconnect from the given database and remove from local cache. - * - * @param \UnitEnum|string|null $name - * @return void - */ - public function purge($name = null) - { - $this->disconnect($name = enum_value($name) ?: $this->getDefaultConnection()); - - unset($this->connections[$name]); - } - - /** - * Disconnect from the given database. - * - * @param \UnitEnum|string|null $name - * @return void - */ - public function disconnect($name = null) - { - if (isset($this->connections[$name = enum_value($name) ?: $this->getDefaultConnection()])) { - $this->connections[$name]->disconnect(); - } - } - - /** - * Reconnect to the given database. + * Get the configuration for a connection. * - * @param \UnitEnum|string|null $name - * @return \Illuminate\Database\Connection - */ - public function reconnect($name = null) - { - $this->disconnect($name = enum_value($name) ?: $this->getDefaultConnection()); - - if (! isset($this->connections[$name])) { - return $this->connection($name); - } - - return tap($this->refreshPdoConnections($name), function ($connection) { - $this->dispatchConnectionEstablishedEvent($connection); - }); - } - - /** - * Set the default database connection for the callback execution. + * @param string $name + * @return array * - * @param \UnitEnum|string $name - * @param callable $callback - * @return mixed + * @throws \InvalidArgumentException */ - public function usingConnection($name, callable $callback) + protected function getConfig($name) { - $previousName = $this->getDefaultConnection(); + $name = $name ?: $this->getDefaultConnection(); - $this->setDefaultConnection($name = enum_value($name)); + // To get the database connection configuration, we will just pull each of the + // connection configurations and get the configurations for the given name. + // If the configuration doesn't exist, we'll throw an exception and bail. + $connections = $this->app['config']['database.connections']; - try { - return $callback(); - } finally { - $this->setDefaultConnection($previousName); + if (is_null($config = Arr::get($connections, $name))) { + throw new InvalidArgumentException("Database [$name] not configured."); } - } - - /** - * Refresh the PDO connections on a given connection. - * - * @param string $name - * @return \Illuminate\Database\Connection - */ - protected function refreshPdoConnections($name) - { - [$database, $type] = $this->parseConnectionName($name); - - $fresh = $this->configure( - $this->makeConnection($database), $type - ); - return $this->connections[$name] - ->setPdo($fresh->getRawPdo()) - ->setReadPdo($fresh->getRawReadPdo()); + return $config; } /** @@ -398,32 +264,29 @@ public function setDefaultConnection($name) } /** - * Get all of the supported drivers. + * Get all of the support drivers. * - * @return string[] + * @return array */ public function supportedDrivers() { - return ['mysql', 'mariadb', 'pgsql', 'sqlite', 'sqlsrv']; + return ['mysql', 'pgsql', 'sqlite', 'sqlsrv']; } /** * Get all of the drivers that are actually available. * - * @return string[] + * @return array */ public function availableDrivers() { - return array_intersect( - $this->supportedDrivers(), - str_replace('dblib', 'sqlsrv', PDO::getAvailableDrivers()) - ); + return array_intersect($this->supportedDrivers(), str_replace('dblib', 'sqlsrv', PDO::getAvailableDrivers())); } /** * Register an extension connection resolver. * - * @param string $name + * @param string $name * @param callable $resolver * @return void */ @@ -432,64 +295,25 @@ public function extend($name, callable $resolver) $this->extensions[$name] = $resolver; } - /** - * Remove an extension connection resolver. - * - * @param string $name - * @return void - */ - public function forgetExtension($name) - { - unset($this->extensions[$name]); - } - /** * Return all of the created connections. * - * @return array + * @return array */ public function getConnections() { return $this->connections; } - /** - * Set the database reconnector callback. - * - * @param callable $reconnector - * @return void - */ - public function setReconnector(callable $reconnector) - { - $this->reconnector = $reconnector; - } - - /** - * Set the application instance used by the manager. - * - * @param \Illuminate\Contracts\Foundation\Application $app - * @return $this - */ - public function setApplication($app) - { - $this->app = $app; - - return $this; - } - /** * Dynamically pass methods to the default connection. * * @param string $method - * @param array $parameters + * @param array $parameters * @return mixed */ public function __call($method, $parameters) { - if (static::hasMacro($method)) { - return $this->macroCall($method, $parameters); - } - - return $this->connection()->$method(...$parameters); + return call_user_func_array([$this->connection(), $method], $parameters); } } diff --git a/DatabaseServiceProvider.php b/DatabaseServiceProvider.php index 8a6115ba1e..1129baaaf7 100755 --- a/DatabaseServiceProvider.php +++ b/DatabaseServiceProvider.php @@ -4,23 +4,14 @@ use Faker\Factory as FakerFactory; use Faker\Generator as FakerGenerator; -use Illuminate\Contracts\Database\ConcurrencyErrorDetector as ConcurrencyErrorDetectorContract; -use Illuminate\Contracts\Database\LostConnectionDetector as LostConnectionDetectorContract; -use Illuminate\Contracts\Queue\EntityResolver; -use Illuminate\Database\Connectors\ConnectionFactory; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\QueueEntityResolver; use Illuminate\Support\ServiceProvider; +use Illuminate\Database\Eloquent\QueueEntityResolver; +use Illuminate\Database\Connectors\ConnectionFactory; +use Illuminate\Database\Eloquent\Factory as EloquentFactory; class DatabaseServiceProvider extends ServiceProvider { - /** - * The array of resolved Faker instances. - * - * @var array - */ - protected static $fakers = []; - /** * Bootstrap the application events. * @@ -42,18 +33,10 @@ public function register() { Model::clearBootedModels(); - $this->registerConnectionServices(); - $this->registerFakerGenerator(); + $this->registerEloquentFactory(); + $this->registerQueueableEntityResolver(); - } - /** - * Register the primary database bindings. - * - * @return void - */ - protected function registerConnectionServices() - { // The connection factory is used to create the actual connection instances on // the database. We will inject the factory into the manager so that it may // make the connections while they are actually needed and not of before. @@ -71,45 +54,23 @@ protected function registerConnectionServices() $this->app->bind('db.connection', function ($app) { return $app['db']->connection(); }); - - $this->app->bind('db.schema', function ($app) { - return $app['db']->connection()->getSchemaBuilder(); - }); - - $this->app->singleton('db.transactions', function () { - return new DatabaseTransactionsManager; - }); - - $this->app->singleton(ConcurrencyErrorDetectorContract::class, function () { - return new ConcurrencyErrorDetector; - }); - - $this->app->singleton(LostConnectionDetectorContract::class, function () { - return new LostConnectionDetector; - }); } /** - * Register the Faker Generator instance in the container. + * Register the Eloquent factory instance in the container. * * @return void */ - protected function registerFakerGenerator() + protected function registerEloquentFactory() { - if (! class_exists(FakerGenerator::class)) { - return; - } - - $this->app->singleton(FakerGenerator::class, function ($app, $parameters) { - $locale = $parameters['locale'] ?? $app['config']->get('app.faker_locale', 'en_US'); - - if (! isset(static::$fakers[$locale])) { - static::$fakers[$locale] = FakerFactory::create($locale); - } + $this->app->singleton(FakerGenerator::class, function () { + return FakerFactory::create(); + }); - static::$fakers[$locale]->unique(true); + $this->app->singleton(EloquentFactory::class, function ($app) { + $faker = $app->make(FakerGenerator::class); - return static::$fakers[$locale]; + return EloquentFactory::construct($faker, database_path('factories')); }); } @@ -120,7 +81,7 @@ protected function registerFakerGenerator() */ protected function registerQueueableEntityResolver() { - $this->app->singleton(EntityResolver::class, function () { + $this->app->singleton('Illuminate\Contracts\Queue\EntityResolver', function () { return new QueueEntityResolver; }); } diff --git a/DatabaseTransactionRecord.php b/DatabaseTransactionRecord.php deleted file mode 100755 index 08fd471323..0000000000 --- a/DatabaseTransactionRecord.php +++ /dev/null @@ -1,121 +0,0 @@ -connection = $connection; - $this->level = $level; - $this->parent = $parent; - } - - /** - * Register a callback to be executed after committing. - * - * @param callable $callback - * @return void - */ - public function addCallback($callback) - { - $this->callbacks[] = $callback; - } - - /** - * Register a callback to be executed after rollback. - * - * @param callable $callback - * @return void - */ - public function addCallbackForRollback($callback) - { - $this->callbacksForRollback[] = $callback; - } - - /** - * Execute all of the callbacks. - * - * @return void - */ - public function executeCallbacks() - { - foreach ($this->callbacks as $callback) { - $callback(); - } - } - - /** - * Execute all of the callbacks for rollback. - * - * @return void - */ - public function executeCallbacksForRollback() - { - foreach ($this->callbacksForRollback as $callback) { - $callback(); - } - } - - /** - * Get all of the callbacks. - * - * @return array - */ - public function getCallbacks() - { - return $this->callbacks; - } - - /** - * Get all of the callbacks for rollback. - * - * @return array - */ - public function getCallbacksForRollback() - { - return $this->callbacksForRollback; - } -} diff --git a/DatabaseTransactionsManager.php b/DatabaseTransactionsManager.php deleted file mode 100755 index 9713c66d82..0000000000 --- a/DatabaseTransactionsManager.php +++ /dev/null @@ -1,267 +0,0 @@ - - */ - protected $committedTransactions; - - /** - * All of the pending transactions. - * - * @var \Illuminate\Support\Collection - */ - protected $pendingTransactions; - - /** - * The current transaction. - * - * @var array - */ - protected $currentTransaction = []; - - /** - * Create a new database transactions manager instance. - */ - public function __construct() - { - $this->committedTransactions = new Collection; - $this->pendingTransactions = new Collection; - } - - /** - * Start a new database transaction. - * - * @param string $connection - * @param int $level - * @return void - */ - public function begin($connection, $level) - { - $this->pendingTransactions->push( - $newTransaction = new DatabaseTransactionRecord( - $connection, - $level, - $this->currentTransaction[$connection] ?? null - ) - ); - - $this->currentTransaction[$connection] = $newTransaction; - } - - /** - * Commit the root database transaction and execute callbacks. - * - * @param string $connection - * @param int $levelBeingCommitted - * @param int $newTransactionLevel - * @return array - */ - public function commit($connection, $levelBeingCommitted, $newTransactionLevel) - { - $this->stageTransactions($connection, $levelBeingCommitted); - - if (isset($this->currentTransaction[$connection])) { - $this->currentTransaction[$connection] = $this->currentTransaction[$connection]->parent; - } - - if (! $this->afterCommitCallbacksShouldBeExecuted($newTransactionLevel) && - $newTransactionLevel !== 0) { - return []; - } - - // This method is only called when the root database transaction is committed so there - // shouldn't be any pending transactions, but going to clear them here anyways just - // in case. This method could be refactored to receive a level in the future too. - $this->pendingTransactions = $this->pendingTransactions->reject( - fn ($transaction) => $transaction->connection === $connection && - $transaction->level >= $levelBeingCommitted - )->values(); - - [$forThisConnection, $forOtherConnections] = $this->committedTransactions->partition( - fn ($transaction) => $transaction->connection == $connection - ); - - $this->committedTransactions = $forOtherConnections->values(); - - $forThisConnection->map->executeCallbacks(); - - return $forThisConnection; - } - - /** - * Move relevant pending transactions to a committed state. - * - * @param string $connection - * @param int $levelBeingCommitted - * @return void - */ - public function stageTransactions($connection, $levelBeingCommitted) - { - $this->committedTransactions = $this->committedTransactions->merge( - $this->pendingTransactions->filter( - fn ($transaction) => $transaction->connection === $connection && - $transaction->level >= $levelBeingCommitted - ) - ); - - $this->pendingTransactions = $this->pendingTransactions->reject( - fn ($transaction) => $transaction->connection === $connection && - $transaction->level >= $levelBeingCommitted - ); - } - - /** - * Rollback the active database transaction. - * - * @param string $connection - * @param int $newTransactionLevel - * @return void - */ - public function rollback($connection, $newTransactionLevel) - { - if ($newTransactionLevel === 0) { - $this->removeAllTransactionsForConnection($connection); - } else { - $this->pendingTransactions = $this->pendingTransactions->reject( - fn ($transaction) => $transaction->connection == $connection && - $transaction->level > $newTransactionLevel - )->values(); - - if ($this->currentTransaction) { - do { - $this->removeCommittedTransactionsThatAreChildrenOf($this->currentTransaction[$connection]); - - $this->currentTransaction[$connection]->executeCallbacksForRollback(); - - $this->currentTransaction[$connection] = $this->currentTransaction[$connection]->parent; - } while ( - isset($this->currentTransaction[$connection]) && - $this->currentTransaction[$connection]->level > $newTransactionLevel - ); - } - } - } - - /** - * Remove all pending, completed, and current transactions for the given connection name. - * - * @param string $connection - * @return void - */ - protected function removeAllTransactionsForConnection($connection) - { - if ($this->currentTransaction) { - for ($currentTransaction = $this->currentTransaction[$connection]; isset($currentTransaction); $currentTransaction = $currentTransaction->parent) { - $currentTransaction->executeCallbacksForRollback(); - } - } - - $this->currentTransaction[$connection] = null; - - $this->pendingTransactions = $this->pendingTransactions->reject( - fn ($transaction) => $transaction->connection == $connection - )->values(); - - $this->committedTransactions = $this->committedTransactions->reject( - fn ($transaction) => $transaction->connection == $connection - )->values(); - } - - /** - * Remove all transactions that are children of the given transaction. - * - * @param \Illuminate\Database\DatabaseTransactionRecord $transaction - * @return void - */ - protected function removeCommittedTransactionsThatAreChildrenOf(DatabaseTransactionRecord $transaction) - { - [$removedTransactions, $this->committedTransactions] = $this->committedTransactions->partition( - fn ($committed) => $committed->connection == $transaction->connection && - $committed->parent === $transaction - ); - - // There may be multiple deeply nested transactions that have already committed that we - // also need to remove. We will recurse down the children of all removed transaction - // instances until there are no more deeply nested child transactions for removal. - $removedTransactions->each( - fn ($transaction) => $this->removeCommittedTransactionsThatAreChildrenOf($transaction) - ); - } - - /** - * Register a transaction callback. - * - * @param callable $callback - * @return void - */ - public function addCallback($callback) - { - if ($current = $this->callbackApplicableTransactions()->last()) { - return $current->addCallback($callback); - } - - $callback(); - } - - /** - * Register a callback for transaction rollback. - * - * @param callable $callback - * @return void - */ - public function addCallbackForRollback($callback) - { - if ($current = $this->callbackApplicableTransactions()->last()) { - return $current->addCallbackForRollback($callback); - } - } - - /** - * Get the transactions that are applicable to callbacks. - * - * @return \Illuminate\Support\Collection - */ - public function callbackApplicableTransactions() - { - return $this->pendingTransactions; - } - - /** - * Determine if after commit callbacks should be executed for the given transaction level. - * - * @param int $level - * @return bool - */ - public function afterCommitCallbacksShouldBeExecuted($level) - { - return $level === 0; - } - - /** - * Get all of the pending transactions. - * - * @return \Illuminate\Support\Collection - */ - public function getPendingTransactions() - { - return $this->pendingTransactions; - } - - /** - * Get all of the committed transactions. - * - * @return \Illuminate\Support\Collection - */ - public function getCommittedTransactions() - { - return $this->committedTransactions; - } -} diff --git a/DeadlockException.php b/DeadlockException.php deleted file mode 100644 index 375a39bc96..0000000000 --- a/DeadlockException.php +++ /dev/null @@ -1,10 +0,0 @@ -bound(ConcurrencyErrorDetectorContract::class) - ? $container[ConcurrencyErrorDetectorContract::class] - : new ConcurrencyErrorDetector(); - - return $detector->causedByConcurrencyError($e); - } -} diff --git a/DetectsLostConnections.php b/DetectsLostConnections.php index ba649afe2a..6c3f69838c 100644 --- a/DetectsLostConnections.php +++ b/DetectsLostConnections.php @@ -2,26 +2,33 @@ namespace Illuminate\Database; -use Illuminate\Container\Container; -use Illuminate\Contracts\Database\LostConnectionDetector as LostConnectionDetectorContract; -use Throwable; +use Exception; +use Illuminate\Support\Str; trait DetectsLostConnections { /** * Determine if the given exception was caused by a lost connection. * - * @param \Throwable $e + * @param \Exception $e * @return bool */ - protected function causedByLostConnection(Throwable $e) + protected function causedByLostConnection(Exception $e) { - $container = Container::getInstance(); + $message = $e->getMessage(); - $detector = $container->bound(LostConnectionDetectorContract::class) - ? $container[LostConnectionDetectorContract::class] - : new LostConnectionDetector(); - - return $detector->causedByLostConnection($e); + return Str::contains($message, [ + 'server has gone away', + 'no connection to the server', + 'Lost connection', + 'is dead or not enabled', + 'Error while sending', + 'decryption failed or bad record mac', + 'server closed the connection unexpectedly', + 'SSL connection has been closed unexpectedly', + 'Deadlock found when trying to get lock', + 'Error writing data to the connection', + 'Resource deadlock avoided', + ]); } } diff --git a/Eloquent/Attributes/Appends.php b/Eloquent/Attributes/Appends.php deleted file mode 100644 index 6b696e3123..0000000000 --- a/Eloquent/Attributes/Appends.php +++ /dev/null @@ -1,18 +0,0 @@ - $columns - */ - public function __construct(public array $columns) - { - } -} diff --git a/Eloquent/Attributes/Boot.php b/Eloquent/Attributes/Boot.php deleted file mode 100644 index f57da7af94..0000000000 --- a/Eloquent/Attributes/Boot.php +++ /dev/null @@ -1,11 +0,0 @@ -> $collectionClass - */ - public function __construct(public string $collectionClass) - { - } -} diff --git a/Eloquent/Attributes/Connection.php b/Eloquent/Attributes/Connection.php deleted file mode 100644 index d02fcc502f..0000000000 --- a/Eloquent/Attributes/Connection.php +++ /dev/null @@ -1,18 +0,0 @@ - $columns - */ - public function __construct(public array $columns) - { - } -} diff --git a/Eloquent/Attributes/Guarded.php b/Eloquent/Attributes/Guarded.php deleted file mode 100644 index d2f9c34e8d..0000000000 --- a/Eloquent/Attributes/Guarded.php +++ /dev/null @@ -1,18 +0,0 @@ - $columns - */ - public function __construct(public array $columns) - { - } -} diff --git a/Eloquent/Attributes/Hidden.php b/Eloquent/Attributes/Hidden.php deleted file mode 100644 index a7dbfbc446..0000000000 --- a/Eloquent/Attributes/Hidden.php +++ /dev/null @@ -1,18 +0,0 @@ - $columns - */ - public function __construct(public array $columns) - { - } -} diff --git a/Eloquent/Attributes/Initialize.php b/Eloquent/Attributes/Initialize.php deleted file mode 100644 index 58f48769eb..0000000000 --- a/Eloquent/Attributes/Initialize.php +++ /dev/null @@ -1,11 +0,0 @@ - $relations - */ - public function __construct(public array $relations) - { - } -} diff --git a/Eloquent/Attributes/Unguarded.php b/Eloquent/Attributes/Unguarded.php deleted file mode 100644 index 005105e47e..0000000000 --- a/Eloquent/Attributes/Unguarded.php +++ /dev/null @@ -1,11 +0,0 @@ - $builderClass - */ - public function __construct(public string $builderClass) - { - } -} diff --git a/Eloquent/Attributes/UseFactory.php b/Eloquent/Attributes/UseFactory.php deleted file mode 100644 index a013102fcd..0000000000 --- a/Eloquent/Attributes/UseFactory.php +++ /dev/null @@ -1,18 +0,0 @@ - $factoryClass - */ - public function __construct(public string $factoryClass) - { - } -} diff --git a/Eloquent/Attributes/UsePolicy.php b/Eloquent/Attributes/UsePolicy.php deleted file mode 100644 index 9306598e07..0000000000 --- a/Eloquent/Attributes/UsePolicy.php +++ /dev/null @@ -1,18 +0,0 @@ - $class - */ - public function __construct(public string $class) - { - } -} diff --git a/Eloquent/Attributes/UseResource.php b/Eloquent/Attributes/UseResource.php deleted file mode 100644 index a1cbc48f3a..0000000000 --- a/Eloquent/Attributes/UseResource.php +++ /dev/null @@ -1,18 +0,0 @@ - $class - */ - public function __construct(public string $class) - { - } -} diff --git a/Eloquent/Attributes/UseResourceCollection.php b/Eloquent/Attributes/UseResourceCollection.php deleted file mode 100644 index c17e1f1768..0000000000 --- a/Eloquent/Attributes/UseResourceCollection.php +++ /dev/null @@ -1,18 +0,0 @@ - $class - */ - public function __construct(public string $class) - { - } -} diff --git a/Eloquent/Attributes/Visible.php b/Eloquent/Attributes/Visible.php deleted file mode 100644 index b9c87575a4..0000000000 --- a/Eloquent/Attributes/Visible.php +++ /dev/null @@ -1,18 +0,0 @@ - $columns - */ - public function __construct(public array $columns) - { - } -} diff --git a/Eloquent/BroadcastableModelEventOccurred.php b/Eloquent/BroadcastableModelEventOccurred.php deleted file mode 100644 index 8bd028032e..0000000000 --- a/Eloquent/BroadcastableModelEventOccurred.php +++ /dev/null @@ -1,144 +0,0 @@ -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 (new BaseCollection($channels)) - ->map(fn ($channel) => $channel instanceof Model ? new PrivateChannel($channel) : $channel) - ->all(); - } - - /** - * The name the event should broadcast as. - * - * @return string - */ - public function broadcastAs() - { - $default = class_basename($this->model).ucfirst($this->event); - - return method_exists($this->model, 'broadcastAs') - ? ($this->model->broadcastAs($this->event) ?: $default) - : $default; - } - - /** - * Get the data that should be sent with the broadcasted event. - * - * @return array|null - */ - public function broadcastWith() - { - return method_exists($this->model, 'broadcastWith') - ? $this->model->broadcastWith($this->event) - : null; - } - - /** - * Manually specify the channels the event should broadcast on. - * - * @param array $channels - * @return $this - */ - public function onChannels(array $channels) - { - $this->channels = $channels; - - return $this; - } - - /** - * Determine if the event should be broadcast synchronously. - * - * @return bool - */ - public function shouldBroadcastNow() - { - return $this->event === 'deleted' && - ! method_exists($this->model, 'bootSoftDeletes'); - } - - /** - * Get the event name. - * - * @return string - */ - public function event() - { - return $this->event; - } -} diff --git a/Eloquent/BroadcastsEvents.php b/Eloquent/BroadcastsEvents.php deleted file mode 100644 index c0461ddb0a..0000000000 --- a/Eloquent/BroadcastsEvents.php +++ /dev/null @@ -1,197 +0,0 @@ -broadcastCreated(); - }); - - static::updated(function ($model) { - $model->broadcastUpdated(); - }); - - if (method_exists(static::class, 'bootSoftDeletes')) { - static::softDeleted(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 (! static::$isBroadcasting) { - return; - } - - 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($this->newBroadcastableEvent($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(); - }); - } - - /** - * Create a new broadcastable model event for the model. - * - * @param string $event - * @return \Illuminate\Database\Eloquent\BroadcastableModelEventOccurred - */ - protected function newBroadcastableEvent(string $event) - { - return new BroadcastableModelEventOccurred($this, $event); - } - - /** - * 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/BroadcastsEventsAfterCommit.php b/Eloquent/BroadcastsEventsAfterCommit.php deleted file mode 100644 index 806af70f87..0000000000 --- a/Eloquent/BroadcastsEventsAfterCommit.php +++ /dev/null @@ -1,18 +0,0 @@ - */ - use BuildsQueries, ForwardsCalls, QueriesRelationships { - BuildsQueries::sole as baseSole; - } +use Illuminate\Pagination\Paginator; +use Illuminate\Database\Query\Expression; +use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Database\Query\Builder as QueryBuilder; +class Builder +{ /** * The base query builder instance. * @@ -49,17 +23,10 @@ class Builder implements BuilderContract /** * The model being queried. * - * @var TModel + * @var \Illuminate\Database\Eloquent\Model */ protected $model; - /** - * The attributes that should be added to new models created by this builder. - * - * @var array - */ - public $pendingAttributes = []; - /** * The relationships that should be eager loaded. * @@ -68,18 +35,11 @@ class Builder implements BuilderContract protected $eagerLoad = []; /** - * All of the globally registered builder macros. - * - * @var array - */ - protected static $macros = []; - - /** - * All of the locally registered builder macros. + * All of the registered builder macros. * * @var array */ - protected $localMacros = []; + protected $macros = []; /** * A replacement for the typical delete function. @@ -88,53 +48,14 @@ class Builder implements BuilderContract */ protected $onDelete; - /** - * The properties that should be returned from query builder. - * - * @var string[] - */ - protected $propertyPassthru = [ - 'from', - ]; - /** * The methods that should be returned from query builder. * - * @var string[] + * @var array */ protected $passthru = [ - 'aggregate', - 'average', - 'avg', - 'count', - 'dd', - 'ddrawsql', - 'doesntexist', - 'doesntexistor', - 'dump', - 'dumprawsql', - 'exists', - 'existsor', - 'explain', - 'getbindings', - 'getconnection', - 'getcountforpagination', - 'getgrammar', - 'getrawbindings', - 'implode', - 'insert', - 'insertgetid', - 'insertorignore', - 'insertusing', - 'insertorignoreusing', - 'max', - 'min', - 'numericaggregate', - 'raw', - 'rawvalue', - 'sum', - 'tosql', - 'torawsql', + 'insert', 'insertGetId', 'getBindings', 'toSql', + 'exists', 'count', 'min', 'max', 'avg', 'sum', 'getConnection', ]; /** @@ -151,41 +72,17 @@ class Builder implements BuilderContract */ protected $removedScopes = []; - /** - * The callbacks that should be invoked after retrieving data from the database. - * - * @var array - */ - protected $afterQueryCallbacks = []; - - /** - * The callbacks that should be invoked on clone. - * - * @var array - */ - protected $onCloneCallbacks = []; - /** * Create a new Eloquent query builder instance. * * @param \Illuminate\Database\Query\Builder $query + * @return void */ public function __construct(QueryBuilder $query) { $this->query = $query; } - /** - * Create and return an un-saved model instance. - * - * @param array $attributes - * @return TModel - */ - public function make(array $attributes = []) - { - return $this->newModelInstance($attributes); - } - /** * Register a new global scope. * @@ -229,34 +126,19 @@ public function withoutGlobalScope($scope) * @param array|null $scopes * @return $this */ - public function withoutGlobalScopes(?array $scopes = null) + public function withoutGlobalScopes(array $scopes = null) { - if (! is_array($scopes)) { - $scopes = array_keys($this->scopes); - } - - foreach ($scopes as $scope) { - $this->withoutGlobalScope($scope); + if (is_array($scopes)) { + foreach ($scopes as $scope) { + $this->withoutGlobalScope($scope); + } + } else { + $this->scopes = []; } return $this; } - /** - * Remove all global scopes except the given scopes. - * - * @param array $scopes - * @return $this - */ - public function withoutGlobalScopesExcept(array $scopes = []) - { - $this->withoutGlobalScopes( - array_diff(array_keys($this->scopes), $scopes) - ); - - return $this; - } - /** * Get an array of global scopes that were removed from the query. * @@ -268,651 +150,459 @@ public function removedScopes() } /** - * Add a where clause on the primary key to the query. + * Find a model by its primary key. * * @param mixed $id - * @return $this + * @param array $columns + * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|static[]|static|null */ - public function whereKey($id) + public function find($id, $columns = ['*']) { - if ($id instanceof Model) { - $id = $id->getKey(); + if (is_array($id)) { + return $this->findMany($id, $columns); } - if (is_array($id) || $id instanceof Arrayable) { - if (in_array($this->model->getKeyType(), ['int', 'integer'])) { - $this->query->whereIntegerInRaw($this->model->getQualifiedKeyName(), $id); - } else { - $this->query->whereIn($this->model->getQualifiedKeyName(), $id); - } + $this->query->where($this->model->getQualifiedKeyName(), '=', $id); - return $this; - } + return $this->first($columns); + } - if ($id !== null && $this->model->getKeyType() === 'string') { - $id = (string) $id; + /** + * Find multiple models by their primary keys. + * + * @param array $ids + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function findMany($ids, $columns = ['*']) + { + if (empty($ids)) { + return $this->model->newCollection(); } - return $this->where($this->model->getQualifiedKeyName(), '=', $id); + $this->query->whereIn($this->model->getQualifiedKeyName(), $ids); + + return $this->get($columns); } /** - * Add a where clause on the primary key to the query. + * Find a model by its primary key or throw an exception. * * @param mixed $id - * @return $this + * @param array $columns + * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection + * + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException */ - public function whereKeyNot($id) + public function findOrFail($id, $columns = ['*']) { - if ($id instanceof Model) { - $id = $id->getKey(); - } + $result = $this->find($id, $columns); - if (is_array($id) || $id instanceof Arrayable) { - if (in_array($this->model->getKeyType(), ['int', 'integer'])) { - $this->query->whereIntegerNotInRaw($this->model->getQualifiedKeyName(), $id); - } else { - $this->query->whereNotIn($this->model->getQualifiedKeyName(), $id); + if (is_array($id)) { + if (count($result) == count(array_unique($id))) { + return $result; } - - return $this; - } - - if ($id !== null && $this->model->getKeyType() === 'string') { - $id = (string) $id; + } elseif (! is_null($result)) { + return $result; } - return $this->where($this->model->getQualifiedKeyName(), '!=', $id); + throw (new ModelNotFoundException)->setModel(get_class($this->model)); } /** - * Exclude the given models from the query results. + * Find a model by its primary key or return fresh model instance. * - * @param iterable|mixed $models - * @return static + * @param mixed $id + * @param array $columns + * @return \Illuminate\Database\Eloquent\Model */ - public function except($models) + public function findOrNew($id, $columns = ['*']) { - return $this->whereKeyNot( - $models instanceof Model - ? $models->getKey() - : Collection::wrap($models)->modelKeys() - ); + if (! is_null($model = $this->find($id, $columns))) { + return $model; + } + + return $this->model->newInstance(); } /** - * Add a basic where clause to the query. + * Get the first record matching the attributes or instantiate it. * - * @param (\Closure(static): mixed)|string|array|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @param string $boolean - * @return $this + * @param array $attributes + * @return \Illuminate\Database\Eloquent\Model */ - public function where($column, $operator = null, $value = null, $boolean = 'and') + public function firstOrNew(array $attributes) { - if ($column instanceof Closure && is_null($operator)) { - $column($query = $this->model->newQueryWithoutRelationships()); - - $this->eagerLoad = array_merge($this->eagerLoad, $query->getEagerLoads()); - - $this->withoutGlobalScopes( - $query->removedScopes() - ); - $this->query->addNestedWhereQuery($query->getQuery(), $boolean); - } else { - $this->query->where(...func_get_args()); + if (! is_null($instance = $this->where($attributes)->first())) { + return $instance; } - return $this; + return $this->model->newInstance($attributes); } /** - * Add a basic where clause to the query, and return the first result. + * Get the first record matching the attributes or create it. * - * @param (\Closure(static): mixed)|string|array|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @param string $boolean - * @return TModel|null + * @param array $attributes + * @param array $values + * @return \Illuminate\Database\Eloquent\Model */ - public function firstWhere($column, $operator = null, $value = null, $boolean = 'and') + public function firstOrCreate(array $attributes, array $values = []) { - return $this->where(...func_get_args())->first(); + if (! is_null($instance = $this->where($attributes)->first())) { + return $instance; + } + + $instance = $this->model->newInstance($attributes + $values); + + $instance->save(); + + return $instance; } /** - * Add an "or where" clause to the query. + * Create or update a record matching the attributes, and fill it with values. * - * @param (\Closure(static): mixed)|array|string|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @return $this + * @param array $attributes + * @param array $values + * @return \Illuminate\Database\Eloquent\Model */ - public function orWhere($column, $operator = null, $value = null) + public function updateOrCreate(array $attributes, array $values = []) { - [$value, $operator] = $this->query->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); + $instance = $this->firstOrNew($attributes); - return $this->where($column, $operator, $value, 'or'); + $instance->fill($values)->save(); + + return $instance; } /** - * Add a basic "where not" clause to the query. + * Execute the query and get the first result. * - * @param (\Closure(static): mixed)|string|array|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @param string $boolean - * @return $this + * @param array $columns + * @return \Illuminate\Database\Eloquent\Model|static|null */ - public function whereNot($column, $operator = null, $value = null, $boolean = 'and') + public function first($columns = ['*']) { - return $this->where($column, $operator, $value, $boolean.' not'); + return $this->take(1)->get($columns)->first(); } /** - * Add an "or where not" clause to the query. + * Execute the query and get the first result or throw an exception. * - * @param (\Closure(static): mixed)|array|string|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @return $this + * @param array $columns + * @return \Illuminate\Database\Eloquent\Model|static + * + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException */ - public function orWhereNot($column, $operator = null, $value = null) + public function firstOrFail($columns = ['*']) { - return $this->whereNot($column, $operator, $value, 'or'); + if (! is_null($model = $this->first($columns))) { + return $model; + } + + throw (new ModelNotFoundException)->setModel(get_class($this->model)); } /** - * Add an "order by" clause for a timestamp to the query. + * Execute the query as a "select" statement. * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @return $this + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection|static[] */ - public function latest($column = null) + public function get($columns = ['*']) { - if (is_null($column)) { - $column = $this->model->getCreatedAtColumn() ?? 'created_at'; - } + $builder = $this->applyScopes(); - $this->query->latest($column); + $models = $builder->getModels($columns); - return $this; + // If we actually found models we will also eager load any relationships that + // have been specified as needing to be eager loaded, which will solve the + // n+1 query issue for the developers to avoid running a lot of queries. + if (count($models) > 0) { + $models = $builder->eagerLoadRelations($models); + } + + return $builder->getModel()->newCollection($models); } /** - * Add an "order by" clause for a timestamp to the query. + * Get a single column's value from the first result of a query. * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @return $this + * @param string $column + * @return mixed */ - public function oldest($column = null) + public function value($column) { - if (is_null($column)) { - $column = $this->model->getCreatedAtColumn() ?? 'created_at'; - } + $result = $this->first([$column]); - $this->query->oldest($column); - - return $this; + if ($result) { + return $result->{$column}; + } } /** - * Create a collection of models from plain arrays. + * Chunk the results of the query. * - * @param array $items - * @return \Illuminate\Database\Eloquent\Collection + * @param int $count + * @param callable $callback + * @return bool */ - public function hydrate(array $items) + public function chunk($count, callable $callback) { - $instance = $this->newModelInstance(); + $results = $this->forPage($page = 1, $count)->get(); - return $instance->newCollection(array_map(function ($item) use ($items, $instance) { - $model = $instance->newFromBuilder($item); - - if (count($items) > 1) { - $model->preventsLazyLoading = Model::preventsLazyLoading(); + while (! $results->isEmpty()) { + // On each chunk result set, we will pass them to the callback and then let the + // developer take care of everything within the callback, which allows us to + // keep the memory low for spinning through large result sets for working. + if (call_user_func($callback, $results) === false) { + return false; } - return $model; - }, $items)); + $page++; + + $results = $this->forPage($page, $count)->get(); + } + + return true; } /** - * Insert into the database after merging the model's default attributes, setting timestamps, and casting values. + * Chunk the results of a query by comparing numeric IDs. * - * @param array> $values + * @param int $count + * @param callable $callback + * @param string $column * @return bool */ - public function fillAndInsert(array $values) + public function chunkById($count, callable $callback, $column = 'id') { - return $this->insert($this->fillForInsert($values)); - } + $lastId = null; - /** - * Insert (ignoring errors) into the database after merging the model's default attributes, setting timestamps, and casting values. - * - * @param array> $values - * @return int - */ - public function fillAndInsertOrIgnore(array $values) - { - return $this->insertOrIgnore($this->fillForInsert($values)); + $results = $this->forPageAfterId($count, 0, $column)->get(); + + while (! $results->isEmpty()) { + if (call_user_func($callback, $results) === false) { + return false; + } + + $lastId = $results->last()->{$column}; + + $results = $this->forPageAfterId($count, $lastId, $column)->get(); + } + + return true; } /** - * Insert a record into the database and get its ID after merging the model's default attributes, setting timestamps, and casting values. + * Execute a callback over each item while chunking. * - * @param array $values - * @return int + * @param callable $callback + * @param int $count + * @return bool */ - public function fillAndInsertGetId(array $values) + public function each(callable $callback, $count = 1000) { - return $this->insertGetId($this->fillForInsert([$values])[0]); + if (is_null($this->query->orders) && is_null($this->query->unionOrders)) { + $this->orderBy($this->model->getQualifiedKeyName(), 'asc'); + } + + return $this->chunk($count, function ($results) use ($callback) { + foreach ($results as $key => $value) { + if ($callback($value, $key) === false) { + return false; + } + } + }); } /** - * Enrich the given values by merging in the model's default attributes, adding timestamps, and casting values. + * Get an array with the values of a given column. * - * @param array> $values - * @return array> + * @param string $column + * @param string|null $key + * @return \Illuminate\Support\Collection */ - public function fillForInsert(array $values) + public function pluck($column, $key = null) { - if (empty($values)) { - return []; - } + $results = $this->toBase()->pluck($column, $key); - if (! is_array(array_first($values))) { - $values = [$values]; + // If the model has a mutator for the requested column, we will spin through + // the results and mutate the values so that the mutated version of these + // columns are returned as you would expect from these Eloquent models. + if (! $this->model->hasGetMutator($column)) { + return $results; } - $this->model->unguarded(function () use (&$values) { - foreach ($values as $key => $rowValues) { - $values[$key] = tap( - $this->newModelInstance($rowValues), - fn ($model) => $model->setUniqueIds() - )->getAttributes(); - } + return $results->map(function ($value) use ($column) { + return $this->model->newFromBuilder([$column => $value])->$column; }); - - return $this->addTimestampsToUpsertValues($values); } /** - * Create a collection of models from a raw query. + * Paginate the given query. + * + * @param int $perPage + * @param array $columns + * @param string $pageName + * @param int|null $page + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator * - * @param string $query - * @param array $bindings - * @return \Illuminate\Database\Eloquent\Collection + * @throws \InvalidArgumentException */ - public function fromQuery($query, $bindings = []) + public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) { - return $this->hydrate( - $this->query->getConnection()->select($query, $bindings) + $query = $this->toBase(); + + $total = $query->getCountForPagination(); + + $this->forPage( + $page = $page ?: Paginator::resolveCurrentPage($pageName), + $perPage = $perPage ?: $this->model->getPerPage() ); + + return new LengthAwarePaginator($this->get($columns), $total, $perPage, $page, [ + 'path' => Paginator::resolveCurrentPath(), + 'pageName' => $pageName, + ]); } /** - * Find a model by its primary key. + * Paginate the given query into a simple paginator. * - * @param mixed $id - * @param array|string $columns - * @return ($id is (\Illuminate\Contracts\Support\Arrayable|array) ? \Illuminate\Database\Eloquent\Collection : TModel|null) + * @param int $perPage + * @param array $columns + * @param string $pageName + * @return \Illuminate\Contracts\Pagination\Paginator */ - public function find($id, $columns = ['*']) + public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page') { - if (is_array($id) || $id instanceof Arrayable) { - return $this->findMany($id, $columns); - } + $page = Paginator::resolveCurrentPage($pageName); - return $this->whereKey($id)->first($columns); + $perPage = $perPage ?: $this->model->getPerPage(); + + $this->skip(($page - 1) * $perPage)->take($perPage + 1); + + return new Paginator($this->get($columns), $perPage, $page, [ + 'path' => Paginator::resolveCurrentPath(), + 'pageName' => $pageName, + ]); } /** - * Find a sole model by its primary key. - * - * @param mixed $id - * @param array|string $columns - * @return TModel + * Update a record in the database. * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - * @throws \Illuminate\Database\MultipleRecordsFoundException + * @param array $values + * @return int */ - public function findSole($id, $columns = ['*']) + public function update(array $values) { - return $this->whereKey($id)->sole($columns); + return $this->toBase()->update($this->addUpdatedAtColumn($values)); } /** - * Find multiple models by their primary keys. + * Increment a column's value by a given amount. * - * @param \Illuminate\Contracts\Support\Arrayable|array $ids - * @param array|string $columns - * @return \Illuminate\Database\Eloquent\Collection + * @param string $column + * @param int $amount + * @param array $extra + * @return int */ - public function findMany($ids, $columns = ['*']) + public function increment($column, $amount = 1, array $extra = []) { - $ids = $ids instanceof Arrayable ? $ids->toArray() : $ids; - - if (empty($ids)) { - return $this->model->newCollection(); - } + $extra = $this->addUpdatedAtColumn($extra); - return $this->whereKey($ids)->get($columns); + return $this->toBase()->increment($column, $amount, $extra); } /** - * Find a model by its primary key or throw an exception. - * - * @param mixed $id - * @param array|string $columns - * @return ($id is (\Illuminate\Contracts\Support\Arrayable|array) ? \Illuminate\Database\Eloquent\Collection : TModel) + * Decrement a column's value by a given amount. * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + * @param string $column + * @param int $amount + * @param array $extra + * @return int */ - public function findOrFail($id, $columns = ['*']) + public function decrement($column, $amount = 1, array $extra = []) { - $result = $this->find($id, $columns); + $extra = $this->addUpdatedAtColumn($extra); - $id = $id instanceof Arrayable ? $id->toArray() : $id; - - if (is_array($id)) { - if (count($result) !== count(array_unique($id))) { - throw (new ModelNotFoundException)->setModel( - get_class($this->model), array_diff($id, $result->modelKeys()) - ); - } - - return $result; - } - - if (is_null($result)) { - throw (new ModelNotFoundException)->setModel( - get_class($this->model), $id - ); - } - - return $result; - } - - /** - * Find a model by its primary key or return fresh model instance. - * - * @param mixed $id - * @param array|string $columns - * @return ($id is (\Illuminate\Contracts\Support\Arrayable|array) ? \Illuminate\Database\Eloquent\Collection : TModel) - */ - public function findOrNew($id, $columns = ['*']) - { - if (! is_null($model = $this->find($id, $columns))) { - return $model; - } - - return $this->newModelInstance(); - } - - /** - * Find a model by its primary key or call a callback. - * - * @template TValue - * - * @param mixed $id - * @param (\Closure(): TValue)|list|string $columns - * @param (\Closure(): TValue)|null $callback - * @return ( - * $id is (\Illuminate\Contracts\Support\Arrayable|array) - * ? \Illuminate\Database\Eloquent\Collection - * : TModel|TValue - * ) - */ - public function findOr($id, $columns = ['*'], ?Closure $callback = null) - { - if ($columns instanceof Closure) { - $callback = $columns; - - $columns = ['*']; - } - - if (! is_null($model = $this->find($id, $columns))) { - return $model; - } - - return $callback(); - } - - /** - * Get the first record matching the attributes or instantiate it. - * - * @param array $attributes - * @param array $values - * @return TModel - */ - public function firstOrNew(array $attributes = [], array $values = []) - { - if (! is_null($instance = $this->where($attributes)->first())) { - return $instance; - } - - return $this->newModelInstance(array_merge($attributes, $values)); - } - - /** - * Get the first record matching the attributes. If the record is not found, create it. - * - * @param array $attributes - * @param (\Closure(): array)|array $values - * @return TModel - */ - public function firstOrCreate(array $attributes = [], Closure|array $values = []) - { - if (! is_null($instance = (clone $this)->where($attributes)->first())) { - return $instance; - } - - return $this->createOrFirst($attributes, $values); - } - - /** - * Attempt to create the record. If a unique constraint violation occurs, attempt to find the matching record. - * - * @param array $attributes - * @param (\Closure(): array)|array $values - * @return TModel - * - * @throws \Illuminate\Database\UniqueConstraintViolationException - */ - public function createOrFirst(array $attributes = [], Closure|array $values = []) - { - try { - return $this->withSavepointIfNeeded(fn () => $this->create(array_merge($attributes, value($values)))); - } catch (UniqueConstraintViolationException $e) { - return $this->useWritePdo()->where($attributes)->first() ?? throw $e; - } - } + return $this->toBase()->decrement($column, $amount, $extra); + } /** - * Create or update a record matching the attributes, and fill it with values. + * Add the "updated at" column to an array of values. * - * @param array $attributes * @param array $values - * @return TModel - */ - public function updateOrCreate(array $attributes, array $values = []) - { - return tap($this->firstOrCreate($attributes, $values), function ($instance) use ($values) { - if (! $instance->wasRecentlyCreated) { - $instance->fill($values)->save(); - } - }); - } - - /** - * Create a record matching the attributes, or increment the existing record. - * - * @param array $attributes - * @param string $column - * @param int|float $default - * @param int|float $step - * @param array $extra - * @return TModel - */ - public function incrementOrCreate(array $attributes, string $column = 'count', $default = 1, $step = 1, array $extra = []) - { - return tap($this->firstOrCreate($attributes, [$column => $default]), function ($instance) use ($column, $step, $extra) { - if (! $instance->wasRecentlyCreated) { - $instance->increment($column, $step, $extra); - } - }); - } - - /** - * Execute the query and get the first result or throw an exception. - * - * @param array|string $columns - * @return TModel - * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - */ - public function firstOrFail($columns = ['*']) - { - if (! is_null($model = $this->first($columns))) { - return $model; - } - - throw (new ModelNotFoundException)->setModel(get_class($this->model)); - } - - /** - * Execute the query and get the first result or call a callback. - * - * @template TValue - * - * @param (\Closure(): TValue)|list $columns - * @param (\Closure(): TValue)|null $callback - * @return TModel|TValue + * @return array */ - public function firstOr($columns = ['*'], ?Closure $callback = null) + protected function addUpdatedAtColumn(array $values) { - if ($columns instanceof Closure) { - $callback = $columns; - - $columns = ['*']; - } - - if (! is_null($model = $this->first($columns))) { - return $model; + if (! $this->model->usesTimestamps()) { + return $values; } - return $callback(); - } + $column = $this->model->getUpdatedAtColumn(); - /** - * Execute the query and get the first result if it's the sole matching record. - * - * @param array|string $columns - * @return TModel - * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - * @throws \Illuminate\Database\MultipleRecordsFoundException - */ - public function sole($columns = ['*']) - { - try { - return $this->baseSole($columns); - } catch (RecordsNotFoundException) { - throw (new ModelNotFoundException)->setModel(get_class($this->model)); - } + return Arr::add($values, $column, $this->model->freshTimestampString()); } /** - * Get a single column's value from the first result of a query. + * Delete a record from the database. * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column * @return mixed */ - public function value($column) + public function delete() { - if ($result = $this->first([$column])) { - $column = $column instanceof Expression ? $column->getValue($this->getGrammar()) : $column; - - return $result->{Str::afterLast($column, '.')}; + if (isset($this->onDelete)) { + return call_user_func($this->onDelete, $this); } - } - - /** - * Get a single column's value from the first result of a query if it's the sole matching record. - * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @return mixed - * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - * @throws \Illuminate\Database\MultipleRecordsFoundException - */ - public function soleValue($column) - { - $column = $column instanceof Expression ? $column->getValue($this->getGrammar()) : $column; - return $this->sole([$column])->{Str::afterLast($column, '.')}; + return $this->toBase()->delete(); } /** - * Get a single column's value from the first result of the query or throw an exception. + * Run the default delete function on the builder. * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column * @return mixed - * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException */ - public function valueOrFail($column) + public function forceDelete() { - $column = $column instanceof Expression ? $column->getValue($this->getGrammar()) : $column; - - return $this->firstOrFail([$column])->{Str::afterLast($column, '.')}; + return $this->query->delete(); } /** - * Execute the query as a "select" statement. + * Register a replacement for the default delete function. * - * @param array|string $columns - * @return \Illuminate\Database\Eloquent\Collection + * @param \Closure $callback + * @return void */ - public function get($columns = ['*']) + public function onDelete(Closure $callback) { - $builder = $this->applyScopes(); - - // If we actually found models we will also eager load any relationships that - // have been specified as needing to be eager loaded, which will solve the - // n+1 query issue for the developers to avoid running a lot of queries. - if (count($models = $builder->getModels($columns)) > 0) { - $models = $builder->eagerLoadRelations($models); - } - - return $this->applyAfterQueryCallbacks( - $builder->getModel()->newCollection($models) - ); + $this->onDelete = $callback; } /** * Get the hydrated models without eager loading. * - * @param array|string $columns - * @return array + * @param array $columns + * @return \Illuminate\Database\Eloquent\Model[] */ public function getModels($columns = ['*']) { - return $this->model->hydrate( - $this->query->get($columns)->all() - )->all(); + $results = $this->query->get($columns)->all(); + + $connection = $this->model->getConnectionName(); + + return $this->model->hydrate($results, $connection)->all(); } /** * Eager load the relationships for the models. * - * @param array $models - * @return array + * @param array $models + * @return array */ public function eagerLoadRelations(array $models) { @@ -920,8 +610,8 @@ public function eagerLoadRelations(array $models) // For nested eager loads we'll skip loading them here and they will be set as an // eager load on the query to retrieve the relation so that they will be eager // loaded on that query, because that is where they get hydrated as models. - if (! str_contains($name, '.')) { - $models = $this->eagerLoadRelation($models, $name, $constraints); + if (strpos($name, '.') === false) { + $models = $this->loadRelation($models, $name, $constraints); } } @@ -936,7 +626,7 @@ public function eagerLoadRelations(array $models) * @param \Closure $constraints * @return array */ - protected function eagerLoadRelation(array $models, $name, Closure $constraints) + protected function loadRelation(array $models, $name, Closure $constraints) { // First we will "back up" the existing where conditions on the query so we can // add our eager constraints. Then we will merge the wheres that were on the @@ -945,37 +635,34 @@ protected function eagerLoadRelation(array $models, $name, Closure $constraints) $relation->addEagerConstraints($models); - $constraints($relation); + call_user_func($constraints, $relation); + + $models = $relation->initRelation($models, $name); // Once we have the results, we just match those back up to their parent models // using the relationship instance. Then we just return the finished arrays // of models which have been eagerly hydrated and are readied for return. - return $relation->match( - $relation->initRelation($models, $name), - $relation->getEager(), $name - ); + $results = $relation->getEager(); + + return $relation->match($models, $results, $name); } /** * Get the relation instance for the given relation name. * * @param string $name - * @return \Illuminate\Database\Eloquent\Relations\Relation<\Illuminate\Database\Eloquent\Model, TModel, *> + * @return \Illuminate\Database\Eloquent\Relations\Relation */ public function getRelation($name) { // We want to run a relationship query without any constrains so that we will // not have to remove these where clauses manually which gets really hacky - // and error prone. We don't want constraints because we add eager ones. + // and is error prone while we remove the developer's own where clauses. $relation = Relation::noConstraints(function () use ($name) { - try { - return $this->getModel()->newInstance()->$name(); - } catch (BadMethodCallException) { - throw RelationNotFoundException::make($this->getModel(), $name); - } + return $this->getModel()->$name(); }); - $nested = $this->relationsNestedUnder($name); + $nested = $this->nestedRelations($name); // If there are nested relationships set on the query, we will put those onto // the query instances so that they can be handled after this relationship @@ -993,7 +680,7 @@ public function getRelation($name) * @param string $relation * @return array */ - protected function relationsNestedUnder($relation) + protected function nestedRelations($relation) { $nested = []; @@ -1001,7 +688,7 @@ protected function relationsNestedUnder($relation) // the given top-level relationship. We will just check for any relations // that start with the given top relations and adds them to our arrays. foreach ($this->eagerLoad as $name => $constraints) { - if ($this->isNestedUnder($relation, $name)) { + if ($this->isNested($name, $relation)) { $nested[substr($name, strlen($relation.'.'))] = $constraints; } } @@ -1012,544 +699,382 @@ protected function relationsNestedUnder($relation) /** * Determine if the relationship is nested. * - * @param string $relation * @param string $name + * @param string $relation * @return bool */ - protected function isNestedUnder($relation, $name) + protected function isNested($name, $relation) { - return str_contains($name, '.') && str_starts_with($name, $relation.'.'); + $dots = Str::contains($name, '.'); + + return $dots && Str::startsWith($name, $relation.'.'); } /** - * Register a closure to be invoked after the query is executed. + * Add a basic where clause to the query. * - * @param \Closure $callback + * @param string $column + * @param string $operator + * @param mixed $value + * @param string $boolean * @return $this */ - public function afterQuery(Closure $callback) + public function where($column, $operator = null, $value = null, $boolean = 'and') { - $this->afterQueryCallbacks[] = $callback; + if ($column instanceof Closure) { + $query = $this->model->newQueryWithoutScopes(); - return $this; - } + call_user_func($column, $query); - /** - * Invoke the "after query" modification callbacks. - * - * @param mixed $result - * @return mixed - */ - public function applyAfterQueryCallbacks($result) - { - foreach ($this->afterQueryCallbacks as $afterQueryCallback) { - $result = $afterQueryCallback($result) ?: $result; + $this->query->addNestedWhereQuery($query->getQuery(), $boolean); + } else { + call_user_func_array([$this->query, 'where'], func_get_args()); } - return $result; + return $this; } /** - * Get a lazy collection for the given query. + * Add an "or where" clause to the query. * - * @return \Illuminate\Support\LazyCollection + * @param string $column + * @param string $operator + * @param mixed $value + * @return \Illuminate\Database\Eloquent\Builder|static */ - public function cursor() + public function orWhere($column, $operator = null, $value = null) { - return $this->applyScopes()->query->cursor()->map(function ($record) { - $model = $this->newModelInstance()->newFromBuilder($record); - - return $this->applyAfterQueryCallbacks($this->newModelInstance()->newCollection([$model]))->first(); - })->reject(fn ($model) => is_null($model)); + return $this->where($column, $operator, $value, 'or'); } /** - * Add a generic "order by" clause if the query doesn't already have one. + * Add a relationship count / exists condition to the query. * - * @return void + * @param string $relation + * @param string $operator + * @param int $count + * @param string $boolean + * @param \Closure|null $callback + * @return \Illuminate\Database\Eloquent\Builder|static */ - protected function enforceOrderBy() + public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', Closure $callback = null) { - if (empty($this->query->orders) && empty($this->query->unionOrders)) { - $this->orderBy($this->model->getQualifiedKeyName(), 'asc'); + if (strpos($relation, '.') !== false) { + return $this->hasNested($relation, $operator, $count, $boolean, $callback); } - } - /** - * Get a collection with the values of a given column. - * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param string|null $key - * @return \Illuminate\Support\Collection - */ - public function pluck($column, $key = null) - { - $results = $this->toBase()->pluck($column, $key); + $relation = $this->getHasRelationQuery($relation); - $column = $column instanceof Expression ? $column->getValue($this->getGrammar()) : $column; + // If we only need to check for the existence of the relation, then we can + // optimize the subquery to only run a "where exists" clause instead of + // the full "count" clause. This will make the query run much faster. + $queryType = $this->shouldRunExistsQuery($operator, $count) + ? 'getRelationQuery' : 'getRelationCountQuery'; - $column = Str::after($column, "{$this->model->getTable()}."); + $query = $relation->{$queryType}($relation->getRelated()->newQuery(), $this); - // If the model has a mutator for the requested column, we will spin through - // the results and mutate the values so that the mutated version of these - // columns are returned as you would expect from these Eloquent models. - if (! $this->model->hasAnyGetMutator($column) && - ! $this->model->hasCast($column) && - ! in_array($column, $this->model->getDates())) { - return $this->applyAfterQueryCallbacks($results); + if ($callback) { + call_user_func($callback, $query); } - return $this->applyAfterQueryCallbacks( - $results->map(function ($value) use ($column) { - return $this->model->newFromBuilder([$column => $value])->{$column}; - }) + return $this->addHasWhere( + $query, $relation, $operator, $count, $boolean ); } /** - * Paginate the given query. - * - * @param int|null|\Closure $perPage - * @param array|string $columns - * @param string $pageName - * @param int|null $page - * @param \Closure|int|null $total - * @return \Illuminate\Pagination\LengthAwarePaginator + * Add nested relationship count / exists conditions to the query. * - * @throws \InvalidArgumentException + * @param string $relations + * @param string $operator + * @param int $count + * @param string $boolean + * @param \Closure|null $callback + * @return \Illuminate\Database\Eloquent\Builder|static */ - public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null, $total = null) + protected function hasNested($relations, $operator = '>=', $count = 1, $boolean = 'and', $callback = null) { - $page = $page ?: Paginator::resolveCurrentPage($pageName); - - $total = value($total) ?? $this->toBase()->getCountForPagination(); - - $perPage = value($perPage, $total) ?: $this->model->getPerPage(); + $relations = explode('.', $relations); - $results = $total - ? $this->forPage($page, $perPage)->get($columns) - : $this->model->newCollection(); + // In order to nest "has", we need to add count relation constraints on the + // callback Closure. We'll do this by simply passing the Closure its own + // reference to itself so it calls itself recursively on each segment. + $closure = function ($q) use (&$closure, &$relations, $operator, $count, $boolean, $callback) { + if (count($relations) > 1) { + $q->whereHas(array_shift($relations), $closure); + } else { + $q->has(array_shift($relations), $operator, $count, 'and', $callback); + } + }; - return $this->paginator($results, $total, $perPage, $page, [ - 'path' => Paginator::resolveCurrentPath(), - 'pageName' => $pageName, - ]); + return $this->has(array_shift($relations), '>=', 1, $boolean, $closure); } /** - * Paginate the given query into a simple paginator. + * Add a relationship count / exists condition to the query. * - * @param int|null $perPage - * @param array|string $columns - * @param string $pageName - * @param int|null $page - * @return \Illuminate\Contracts\Pagination\Paginator + * @param string $relation + * @param string $boolean + * @param \Closure|null $callback + * @return \Illuminate\Database\Eloquent\Builder|static */ - public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) + public function doesntHave($relation, $boolean = 'and', Closure $callback = null) { - $page = $page ?: Paginator::resolveCurrentPage($pageName); - - $perPage = $perPage ?: $this->model->getPerPage(); - - // Next we will set the limit and offset for this query so that when we get the - // results we get the proper section of results. Then, we'll create the full - // paginator instances for these results with the given page and per page. - $this->offset(($page - 1) * $perPage)->limit($perPage + 1); - - return $this->simplePaginator($this->get($columns), $perPage, $page, [ - 'path' => Paginator::resolveCurrentPath(), - 'pageName' => $pageName, - ]); + return $this->has($relation, '<', 1, $boolean, $callback); } /** - * Paginate the given query into a cursor paginator. + * Add a relationship count / exists condition to the query with where clauses. * - * @param int|null $perPage - * @param array|string $columns - * @param string $cursorName - * @param \Illuminate\Pagination\Cursor|string|null $cursor - * @return \Illuminate\Contracts\Pagination\CursorPaginator + * @param string $relation + * @param \Closure $callback + * @param string $operator + * @param int $count + * @return \Illuminate\Database\Eloquent\Builder|static */ - public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null) + public function whereHas($relation, Closure $callback, $operator = '>=', $count = 1) { - $perPage = $perPage ?: $this->model->getPerPage(); - - return $this->paginateUsingCursor($perPage, $columns, $cursorName, $cursor); + return $this->has($relation, $operator, $count, 'and', $callback); } /** - * Ensure the proper order by required for cursor pagination. + * Add a relationship count / exists condition to the query with where clauses. * - * @param bool $shouldReverse - * @return \Illuminate\Support\Collection + * @param string $relation + * @param \Closure|null $callback + * @return \Illuminate\Database\Eloquent\Builder|static */ - protected function ensureOrderForCursorPagination($shouldReverse = false) + public function whereDoesntHave($relation, Closure $callback = null) { - if (empty($this->query->orders) && empty($this->query->unionOrders)) { - $this->enforceOrderBy(); - } - - $reverseDirection = function ($order) { - if (! isset($order['direction'])) { - return $order; - } - - $order['direction'] = $order['direction'] === 'asc' ? 'desc' : 'asc'; - - return $order; - }; - - if ($shouldReverse) { - $this->query->orders = (new BaseCollection($this->query->orders))->map($reverseDirection)->toArray(); - $this->query->unionOrders = (new BaseCollection($this->query->unionOrders))->map($reverseDirection)->toArray(); - } - - $orders = ! empty($this->query->unionOrders) ? $this->query->unionOrders : $this->query->orders; - - return (new BaseCollection($orders)) - ->filter(fn ($order) => Arr::has($order, 'direction')) - ->values(); + return $this->doesntHave($relation, 'and', $callback); } /** - * Save a new model and return the instance. + * Add a relationship count / exists condition to the query with an "or". * - * @param array $attributes - * @return TModel + * @param string $relation + * @param string $operator + * @param int $count + * @return \Illuminate\Database\Eloquent\Builder|static */ - public function create(array $attributes = []) + public function orHas($relation, $operator = '>=', $count = 1) { - return tap($this->newModelInstance($attributes), function ($instance) { - $instance->save(); - }); + return $this->has($relation, $operator, $count, 'or'); } /** - * Save a new model and return the instance without raising model events. + * Add a relationship count / exists condition to the query with where clauses and an "or". * - * @param array $attributes - * @return TModel + * @param string $relation + * @param \Closure $callback + * @param string $operator + * @param int $count + * @return \Illuminate\Database\Eloquent\Builder|static */ - public function createQuietly(array $attributes = []) + public function orWhereHas($relation, Closure $callback, $operator = '>=', $count = 1) { - return Model::withoutEvents(fn () => $this->create($attributes)); + return $this->has($relation, $operator, $count, 'or', $callback); } /** - * Save a new model and return the instance. Allow mass-assignment. + * Add the "has" condition where clause to the query. * - * @param array $attributes - * @return TModel + * @param \Illuminate\Database\Eloquent\Builder $hasQuery + * @param \Illuminate\Database\Eloquent\Relations\Relation $relation + * @param string $operator + * @param int $count + * @param string $boolean + * @return \Illuminate\Database\Eloquent\Builder */ - public function forceCreate(array $attributes) + protected function addHasWhere(Builder $hasQuery, Relation $relation, $operator, $count, $boolean) { - return $this->model->unguarded(function () use ($attributes) { - return $this->newModelInstance()->create($attributes); - }); - } + $this->mergeModelDefinedRelationWheresToHasQuery($hasQuery, $relation); - /** - * Save a new model instance with mass assignment without raising model events. - * - * @param array $attributes - * @return TModel - */ - public function forceCreateQuietly(array $attributes = []) - { - return Model::withoutEvents(fn () => $this->forceCreate($attributes)); + if ($this->shouldRunExistsQuery($operator, $count)) { + $not = ($operator === '<' && $count === 1); + + return $this->addWhereExistsQuery($hasQuery->toBase(), $boolean, $not); + } + + return $this->whereCountQuery($hasQuery->toBase(), $operator, $count, $boolean); } /** - * Update records in the database. + * Check if we can run an "exists" query to optimize performance. * - * @param array $values - * @return int + * @param string $operator + * @param int $count + * @return bool */ - public function update(array $values) + protected function shouldRunExistsQuery($operator, $count) { - return $this->toBase()->update($this->addUpdatedAtColumn($values)); + return ($operator === '>=' || $operator === '<') && $count === 1; } /** - * Insert new records or update the existing ones. + * Add a sub query count clause to the query. * - * @param array $values - * @param array|string $uniqueBy - * @param array|null $update - * @return int + * @param \Illuminate\Database\Query\Builder $query + * @param string $operator + * @param int $count + * @param string $boolean + * @return $this */ - public function upsert(array $values, $uniqueBy, $update = null) + protected function whereCountQuery(QueryBuilder $query, $operator = '>=', $count = 1, $boolean = 'and') { - if (empty($values)) { - return 0; + if (is_numeric($count)) { + $count = new Expression($count); } - if (! is_array(array_first($values))) { - $values = [$values]; - } - - if (is_null($update)) { - $update = array_keys(array_first($values)); - } + $this->query->addBinding($query->getBindings(), 'where'); - return $this->toBase()->upsert( - $this->addTimestampsToUpsertValues($this->addUniqueIdsToUpsertValues($values)), - $uniqueBy, - $this->addUpdatedAtToUpsertColumns($update) - ); + return $this->where(new Expression('('.$query->toSql().')'), $operator, $count, $boolean); } /** - * Update the column's update timestamp. + * Merge the "wheres" from a relation query to a has query. * - * @param string|null $column - * @return int|false + * @param \Illuminate\Database\Eloquent\Builder $hasQuery + * @param \Illuminate\Database\Eloquent\Relations\Relation $relation + * @return void */ - public function touch($column = null) + protected function mergeModelDefinedRelationWheresToHasQuery(Builder $hasQuery, Relation $relation) { - $time = $this->model->freshTimestamp(); - - if ($column) { - return $this->toBase()->update([$column => $time]); - } - - $column = $this->model->getUpdatedAtColumn(); - - if (! $this->model->usesTimestamps() || is_null($column)) { - return false; - } + $removedScopes = $hasQuery->removedScopes(); - return $this->toBase()->update([$column => $time]); - } + $relationQuery = $relation->withoutGlobalScopes($removedScopes)->toBase(); - /** - * Increment a column's value by a given amount. - * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param float|int $amount - * @param array $extra - * @return int - */ - public function increment($column, $amount = 1, array $extra = []) - { - return $this->toBase()->increment( - $column, $amount, $this->addUpdatedAtColumn($extra) + // Here we have the "has" query and the original relation. We need to copy over any + // where clauses the developer may have put in the relationship function over to + // the has query, and then copy the bindings from the "has" query to the main. + $hasQuery->withoutGlobalScopes()->mergeWheres( + $relationQuery->wheres, $relationQuery->getBindings() ); } /** - * Decrement a column's value by a given amount. + * Get the "has relation" base query instance. * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param float|int $amount - * @param array $extra - * @return int + * @param string $relation + * @return \Illuminate\Database\Eloquent\Relations\Relation */ - public function decrement($column, $amount = 1, array $extra = []) + protected function getHasRelationQuery($relation) { - return $this->toBase()->decrement( - $column, $amount, $this->addUpdatedAtColumn($extra) - ); + return Relation::noConstraints(function () use ($relation) { + return $this->getModel()->$relation(); + }); } /** - * Add the "updated at" column to an array of values. + * Set the relationships that should be eager loaded. * - * @param array $values - * @return array + * @param mixed $relations + * @return $this */ - protected function addUpdatedAtColumn(array $values) + public function with($relations) { - if (! $this->model->usesTimestamps() || - is_null($this->model->getUpdatedAtColumn())) { - return $values; - } - - $column = $this->model->getUpdatedAtColumn(); - - if (! array_key_exists($column, $values)) { - $timestamp = $this->model->freshTimestampString(); - - if ( - $this->model->hasSetMutator($column) - || $this->model->hasAttributeSetMutator($column) - || $this->model->hasCast($column) - ) { - $timestamp = $this->model->newInstance() - ->forceFill([$column => $timestamp]) - ->getAttributes()[$column] ?? $timestamp; - } - - $values = array_merge([$column => $timestamp], $values); + if (is_string($relations)) { + $relations = func_get_args(); } - $segments = preg_split('/\s+as\s+/i', $this->query->from); + $eagers = $this->parseWithRelations($relations); - $qualifiedColumn = array_last($segments).'.'.$column; + $this->eagerLoad = array_merge($this->eagerLoad, $eagers); - $values[$qualifiedColumn] = Arr::get($values, $qualifiedColumn, $values[$column]); - - unset($values[$column]); - - return $values; + return $this; } /** - * Add unique IDs to the inserted values. + * Parse a list of relations into individuals. * - * @param array $values + * @param array $relations * @return array */ - protected function addUniqueIdsToUpsertValues(array $values) + protected function parseWithRelations(array $relations) { - if (! $this->model->usesUniqueIds()) { - return $values; - } - - foreach ($this->model->uniqueIds() as $uniqueIdAttribute) { - foreach ($values as &$row) { - if (! array_key_exists($uniqueIdAttribute, $row)) { - $row = array_merge([$uniqueIdAttribute => $this->model->newUniqueId()], $row); - } - } - } - - return $values; - } + $results = []; - /** - * Add timestamps to the inserted values. - * - * @param array $values - * @return array - */ - protected function addTimestampsToUpsertValues(array $values) - { - if (! $this->model->usesTimestamps()) { - return $values; - } + foreach ($relations as $name => $constraints) { + // If the "relation" value is actually a numeric key, we can assume that no + // constraints have been specified for the eager load and we'll just put + // an empty Closure with the loader so that we can treat all the same. + if (is_numeric($name)) { + $f = function () { + // + }; - $timestamp = $this->model->freshTimestampString(); + list($name, $constraints) = [$constraints, $f]; + } - $columns = array_filter([ - $this->model->getCreatedAtColumn(), - $this->model->getUpdatedAtColumn(), - ]); + // We need to separate out any nested includes. Which allows the developers + // to load deep relationships using "dots" without stating each level of + // the relationship with its own key in the array of eager load names. + $results = $this->parseNestedWith($name, $results); - foreach ($columns as $column) { - foreach ($values as &$row) { - $row = array_merge([$column => $timestamp], $row); - } + $results[$name] = $constraints; } - return $values; + return $results; } /** - * Add the "updated at" column to the updated columns. + * Parse the nested relationships in a relation. * - * @param array $update + * @param string $name + * @param array $results * @return array */ - protected function addUpdatedAtToUpsertColumns(array $update) + protected function parseNestedWith($name, $results) { - if (! $this->model->usesTimestamps()) { - return $update; - } - - $column = $this->model->getUpdatedAtColumn(); - - if (! is_null($column) && - ! array_key_exists($column, $update) && - ! in_array($column, $update)) { - $update[] = $column; - } + $progress = []; - return $update; - } + // If the relation has already been set on the result array, we will not set it + // again, since that would override any constraints that were already placed + // on the relationships. We will only set the ones that are not specified. + foreach (explode('.', $name) as $segment) { + $progress[] = $segment; - /** - * Delete records from the database. - * - * @return mixed - */ - public function delete() - { - if (isset($this->onDelete)) { - return call_user_func($this->onDelete, $this); + if (! isset($results[$last = implode('.', $progress)])) { + $results[$last] = function () { + // + }; + } } - return $this->toBase()->delete(); - } - - /** - * Run the default delete function on the builder. - * - * Since we do not apply scopes here, the row will actually be deleted. - * - * @return mixed - */ - public function forceDelete() - { - return $this->query->delete(); - } - - /** - * Register a replacement for the default delete function. - * - * @param \Closure $callback - * @return void - */ - public function onDelete(Closure $callback) - { - $this->onDelete = $callback; + return $results; } /** - * Determine if the given model has a scope. + * Call the given model scope on the underlying model. * * @param string $scope - * @return bool + * @param array $parameters + * @return \Illuminate\Database\Query\Builder */ - public function hasNamedScope($scope) + protected function callScope($scope, $parameters) { - return $this->model && $this->model->hasNamedScope($scope); - } + array_unshift($parameters, $this); - /** - * Call the given local model scopes. - * - * @param array|string $scopes - * @return static|mixed - */ - public function scopes($scopes) - { - $builder = $this; + $query = $this->getQuery(); - foreach (Arr::wrap($scopes) as $scope => $parameters) { - // If the scope key is an integer, then the scope was passed as the value and - // the parameter list is empty, so we will format the scope name and these - // parameters here. Then, we'll be ready to call the scope on the model. - if (is_int($scope)) { - [$scope, $parameters] = [$parameters, []]; - } + // We will keep track of how many wheres are on the query before running the + // scope so that we can properly group the added scope constraints in the + // query as their own isolated nested where statement and avoid issues. + $originalWhereCount = count($query->wheres); - // Next we'll pass the scope callback to the callScope method which will take - // care of grouping the "wheres" properly so the logical order doesn't get - // messed up when adding scopes. Then we'll return back out the builder. - $builder = $builder->callNamedScope( - $scope, Arr::wrap($parameters) - ); + $result = call_user_func_array([$this->model, $scope], $parameters) ?: $this; + + if ($this->shouldNestWheresForScope($query, $originalWhereCount)) { + $this->nestWheresForScope($query, $originalWhereCount); } - return $builder; + return $result; } /** * Apply the scopes to the Eloquent builder instance and return it. * - * @return static + * @return \Illuminate\Database\Eloquent\Builder|static */ public function applyScopes() { @@ -1559,82 +1084,67 @@ public function applyScopes() $builder = clone $this; - foreach ($this->scopes as $identifier => $scope) { - if (! isset($builder->scopes[$identifier])) { - continue; - } + $query = $builder->getQuery(); - $builder->callScope(function (self $builder) use ($scope) { - // If the scope is a Closure we will just go ahead and call the scope with the - // builder instance. The "callScope" method will properly group the clauses - // that are added to this query so "where" clauses maintain proper logic. - if ($scope instanceof Closure) { - $scope($builder); - } + // We will keep track of how many wheres are on the query before running the + // scope so that we can properly group the added scope constraints in the + // query as their own isolated nested where statement and avoid issues. + $originalWhereCount = count($query->wheres); - // If the scope is a scope object, we will call the apply method on this scope - // passing in the builder and the model instance. After we run all of these - // scopes we will return back the builder instance to the outside caller. - if ($scope instanceof Scope) { - $scope->apply($builder, $this->getModel()); - } - }); + $whereCounts = [$originalWhereCount]; + + foreach ($this->scopes as $scope) { + $this->applyScope($scope, $builder); + + // Again, we will keep track of the count each time we add where clauses so that + // we will properly isolate each set of scope constraints inside of their own + // nested where clause to avoid any conflicts or issues with logical order. + $whereCounts[] = count($query->wheres); + } + + if ($this->shouldNestWheresForScope($query, $originalWhereCount)) { + $this->nestWheresForScope($query, $whereCounts); } return $builder; } /** - * Apply the given scope on the current builder instance. + * Apply a single scope on the given builder instance. * - * @param callable $scope - * @param array $parameters - * @return mixed + * @param \Illuminate\Database\Eloquent\Scope|\Closure $scope + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return void */ - protected function callScope(callable $scope, array $parameters = []) + protected function applyScope($scope, $builder) { - array_unshift($parameters, $this); - - $query = $this->getQuery(); - - // We will keep track of how many wheres are on the query before running the - // scope so that we can properly group the added scope constraints in the - // query as their own isolated nested where statement and avoid issues. - $originalWhereCount = is_null($query->wheres) - ? 0 - : count($query->wheres); - - $result = $scope(...$parameters) ?? $this; - - if (count((array) $query->wheres) > $originalWhereCount) { - $this->addNewWheresWithinGroup($query, $originalWhereCount); + if ($scope instanceof Closure) { + $scope($builder); + } elseif ($scope instanceof Scope) { + $scope->apply($builder, $this->getModel()); } - - return $result; } /** - * Apply the given named scope on the current builder instance. + * Determine if the scope added after the given offset should be nested. * - * @param string $scope - * @param array $parameters - * @return mixed + * @param \Illuminate\Database\Query\Builder $query + * @param int $originalWhereCount + * @return bool */ - protected function callNamedScope($scope, array $parameters = []) + protected function shouldNestWheresForScope(QueryBuilder $query, $originalWhereCount) { - return $this->callScope(function (...$parameters) use ($scope) { - return $this->model->callNamedScope($scope, $parameters); - }, $parameters); + return count($query->wheres) > $originalWhereCount; } /** * Nest where conditions by slicing them at the given where count. * * @param \Illuminate\Database\Query\Builder $query - * @param int $originalWhereCount + * @param int|array $whereCounts * @return void */ - protected function addNewWheresWithinGroup(QueryBuilder $query, $originalWhereCount) + protected function nestWheresForScope(QueryBuilder $query, $whereCounts) { // Here, we totally remove all of the where clauses since we are going to // rebuild them as nested queries by slicing the groups of wheres into @@ -1643,33 +1153,43 @@ protected function addNewWheresWithinGroup(QueryBuilder $query, $originalWhereCo $query->wheres = []; - $this->groupWhereSliceForScope( - $query, array_slice($allWheres, 0, $originalWhereCount) - ); + // We will construct where offsets by adding the outer most offsets to the + // collection (0 and total where count) while also flattening the array + // and extracting unique values, ensuring that all wheres are sliced. + $whereOffsets = collect([0, $whereCounts, count($allWheres)]) + ->flatten()->unique(); - $this->groupWhereSliceForScope( - $query, array_slice($allWheres, $originalWhereCount) - ); + $sliceFrom = $whereOffsets->shift(); + + foreach ($whereOffsets as $sliceTo) { + $this->sliceWhereConditions( + $query, $allWheres, $sliceFrom, $sliceTo + ); + + $sliceFrom = $sliceTo; + } } /** - * Slice where conditions at the given offset and add them to the query as a nested condition. + * Create a slice of where conditions at the given offsets and nest them if needed. * * @param \Illuminate\Database\Query\Builder $query - * @param array $whereSlice + * @param array $wheres + * @param int $sliceFrom + * @param int $sliceTo * @return void */ - protected function groupWhereSliceForScope(QueryBuilder $query, $whereSlice) + protected function sliceWhereConditions(QueryBuilder $query, array $wheres, $sliceFrom, $sliceTo) { - $whereBooleans = (new BaseCollection($whereSlice))->pluck('boolean'); + $whereSlice = array_slice($wheres, $sliceFrom, $sliceTo - $sliceFrom); + + $whereBooleans = collect($whereSlice)->pluck('boolean'); // Here we'll check if the given subset of where clauses contains any "or" // booleans and in this case create a nested where expression. That way // we don't add any unnecessary nesting thus keeping the query clean. - if ($whereBooleans->contains(fn ($logicalOperator) => str_contains($logicalOperator, 'or'))) { - $query->wheres[] = $this->createNestedWhere( - $whereSlice, str_replace(' not', '', $whereBooleans->first()) - ); + if ($whereBooleans->contains('or')) { + $query->wheres[] = $this->nestWhereSlice($whereSlice, $whereBooleans->first()); } else { $query->wheres = array_merge($query->wheres, $whereSlice); } @@ -1682,7 +1202,7 @@ protected function groupWhereSliceForScope(QueryBuilder $query, $whereSlice) * @param string $boolean * @return array */ - protected function createNestedWhere($whereSlice, $boolean = 'and') + protected function nestWhereSlice($whereSlice, $boolean = 'and') { $whereGroup = $this->getQuery()->forNestedWhere(); @@ -1692,416 +1212,76 @@ protected function createNestedWhere($whereSlice, $boolean = 'and') } /** - * Specify relationships that should be eager loaded. + * Get the underlying query builder instance. * - * @param array): mixed)|string>|string $relations - * @param (\Closure(\Illuminate\Database\Eloquent\Relations\Relation<*,*,*>): mixed)|string|null $callback - * @return $this + * @return \Illuminate\Database\Query\Builder|static */ - public function with($relations, $callback = null) + public function getQuery() { - if ($callback instanceof Closure) { - $eagerLoad = $this->parseWithRelations([$relations => $callback]); - } else { - $eagerLoad = $this->parseWithRelations(is_string($relations) ? func_get_args() : $relations); - } - - $this->eagerLoad = array_merge($this->eagerLoad, $eagerLoad); - - return $this; + return $this->query; } /** - * Prevent the specified relations from being eager loaded. + * Get a base query builder instance. * - * @param mixed $relations - * @return $this + * @return \Illuminate\Database\Query\Builder */ - public function without($relations) + public function toBase() { - $this->eagerLoad = array_diff_key($this->eagerLoad, array_flip( - is_string($relations) ? func_get_args() : $relations - )); - - return $this; + return $this->applyScopes()->getQuery(); } /** - * Set the relationships that should be eager loaded while removing any previously added eager loading specifications. + * Set the underlying query builder instance. * - * @param array): mixed)|string>|string $relations + * @param \Illuminate\Database\Query\Builder $query * @return $this */ - public function withOnly($relations) + public function setQuery($query) { - $this->eagerLoad = []; + $this->query = $query; - return $this->with($relations); + return $this; } /** - * Create a new instance of the model being queried. + * Get the relationships being eagerly loaded. * - * @param array $attributes - * @return TModel + * @return array */ - public function newModelInstance($attributes = []) + public function getEagerLoads() { - $attributes = array_merge($this->pendingAttributes, $attributes); - - return $this->model->newInstance($attributes)->setConnection( - $this->query->getConnection()->getName() - ); + return $this->eagerLoad; } /** - * Parse a list of relations into individuals. + * Set the relationships being eagerly loaded. * - * @param array $relations - * @return array + * @param array $eagerLoad + * @return $this */ - protected function parseWithRelations(array $relations) + public function setEagerLoads(array $eagerLoad) { - if ($relations === []) { - return []; - } - - $results = []; - - foreach ($this->prepareNestedWithRelationships($relations) as $name => $constraints) { - // We need to separate out any nested includes, which allows the developers - // to load deep relationships using "dots" without stating each level of - // the relationship with its own key in the array of eager-load names. - $results = $this->addNestedWiths($name, $results); - - $results[$name] = $constraints; - } + $this->eagerLoad = $eagerLoad; - return $results; + return $this; } /** - * Prepare nested with relationships. + * Get the model instance being queried. * - * @param array $relations - * @param string $prefix - * @return array + * @return \Illuminate\Database\Eloquent\Model */ - protected function prepareNestedWithRelationships($relations, $prefix = '') + public function getModel() { - $preparedRelationships = []; - - if ($prefix !== '') { - $prefix .= '.'; - } - - // If any of the relationships are formatted with the [$attribute => array()] - // syntax, we shall loop over the nested relations and prepend each key of - // this array while flattening into the traditional dot notation format. - foreach ($relations as $key => $value) { - if (! is_string($key) || ! is_array($value)) { - continue; - } - - [$attribute, $attributeSelectConstraint] = $this->parseNameAndAttributeSelectionConstraint($key); - - $preparedRelationships = array_merge( - $preparedRelationships, - ["{$prefix}{$attribute}" => $attributeSelectConstraint], - $this->prepareNestedWithRelationships($value, "{$prefix}{$attribute}"), - ); - - unset($relations[$key]); - } - - // We now know that the remaining relationships are in a dot notation format - // and may be a string or Closure. We'll loop over them and ensure all of - // the present Closures are merged + strings are made into constraints. - foreach ($relations as $key => $value) { - if (is_numeric($key) && is_string($value)) { - [$key, $value] = $this->parseNameAndAttributeSelectionConstraint($value); - } - - $preparedRelationships[$prefix.$key] = $this->combineConstraints([ - $value, - $preparedRelationships[$prefix.$key] ?? static function () { - // - }, - ]); - } - - return $preparedRelationships; - } - - /** - * Combine an array of constraints into a single constraint. - * - * @param array $constraints - * @return \Closure - */ - protected function combineConstraints(array $constraints) - { - return function ($builder) use ($constraints) { - foreach ($constraints as $constraint) { - $builder = $constraint($builder) ?? $builder; - } - - return $builder; - }; - } - - /** - * Parse the attribute select constraints from the name. - * - * @param string $name - * @return array - */ - protected function parseNameAndAttributeSelectionConstraint($name) - { - return str_contains($name, ':') - ? $this->createSelectWithConstraint($name) - : [$name, static function () { - // - }]; - } - - /** - * Create a constraint to select the given columns for the relation. - * - * @param string $name - * @return array - */ - protected function createSelectWithConstraint($name) - { - return [explode(':', $name)[0], static function ($query) use ($name) { - $query->select(array_map(static function ($column) use ($query) { - return $query instanceof BelongsToMany - ? $query->getRelated()->qualifyColumn($column) - : $column; - }, explode(',', explode(':', $name)[1]))); - }]; - } - - /** - * Parse the nested relationships in a relation. - * - * @param string $name - * @param array $results - * @return array - */ - protected function addNestedWiths($name, $results) - { - $progress = []; - - // If the relation has already been set on the result array, we will not set it - // again, since that would override any constraints that were already placed - // on the relationships. We will only set the ones that are not specified. - foreach (explode('.', $name) as $segment) { - $progress[] = $segment; - - if (! isset($results[$last = implode('.', $progress)])) { - $results[$last] = static function () { - // - }; - } - } - - return $results; - } - - /** - * Specify attributes that should be added to any new models created by this builder. - * - * The given key / value pairs will also be added as where conditions to the query. - * - * @param \Illuminate\Contracts\Database\Query\Expression|array|string $attributes - * @param mixed $value - * @param bool $asConditions - * @return $this - */ - public function withAttributes(Expression|array|string $attributes, $value = null, $asConditions = true) - { - if (! is_array($attributes)) { - $attributes = [$attributes => $value]; - } - - if ($asConditions) { - foreach ($attributes as $column => $value) { - $this->where($this->qualifyColumn($column), $value); - } - } - - $this->pendingAttributes = array_merge($this->pendingAttributes, $attributes); - - return $this; - } - - /** - * Apply query-time casts to the model instance. - * - * @param array $casts - * @return $this - */ - public function withCasts($casts) - { - $this->model->mergeCasts($casts); - - return $this; - } - - /** - * Execute the given Closure within a transaction savepoint if needed. - * - * @template TModelValue - * - * @param \Closure(): TModelValue $scope - * @return TModelValue - */ - public function withSavepointIfNeeded(Closure $scope): mixed - { - return $this->getQuery()->getConnection()->transactionLevel() > 0 - ? $this->getQuery()->getConnection()->transaction($scope) - : $scope(); - } - - /** - * Get the Eloquent builder instances that are used in the union of the query. - * - * @return \Illuminate\Support\Collection - */ - protected function getUnionBuilders() - { - return isset($this->query->unions) - ? (new BaseCollection($this->query->unions))->pluck('query') - : new BaseCollection; - } - - /** - * Get the underlying query builder instance. - * - * @return \Illuminate\Database\Query\Builder - */ - public function getQuery() - { - return $this->query; - } - - /** - * Set the underlying query builder instance. - * - * @param \Illuminate\Database\Query\Builder $query - * @return $this - */ - public function setQuery($query) - { - $this->query = $query; - - return $this; - } - - /** - * Get a base query builder instance. - * - * @return \Illuminate\Database\Query\Builder - */ - public function toBase() - { - return $this->applyScopes()->getQuery(); - } - - /** - * Get the relationships being eagerly loaded. - * - * @return array - */ - public function getEagerLoads() - { - return $this->eagerLoad; - } - - /** - * Set the relationships being eagerly loaded. - * - * @param array $eagerLoad - * @return $this - */ - public function setEagerLoads(array $eagerLoad) - { - $this->eagerLoad = $eagerLoad; - - return $this; - } - - /** - * Indicate that the given relationships should not be eagerly loaded. - * - * @param array $relations - * @return $this - */ - public function withoutEagerLoad(array $relations) - { - $relations = array_diff(array_keys($this->model->getRelations()), $relations); - - return $this->with($relations); - } - - /** - * Flush the relationships being eagerly loaded. - * - * @return $this - */ - public function withoutEagerLoads() - { - return $this->setEagerLoads([]); - } - - /** - * Get the "limit" value from the query or null if it's not set. - * - * @return mixed - */ - public function getLimit() - { - return $this->query->getLimit(); - } - - /** - * Get the "offset" value from the query or null if it's not set. - * - * @return mixed - */ - public function getOffset() - { - return $this->query->getOffset(); - } - - /** - * Get the default key name of the table. - * - * @return string - */ - protected function defaultKeyName() - { - return $this->getModel()->getKeyName(); - } - - /** - * Get the model instance being queried. - * - * @return TModel - */ - public function getModel() - { - return $this->model; - } + return $this->model; + } /** * Set a model instance for the model being queried. * - * @template TModelNew of \Illuminate\Database\Eloquent\Model - * - * @param TModelNew $model - * @return static + * @param \Illuminate\Database\Eloquent\Model $model + * @return $this */ public function setModel(Model $model) { @@ -2113,27 +1293,15 @@ public function setModel(Model $model) } /** - * Qualify the given column name by the model's table. - * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @return string - */ - public function qualifyColumn($column) - { - $column = $column instanceof Expression ? $column->getValue($this->getGrammar()) : $column; - - return $this->model->qualifyColumn($column); - } - - /** - * Qualify the given columns with the model's table. + * Extend the builder with a given callback. * - * @param array|\Illuminate\Contracts\Database\Query\Expression $columns - * @return array + * @param string $name + * @param \Closure $callback + * @return void */ - public function qualifyColumns($columns) + public function macro($name, Closure $callback) { - return $this->model->qualifyColumns($columns); + $this->macros[$name] = $callback; } /** @@ -2144,180 +1312,33 @@ public function qualifyColumns($columns) */ public function getMacro($name) { - return Arr::get($this->localMacros, $name); - } - - /** - * Checks if a macro is registered. - * - * @param string $name - * @return bool - */ - public function hasMacro($name) - { - return isset($this->localMacros[$name]); - } - - /** - * Get the given global macro by name. - * - * @param string $name - * @return \Closure - */ - public static function getGlobalMacro($name) - { - return Arr::get(static::$macros, $name); - } - - /** - * Checks if a global macro is registered. - * - * @param string $name - * @return bool - */ - public static function hasGlobalMacro($name) - { - return isset(static::$macros[$name]); - } - - /** - * Dynamically access builder proxies. - * - * @param string $key - * @return mixed - * - * @throws \Exception - */ - public function __get($key) - { - if (in_array($key, ['orWhere', 'whereNot', 'orWhereNot'])) { - return new HigherOrderBuilderProxy($this, $key); - } - - if (in_array($key, $this->propertyPassthru)) { - return $this->toBase()->{$key}; - } - - throw new Exception("Property [{$key}] does not exist on the Eloquent builder instance."); + return Arr::get($this->macros, $name); } /** * Dynamically handle calls into the query instance. * * @param string $method - * @param array $parameters + * @param array $parameters * @return mixed */ public function __call($method, $parameters) { - if ($method === 'macro') { - $this->localMacros[$parameters[0]] = $parameters[1]; - - return; - } - - if ($this->hasMacro($method)) { + if (isset($this->macros[$method])) { array_unshift($parameters, $this); - return $this->localMacros[$method](...$parameters); - } - - if (static::hasGlobalMacro($method)) { - $callable = static::$macros[$method]; - - if ($callable instanceof Closure) { - $callable = $callable->bindTo($this, static::class); - } - - return $callable(...$parameters); + return call_user_func_array($this->macros[$method], $parameters); } - if ($this->hasNamedScope($method)) { - return $this->callNamedScope($method, $parameters); + if (method_exists($this->model, $scope = 'scope'.ucfirst($method))) { + return $this->callScope($scope, $parameters); } - if (in_array(strtolower($method), $this->passthru)) { - return $this->toBase()->{$method}(...$parameters); + if (in_array($method, $this->passthru)) { + return call_user_func_array([$this->toBase(), $method], $parameters); } - $this->forwardCallTo($this->query, $method, $parameters); - - return $this; - } - - /** - * Dynamically handle calls into the query instance. - * - * @param string $method - * @param array $parameters - * @return mixed - * - * @throws \BadMethodCallException - */ - public static function __callStatic($method, $parameters) - { - if ($method === 'macro') { - static::$macros[$parameters[0]] = $parameters[1]; - - return; - } - - if ($method === 'mixin') { - return static::registerMixin($parameters[0], $parameters[1] ?? true); - } - - if (! static::hasGlobalMacro($method)) { - static::throwBadMethodCallException($method); - } - - $callable = static::$macros[$method]; - - if ($callable instanceof Closure) { - $callable = $callable->bindTo(null, static::class); - } - - return $callable(...$parameters); - } - - /** - * Register the given mixin with the builder. - * - * @param string $mixin - * @param bool $replace - * @return void - */ - protected static function registerMixin($mixin, $replace) - { - $methods = (new ReflectionClass($mixin))->getMethods( - ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED - ); - - foreach ($methods as $method) { - if ($replace || ! static::hasGlobalMacro($method->name)) { - static::macro($method->name, $method->invoke($mixin)); - } - } - } - - /** - * Clone the Eloquent query builder. - * - * @return static - */ - public function clone() - { - return clone $this; - } - - /** - * Register a closure to be invoked on a clone. - * - * @param \Closure $callback - * @return $this - */ - public function onClone(Closure $callback) - { - $this->onCloneCallbacks[] = $callback; + call_user_func_array([$this->query, $method], $parameters); return $this; } @@ -2330,9 +1351,5 @@ public function onClone(Closure $callback) public function __clone() { $this->query = clone $this->query; - - foreach ($this->onCloneCallbacks as $onCloneCallback) { - $onCloneCallback($this); - } } } diff --git a/Eloquent/Casts/ArrayObject.php b/Eloquent/Casts/ArrayObject.php deleted file mode 100644 index 563545dacb..0000000000 --- a/Eloquent/Casts/ArrayObject.php +++ /dev/null @@ -1,47 +0,0 @@ - - */ -class ArrayObject extends BaseArrayObject implements Arrayable, JsonSerializable -{ - /** - * Get a collection containing the underlying array. - * - * @return \Illuminate\Support\Collection - */ - public function collect() - { - return new Collection($this->getArrayCopy()); - } - - /** - * Get the instance as an array. - * - * @return array - */ - public function toArray() - { - return $this->getArrayCopy(); - } - - /** - * Get the array that should be JSON serialized. - * - * @return array - */ - public function jsonSerialize(): array - { - return $this->getArrayCopy(); - } -} diff --git a/Eloquent/Casts/AsArrayObject.php b/Eloquent/Casts/AsArrayObject.php deleted file mode 100644 index 5ee80d0bb4..0000000000 --- a/Eloquent/Casts/AsArrayObject.php +++ /dev/null @@ -1,42 +0,0 @@ -, iterable> - */ - public static function castUsing(array $arguments) - { - return new class implements CastsAttributes - { - public function get($model, $key, $value, $attributes) - { - if (! isset($attributes[$key])) { - return; - } - - $data = Json::decode($attributes[$key]); - - return is_array($data) ? new ArrayObject($data, ArrayObject::ARRAY_AS_PROPS) : null; - } - - public function set($model, $key, $value, $attributes) - { - return [$key => Json::encode($value)]; - } - - public function serialize($model, string $key, $value, array $attributes) - { - return $value->getArrayCopy(); - } - }; - } -} diff --git a/Eloquent/Casts/AsBinary.php b/Eloquent/Casts/AsBinary.php deleted file mode 100644 index 4f63154777..0000000000 --- a/Eloquent/Casts/AsBinary.php +++ /dev/null @@ -1,75 +0,0 @@ -format = $this->arguments[0] - ?? throw new InvalidArgumentException('The binary codec format is required.'); - - if (! in_array($this->format, BinaryCodec::formats(), true)) { - throw new InvalidArgumentException(sprintf( - 'Unsupported binary codec format [%s]. Allowed formats are: %s.', - $this->format, - implode(', ', BinaryCodec::formats()), - )); - } - } - - public function get($model, $key, $value, $attributes) - { - return BinaryCodec::decode($attributes[$key] ?? null, $this->format); - } - - public function set($model, $key, $value, $attributes) - { - return [$key => BinaryCodec::encode($value, $this->format)]; - } - }; - } - - /** - * Encode / decode values as binary UUIDs. - */ - public static function uuid(): string - { - return self::class.':uuid'; - } - - /** - * Encode / decode values as binary ULIDs. - */ - public static function ulid(): string - { - return self::class.':ulid'; - } - - /** - * Encode / decode values using the given format. - */ - public static function of(string $format): string - { - return self::class.':'.$format; - } -} diff --git a/Eloquent/Casts/AsCollection.php b/Eloquent/Casts/AsCollection.php deleted file mode 100644 index c6b0ffe4b7..0000000000 --- a/Eloquent/Casts/AsCollection.php +++ /dev/null @@ -1,96 +0,0 @@ -, iterable> - * - * @throws \InvalidArgumentException - */ - public static function castUsing(array $arguments) - { - return new class($arguments) implements CastsAttributes - { - public function __construct(protected array $arguments) - { - $this->arguments = array_pad(array_values($this->arguments), 2, ''); - } - - public function get($model, $key, $value, $attributes) - { - if (! isset($attributes[$key])) { - return; - } - - $data = Json::decode($attributes[$key]); - - $collectionClass = empty($this->arguments[0]) ? Collection::class : $this->arguments[0]; - - if (! is_a($collectionClass, Collection::class, true)) { - throw new InvalidArgumentException('The provided class must extend ['.Collection::class.'].'); - } - - if (! is_array($data)) { - return null; - } - - $instance = new $collectionClass($data); - - if (! isset($this->arguments[1]) || ! $this->arguments[1]) { - return $instance; - } - - if (is_string($this->arguments[1])) { - $this->arguments[1] = Str::parseCallback($this->arguments[1]); - } - - return is_callable($this->arguments[1]) - ? $instance->map($this->arguments[1]) - : $instance->mapInto($this->arguments[1][0]); - } - - public function set($model, $key, $value, $attributes) - { - return [$key => Json::encode($value)]; - } - }; - } - - /** - * Specify the type of object each item in the collection should be mapped to. - * - * @param array{class-string, string}|class-string $map - * @return string - */ - public static function of($map) - { - return static::using('', $map); - } - - /** - * Specify the collection type for the cast. - * - * @param class-string $class - * @param array{class-string, string}|class-string|null $map - * @return string - */ - public static function using($class, $map = null) - { - if (is_array($map) && is_callable($map)) { - $map = $map[0].'@'.$map[1]; - } - - return static::class.':'.implode(',', [$class, $map]); - } -} diff --git a/Eloquent/Casts/AsEncryptedArrayObject.php b/Eloquent/Casts/AsEncryptedArrayObject.php deleted file mode 100644 index 2122a7b1f9..0000000000 --- a/Eloquent/Casts/AsEncryptedArrayObject.php +++ /dev/null @@ -1,45 +0,0 @@ -, iterable> - */ - public static function castUsing(array $arguments) - { - return new class implements CastsAttributes - { - public function get($model, $key, $value, $attributes) - { - if (isset($attributes[$key])) { - return new ArrayObject(Json::decode(Crypt::decryptString($attributes[$key])), ArrayObject::ARRAY_AS_PROPS); - } - - return null; - } - - public function set($model, $key, $value, $attributes) - { - if (! is_null($value)) { - return [$key => Crypt::encryptString(Json::encode($value))]; - } - - return null; - } - - public function serialize($model, string $key, $value, array $attributes) - { - return ! is_null($value) ? $value->getArrayCopy() : null; - } - }; - } -} diff --git a/Eloquent/Casts/AsEncryptedCollection.php b/Eloquent/Casts/AsEncryptedCollection.php deleted file mode 100644 index a9078c2b38..0000000000 --- a/Eloquent/Casts/AsEncryptedCollection.php +++ /dev/null @@ -1,95 +0,0 @@ -, iterable> - * - * @throws \InvalidArgumentException - */ - public static function castUsing(array $arguments) - { - return new class($arguments) implements CastsAttributes - { - public function __construct(protected array $arguments) - { - $this->arguments = array_pad(array_values($this->arguments), 2, ''); - } - - public function get($model, $key, $value, $attributes) - { - $collectionClass = empty($this->arguments[0]) ? Collection::class : $this->arguments[0]; - - if (! is_a($collectionClass, Collection::class, true)) { - throw new InvalidArgumentException('The provided class must extend ['.Collection::class.'].'); - } - - if (! isset($attributes[$key])) { - return null; - } - - $instance = new $collectionClass(Json::decode(Crypt::decryptString($attributes[$key]))); - - if (! isset($this->arguments[1]) || ! $this->arguments[1]) { - return $instance; - } - - if (is_string($this->arguments[1])) { - $this->arguments[1] = Str::parseCallback($this->arguments[1]); - } - - return is_callable($this->arguments[1]) - ? $instance->map($this->arguments[1]) - : $instance->mapInto($this->arguments[1][0]); - } - - public function set($model, $key, $value, $attributes) - { - if (! is_null($value)) { - return [$key => Crypt::encryptString(Json::encode($value))]; - } - - return null; - } - }; - } - - /** - * Specify the type of object each item in the collection should be mapped to. - * - * @param array{class-string, string}|class-string $map - * @return string - */ - public static function of($map) - { - return static::using('', $map); - } - - /** - * Specify the collection for the cast. - * - * @param class-string $class - * @param array{class-string, string}|class-string|null $map - * @return string - */ - public static function using($class, $map = null) - { - if (is_array($map) && is_callable($map)) { - $map = $map[0].'@'.$map[1]; - } - - return static::class.':'.implode(',', [$class, $map]); - } -} diff --git a/Eloquent/Casts/AsEnumArrayObject.php b/Eloquent/Casts/AsEnumArrayObject.php deleted file mode 100644 index 061dcbf57e..0000000000 --- a/Eloquent/Casts/AsEnumArrayObject.php +++ /dev/null @@ -1,97 +0,0 @@ -} $arguments - * @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Database\Eloquent\Casts\ArrayObject, iterable> - */ - public static function castUsing(array $arguments) - { - return new class($arguments) implements CastsAttributes - { - protected $arguments; - - public function __construct(array $arguments) - { - $this->arguments = $arguments; - } - - public function get($model, $key, $value, $attributes) - { - if (! isset($attributes[$key])) { - return; - } - - $data = Json::decode($attributes[$key]); - - if (! is_array($data)) { - return; - } - - $enumClass = $this->arguments[0]; - - return new ArrayObject((new Collection($data))->map(function ($value) use ($enumClass) { - return is_subclass_of($enumClass, BackedEnum::class) - ? $enumClass::from($value) - : constant($enumClass.'::'.$value); - })->toArray()); - } - - public function set($model, $key, $value, $attributes) - { - if ($value === null) { - return [$key => null]; - } - - $storable = []; - - foreach ($value as $enum) { - $storable[] = $this->getStorableEnumValue($enum); - } - - return [$key => Json::encode($storable)]; - } - - public function serialize($model, string $key, $value, array $attributes) - { - return (new Collection($value->getArrayCopy())) - ->map(fn ($enum) => $this->getStorableEnumValue($enum)) - ->toArray(); - } - - protected function getStorableEnumValue($enum) - { - if (is_string($enum) || is_int($enum)) { - return $enum; - } - - return enum_value($enum); - } - }; - } - - /** - * Specify the Enum for the cast. - * - * @param class-string $class - * @return string - */ - public static function of($class) - { - return static::class.':'.$class; - } -} diff --git a/Eloquent/Casts/AsEnumCollection.php b/Eloquent/Casts/AsEnumCollection.php deleted file mode 100644 index fa7116a0d0..0000000000 --- a/Eloquent/Casts/AsEnumCollection.php +++ /dev/null @@ -1,93 +0,0 @@ -} $arguments - * @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection, iterable> - */ - public static function castUsing(array $arguments) - { - return new class($arguments) implements CastsAttributes - { - protected $arguments; - - public function __construct(array $arguments) - { - $this->arguments = $arguments; - } - - public function get($model, $key, $value, $attributes) - { - if (! isset($attributes[$key])) { - return; - } - - $data = Json::decode($attributes[$key]); - - if (! is_array($data)) { - return; - } - - $enumClass = $this->arguments[0]; - - return (new Collection($data))->map(function ($value) use ($enumClass) { - return is_subclass_of($enumClass, BackedEnum::class) - ? $enumClass::from($value) - : constant($enumClass.'::'.$value); - }); - } - - public function set($model, $key, $value, $attributes) - { - $value = $value !== null - ? Json::encode((new Collection($value))->map(function ($enum) { - return $this->getStorableEnumValue($enum); - })->jsonSerialize()) - : null; - - return [$key => $value]; - } - - public function serialize($model, string $key, $value, array $attributes) - { - return (new Collection($value)) - ->map(fn ($enum) => $this->getStorableEnumValue($enum)) - ->toArray(); - } - - protected function getStorableEnumValue($enum) - { - if (is_string($enum) || is_int($enum)) { - return $enum; - } - - return enum_value($enum); - } - }; - } - - /** - * Specify the Enum for the cast. - * - * @param class-string $class - * @return string - */ - public static function of($class) - { - return static::class.':'.$class; - } -} diff --git a/Eloquent/Casts/AsFluent.php b/Eloquent/Casts/AsFluent.php deleted file mode 100644 index bba1b1dac9..0000000000 --- a/Eloquent/Casts/AsFluent.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ - public static function castUsing(array $arguments) - { - return new class implements CastsAttributes - { - public function get($model, $key, $value, $attributes) - { - return isset($value) ? new Fluent(Json::decode($value)) : null; - } - - public function set($model, $key, $value, $attributes) - { - return isset($value) ? [$key => Json::encode($value)] : null; - } - }; - } -} diff --git a/Eloquent/Casts/AsHtmlString.php b/Eloquent/Casts/AsHtmlString.php deleted file mode 100644 index d4182d258f..0000000000 --- a/Eloquent/Casts/AsHtmlString.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ - public static function castUsing(array $arguments) - { - return new class implements CastsAttributes - { - public function get($model, $key, $value, $attributes) - { - return isset($value) ? new HtmlString($value) : null; - } - - public function set($model, $key, $value, $attributes) - { - return isset($value) ? (string) $value : null; - } - }; - } -} diff --git a/Eloquent/Casts/AsStringable.php b/Eloquent/Casts/AsStringable.php deleted file mode 100644 index 4f6c787c85..0000000000 --- a/Eloquent/Casts/AsStringable.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ - public static function castUsing(array $arguments) - { - return new class implements CastsAttributes - { - public function get($model, $key, $value, $attributes) - { - return isset($value) ? new Stringable($value) : null; - } - - public function set($model, $key, $value, $attributes) - { - return isset($value) ? (string) $value : null; - } - }; - } -} diff --git a/Eloquent/Casts/AsUri.php b/Eloquent/Casts/AsUri.php deleted file mode 100644 index d55c6d7996..0000000000 --- a/Eloquent/Casts/AsUri.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ - public static function castUsing(array $arguments) - { - return new class implements CastsAttributes - { - public function get($model, $key, $value, $attributes) - { - return isset($value) ? new Uri($value) : null; - } - - public function set($model, $key, $value, $attributes) - { - return isset($value) ? (string) $value : null; - } - }; - } -} diff --git a/Eloquent/Casts/Attribute.php b/Eloquent/Casts/Attribute.php deleted file mode 100644 index 26d13ba3fb..0000000000 --- a/Eloquent/Casts/Attribute.php +++ /dev/null @@ -1,104 +0,0 @@ -get = $get; - $this->set = $set; - } - - /** - * Create a new attribute accessor / mutator. - * - * @param callable|null $get - * @param callable|null $set - * @return static - */ - public static function make(?callable $get = null, ?callable $set = null): static - { - return new static($get, $set); - } - - /** - * Create a new attribute accessor. - * - * @param callable $get - * @return static - */ - public static function get(callable $get) - { - return new static($get); - } - - /** - * Create a new attribute mutator. - * - * @param callable $set - * @return static - */ - public static function set(callable $set) - { - return new static(null, $set); - } - - /** - * Disable object caching for the attribute. - * - * @return static - */ - public function withoutObjectCaching() - { - $this->withObjectCaching = false; - - return $this; - } - - /** - * Enable caching for the attribute. - * - * @return static - */ - public function shouldCache() - { - $this->withCaching = true; - - return $this; - } -} diff --git a/Eloquent/Casts/Json.php b/Eloquent/Casts/Json.php deleted file mode 100644 index 783d5b9986..0000000000 --- a/Eloquent/Casts/Json.php +++ /dev/null @@ -1,56 +0,0 @@ - - */ -class Collection extends BaseCollection implements QueueableCollection -{ - use InteractsWithDictionary; +class Collection extends BaseCollection +{ /** * Find a model in the collection by key. * - * @template TFindDefault - * * @param mixed $key - * @param TFindDefault $default - * @return ($key is (\Illuminate\Contracts\Support\Arrayable|array) ? static : TModel|TFindDefault) + * @param mixed $default + * @return \Illuminate\Database\Eloquent\Model */ public function find($key, $default = null) { @@ -35,66 +20,26 @@ public function find($key, $default = null) $key = $key->getKey(); } - if ($key instanceof Arrayable) { - $key = $key->toArray(); - } - - if (is_array($key)) { - if ($this->isEmpty()) { - return new static; - } + return Arr::first($this->items, function ($model) use ($key) { + return $model->getKey() == $key; - return $this->whereIn($this->first()->getKeyName(), $key); - } - - return Arr::first($this->items, fn ($model) => $model->getKey() == $key, $default); - } - - /** - * Find a model in the collection by key or throw an exception. - * - * @param mixed $key - * @return TModel - * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - */ - public function findOrFail($key) - { - $result = $this->find($key); - - if (is_array($key) && count($result) === count(array_unique($key))) { - return $result; - } elseif (! is_array($key) && ! is_null($result)) { - return $result; - } - - $exception = new ModelNotFoundException; - - if (! $model = head($this->items)) { - throw $exception; - } - - $ids = is_array($key) ? array_diff($key, $result->modelKeys()) : $key; - - $exception->setModel(get_class($model), $ids); - - throw $exception; + }, $default); } /** * Load a set of relationships onto the collection. * - * @param array): mixed)|string>|string $relations + * @param mixed $relations * @return $this */ public function load($relations) { - if ($this->isNotEmpty()) { + if (count($this->items) > 0) { if (is_string($relations)) { $relations = func_get_args(); } - $query = $this->first()->newQueryWithoutRelationships()->with($relations); + $query = $this->first()->newQuery()->with($relations); $this->items = $query->eagerLoadRelations($this->items); } @@ -103,242 +48,14 @@ public function load($relations) } /** - * Load a set of aggregations over relationship's column onto the collection. - * - * @param array): mixed)|string>|string $relations - * @param string $column - * @param string|null $function - * @return $this - */ - public function loadAggregate($relations, $column, $function = null) - { - if ($this->isEmpty()) { - return $this; - } - - $models = $this->first()->newModelQuery() - ->whereKey($this->modelKeys()) - ->select($this->first()->getKeyName()) - ->withAggregate($relations, $column, $function) - ->get() - ->keyBy($this->first()->getKeyName()); - - $attributes = Arr::except( - array_keys($models->first()->getAttributes()), - $models->first()->getKeyName() - ); - - $this->each(function ($model) use ($models, $attributes) { - $extraAttributes = Arr::only($models->get($model->getKey())->getAttributes(), $attributes); - - $model->forceFill($extraAttributes) - ->syncOriginalAttributes($attributes) - ->mergeCasts($models->get($model->getKey())->getCasts()); - }); - - return $this; - } - - /** - * Load a set of relationship counts onto the collection. - * - * @param array): mixed)|string>|string $relations - * @return $this - */ - public function loadCount($relations) - { - return $this->loadAggregate($relations, '*', 'count'); - } - - /** - * Load a set of relationship's max column values onto the collection. - * - * @param array): mixed)|string>|string $relations - * @param string $column - * @return $this - */ - public function loadMax($relations, $column) - { - return $this->loadAggregate($relations, $column, 'max'); - } - - /** - * Load a set of relationship's min column values onto the collection. - * - * @param array): mixed)|string>|string $relations - * @param string $column - * @return $this - */ - public function loadMin($relations, $column) - { - return $this->loadAggregate($relations, $column, 'min'); - } - - /** - * Load a set of relationship's column summations onto the collection. - * - * @param array): mixed)|string>|string $relations - * @param string $column - * @return $this - */ - public function loadSum($relations, $column) - { - return $this->loadAggregate($relations, $column, 'sum'); - } - - /** - * Load a set of relationship's average column values onto the collection. - * - * @param array): mixed)|string>|string $relations - * @param string $column - * @return $this - */ - public function loadAvg($relations, $column) - { - return $this->loadAggregate($relations, $column, 'avg'); - } - - /** - * Load a set of related existences onto the collection. - * - * @param array): mixed)|string>|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. - * - * @param array): mixed)|string>|string $relations - * @return $this - */ - public function loadMissing($relations) - { - if (is_string($relations)) { - $relations = func_get_args(); - } - - if ($this->isNotEmpty()) { - $query = $this->first()->newQueryWithoutRelationships()->with($relations); - - foreach ($query->getEagerLoads() as $key => $value) { - $segments = explode('.', explode(':', $key)[0]); - - if (str_contains($key, ':')) { - $segments[count($segments) - 1] .= ':'.explode(':', $key)[1]; - } - - $path = []; - - foreach ($segments as $segment) { - $path[] = [$segment => $segment]; - } - - if (is_callable($value)) { - $path[count($segments) - 1][array_last($segments)] = $value; - } - - $this->loadMissingRelation($this, $path); - } - } - - return $this; - } - - /** - * Load a relationship path for models of the given type if it is not already eager loaded. - * - * @param array> $tuples - * @return void - */ - public function loadMissingRelationshipChain(array $tuples) - { - [$relation, $class] = array_shift($tuples); - - $this->filter(function ($model) use ($relation, $class) { - return ! is_null($model) && - ! $model->relationLoaded($relation) && - $model::class === $class; - })->load($relation); - - if (empty($tuples)) { - return; - } - - $models = $this->pluck($relation)->whereNotNull(); - - if ($models->first() instanceof BaseCollection) { - $models = $models->collapse(); - } - - (new static($models))->loadMissingRelationshipChain($tuples); - } - - /** - * Load a relationship path if it is not already eager loaded. - * - * @param \Illuminate\Database\Eloquent\Collection $models - * @param array $path - * @return void - */ - protected function loadMissingRelation(self $models, array $path) - { - $relation = array_shift($path); - - $name = explode(':', key($relation))[0]; - - if (is_string(reset($relation))) { - $relation = reset($relation); - } - - $models->filter(fn ($model) => ! is_null($model) && ! $model->relationLoaded($name))->load($relation); - - if (empty($path)) { - return; - } - - $models = $models->pluck($name)->filter(); - - if ($models->first() instanceof BaseCollection) { - $models = $models->collapse(); - } - - $this->loadMissingRelation(new static($models), $path); - } - - /** - * Load a set of relationships onto the mixed relationship collection. - * - * @param string $relation - * @param array): mixed)|string> $relations - * @return $this - */ - public function loadMorph($relation, $relations) - { - $this->pluck($relation) - ->filter() - ->groupBy(fn ($model) => get_class($model)) - ->each(fn ($models, $className) => static::make($models)->load($relations[$className] ?? [])); - - return $this; - } - - /** - * Load a set of relationship counts onto the mixed relationship collection. + * Add an item to the collection. * - * @param string $relation - * @param array): mixed)|string> $relations + * @param mixed $item * @return $this */ - public function loadMorphCount($relation, $relations) + public function add($item) { - $this->pluck($relation) - ->filter() - ->groupBy(fn ($model) => get_class($model)) - ->each(fn ($models, $className) => static::make($models)->loadCount($relations[$className] ?? [])); + $this->items[] = $item; return $this; } @@ -346,51 +63,43 @@ public function loadMorphCount($relation, $relations) /** * Determine if a key exists in the collection. * - * @param (callable(TModel, TKey): bool)|TModel|string|int $key - * @param mixed $operator + * @param mixed $key * @param mixed $value * @return bool */ - public function contains($key, $operator = null, $value = null) + public function contains($key, $value = null) { - if (func_num_args() > 1 || $this->useAsCallable($key)) { - return parent::contains(...func_get_args()); + if (func_num_args() == 2) { + return parent::contains($key, $value); } - if ($key instanceof Model) { - return parent::contains(fn ($model) => $model->is($key)); + if ($this->useAsCallable($key)) { + return parent::contains($key); } - return parent::contains(fn ($model) => $model->getKey() == $key); - } + $key = $key instanceof Model ? $key->getKey() : $key; - /** - * Determine if a key does not exist in the collection. - * - * @param (callable(TModel, TKey): bool)|TModel|string|int $key - * @param mixed $operator - * @param mixed $value - * @return bool - */ - public function doesntContain($key, $operator = null, $value = null) - { - return ! $this->contains(...func_get_args()); + return parent::contains(function ($model) use ($key) { + return $model->getKey() == $key; + }); } /** * Get the array of primary keys. * - * @return array + * @return array */ public function modelKeys() { - return array_map(fn ($model) => $model->getKey(), $this->items); + return array_map(function ($model) { + return $model->getKey(); + }, $this->items); } /** * Merge the collection with the given items. * - * @param iterable $items + * @param \ArrayAccess|array $items * @return static */ public function merge($items) @@ -398,73 +107,16 @@ public function merge($items) $dictionary = $this->getDictionary(); foreach ($items as $item) { - $dictionary[$this->getDictionaryKey($item->getKey())] = $item; + $dictionary[$item->getKey()] = $item; } return new static(array_values($dictionary)); } - /** - * Run a map over each of the items. - * - * @template TMapValue - * - * @param callable(TModel, TKey): TMapValue $callback - * @return \Illuminate\Support\Collection|static - */ - public function map(callable $callback) - { - $result = parent::map($callback); - - return $result->contains(fn ($item) => ! $item instanceof Model) ? $result->toBase() : $result; - } - - /** - * Run an associative map over each of the items. - * - * The callback should return an associative array with a single key / value pair. - * - * @template TMapWithKeysKey of array-key - * @template TMapWithKeysValue - * - * @param callable(TModel, TKey): array $callback - * @return \Illuminate\Support\Collection|static - */ - public function mapWithKeys(callable $callback) - { - $result = parent::mapWithKeys($callback); - - return $result->contains(fn ($item) => ! $item instanceof Model) ? $result->toBase() : $result; - } - - /** - * Reload a fresh model instance from the database for all the entities. - * - * @param array|string $with - * @return static - */ - public function fresh($with = []) - { - if ($this->isEmpty()) { - return new static; - } - - $model = $this->first(); - - $freshModels = $model->newQueryWithoutScopes() - ->with(is_string($with) ? func_get_args() : $with) - ->whereIn($model->getKeyName(), $this->modelKeys()) - ->get() - ->getDictionary(); - - return $this->filter(fn ($model) => $model->exists && isset($freshModels[$model->getKey()])) - ->map(fn ($model) => $freshModels[$model->getKey()]); - } - /** * Diff the collection with the given items. * - * @param iterable $items + * @param \ArrayAccess|array $items * @return static */ public function diff($items) @@ -474,7 +126,7 @@ public function diff($items) $dictionary = $this->getDictionary($items); foreach ($this->items as $item) { - if (! isset($dictionary[$this->getDictionaryKey($item->getKey())])) { + if (! isset($dictionary[$item->getKey()])) { $diff->add($item); } } @@ -485,21 +137,17 @@ public function diff($items) /** * Intersect the collection with the given items. * - * @param iterable $items + * @param \ArrayAccess|array $items * @return static */ public function intersect($items) { $intersect = new static; - if (empty($items)) { - return $intersect; - } - $dictionary = $this->getDictionary($items); foreach ($this->items as $item) { - if (isset($dictionary[$this->getDictionaryKey($item->getKey())])) { + if (isset($dictionary[$item->getKey()])) { $intersect->add($item); } } @@ -510,14 +158,13 @@ public function intersect($items) /** * Return only unique items from the collection. * - * @param (callable(TModel, TKey): mixed)|string|null $key - * @param bool $strict + * @param string|callable|null $key * @return static */ - public function unique($key = null, $strict = false) + public function unique($key = null) { if (! is_null($key)) { - return parent::unique($key, $strict); + return parent::unique($key); } return new static(array_values($this->getDictionary())); @@ -526,16 +173,12 @@ public function unique($key = null, $strict = false) /** * Returns only the models from the collection with the specified keys. * - * @param array|null $keys + * @param mixed $keys * @return static */ public function only($keys) { - if (is_null($keys)) { - return new static($this->items); - } - - $dictionary = Arr::only($this->getDictionary(), array_map($this->getDictionaryKey(...), (array) $keys)); + $dictionary = Arr::only($this->getDictionary(), $keys); return new static(array_values($dictionary)); } @@ -543,16 +186,12 @@ public function only($keys) /** * Returns all models in the collection except the models with specified keys. * - * @param array|null $keys + * @param mixed $keys * @return static */ public function except($keys) { - if (is_null($keys)) { - return new static($this->items); - } - - $dictionary = Arr::except($this->getDictionary(), array_map($this->getDictionaryKey(...), (array) $keys)); + $dictionary = Arr::except($this->getDictionary(), $keys); return new static(array_values($dictionary)); } @@ -560,106 +199,34 @@ public function except($keys) /** * Make the given, typically visible, attributes hidden across the entire collection. * - * @param array|string $attributes + * @param array|string $attributes * @return $this */ public function makeHidden($attributes) { - return $this->each->makeHidden($attributes); - } - - /** - * Merge the given, typically visible, attributes hidden across the entire collection. - * - * @param array|string $attributes - * @return $this - */ - public function mergeHidden($attributes) - { - return $this->each->mergeHidden($attributes); - } - - /** - * Set the hidden attributes across the entire collection. - * - * @param array $hidden - * @return $this - */ - public function setHidden($hidden) - { - return $this->each->setHidden($hidden); + return $this->each(function ($model) use ($attributes) { + $model->addHidden($attributes); + }); } /** * Make the given, typically hidden, attributes visible across the entire collection. * - * @param array|string $attributes + * @param array|string $attributes * @return $this */ public function makeVisible($attributes) { - return $this->each->makeVisible($attributes); - } - - /** - * Merge the given, typically hidden, attributes visible across the entire collection. - * - * @param array|string $attributes - * @return $this - */ - public function mergeVisible($attributes) - { - return $this->each->mergeVisible($attributes); - } - - /** - * Set the visible attributes across the entire collection. - * - * @param array $visible - * @return $this - */ - public function setVisible($visible) - { - return $this->each->setVisible($visible); - } - - /** - * Append an attribute across the entire collection. - * - * @param array|string $attributes - * @return $this - */ - public function append($attributes) - { - return $this->each->append($attributes); - } - - /** - * Sets the appends on every element of the collection, overwriting the existing appends for each. - * - * @param array $appends - * @return $this - */ - public function setAppends(array $appends) - { - return $this->each->setAppends($appends); - } - - /** - * Remove appended properties from every element in the collection. - * - * @return $this - */ - public function withoutAppends() - { - return $this->setAppends([]); + return $this->each(function ($model) use ($attributes) { + $model->makeVisible($attributes); + }); } /** * Get a dictionary keyed by primary keys. * - * @param iterable|null $items - * @return array + * @param \ArrayAccess|array|null $items + * @return array */ public function getDictionary($items = null) { @@ -668,7 +235,7 @@ public function getDictionary($items = null) $dictionary = []; foreach ($items as $value) { - $dictionary[$this->getDictionaryKey($value->getKey())] = $value; + $dictionary[$value->getKey()] = $value; } return $dictionary; @@ -679,256 +246,76 @@ public function getDictionary($items = null) */ /** - * {@inheritDoc} + * Get an array with the values of a given key. * - * @return \Illuminate\Support\Collection + * @param string $value + * @param string|null $key + * @return \Illuminate\Support\Collection */ - #[\Override] - public function countBy($countBy = null) - { - return $this->toBase()->countBy($countBy); - } - - /** - * {@inheritDoc} - * - * @return \Illuminate\Support\Collection - */ - #[\Override] - public function collapse() - { - return $this->toBase()->collapse(); - } - - /** - * {@inheritDoc} - * - * @return \Illuminate\Support\Collection - */ - #[\Override] - public function flatten($depth = INF) - { - return $this->toBase()->flatten($depth); - } - - /** - * {@inheritDoc} - * - * @return \Illuminate\Support\Collection - */ - #[\Override] - public function flip() + public function pluck($value, $key = null) { - return $this->toBase()->flip(); + return $this->toBase()->pluck($value, $key); } /** - * {@inheritDoc} + * Get the keys of the collection items. * - * @return \Illuminate\Support\Collection + * @return \Illuminate\Support\Collection */ - #[\Override] public function keys() { return $this->toBase()->keys(); } /** - * {@inheritDoc} - * - * @template TPadValue - * - * @return \Illuminate\Support\Collection - */ - #[\Override] - public function pad($size, $value) - { - return $this->toBase()->pad($size, $value); - } - - /** - * {@inheritDoc} - * - * @return \Illuminate\Support\Collection, static> - */ - #[\Override] - public function partition($key, $operator = null, $value = null) - { - return parent::partition(...func_get_args())->toBase(); - } - - /** - * {@inheritDoc} + * Zip the collection together with one or more arrays. * - * @return \Illuminate\Support\Collection + * @param mixed ...$items + * @return \Illuminate\Support\Collection */ - #[\Override] - public function pluck($value, $key = null) - { - return $this->toBase()->pluck($value, $key); - } - - /** - * {@inheritDoc} - * - * @template TZipValue - * - * @return \Illuminate\Support\Collection> - */ - #[\Override] public function zip($items) { - return $this->toBase()->zip(...func_get_args()); - } - - /** - * Get the comparison function to detect duplicates. - * - * @return callable(TModel, TModel): bool - */ - protected function duplicateComparator($strict) - { - return fn ($a, $b) => $a->is($b); - } - - /** - * Enable relationship autoloading for all models in this collection. - * - * @return $this - */ - public function withRelationshipAutoloading() - { - $callback = fn ($tuples) => $this->loadMissingRelationshipChain($tuples); - - foreach ($this as $model) { - if (! $model->hasRelationAutoloadCallback()) { - $model->autoloadRelationsUsing($callback, $this); - } - } - - return $this; - } - - /** - * Get the type of the entities being queued. - * - * @return string|null - * - * @throws \LogicException - */ - public function getQueueableClass() - { - if ($this->isEmpty()) { - return; - } - - $class = $this->getQueueableModelClass($this->first()); - - $this->each(function ($model) use ($class) { - if ($this->getQueueableModelClass($model) !== $class) { - throw new LogicException('Queueing collections with multiple model types is not supported.'); - } - }); - - return $class; - } - - /** - * Get the queueable class name for the given model. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return string - */ - protected function getQueueableModelClass($model) - { - return method_exists($model, 'getQueueableClassName') - ? $model->getQueueableClassName() - : get_class($model); + return call_user_func_array([$this->toBase(), 'zip'], func_get_args()); } /** - * Get the identifiers for all of the entities. + * Collapse the collection of items into a single array. * - * @return array + * @return \Illuminate\Support\Collection */ - public function getQueueableIds() + public function collapse() { - if ($this->isEmpty()) { - return []; - } - - return $this->first() instanceof QueueableEntity - ? $this->map->getQueueableId()->all() - : $this->modelKeys(); + return $this->toBase()->collapse(); } /** - * Get the relationships of the entities being queued. + * Get a flattened array of the items in the collection. * - * @return array + * @param int $depth + * @return \Illuminate\Support\Collection */ - public function getQueueableRelations() + public function flatten($depth = INF) { - if ($this->isEmpty()) { - return []; - } - - $relations = $this->map->getQueueableRelations()->all(); - - if (count($relations) === 0 || $relations === [[]]) { - return []; - } elseif (count($relations) === 1) { - return reset($relations); - } else { - return array_intersect(...array_values($relations)); - } + return $this->toBase()->flatten($depth); } /** - * Get the connection of the entities being queued. - * - * @return string|null + * Flip the items in the collection. * - * @throws \LogicException + * @return \Illuminate\Support\Collection */ - public function getQueueableConnection() + public function flip() { - if ($this->isEmpty()) { - return; - } - - $connection = $this->first()->getConnectionName(); - - $this->each(function ($model) use ($connection) { - if ($model->getConnectionName() !== $connection) { - throw new LogicException('Queueing collections with multiple model connections is not supported.'); - } - }); - - return $connection; + return $this->toBase()->flip(); } /** - * Get the Eloquent query builder from the collection. + * Get a base Support collection instance from this collection. * - * @return \Illuminate\Database\Eloquent\Builder - * - * @throws \LogicException + * @return \Illuminate\Support\Collection */ - public function toQuery() + public function toBase() { - $model = $this->first(); - - if (! $model) { - throw new LogicException('Unable to create query for empty collection.'); - } - - $class = get_class($model); - - if ($this->reject(fn ($model) => $model instanceof $class)->isNotEmpty()) { - throw new LogicException('Unable to create query for collection with mixed types.'); - } - - return $model->newModelQuery()->whereKey($this->modelKeys()); + return new BaseCollection($this->items); } } diff --git a/Eloquent/Concerns/GuardsAttributes.php b/Eloquent/Concerns/GuardsAttributes.php deleted file mode 100644 index aa9b2c60c9..0000000000 --- a/Eloquent/Concerns/GuardsAttributes.php +++ /dev/null @@ -1,288 +0,0 @@ - - */ - protected $fillable = []; - - /** - * The attributes that aren't mass assignable. - * - * @var array - */ - protected $guarded = ['*']; - - /** - * Indicates if all mass assignment is enabled. - * - * @var bool - */ - protected static $unguarded = false; - - /** - * The actual columns that exist on the database and can be guarded. - * - * @var array> - */ - protected static $guardableColumns = []; - - /** - * Initialize the GuardsAttributes trait. - * - * @return void - */ - #[Initialize] - public function initializeGuardsAttributes() - { - if (empty($this->fillable)) { - $this->fillable = static::resolveClassAttribute(Fillable::class, 'columns') ?? []; - } - - if ($this->guarded === ['*']) { - if (static::resolveClassAttribute(Unguarded::class) !== null) { - $this->guarded = []; - } else { - $this->guarded = static::resolveClassAttribute(Guarded::class, 'columns') ?? ['*']; - } - } - } - - /** - * Get the fillable attributes for the model. - * - * @return array - */ - public function getFillable() - { - return $this->fillable; - } - - /** - * Set the fillable attributes for the model. - * - * @param array $fillable - * @return $this - */ - public function fillable(array $fillable) - { - $this->fillable = $fillable; - - return $this; - } - - /** - * Merge new fillable attributes with existing fillable attributes on the model. - * - * @param array $fillable - * @return $this - */ - public function mergeFillable(array $fillable) - { - $this->fillable = array_values(array_unique(array_merge($this->fillable, $fillable))); - - return $this; - } - - /** - * Get the guarded attributes for the model. - * - * @return array - */ - public function getGuarded() - { - return self::$unguarded === true - ? [] - : $this->guarded; - } - - /** - * Set the guarded attributes for the model. - * - * @param array $guarded - * @return $this - */ - public function guard(array $guarded) - { - $this->guarded = $guarded; - - return $this; - } - - /** - * Merge new guarded attributes with existing guarded attributes on the model. - * - * @param array $guarded - * @return $this - */ - public function mergeGuarded(array $guarded) - { - $this->guarded = array_values(array_unique(array_merge($this->guarded, $guarded))); - - return $this; - } - - /** - * Disable all mass assignable restrictions. - * - * @param bool $state - * @return void - */ - public static function unguard($state = true) - { - static::$unguarded = $state; - } - - /** - * Enable the mass assignment restrictions. - * - * @return void - */ - public static function reguard() - { - static::$unguarded = false; - } - - /** - * Determine if the current state is "unguarded". - * - * @return bool - */ - public static function isUnguarded() - { - return static::$unguarded; - } - - /** - * Run the given callable while being unguarded. - * - * @template TReturn - * - * @param callable(): TReturn $callback - * @return TReturn - */ - public static function unguarded(callable $callback) - { - if (static::$unguarded) { - return $callback(); - } - - static::unguard(); - - try { - return $callback(); - } finally { - static::reguard(); - } - } - - /** - * Determine if the given attribute may be mass assigned. - * - * @param string $key - * @return bool - */ - public function isFillable($key) - { - if (static::$unguarded) { - return true; - } - - // If the key is in the "fillable" array, we can of course assume that it's - // a fillable attribute. Otherwise, we will check the guarded array when - // we need to determine if the attribute is black-listed on the model. - if (in_array($key, $this->getFillable())) { - return true; - } - - // If the attribute is explicitly listed in the "guarded" array then we can - // return false immediately. This means this attribute is definitely not - // fillable and there is no point in going any further in this method. - if ($this->isGuarded($key)) { - return false; - } - - return empty($this->getFillable()) && - ! str_contains($key, '.') && - ! str_starts_with($key, '_'); - } - - /** - * Determine if the given key is guarded. - * - * @param string $key - * @return bool - */ - public function isGuarded($key) - { - if (empty($this->getGuarded())) { - return false; - } - - return $this->getGuarded() == ['*'] || - ! empty(preg_grep('/^'.preg_quote($key, '/').'$/i', $this->getGuarded())) || - ! $this->isGuardableColumn($key); - } - - /** - * Determine if the given column is a valid, guardable column. - * - * @param string $key - * @return bool - */ - protected function isGuardableColumn($key) - { - if ($this->hasSetMutator($key) || $this->hasAttributeSetMutator($key) || $this->isClassCastable($key)) { - return true; - } - - if (! isset(static::$guardableColumns[get_class($this)])) { - $columns = $this->getConnection() - ->getSchemaBuilder() - ->getColumnListing($this->getTable()); - - if (empty($columns)) { - return true; - } - - static::$guardableColumns[get_class($this)] = $columns; - } - - return in_array($key, static::$guardableColumns[get_class($this)]); - } - - /** - * Determine if the model is totally guarded. - * - * @return bool - */ - public function totallyGuarded() - { - return count($this->getFillable()) === 0 && $this->getGuarded() == ['*']; - } - - /** - * Get the fillable attributes of a given array. - * - * @param array $attributes - * @return array - */ - protected function fillableFromArray(array $attributes) - { - if (count($this->getFillable()) > 0 && ! static::$unguarded) { - return array_intersect_key($attributes, array_flip($this->getFillable())); - } - - return $attributes; - } -} diff --git a/Eloquent/Concerns/HasAttributes.php b/Eloquent/Concerns/HasAttributes.php deleted file mode 100644 index a5717d2c30..0000000000 --- a/Eloquent/Concerns/HasAttributes.php +++ /dev/null @@ -1,2584 +0,0 @@ - - */ - protected $attributes = []; - - /** - * The model attribute's original state. - * - * @var array - */ - protected $original = []; - - /** - * The changed model attributes. - * - * @var array - */ - protected $changes = []; - - /** - * The previous state of the changed model attributes. - * - * @var array - */ - protected $previous = []; - - /** - * The attributes that should be cast. - * - * @var array - */ - protected $casts = []; - - /** - * The attributes that have been cast using custom classes. - * - * @var array - */ - protected $classCastCache = []; - - /** - * The attributes that have been cast using "Attribute" return type mutators. - * - * @var array - */ - protected $attributeCastCache = []; - - /** - * The built-in, primitive cast types supported by Eloquent. - * - * @var string[] - */ - protected static $primitiveCastTypes = [ - 'array', - 'bool', - 'boolean', - 'collection', - 'custom_datetime', - 'date', - 'datetime', - 'decimal', - 'double', - 'encrypted', - 'encrypted:array', - 'encrypted:collection', - 'encrypted:json', - 'encrypted:object', - 'float', - 'hashed', - 'immutable_date', - 'immutable_datetime', - 'immutable_custom_datetime', - 'int', - 'integer', - 'json', - 'json:unicode', - 'object', - 'real', - 'string', - 'timestamp', - ]; - - /** - * The storage format of the model's date columns. - * - * @var string|null - */ - protected $dateFormat; - - /** - * The accessors to append to the model's array form. - * - * @var array - */ - protected $appends = []; - - /** - * Indicates whether attributes are snake cased on arrays. - * - * @var bool - */ - public static $snakeAttributes = true; - - /** - * The cache of the mutated attributes for each class. - * - * @var array - */ - protected static $mutatorCache = []; - - /** - * The cache of the "Attribute" return type marked mutated attributes for each class. - * - * @var array - */ - protected static $attributeMutatorCache = []; - - /** - * The cache of the "Attribute" return type marked mutated, gettable attributes for each class. - * - * @var array - */ - protected static $getAttributeMutatorCache = []; - - /** - * The cache of the "Attribute" return type marked mutated, settable attributes for each class. - * - * @var array - */ - protected static $setAttributeMutatorCache = []; - - /** - * The cache of the converted cast types. - * - * @var array - */ - protected static $castTypeCache = []; - - /** - * The encrypter instance that is used to encrypt attributes. - * - * @var \Illuminate\Contracts\Encryption\Encrypter|null - */ - public static $encrypter; - - /** - * Initialize the trait. - * - * @return void - */ - protected function initializeHasAttributes() - { - $this->casts = $this->ensureCastsAreStringValues( - array_merge($this->casts, $this->casts()), - ); - - $this->dateFormat ??= static::resolveClassAttribute(Table::class)->dateFormat ?? null; - - if (empty($this->appends)) { - $this->appends = static::resolveClassAttribute(Appends::class, 'columns') ?? []; - } - } - - /** - * Convert the model's attributes to an array. - * - * @return array - */ - public function attributesToArray() - { - // If an attribute is a date, we will cast it to a string after converting it - // to a DateTime / Carbon instance. This is so we will get some consistent - // formatting while accessing attributes vs. arraying / JSONing a model. - $attributes = $this->addDateAttributesToArray( - $attributes = $this->getArrayableAttributes() - ); - - $attributes = $this->addMutatedAttributesToArray( - $attributes, $mutatedAttributes = $this->getMutatedAttributes() - ); - - // Next we will handle any casts that have been setup for this model and cast - // the values to their appropriate type. If the attribute has a mutator we - // will not perform the cast on those attributes to avoid any confusion. - $attributes = $this->addCastAttributesToArray( - $attributes, $mutatedAttributes - ); - - // Here we will grab all of the appended, calculated attributes to this model - // as these attributes are not really in the attributes array, but are run - // when we need to array or JSON the model for convenience to the coder. - foreach ($this->getArrayableAppends() as $key) { - $attributes[$key] = $this->mutateAttributeForArray($key, null); - } - - return $attributes; - } - - /** - * Add the date attributes to the attributes array. - * - * @param array $attributes - * @return array - */ - protected function addDateAttributesToArray(array $attributes) - { - foreach ($this->getDates() as $key) { - if (is_null($key) || ! isset($attributes[$key])) { - continue; - } - - $attributes[$key] = $this->serializeDate( - $this->asDateTime($attributes[$key]) - ); - } - - return $attributes; - } - - /** - * Add the mutated attributes to the attributes array. - * - * @param array $attributes - * @param array $mutatedAttributes - * @return array - */ - protected function addMutatedAttributesToArray(array $attributes, array $mutatedAttributes) - { - foreach ($mutatedAttributes as $key) { - // We want to spin through all the mutated attributes for this model and call - // the mutator for the attribute. We cache off every mutated attributes so - // we don't have to constantly check on attributes that actually change. - if (! array_key_exists($key, $attributes)) { - continue; - } - - // Next, we will call the mutator for this attribute so that we can get these - // mutated attribute's actual values. After we finish mutating each of the - // attributes we will return this final array of the mutated attributes. - $attributes[$key] = $this->mutateAttributeForArray( - $key, $attributes[$key] - ); - } - - return $attributes; - } - - /** - * Add the casted attributes to the attributes array. - * - * @param array $attributes - * @param array $mutatedAttributes - * @return array - */ - protected function addCastAttributesToArray(array $attributes, array $mutatedAttributes) - { - foreach ($this->getCasts() as $key => $value) { - if (! array_key_exists($key, $attributes) || - in_array($key, $mutatedAttributes)) { - continue; - } - - // Here we will cast the attribute. Then, if the cast is a date or datetime cast - // then we will serialize the date for the array. This will convert the dates - // to strings based on the date format specified for these Eloquent models. - $attributes[$key] = $this->castAttribute( - $key, $attributes[$key] - ); - - // If the attribute cast was a date or a datetime, we will serialize the date as - // a string. This allows the developers to customize how dates are serialized - // into an array without affecting how they are persisted into the storage. - if (isset($attributes[$key]) && in_array($value, ['date', 'datetime', 'immutable_date', 'immutable_datetime'])) { - $attributes[$key] = $this->serializeDate($attributes[$key]); - } - - if (isset($attributes[$key]) && ($this->isCustomDateTimeCast($value) || - $this->isImmutableCustomDateTimeCast($value))) { - $attributes[$key] = $attributes[$key]->format(explode(':', $value, 2)[1]); - } - - if ($attributes[$key] instanceof DateTimeInterface && - $this->isClassCastable($key)) { - $attributes[$key] = $this->serializeDate($attributes[$key]); - } - - if (isset($attributes[$key]) && $this->isClassSerializable($key)) { - $attributes[$key] = $this->serializeClassCastableAttribute($key, $attributes[$key]); - } - - if ($this->isEnumCastable($key) && (! ($attributes[$key] ?? null) instanceof Arrayable)) { - $attributes[$key] = isset($attributes[$key]) ? $this->getStorableEnumValue($this->getCasts()[$key], $attributes[$key]) : null; - } - - if ($attributes[$key] instanceof Arrayable) { - $attributes[$key] = $attributes[$key]->toArray(); - } - } - - return $attributes; - } - - /** - * Get an attribute array of all arrayable attributes. - * - * @return array - */ - protected function getArrayableAttributes() - { - return $this->getArrayableItems($this->getAttributes()); - } - - /** - * Get all of the appendable values that are arrayable. - * - * @return array - */ - protected function getArrayableAppends() - { - $appends = $this->getAppends(); - - if (! count($appends)) { - return []; - } - - return $this->getArrayableItems( - array_combine($appends, $appends) - ); - } - - /** - * Get the model's relationships in array form. - * - * @return array - */ - public function relationsToArray() - { - $attributes = []; - - foreach ($this->getArrayableRelations() as $key => $value) { - // If the values implement the Arrayable interface we can just call this - // toArray method on the instances which will convert both models and - // collections to their proper array form and we'll set the values. - if ($value instanceof Arrayable) { - $relation = $value->toArray(); - } - - // If the value is null, we'll still go ahead and set it in this list of - // attributes, since null is used to represent empty relationships if - // it has a has one or belongs to type relationships on the models. - elseif (is_null($value)) { - $relation = $value; - } - - // If the relationships snake-casing is enabled, we will snake case this - // key so that the relation attribute is snake cased in this returned - // array to the developers, making this consistent with attributes. - if (static::$snakeAttributes) { - $key = Str::snake($key); - } - - // If the relation value has been set, we will set it on this attributes - // list for returning. If it was not arrayable or null, we'll not set - // the value on the array because it is some type of invalid value. - if (array_key_exists('relation', get_defined_vars())) { // check if $relation is in scope (could be null) - $attributes[$key] = $relation ?? null; - } - - unset($relation); - } - - return $attributes; - } - - /** - * Get an attribute array of all arrayable relations. - * - * @return array - */ - protected function getArrayableRelations() - { - return $this->getArrayableItems($this->relations); - } - - /** - * Get an attribute array of all arrayable values. - * - * @param array $values - * @return array - */ - protected function getArrayableItems(array $values) - { - if (count($this->getVisible()) > 0) { - $values = array_intersect_key($values, array_flip($this->getVisible())); - } - - if (count($this->getHidden()) > 0) { - $values = array_diff_key($values, array_flip($this->getHidden())); - } - - return $values; - } - - /** - * Determine whether an attribute exists on the model. - * - * @param string $key - * @return bool - */ - public function hasAttribute($key) - { - if (! $key) { - return false; - } - - return array_key_exists($key, $this->attributes) || - array_key_exists($key, $this->casts) || - $this->hasGetMutator($key) || - $this->hasAttributeMutator($key) || - $this->isClassCastable($key); - } - - /** - * Get an attribute from the model. - * - * @param string $key - * @return mixed - */ - public function getAttribute($key) - { - if (! $key) { - return; - } - - // If the attribute exists in the attribute array or has a "get" mutator we will - // get the attribute's value. Otherwise, we will proceed as if the developers - // are asking for a relationship's value. This covers both types of values. - if ($this->hasAttribute($key)) { - return $this->getAttributeValue($key); - } - - // Here we will determine if the model base class itself contains this given key - // since we don't want to treat any of those methods as relationships because - // they are all intended as helper methods and none of these are relations. - if (method_exists(self::class, $key)) { - return $this->throwMissingAttributeExceptionIfApplicable($key); - } - - return $this->isRelation($key) || $this->relationLoaded($key) - ? $this->getRelationValue($key) - : $this->throwMissingAttributeExceptionIfApplicable($key); - } - - /** - * Either throw a missing attribute exception or return null depending on Eloquent's configuration. - * - * @param string $key - * @return null - * - * @throws \Illuminate\Database\Eloquent\MissingAttributeException - */ - protected function throwMissingAttributeExceptionIfApplicable($key) - { - if ($this->exists && - ! $this->wasRecentlyCreated && - static::preventsAccessingMissingAttributes()) { - if (isset(static::$missingAttributeViolationCallback)) { - return call_user_func(static::$missingAttributeViolationCallback, $this, $key); - } - - throw new MissingAttributeException($this, $key); - } - - return null; - } - - /** - * Get a plain attribute (not a relationship). - * - * @param string $key - * @return mixed - */ - public function getAttributeValue($key) - { - return $this->transformModelValue($key, $this->getAttributeFromArray($key)); - } - - /** - * Get an attribute from the $attributes array. - * - * @param string $key - * @return mixed - */ - protected function getAttributeFromArray($key) - { - $this->mergeAttributeFromCachedCasts($key); - - return $this->attributes[$key] ?? null; - } - - /** - * Get a relationship. - * - * @param string $key - * @return mixed - */ - public function getRelationValue($key) - { - // If the key already exists in the relationships array, it just means the - // relationship has already been loaded, so we'll just return it out of - // here because there is no need to query within the relations twice. - if ($this->relationLoaded($key)) { - return $this->relations[$key]; - } - - if (! $this->isRelation($key)) { - return; - } - - if ($this->attemptToAutoloadRelation($key)) { - return $this->relations[$key]; - } - - if ($this->preventsLazyLoading) { - $this->handleLazyLoadingViolation($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. - 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) - { - if ($this->hasAttributeMutator($key)) { - return false; - } - - return method_exists($this, $key) || - $this->relationResolver(static::class, $key); - } - - /** - * Handle a lazy loading violation. - * - * @param string $key - * @return mixed - * - * @throws \Illuminate\Database\LazyLoadingViolationException - */ - protected function handleLazyLoadingViolation($key) - { - if (isset(static::$lazyLoadingViolationCallback)) { - return call_user_func(static::$lazyLoadingViolationCallback, $this, $key); - } - - if (! $this->exists || $this->wasRecentlyCreated) { - return; - } - - throw new LazyLoadingViolationException($this, $key); - } - - /** - * Get a relationship value from a method. - * - * @param string $method - * @return mixed - * - * @throws \LogicException - */ - protected function getRelationshipFromMethod($method) - { - $relation = $this->$method(); - - if (! $relation instanceof Relation) { - if (is_null($relation)) { - throw new LogicException(sprintf( - '%s::%s must return a relationship instance, but "null" was returned. Was the "return" keyword used?', static::class, $method - )); - } - - throw new LogicException(sprintf( - '%s::%s must return a relationship instance.', static::class, $method - )); - } - - return tap($relation->getResults(), function ($results) use ($method) { - $this->setRelation($method, $results); - }); - } - - /** - * Determine if a get mutator exists for an attribute. - * - * @param string $key - * @return bool - */ - public function hasGetMutator($key) - { - return method_exists($this, 'get'.Str::studly($key).'Attribute'); - } - - /** - * Determine if a "Attribute" return type marked mutator exists for an attribute. - * - * @param string $key - * @return bool - */ - public function hasAttributeMutator($key) - { - if (isset(static::$attributeMutatorCache[get_class($this)][$key])) { - return static::$attributeMutatorCache[get_class($this)][$key]; - } - - if (! method_exists($this, $method = Str::camel($key))) { - return static::$attributeMutatorCache[get_class($this)][$key] = false; - } - - $returnType = (new ReflectionMethod($this, $method))->getReturnType(); - - return static::$attributeMutatorCache[get_class($this)][$key] = - $returnType instanceof ReflectionNamedType && - $returnType->getName() === Attribute::class; - } - - /** - * Determine if a "Attribute" return type marked get mutator exists for an attribute. - * - * @param string $key - * @return bool - */ - public function hasAttributeGetMutator($key) - { - if (isset(static::$getAttributeMutatorCache[get_class($this)][$key])) { - return static::$getAttributeMutatorCache[get_class($this)][$key]; - } - - if (! $this->hasAttributeMutator($key)) { - return static::$getAttributeMutatorCache[get_class($this)][$key] = false; - } - - return static::$getAttributeMutatorCache[get_class($this)][$key] = is_callable($this->{Str::camel($key)}()->get); - } - - /** - * Determine if any get mutator exists for an attribute. - * - * @param string $key - * @return bool - */ - public function hasAnyGetMutator($key) - { - return $this->hasGetMutator($key) || $this->hasAttributeGetMutator($key); - } - - /** - * Get the value of an attribute using its mutator. - * - * @param string $key - * @param mixed $value - * @return mixed - */ - protected function mutateAttribute($key, $value) - { - $this->mergeAttributesFromCachedCasts(); - - return $this->{'get'.Str::studly($key).'Attribute'}($value); - } - - /** - * Get the value of an "Attribute" return type marked attribute using its mutator. - * - * @param string $key - * @param mixed $value - * @return mixed - */ - protected function mutateAttributeMarkedAttribute($key, $value) - { - if (array_key_exists($key, $this->attributeCastCache)) { - return $this->attributeCastCache[$key]; - } - - $this->mergeAttributesFromCachedCasts(); - - $attribute = $this->{Str::camel($key)}(); - - $value = call_user_func($attribute->get ?: function ($value) { - return $value; - }, $value, $this->attributes); - - if ($attribute->withCaching || (is_object($value) && $attribute->withObjectCaching)) { - $this->attributeCastCache[$key] = $value; - } else { - unset($this->attributeCastCache[$key]); - } - - return $value; - } - - /** - * Get the value of an attribute using its mutator for array conversion. - * - * @param string $key - * @param mixed $value - * @return mixed - */ - protected function mutateAttributeForArray($key, $value) - { - if ($this->isClassCastable($key)) { - $value = $this->getClassCastableAttributeValue($key, $value); - } elseif (isset(static::$getAttributeMutatorCache[get_class($this)][$key]) && - static::$getAttributeMutatorCache[get_class($this)][$key] === true) { - $value = $this->mutateAttributeMarkedAttribute($key, $value); - - $value = $value instanceof DateTimeInterface - ? $this->serializeDate($value) - : $value; - } else { - $value = $this->mutateAttribute($key, $value); - } - - return $value instanceof Arrayable ? $value->toArray() : $value; - } - - /** - * Merge new casts with existing casts on the model. - * - * @param array $casts - * @return $this - */ - public function mergeCasts($casts) - { - $casts = $this->ensureCastsAreStringValues($casts); - - $this->casts = array_merge($this->casts, $casts); - - return $this; - } - - /** - * Ensure that the given casts are strings. - * - * @param array $casts - * @return array - * - * @throws \InvalidArgumentException - */ - protected function ensureCastsAreStringValues($casts) - { - foreach ($casts as $attribute => $cast) { - $casts[$attribute] = match (true) { - is_object($cast) => value(function () use ($cast, $attribute) { - if ($cast instanceof Stringable) { - return (string) $cast; - } - - throw new InvalidArgumentException( - "The cast object for the {$attribute} attribute must implement Stringable." - ); - }), - is_array($cast) => value(function () use ($cast) { - if (count($cast) === 1) { - return $cast[0]; - } - - [$cast, $arguments] = [array_shift($cast), $cast]; - - return $cast.':'.implode(',', $arguments); - }), - default => $cast, - }; - } - - return $casts; - } - - /** - * Cast an attribute to a native PHP type. - * - * @param string $key - * @param mixed $value - * @return mixed - */ - protected function castAttribute($key, $value) - { - $castType = $this->getCastType($key); - - if (is_null($value) && in_array($castType, static::$primitiveCastTypes)) { - return $value; - } - - // If the key is one of the encrypted castable types, we'll first decrypt - // the value and update the cast type so we may leverage the following - // logic for casting this value to any additionally specified types. - if ($this->isEncryptedCastable($key)) { - $value = $this->fromEncryptedString($value); - - $castType = Str::after($castType, 'encrypted:'); - } - - switch ($castType) { - case 'int': - case 'integer': - return (int) $value; - case 'real': - case 'float': - case 'double': - return $this->fromFloat($value); - case 'decimal': - return $this->asDecimal($value, explode(':', $this->getCasts()[$key], 2)[1]); - case 'string': - return (string) $value; - case 'bool': - case 'boolean': - return (bool) $value; - case 'object': - return $this->fromJson($value, true); - case 'array': - case 'json': - case 'json:unicode': - return $this->fromJson($value); - case 'collection': - return new BaseCollection($this->fromJson($value)); - case 'date': - return $this->asDate($value); - case 'datetime': - case 'custom_datetime': - return $this->asDateTime($value); - case 'immutable_date': - return $this->asDate($value)->toImmutable(); - case 'immutable_custom_datetime': - case 'immutable_datetime': - return $this->asDateTime($value)->toImmutable(); - case 'timestamp': - return $this->asTimestamp($value); - } - - if ($this->isEnumCastable($key)) { - return $this->getEnumCastableAttributeValue($key, $value); - } - - if ($this->isClassCastable($key)) { - return $this->getClassCastableAttributeValue($key, $value); - } - - return $value; - } - - /** - * Cast the given attribute using a custom cast class. - * - * @param string $key - * @param mixed $value - * @return mixed - */ - protected function getClassCastableAttributeValue($key, $value) - { - $caster = $this->resolveCasterClass($key); - - $objectCachingDisabled = $caster->withoutObjectCaching ?? false; - - if (isset($this->classCastCache[$key]) && ! $objectCachingDisabled) { - return $this->classCastCache[$key]; - } else { - $value = $caster instanceof CastsInboundAttributes - ? $value - : $caster->get($this, $key, $value, $this->attributes); - - if ($caster instanceof CastsInboundAttributes || - ! is_object($value) || - $objectCachingDisabled) { - unset($this->classCastCache[$key]); - } else { - $this->classCastCache[$key] = $value; - } - - return $value; - } - } - - /** - * Cast the given attribute to an enum. - * - * @param string $key - * @param mixed $value - * @return mixed - */ - protected function getEnumCastableAttributeValue($key, $value) - { - if (is_null($value)) { - return; - } - - $castType = $this->getCasts()[$key]; - - if ($value instanceof $castType) { - return $value; - } - - return $this->getEnumCaseFromValue($castType, $value); - } - - /** - * Get the type of cast for a model attribute. - * - * @param string $key - * @return string - */ - protected function getCastType($key) - { - $castType = $this->getCasts()[$key]; - - if (isset(static::$castTypeCache[$castType])) { - return static::$castTypeCache[$castType]; - } - - if ($this->isCustomDateTimeCast($castType)) { - $convertedCastType = 'custom_datetime'; - } elseif ($this->isImmutableCustomDateTimeCast($castType)) { - $convertedCastType = 'immutable_custom_datetime'; - } elseif ($this->isDecimalCast($castType)) { - $convertedCastType = 'decimal'; - } elseif (class_exists($castType)) { - $convertedCastType = $castType; - } else { - $convertedCastType = trim(strtolower($castType)); - } - - return static::$castTypeCache[$castType] = $convertedCastType; - } - - /** - * Increment or decrement the given attribute using the custom cast class. - * - * @param string $method - * @param string $key - * @param mixed $value - * @return mixed - */ - protected function deviateClassCastableAttribute($method, $key, $value) - { - return $this->resolveCasterClass($key)->{$method}( - $this, $key, $value, $this->attributes - ); - } - - /** - * Serialize the given attribute using the custom cast class. - * - * @param string $key - * @param mixed $value - * @return mixed - */ - protected function serializeClassCastableAttribute($key, $value) - { - return $this->resolveCasterClass($key)->serialize( - $this, $key, $value, $this->attributes - ); - } - - /** - * Compare two values for the given attribute using the custom cast class. - * - * @param string $key - * @param mixed $original - * @param mixed $value - * @return bool - */ - protected function compareClassCastableAttribute($key, $original, $value) - { - return $this->resolveCasterClass($key)->compare( - $this, $key, $original, $value - ); - } - - /** - * Determine if the cast type is a custom date time cast. - * - * @param string $cast - * @return bool - */ - protected function isCustomDateTimeCast($cast) - { - return str_starts_with($cast, 'date:') || - str_starts_with($cast, 'datetime:'); - } - - /** - * Determine if the cast type is an immutable custom date time cast. - * - * @param string $cast - * @return bool - */ - protected function isImmutableCustomDateTimeCast($cast) - { - return str_starts_with($cast, 'immutable_date:') || - str_starts_with($cast, 'immutable_datetime:'); - } - - /** - * Determine if the cast type is a decimal cast. - * - * @param string $cast - * @return bool - */ - protected function isDecimalCast($cast) - { - return str_starts_with($cast, 'decimal:'); - } - - /** - * Set a given attribute on the model. - * - * @param string $key - * @param mixed $value - * @return mixed - */ - public function setAttribute($key, $value) - { - // First we will check for the presence of a mutator for the set operation - // which simply lets the developers tweak the attribute as it is set on - // this model, such as "json_encoding" a listing of data for storage. - if ($this->hasSetMutator($key)) { - return $this->setMutatedAttributeValue($key, $value); - } elseif ($this->hasAttributeSetMutator($key)) { - return $this->setAttributeMarkedMutatedAttributeValue($key, $value); - } - - // If an attribute is listed as a "date", we'll convert it from a DateTime - // instance into a form proper for storage on the database tables using - // the connection grammar's date format. We will auto set the values. - elseif (! is_null($value) && $this->isDateAttribute($key)) { - $value = $this->fromDateTime($value); - } - - if ($this->isEnumCastable($key)) { - $this->setEnumCastableAttribute($key, $value); - - return $this; - } - - if ($this->isClassCastable($key)) { - $this->setClassCastableAttribute($key, $value); - - return $this; - } - - if (! is_null($value) && $this->isJsonCastable($key)) { - $value = $this->castAttributeAsJson($key, $value); - } - - // If this attribute contains a JSON ->, we'll set the proper value in the - // attribute's underlying array. This takes care of properly nesting an - // attribute in the array's value in the case of deeply nested items. - if (str_contains($key, '->')) { - return $this->fillJsonAttribute($key, $value); - } - - if (! is_null($value) && $this->isEncryptedCastable($key)) { - $value = $this->castAttributeAsEncryptedString($key, $value); - } - - if (! is_null($value) && $this->hasCast($key, 'hashed')) { - $value = $this->castAttributeAsHashedString($key, $value); - } - - $this->attributes[$key] = $value; - - return $this; - } - - /** - * Determine if a set mutator exists for an attribute. - * - * @param string $key - * @return bool - */ - public function hasSetMutator($key) - { - return method_exists($this, 'set'.Str::studly($key).'Attribute'); - } - - /** - * Determine if an "Attribute" return type marked set mutator exists for an attribute. - * - * @param string $key - * @return bool - */ - public function hasAttributeSetMutator($key) - { - $class = get_class($this); - - if (isset(static::$setAttributeMutatorCache[$class][$key])) { - return static::$setAttributeMutatorCache[$class][$key]; - } - - if (! method_exists($this, $method = Str::camel($key))) { - return static::$setAttributeMutatorCache[$class][$key] = false; - } - - $returnType = (new ReflectionMethod($this, $method))->getReturnType(); - - return static::$setAttributeMutatorCache[$class][$key] = - $returnType instanceof ReflectionNamedType && - $returnType->getName() === Attribute::class && - is_callable($this->{$method}()->set); - } - - /** - * Set the value of an attribute using its mutator. - * - * @param string $key - * @param mixed $value - * @return mixed - */ - protected function setMutatedAttributeValue($key, $value) - { - $this->mergeAttributesFromCachedCasts(); - - return $this->{'set'.Str::studly($key).'Attribute'}($value); - } - - /** - * Set the value of a "Attribute" return type marked attribute using its mutator. - * - * @param string $key - * @param mixed $value - * @return mixed - */ - protected function setAttributeMarkedMutatedAttributeValue($key, $value) - { - $this->mergeAttributesFromCachedCasts(); - - $attribute = $this->{Str::camel($key)}(); - - $callback = $attribute->set ?: function ($value) use ($key) { - $this->attributes[$key] = $value; - }; - - $this->attributes = array_merge( - $this->attributes, - $this->normalizeCastClassResponse( - $key, $callback($value, $this->attributes) - ) - ); - - if ($attribute->withCaching || (is_object($value) && $attribute->withObjectCaching)) { - $this->attributeCastCache[$key] = $value; - } else { - unset($this->attributeCastCache[$key]); - } - - return $this; - } - - /** - * Determine if the given attribute is a date or date castable. - * - * @param string $key - * @return bool - */ - protected function isDateAttribute($key) - { - return in_array($key, $this->getDates(), true) || - $this->isDateCastable($key); - } - - /** - * Set a given JSON attribute on the model. - * - * @param string $key - * @param mixed $value - * @return $this - */ - public function fillJsonAttribute($key, $value) - { - [$key, $path] = explode('->', $key, 2); - - $value = $this->asJson($this->getArrayAttributeWithValue( - $path, $key, $value - ), $this->getJsonCastFlags($key)); - - $this->attributes[$key] = $this->isEncryptedCastable($key) - ? $this->castAttributeAsEncryptedString($key, $value) - : $value; - - if ($this->isClassCastable($key)) { - unset($this->classCastCache[$key]); - } - - return $this; - } - - /** - * Set the value of a class castable attribute. - * - * @param string $key - * @param mixed $value - * @return void - */ - protected function setClassCastableAttribute($key, $value) - { - $caster = $this->resolveCasterClass($key); - - $this->attributes = array_replace( - $this->attributes, - $this->normalizeCastClassResponse($key, $caster->set( - $this, $key, $value, $this->attributes - )) - ); - - if ($caster instanceof CastsInboundAttributes || - ! is_object($value) || - ($caster->withoutObjectCaching ?? false)) { - unset($this->classCastCache[$key]); - } else { - $this->classCastCache[$key] = $value; - } - } - - /** - * Set the value of an enum castable attribute. - * - * @param string $key - * @param \UnitEnum|string|int|null $value - * @return void - */ - protected function setEnumCastableAttribute($key, $value) - { - $enumClass = $this->getCasts()[$key]; - - if (! isset($value)) { - $this->attributes[$key] = null; - } elseif (is_object($value)) { - $this->attributes[$key] = $this->getStorableEnumValue($enumClass, $value); - } else { - $this->attributes[$key] = $this->getStorableEnumValue( - $enumClass, $this->getEnumCaseFromValue($enumClass, $value) - ); - } - } - - /** - * Get an enum case instance from a given class and value. - * - * @param string $enumClass - * @param string|int $value - * @return \UnitEnum - */ - protected function getEnumCaseFromValue($enumClass, $value) - { - return is_subclass_of($enumClass, BackedEnum::class) - ? $enumClass::from($value) - : constant($enumClass.'::'.$value); - } - - /** - * Get the storable value from the given enum. - * - * @param string $expectedEnum - * @param \UnitEnum $value - * @return string|int - * - * @throws \ValueError - */ - protected function getStorableEnumValue($expectedEnum, $value) - { - if (! $value instanceof $expectedEnum) { - throw new ValueError(sprintf('Value [%s] is not of the expected enum type [%s].', var_export($value, true), $expectedEnum)); - } - - return enum_value($value); - } - - /** - * Get an array attribute with the given key and value set. - * - * @param string $path - * @param string $key - * @param mixed $value - * @return array - */ - protected function getArrayAttributeWithValue($path, $key, $value) - { - return tap($this->getArrayAttributeByKey($key), function (&$array) use ($path, $value) { - Arr::set($array, str_replace('->', '.', $path), $value); - }); - } - - /** - * Get an array attribute or return an empty array if it is not set. - * - * @param string $key - * @return array - */ - protected function getArrayAttributeByKey($key) - { - if (! isset($this->attributes[$key])) { - return []; - } - - return $this->fromJson( - $this->isEncryptedCastable($key) - ? $this->fromEncryptedString($this->attributes[$key]) - : $this->attributes[$key] - ); - } - - /** - * Cast the given attribute to JSON. - * - * @param string $key - * @param mixed $value - * @return string - * - * @throws \Illuminate\Database\Eloquent\JsonEncodingException - */ - protected function castAttributeAsJson($key, $value) - { - $value = $this->asJson($value, $this->getJsonCastFlags($key)); - - if ($value === false) { - throw JsonEncodingException::forAttribute( - $this, $key, json_last_error_msg() - ); - } - - return $value; - } - - /** - * Get the JSON casting flags for the given attribute. - * - * @param string $key - * @return int - */ - protected function getJsonCastFlags($key) - { - $flags = 0; - - if ($this->hasCast($key, ['json:unicode'])) { - $flags |= JSON_UNESCAPED_UNICODE; - } - - return $flags; - } - - /** - * Encode the given value as JSON. - * - * @param mixed $value - * @param int $flags - * @return string - */ - protected function asJson($value, $flags = 0) - { - return Json::encode($value, $flags); - } - - /** - * Decode the given JSON back into an array or object. - * - * @param string|null $value - * @param bool $asObject - * @return mixed - */ - public function fromJson($value, $asObject = false) - { - if ($value === null || $value === '') { - return null; - } - - return Json::decode($value, ! $asObject); - } - - /** - * Decrypt the given encrypted string. - * - * @param string $value - * @return mixed - */ - public function fromEncryptedString($value) - { - return static::currentEncrypter()->decrypt($value, false); - } - - /** - * Cast the given attribute to an encrypted string. - * - * @param string $key - * @param mixed $value - * @return string - */ - protected function castAttributeAsEncryptedString($key, #[\SensitiveParameter] $value) - { - return static::currentEncrypter()->encrypt($value, false); - } - - /** - * Set the encrypter instance that will be used to encrypt attributes. - * - * @param \Illuminate\Contracts\Encryption\Encrypter|null $encrypter - * @return void - */ - public static function encryptUsing($encrypter) - { - static::$encrypter = $encrypter; - } - - /** - * Get the current encrypter being used by the model. - * - * @return \Illuminate\Contracts\Encryption\Encrypter - */ - public static function currentEncrypter() - { - return static::$encrypter ?? Crypt::getFacadeRoot(); - } - - /** - * Cast the given attribute to a hashed string. - * - * @param string $key - * @param mixed $value - * @return string - * - * @throws \RuntimeException - */ - protected function castAttributeAsHashedString($key, #[\SensitiveParameter] $value) - { - if ($value === null) { - return null; - } - - if (! Hash::isHashed($value)) { - return Hash::make($value); - } - - /** @phpstan-ignore staticMethod.notFound */ - if (! Hash::verifyConfiguration($value)) { - throw new RuntimeException("Could not verify the hashed value's configuration."); - } - - return $value; - } - - /** - * Decode the given float. - * - * @param mixed $value - * @return mixed - */ - public function fromFloat($value) - { - return match ((string) $value) { - 'Infinity' => INF, - '-Infinity' => -INF, - 'NaN' => NAN, - default => (float) $value, - }; - } - - /** - * Return a decimal as string. - * - * @param float|string $value - * @param int $decimals - * @return string - * - * @throws \Illuminate\Support\Exceptions\MathException - */ - protected function asDecimal($value, $decimals) - { - try { - return (string) BigDecimal::of((string) $value)->toScale($decimals, RoundingMode::HALF_UP); - } catch (BrickMathException $e) { - throw new MathException('Unable to cast value to a decimal.', previous: $e); - } - } - - /** - * Return a timestamp as DateTime object with time set to 00:00:00. - * - * @param mixed $value - * @return \Illuminate\Support\Carbon - */ - protected function asDate($value) - { - return $this->asDateTime($value)->startOfDay(); - } - - /** - * Return a timestamp as DateTime object. - * - * @param mixed $value - * @return \Illuminate\Support\Carbon - */ - protected function asDateTime($value) - { - // If this value is already a Carbon instance, we shall just return it as is. - // This prevents us having to re-instantiate a Carbon instance when we know - // it already is one, which wouldn't be fulfilled by the DateTime check. - if ($value instanceof CarbonInterface) { - return Date::instance($value); - } - - // If the value is already a DateTime instance, we will just skip the rest of - // these checks since they will be a waste of time, and hinder performance - // when checking the field. We will just return the DateTime right away. - if ($value instanceof DateTimeInterface) { - return Date::parse( - $value->format('Y-m-d H:i:s.u'), $value->getTimezone() - ); - } - - // If this value is an integer, we will assume it is a UNIX timestamp's value - // and format a Carbon object from this timestamp. This allows flexibility - // when defining your date fields as they might be UNIX timestamps here. - if (is_numeric($value)) { - return Date::createFromTimestamp($value, date_default_timezone_get()); - } - - // If the value is in simply year, month, day format, we will instantiate the - // Carbon instances from that format. Again, this provides for simple date - // fields on the database, while still supporting Carbonized conversion. - if ($this->isStandardDateFormat($value)) { - return Date::instance(Carbon::createFromFormat('Y-m-d', $value)->startOfDay()); - } - - $format = $this->getDateFormat(); - - // Finally, we will just assume this date is in the format used by default on - // the database connection and use that format to create the Carbon object - // that is returned back out to the developers after we convert it here. - try { - $date = Date::createFromFormat($format, $value); - } catch (InvalidArgumentException) { - $date = false; - } - - return $date ?: Date::parse($value); - } - - /** - * Determine if the given value is a standard date format. - * - * @param string $value - * @return bool - */ - protected function isStandardDateFormat($value) - { - return preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $value); - } - - /** - * Convert a DateTime to a storable string. - * - * @param mixed $value - * @return string|null - */ - public function fromDateTime($value) - { - return empty($value) ? $value : $this->asDateTime($value)->format( - $this->getDateFormat() - ); - } - - /** - * Return a timestamp as unix timestamp. - * - * @param mixed $value - * @return int - */ - protected function asTimestamp($value) - { - return $this->asDateTime($value)->getTimestamp(); - } - - /** - * Prepare a date for array / JSON serialization. - * - * @param \DateTimeInterface $date - * @return string - */ - protected function serializeDate(DateTimeInterface $date) - { - return $date instanceof DateTimeImmutable ? - CarbonImmutable::instance($date)->toJSON() : - Carbon::instance($date)->toJSON(); - } - - /** - * Get the attributes that should be converted to dates. - * - * @return array - */ - public function getDates() - { - return $this->usesTimestamps() ? [ - $this->getCreatedAtColumn(), - $this->getUpdatedAtColumn(), - ] : []; - } - - /** - * Get the format for database stored dates. - * - * @return string - */ - public function getDateFormat() - { - return $this->dateFormat ?: $this->getConnection()->getQueryGrammar()->getDateFormat(); - } - - /** - * Set the date format used by the model. - * - * @param string $format - * @return $this - */ - public function setDateFormat($format) - { - $this->dateFormat = $format; - - return $this; - } - - /** - * Determine whether an attribute should be cast to a native type. - * - * @param string $key - * @param array|string|null $types - * @return bool - */ - public function hasCast($key, $types = null) - { - if (array_key_exists($key, $this->getCasts())) { - return $types ? in_array($this->getCastType($key), (array) $types, true) : true; - } - - return false; - } - - /** - * Get the attributes that should be cast. - * - * @return array - */ - public function getCasts() - { - if ($this->getIncrementing()) { - return array_merge([$this->getKeyName() => $this->getKeyType()], $this->casts); - } - - return $this->casts; - } - - /** - * Get the attributes that should be cast. - * - * @return array - */ - protected function casts() - { - return []; - } - - /** - * Determine whether a value is Date / DateTime castable for inbound manipulation. - * - * @param string $key - * @return bool - */ - protected function isDateCastable($key) - { - return $this->hasCast($key, ['date', 'datetime', 'immutable_date', 'immutable_datetime']); - } - - /** - * Determine whether a value is Date / DateTime custom-castable for inbound manipulation. - * - * @param string $key - * @return bool - */ - protected function isDateCastableWithCustomFormat($key) - { - return $this->hasCast($key, ['custom_datetime', 'immutable_custom_datetime']); - } - - /** - * Determine whether a value is JSON castable for inbound manipulation. - * - * @param string $key - * @return bool - */ - protected function isJsonCastable($key) - { - return $this->hasCast($key, ['array', 'json', 'json:unicode', 'object', 'collection', 'encrypted:array', 'encrypted:collection', 'encrypted:json', 'encrypted:object']); - } - - /** - * Determine whether a value is an encrypted castable for inbound manipulation. - * - * @param string $key - * @return bool - */ - protected function isEncryptedCastable($key) - { - return $this->hasCast($key, ['encrypted', 'encrypted:array', 'encrypted:collection', 'encrypted:json', 'encrypted:object']); - } - - /** - * Determine if the given key is cast using a custom class. - * - * @param string $key - * @return bool - * - * @throws \Illuminate\Database\Eloquent\InvalidCastException - */ - protected function isClassCastable($key) - { - $casts = $this->getCasts(); - - if (! array_key_exists($key, $casts)) { - return false; - } - - $castType = $this->parseCasterClass($casts[$key]); - - if (in_array($castType, static::$primitiveCastTypes)) { - return false; - } - - if (class_exists($castType)) { - return true; - } - - throw new InvalidCastException($this->getModel(), $key, $castType); - } - - /** - * Determine if the given key is cast using an enum. - * - * @param string $key - * @return bool - */ - protected function isEnumCastable($key) - { - $casts = $this->getCasts(); - - if (! array_key_exists($key, $casts)) { - return false; - } - - $castType = $casts[$key]; - - if (in_array($castType, static::$primitiveCastTypes)) { - return false; - } - - if (is_subclass_of($castType, Castable::class)) { - return false; - } - - return enum_exists($castType); - } - - /** - * Determine if the key is deviable using a custom class. - * - * @param string $key - * @return bool - * - * @throws \Illuminate\Database\Eloquent\InvalidCastException - */ - protected function isClassDeviable($key) - { - if (! $this->isClassCastable($key)) { - return false; - } - - $castType = $this->resolveCasterClass($key); - - return method_exists($castType::class, 'increment') && method_exists($castType::class, 'decrement'); - } - - /** - * Determine if the key is serializable using a custom class. - * - * @param string $key - * @return bool - * - * @throws \Illuminate\Database\Eloquent\InvalidCastException - */ - protected function isClassSerializable($key) - { - return ! $this->isEnumCastable($key) && - $this->isClassCastable($key) && - method_exists($this->resolveCasterClass($key), 'serialize'); - } - - /** - * Determine if the key is comparable using a custom class. - * - * @param string $key - * @return bool - */ - protected function isClassComparable($key) - { - return ! $this->isEnumCastable($key) && - $this->isClassCastable($key) && - method_exists($this->resolveCasterClass($key), 'compare'); - } - - /** - * Resolve the custom caster class for a given key. - * - * @param string $key - * @return mixed - */ - protected function resolveCasterClass($key) - { - $castType = $this->getCasts()[$key]; - - $arguments = []; - - if (is_string($castType) && str_contains($castType, ':')) { - $segments = explode(':', $castType, 2); - - $castType = $segments[0]; - $arguments = explode(',', $segments[1]); - } - - if (is_subclass_of($castType, Castable::class)) { - $castType = $castType::castUsing($arguments); - } - - if (is_object($castType)) { - return $castType; - } - - return new $castType(...$arguments); - } - - /** - * Parse the given caster class, removing any arguments. - * - * @param string $class - * @return string - */ - protected function parseCasterClass($class) - { - return ! str_contains($class, ':') - ? $class - : explode(':', $class, 2)[0]; - } - - /** - * Merge the cast class and attribute cast attributes back into the model. - * - * @return void - */ - protected function mergeAttributesFromCachedCasts() - { - $this->mergeAttributesFromClassCasts(); - $this->mergeAttributesFromAttributeCasts(); - } - - /** - * Merge the a cast class and attribute cast attribute back into the model. - * - * @return void - */ - protected function mergeAttributeFromCachedCasts(string $key) - { - $this->mergeAttributeFromClassCasts($key); - $this->mergeAttributeFromAttributeCasts($key); - } - - /** - * Merge the cast class attributes back into the model. - * - * @return void - */ - protected function mergeAttributesFromClassCasts() - { - foreach ($this->classCastCache as $key => $value) { - $this->mergeAttributeFromClassCasts($key); - } - } - - /** - * Merge the cast class attribute back into the model. - * - * @return void - */ - protected function mergeAttributeFromClassCasts(string $key): void - { - if (! isset($this->classCastCache[$key])) { - return; - } - - $value = $this->classCastCache[$key]; - - $caster = $this->resolveCasterClass($key); - - $this->attributes = array_merge( - $this->attributes, - $caster instanceof CastsInboundAttributes - ? [$key => $value] - : $this->normalizeCastClassResponse($key, $caster->set($this, $key, $value, $this->attributes)) - ); - } - - /** - * Merge the cast class attributes back into the model. - * - * @return void - */ - protected function mergeAttributesFromAttributeCasts() - { - foreach ($this->attributeCastCache as $key => $value) { - $this->mergeAttributeFromAttributeCasts($key); - } - } - - /** - * Merge the cast class attribute back into the model. - * - * @return void - */ - protected function mergeAttributeFromAttributeCasts(string $key): void - { - if (! isset($this->attributeCastCache[$key])) { - return; - } - - $value = $this->attributeCastCache[$key]; - - $attribute = $this->{Str::camel($key)}(); - - if ($attribute->get && ! $attribute->set) { - return; - } - - $callback = $attribute->set ?: function ($value) use ($key) { - $this->attributes[$key] = $value; - }; - - $this->attributes = array_merge( - $this->attributes, - $this->normalizeCastClassResponse( - $key, $callback($value, $this->attributes) - ) - ); - } - - /** - * Normalize the response from a custom class caster. - * - * @param string $key - * @param mixed $value - * @return array - */ - protected function normalizeCastClassResponse($key, $value) - { - return is_array($value) ? $value : [$key => $value]; - } - - /** - * Get all of the current attributes on the model. - * - * @return array - */ - public function getAttributes() - { - $this->mergeAttributesFromCachedCasts(); - - return $this->attributes; - } - - /** - * Get all of the current attributes on the model for an insert operation. - * - * @return array - */ - protected function getAttributesForInsert() - { - return $this->getAttributes(); - } - - /** - * Set the array of model attributes. No checking is done. - * - * @param array $attributes - * @param bool $sync - * @return $this - */ - public function setRawAttributes(array $attributes, $sync = false) - { - $this->attributes = $attributes; - - if ($sync) { - $this->syncOriginal(); - } - - $this->classCastCache = []; - $this->attributeCastCache = []; - - return $this; - } - - /** - * Get the model's original attribute values. - * - * @param string|null $key - * @param mixed $default - * @return ($key is null ? array : mixed) - */ - public function getOriginal($key = null, $default = null) - { - return (new static)->setRawAttributes( - $this->original, $sync = true - )->getOriginalWithoutRewindingModel($key, $default); - } - - /** - * Get the model's original attribute values. - * - * @param string|null $key - * @param mixed $default - * @return ($key is null ? array : mixed) - */ - protected function getOriginalWithoutRewindingModel($key = null, $default = null) - { - if ($key) { - return $this->transformModelValue( - $key, Arr::get($this->original, $key, $default) - ); - } - - return (new Collection($this->original)) - ->mapWithKeys(fn ($value, $key) => [$key => $this->transformModelValue($key, $value)]) - ->all(); - } - - /** - * Get the model's raw original attribute values. - * - * @param string|null $key - * @param mixed $default - * @return ($key is null ? array : mixed) - */ - public function getRawOriginal($key = null, $default = null) - { - return Arr::get($this->original, $key, $default); - } - - /** - * Get a subset of the model's attributes. - * - * @param array|mixed $attributes - * @return array - */ - public function only($attributes) - { - $results = []; - - foreach (is_array($attributes) ? $attributes : func_get_args() as $attribute) { - $results[$attribute] = $this->getAttribute($attribute); - } - - return $results; - } - - /** - * Get all attributes except the given ones. - * - * @param array|mixed $attributes - * @return array - */ - public function except($attributes) - { - $attributes = is_array($attributes) ? $attributes : func_get_args(); - - $results = []; - - foreach ($this->getAttributes() as $key => $value) { - if (! in_array($key, $attributes)) { - $results[$key] = $this->getAttribute($key); - } - } - - return $results; - } - - /** - * Sync the original attributes with the current. - * - * @return $this - */ - public function syncOriginal() - { - $this->original = $this->getAttributes(); - - return $this; - } - - /** - * Sync a single original attribute with its current value. - * - * @param string $attribute - * @return $this - */ - public function syncOriginalAttribute($attribute) - { - return $this->syncOriginalAttributes($attribute); - } - - /** - * Sync multiple original attribute with their current values. - * - * @param array|string $attributes - * @return $this - */ - public function syncOriginalAttributes($attributes) - { - $attributes = is_array($attributes) ? $attributes : func_get_args(); - - $modelAttributes = $this->getAttributes(); - - foreach ($attributes as $attribute) { - $this->original[$attribute] = $modelAttributes[$attribute]; - } - - return $this; - } - - /** - * Sync the changed attributes. - * - * @return $this - */ - public function syncChanges() - { - $this->changes = $this->getDirty(); - $this->previous = array_intersect_key($this->getRawOriginal(), $this->changes); - - return $this; - } - - /** - * Determine if the model or any of the given attribute(s) have been modified. - * - * @param array|string|null $attributes - * @return bool - */ - public function isDirty($attributes = null) - { - return $this->hasChanges( - $this->getDirty(), is_array($attributes) ? $attributes : func_get_args() - ); - } - - /** - * Determine if the model or all the given attribute(s) have remained the same. - * - * @param array|string|null $attributes - * @return bool - */ - public function isClean($attributes = null) - { - return ! $this->isDirty(...func_get_args()); - } - - /** - * Discard attribute changes and reset the attributes to their original state. - * - * @return $this - */ - public function discardChanges() - { - [$this->attributes, $this->changes, $this->previous] = [$this->original, [], []]; - - $this->classCastCache = []; - $this->attributeCastCache = []; - - return $this; - } - - /** - * Determine if the model or any of the given attribute(s) were changed when the model was last saved. - * - * @param array|string|null $attributes - * @return bool - */ - public function wasChanged($attributes = null) - { - return $this->hasChanges( - $this->getChanges(), is_array($attributes) ? $attributes : func_get_args() - ); - } - - /** - * Determine if any of the given attributes were changed when the model was last saved. - * - * @param array $changes - * @param array|string|null $attributes - * @return bool - */ - protected function hasChanges($changes, $attributes = null) - { - // If no specific attributes were provided, we will just see if the dirty array - // already contains any attributes. If it does we will just return that this - // count is greater than zero. Else, we need to check specific attributes. - if (empty($attributes)) { - return count($changes) > 0; - } - - // Here we will spin through every attribute and see if this is in the array of - // dirty attributes. If it is, we will return true and if we make it through - // all of the attributes for the entire array we will return false at end. - foreach (Arr::wrap($attributes) as $attribute) { - if (array_key_exists($attribute, $changes)) { - return true; - } - } - - return false; - } - - /** - * Get the attributes that have been changed since the last sync. - * - * @return array - */ - public function getDirty() - { - $dirty = []; - - foreach ($this->getAttributes() as $key => $value) { - if (! $this->originalIsEquivalent($key)) { - $dirty[$key] = $value; - } - } - - return $dirty; - } - - /** - * Get the attributes that have been changed since the last sync for an update operation. - * - * @return array - */ - protected function getDirtyForUpdate() - { - return $this->getDirty(); - } - - /** - * Get the attributes that were changed when the model was last saved. - * - * @return array - */ - public function getChanges() - { - return $this->changes; - } - - /** - * Get the attributes that were previously original before the model was last saved. - * - * @return array - */ - public function getPrevious() - { - return $this->previous; - } - - /** - * Determine if the new and old values for a given key are equivalent. - * - * @param string $key - * @return bool - */ - public function originalIsEquivalent($key) - { - if (! array_key_exists($key, $this->original)) { - return false; - } - - $attribute = Arr::get($this->attributes, $key); - $original = Arr::get($this->original, $key); - - if ($attribute === $original) { - return true; - } elseif (is_null($attribute)) { - return false; - } elseif ($this->isDateAttribute($key) || $this->isDateCastableWithCustomFormat($key)) { - return $this->fromDateTime($attribute) === - $this->fromDateTime($original); - } elseif ($this->hasCast($key, ['object', 'collection'])) { - return $this->fromJson($attribute) === - $this->fromJson($original); - } elseif ($this->hasCast($key, ['real', 'float', 'double'])) { - if ($original === null) { - return false; - } - - return abs($this->castAttribute($key, $attribute) - $this->castAttribute($key, $original)) < PHP_FLOAT_EPSILON * 4; - } elseif ($this->isEncryptedCastable($key) && ! empty(static::currentEncrypter()->getPreviousKeys())) { - return false; - } elseif ($this->hasCast($key, static::$primitiveCastTypes)) { - return $this->castAttribute($key, $attribute) === - $this->castAttribute($key, $original); - } elseif ($this->isClassCastable($key) && Str::startsWith($this->getCasts()[$key], [AsArrayObject::class, AsCollection::class])) { - return $this->fromJson($attribute) === $this->fromJson($original); - } elseif ($this->isClassCastable($key) && Str::startsWith($this->getCasts()[$key], [AsEnumArrayObject::class, AsEnumCollection::class])) { - return $this->fromJson($attribute) === $this->fromJson($original); - } elseif ($this->isClassCastable($key) && $original !== null && Str::startsWith($this->getCasts()[$key], [AsEncryptedArrayObject::class, AsEncryptedCollection::class])) { - if (empty(static::currentEncrypter()->getPreviousKeys())) { - return $this->fromEncryptedString($attribute) === $this->fromEncryptedString($original); - } - - return false; - } elseif ($this->isClassComparable($key)) { - return $this->compareClassCastableAttribute($key, $original, $attribute); - } - - return is_numeric($attribute) && is_numeric($original) - && strcmp((string) $attribute, (string) $original) === 0; - } - - /** - * Transform a raw model value using mutators, casts, etc. - * - * @param string $key - * @param mixed $value - * @return mixed - */ - protected function transformModelValue($key, $value) - { - // If the attribute has a get mutator, we will call that then return what - // it returns as the value, which is useful for transforming values on - // retrieval from the model to a form that is more useful for usage. - if ($this->hasGetMutator($key)) { - return $this->mutateAttribute($key, $value); - } elseif ($this->hasAttributeGetMutator($key)) { - return $this->mutateAttributeMarkedAttribute($key, $value); - } - - // If the attribute exists within the cast array, we will convert it to - // an appropriate native PHP type dependent upon the associated value - // given with the key in the pair. Dayle made this comment line up. - if ($this->hasCast($key)) { - if (static::preventsAccessingMissingAttributes() && - ! array_key_exists($key, $this->attributes) && - ($this->isEnumCastable($key) || - in_array($this->getCastType($key), static::$primitiveCastTypes))) { - $this->throwMissingAttributeExceptionIfApplicable($key); - } - - return $this->castAttribute($key, $value); - } - - // If the attribute is listed as a date, we will convert it to a DateTime - // instance on retrieval, which makes it quite convenient to work with - // date fields without having to create a mutator for each property. - if ($value !== null - && \in_array($key, $this->getDates(), false)) { - return $this->asDateTime($value); - } - - return $value; - } - - /** - * Append attributes to query when building a query. - * - * @param array|string $attributes - * @return $this - */ - public function append($attributes) - { - $this->appends = array_values(array_unique( - array_merge($this->appends, is_string($attributes) ? func_get_args() : $attributes) - )); - - return $this; - } - - /** - * Get the accessors that are being appended to model arrays. - * - * @return array - */ - public function getAppends() - { - return $this->appends; - } - - /** - * Set the accessors to append to model arrays. - * - * @param array $appends - * @return $this - */ - public function setAppends(array $appends) - { - $this->appends = $appends; - - return $this; - } - - /** - * Merge new appended attributes with existing appended attributes on the model. - * - * @param array $appends - * @return $this - */ - public function mergeAppends(array $appends) - { - $this->appends = array_values(array_unique(array_merge($this->appends, $appends))); - - return $this; - } - - /** - * Return whether the accessor attribute has been appended. - * - * @param string $attribute - * @return bool - */ - public function hasAppended($attribute) - { - return in_array($attribute, $this->getAppends()); - } - - /** - * Remove all appended properties from the model. - * - * @return $this - */ - public function withoutAppends() - { - return $this->setAppends([]); - } - - /** - * Get the mutated attributes for a given instance. - * - * @return array - */ - public function getMutatedAttributes() - { - if (! isset(static::$mutatorCache[static::class])) { - static::cacheMutatedAttributes($this); - } - - return static::$mutatorCache[static::class]; - } - - /** - * Extract and cache all the mutated attributes of a class. - * - * @param object|string $classOrInstance - * @return void - */ - public static function cacheMutatedAttributes($classOrInstance) - { - $reflection = new ReflectionClass($classOrInstance); - - $class = $reflection->getName(); - - static::$getAttributeMutatorCache[$class] = (new Collection($attributeMutatorMethods = static::getAttributeMarkedMutatorMethods($classOrInstance))) - ->mapWithKeys(fn ($match) => [lcfirst(static::$snakeAttributes ? Str::snake($match) : $match) => true]) - ->all(); - - static::$mutatorCache[$class] = (new Collection(static::getMutatorMethods($class))) - ->merge($attributeMutatorMethods) - ->map(fn ($match) => lcfirst(static::$snakeAttributes ? Str::snake($match) : $match)) - ->all(); - } - - /** - * Get all of the attribute mutator methods. - * - * @param mixed $class - * @return array - */ - protected static function getMutatorMethods($class) - { - preg_match_all('/(?<=^|;)get([^;]+?)Attribute(;|$)/', implode(';', get_class_methods($class)), $matches); - - return $matches[1]; - } - - /** - * Get all of the "Attribute" return typed attribute mutator methods. - * - * @param mixed $class - * @return array - */ - protected static function getAttributeMarkedMutatorMethods($class) - { - $instance = is_object($class) ? $class : new $class; - - return (new Collection((new ReflectionClass($instance))->getMethods()))->filter(function ($method) use ($instance) { - $returnType = $method->getReturnType(); - - if ($returnType instanceof ReflectionNamedType && - $returnType->getName() === Attribute::class) { - if (is_callable($method->invoke($instance)->get)) { - return true; - } - } - - return false; - })->map->name->values()->all(); - } -} diff --git a/Eloquent/Concerns/HasEvents.php b/Eloquent/Concerns/HasEvents.php deleted file mode 100644 index cc0368e67d..0000000000 --- a/Eloquent/Concerns/HasEvents.php +++ /dev/null @@ -1,461 +0,0 @@ - - */ - protected $dispatchesEvents = []; - - /** - * User exposed observable events. - * - * These are extra user-defined events observers may subscribe to. - * - * @var string[] - */ - protected $observables = []; - - /** - * Boot the has event trait for a model. - * - * @return void - */ - public static function bootHasEvents() - { - static::whenBooted(fn () => static::observe(static::resolveObserveAttributes())); - } - - /** - * Resolve the observe class names from the attributes. - * - * @return array - */ - public static function resolveObserveAttributes() - { - $reflectionClass = new ReflectionClass(static::class); - - $isEloquentGrandchild = is_subclass_of(static::class, Model::class) - && get_parent_class(static::class) !== Model::class; - - return (new Collection($reflectionClass->getAttributes(ObservedBy::class))) - ->map(fn ($attribute) => $attribute->getArguments()) - ->flatten() - ->when($isEloquentGrandchild, function (Collection $attributes) { - return (new Collection(get_parent_class(static::class)::resolveObserveAttributes())) - ->merge($attributes); - }) - ->all(); - } - - /** - * Register observers with the model. - * - * @param object|string[]|string $classes - * @return void - * - * @throws \RuntimeException - */ - public static function observe($classes) - { - $instance = new static; - - foreach (Arr::wrap($classes) as $class) { - $instance->registerObserver($class); - } - } - - /** - * Register a single observer with the model. - * - * @param object|string $class - * @return void - * - * @throws \RuntimeException - */ - protected function registerObserver($class) - { - $className = $this->resolveObserverClassName($class); - - // When registering a model observer, we will spin through the possible events - // and determine if this observer has that method. If it does, we will hook - // it into the model's event system, making it convenient to watch these. - foreach ($this->getObservableEvents() as $event) { - if (method_exists($class, $event)) { - static::registerModelEvent($event, $className.'@'.$event); - } - } - } - - /** - * Resolve the observer's class name from an object or string. - * - * @param object|string $class - * @return class-string - * - * @throws \InvalidArgumentException - */ - private function resolveObserverClassName($class) - { - if (is_object($class)) { - return get_class($class); - } - - if (class_exists($class)) { - return $class; - } - - throw new InvalidArgumentException('Unable to find observer: '.$class); - } - - /** - * Get the observable event names. - * - * @return string[] - */ - public function getObservableEvents() - { - return array_merge( - [ - 'retrieved', 'creating', 'created', 'updating', 'updated', - 'saving', 'saved', 'restoring', 'restored', 'replicating', - 'trashed', 'deleting', 'deleted', 'forceDeleting', 'forceDeleted', - ], - $this->observables - ); - } - - /** - * Set the observable event names. - * - * @param string[] $observables - * @return $this - */ - public function setObservableEvents(array $observables) - { - $this->observables = $observables; - - return $this; - } - - /** - * Add an observable event name. - * - * @param string|string[] $observables - * @return void - */ - public function addObservableEvents($observables) - { - $this->observables = array_unique(array_merge( - $this->observables, is_array($observables) ? $observables : func_get_args() - )); - } - - /** - * Remove an observable event name. - * - * @param string|string[] $observables - * @return void - */ - public function removeObservableEvents($observables) - { - $this->observables = array_diff( - $this->observables, is_array($observables) ? $observables : func_get_args() - ); - } - - /** - * Register a model event with the dispatcher. - * - * @param string $event - * @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback - * @return void - */ - protected static function registerModelEvent($event, $callback) - { - if (isset(static::$dispatcher)) { - $name = static::class; - - static::$dispatcher->listen("eloquent.{$event}: {$name}", $callback); - } - } - - /** - * Fire the given event for the model. - * - * @param string $event - * @param bool $halt - * @return mixed - */ - protected function fireModelEvent($event, $halt = true) - { - if (! isset(static::$dispatcher)) { - return true; - } - - // First, we will get the proper method to call on the event dispatcher, and then we - // will attempt to fire a custom, object based event for the given event. If that - // returns a result we can return that result, or we'll call the string events. - $method = $halt ? 'until' : 'dispatch'; - - $result = $this->filterModelEventResults( - $this->fireCustomModelEvent($event, $method) - ); - - if ($result === false) { - return false; - } - - return ! empty($result) ? $result : static::$dispatcher->{$method}( - "eloquent.{$event}: ".static::class, $this - ); - } - - /** - * Fire a custom model event for the given event. - * - * @param string $event - * @param 'until'|'dispatch' $method - * @return array|null|void - */ - protected function fireCustomModelEvent($event, $method) - { - if (! isset($this->dispatchesEvents[$event])) { - return; - } - - $result = static::$dispatcher->$method(new $this->dispatchesEvents[$event]($this)); - - if (! is_null($result)) { - return $result; - } - } - - /** - * Filter the model event results. - * - * @param mixed $result - * @return mixed - */ - protected function filterModelEventResults($result) - { - if (is_array($result)) { - $result = array_filter($result, function ($response) { - return ! is_null($response); - }); - } - - return $result; - } - - /** - * Register a retrieved model event with the dispatcher. - * - * @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback - * @return void - */ - public static function retrieved($callback) - { - static::registerModelEvent('retrieved', $callback); - } - - /** - * Register a saving model event with the dispatcher. - * - * @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback - * @return void - */ - public static function saving($callback) - { - static::registerModelEvent('saving', $callback); - } - - /** - * Register a saved model event with the dispatcher. - * - * @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback - * @return void - */ - public static function saved($callback) - { - static::registerModelEvent('saved', $callback); - } - - /** - * Register an updating model event with the dispatcher. - * - * @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback - * @return void - */ - public static function updating($callback) - { - static::registerModelEvent('updating', $callback); - } - - /** - * Register an updated model event with the dispatcher. - * - * @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback - * @return void - */ - public static function updated($callback) - { - static::registerModelEvent('updated', $callback); - } - - /** - * Register a creating model event with the dispatcher. - * - * @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback - * @return void - */ - public static function creating($callback) - { - static::registerModelEvent('creating', $callback); - } - - /** - * Register a created model event with the dispatcher. - * - * @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback - * @return void - */ - public static function created($callback) - { - static::registerModelEvent('created', $callback); - } - - /** - * Register a replicating model event with the dispatcher. - * - * @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback - * @return void - */ - public static function replicating($callback) - { - static::registerModelEvent('replicating', $callback); - } - - /** - * Register a deleting model event with the dispatcher. - * - * @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback - * @return void - */ - public static function deleting($callback) - { - static::registerModelEvent('deleting', $callback); - } - - /** - * Register a deleted model event with the dispatcher. - * - * @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback - * @return void - */ - public static function deleted($callback) - { - static::registerModelEvent('deleted', $callback); - } - - /** - * Remove all the event listeners for the model. - * - * @return void - */ - public static function flushEventListeners() - { - if (! isset(static::$dispatcher)) { - return; - } - - $instance = new static; - - foreach ($instance->getObservableEvents() as $event) { - static::$dispatcher->forget("eloquent.{$event}: ".static::class); - } - - foreach ($instance->dispatchesEvents as $event) { - static::$dispatcher->forget($event); - } - } - - /** - * Get the event map for the model. - * - * @return array - */ - public function dispatchesEvents() - { - return $this->dispatchesEvents; - } - - /** - * Get the event dispatcher instance. - * - * @return \Illuminate\Contracts\Events\Dispatcher|null - */ - public static function getEventDispatcher() - { - return static::$dispatcher; - } - - /** - * Set the event dispatcher instance. - * - * @param \Illuminate\Contracts\Events\Dispatcher $dispatcher - * @return void - */ - public static function setEventDispatcher(Dispatcher $dispatcher) - { - static::$dispatcher = $dispatcher; - } - - /** - * Unset the event dispatcher for models. - * - * @return void - */ - public static function unsetEventDispatcher() - { - static::$dispatcher = null; - } - - /** - * Execute a callback without firing any model events for any model type. - * - * @param callable $callback - * @return mixed - */ - public static function withoutEvents(callable $callback) - { - $dispatcher = static::getEventDispatcher(); - - if ($dispatcher) { - static::setEventDispatcher(new NullDispatcher($dispatcher)); - } - - try { - return $callback(); - } finally { - if ($dispatcher) { - static::setEventDispatcher($dispatcher); - } - } - } -} diff --git a/Eloquent/Concerns/HasGlobalScopes.php b/Eloquent/Concerns/HasGlobalScopes.php deleted file mode 100644 index 635ac8d1fe..0000000000 --- a/Eloquent/Concerns/HasGlobalScopes.php +++ /dev/null @@ -1,145 +0,0 @@ -getAttributes(ScopedBy::class, ReflectionAttribute::IS_INSTANCEOF))); - - foreach ($reflectionClass->getTraits() as $trait) { - $attributes->push(...$trait->getAttributes(ScopedBy::class, ReflectionAttribute::IS_INSTANCEOF)); - } - - return $attributes->map(fn ($attribute) => $attribute->getArguments()) - ->flatten() - ->all(); - } - - /** - * Register a new global scope on the model. - * - * @param \Illuminate\Database\Eloquent\Scope|(\Closure(\Illuminate\Database\Eloquent\Builder): mixed)|string $scope - * @param \Illuminate\Database\Eloquent\Scope|(\Closure(\Illuminate\Database\Eloquent\Builder): mixed)|null $implementation - * @return mixed - * - * @throws \InvalidArgumentException - */ - public static function addGlobalScope($scope, $implementation = null) - { - if (is_string($scope) && ($implementation instanceof Closure || $implementation instanceof Scope)) { - return static::$globalScopes[static::class][$scope] = $implementation; - } elseif ($scope instanceof Closure) { - return static::$globalScopes[static::class][spl_object_hash($scope)] = $scope; - } elseif ($scope instanceof Scope) { - return static::$globalScopes[static::class][get_class($scope)] = $scope; - } elseif (is_string($scope) && class_exists($scope) && is_subclass_of($scope, Scope::class)) { - return static::$globalScopes[static::class][$scope] = new $scope; - } - - throw new InvalidArgumentException('Global scope must be an instance of Closure or Scope or be a class name of a class extending '.Scope::class); - } - - /** - * Register multiple global scopes on the model. - * - * @param array $scopes - * @return void - */ - public static function addGlobalScopes(array $scopes) - { - foreach ($scopes as $key => $scope) { - if (is_string($key)) { - static::addGlobalScope($key, $scope); - } else { - static::addGlobalScope($scope); - } - } - } - - /** - * Determine if a model has a global scope. - * - * @param \Illuminate\Database\Eloquent\Scope|string $scope - * @return bool - */ - public static function hasGlobalScope($scope) - { - return ! is_null(static::getGlobalScope($scope)); - } - - /** - * Get a global scope registered with the model. - * - * @param \Illuminate\Database\Eloquent\Scope|string $scope - * @return \Illuminate\Database\Eloquent\Scope|(\Closure(\Illuminate\Database\Eloquent\Builder): mixed)|null - */ - public static function getGlobalScope($scope) - { - if (is_string($scope)) { - return Arr::get(static::$globalScopes, static::class.'.'.$scope); - } - - return Arr::get( - static::$globalScopes, static::class.'.'.get_class($scope) - ); - } - - /** - * Get all of the global scopes that are currently registered. - * - * @return array - */ - public static function getAllGlobalScopes() - { - return static::$globalScopes; - } - - /** - * Set the current global scopes. - * - * @param array $scopes - * @return void - */ - public static function setAllGlobalScopes($scopes) - { - static::$globalScopes = $scopes; - } - - /** - * Get the global scopes for this class instance. - * - * @return array - */ - public function getGlobalScopes() - { - return Arr::get(static::$globalScopes, static::class, []); - } -} diff --git a/Eloquent/Concerns/HasRelationships.php b/Eloquent/Concerns/HasRelationships.php deleted file mode 100644 index 9380eb10e0..0000000000 --- a/Eloquent/Concerns/HasRelationships.php +++ /dev/null @@ -1,1204 +0,0 @@ -touches)) { - $this->touches = static::resolveClassAttribute(Touches::class, 'relations') ?? []; - } - } - - /** - * Get the dynamic relation resolver if defined or inherited, or return null. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param class-string $class - * @param string $key - * @return Closure|null - */ - public function relationResolver($class, $key) - { - if ($resolver = static::$relationResolvers[$class][$key] ?? null) { - return $resolver; - } - - if ($parent = get_parent_class($class)) { - return $this->relationResolver($parent, $key); - } - - return null; - } - - /** - * Define a dynamic relation resolver. - * - * @param string $name - * @param \Closure $callback - * @return void - */ - public static function resolveRelationUsing($name, Closure $callback) - { - static::$relationResolvers = array_replace_recursive( - static::$relationResolvers, - [static::class => [$name => $callback]] - ); - } - - /** - * Determine if a relationship autoloader callback has been defined. - * - * @return bool - */ - public function hasRelationAutoloadCallback() - { - return ! is_null($this->relationAutoloadCallback); - } - - /** - * Define an automatic relationship autoloader callback for this model and its relations. - * - * @param \Closure $callback - * @param mixed $context - * @return $this - */ - public function autoloadRelationsUsing(Closure $callback, $context = null) - { - // Prevent circular relation autoloading... - if ($context && $this->relationAutoloadContext === $context) { - return $this; - } - - $this->relationAutoloadCallback = $callback; - $this->relationAutoloadContext = $context; - - foreach ($this->relations as $key => $value) { - $this->propagateRelationAutoloadCallbackToRelation($key, $value); - } - - return $this; - } - - /** - * Attempt to autoload the given relationship using the autoload callback. - * - * @param string $key - * @return bool - */ - protected function attemptToAutoloadRelation($key) - { - if (! $this->hasRelationAutoloadCallback()) { - return false; - } - - $this->invokeRelationAutoloadCallbackFor($key, []); - - return $this->relationLoaded($key); - } - - /** - * Invoke the relationship autoloader callback for the given relationships. - * - * @param string $key - * @param array $tuples - * @return void - */ - protected function invokeRelationAutoloadCallbackFor($key, $tuples) - { - $tuples = array_merge([[$key, get_class($this)]], $tuples); - - call_user_func($this->relationAutoloadCallback, $tuples); - } - - /** - * Propagate the relationship autoloader callback to the given related models. - * - * @param string $key - * @param mixed $models - * @return void - */ - protected function propagateRelationAutoloadCallbackToRelation($key, $models) - { - if (! $this->hasRelationAutoloadCallback() || ! $models) { - return; - } - - if ($models instanceof Model) { - $models = [$models]; - } - - if (! is_iterable($models)) { - return; - } - - $callback = fn (array $tuples) => $this->invokeRelationAutoloadCallbackFor($key, $tuples); - - foreach ($models as $model) { - $model->autoloadRelationsUsing($callback, $this->relationAutoloadContext); - } - } - - /** - * Define a one-to-one relationship. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param class-string $related - * @param string|null $foreignKey - * @param string|null $localKey - * @return \Illuminate\Database\Eloquent\Relations\HasOne - */ - public function hasOne($related, $foreignKey = null, $localKey = null) - { - $instance = $this->newRelatedInstance($related); - - $foreignKey = $foreignKey ?: $this->getForeignKey(); - - $localKey = $localKey ?: $this->getKeyName(); - - return $this->newHasOne($instance->newQuery(), $this, $instance->qualifyColumn($foreignKey), $localKey); - } - - /** - * Instantiate a new HasOne relationship. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param TDeclaringModel $parent - * @param string $foreignKey - * @param string $localKey - * @return \Illuminate\Database\Eloquent\Relations\HasOne - */ - protected function newHasOne(Builder $query, Model $parent, $foreignKey, $localKey) - { - return new HasOne($query, $parent, $foreignKey, $localKey); - } - - /** - * Define a has-one-through relationship. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TIntermediateModel of \Illuminate\Database\Eloquent\Model - * - * @param class-string $related - * @param class-string $through - * @param string|null $firstKey - * @param string|null $secondKey - * @param string|null $localKey - * @param string|null $secondLocalKey - * @return \Illuminate\Database\Eloquent\Relations\HasOneThrough - */ - public function hasOneThrough($related, $through, $firstKey = null, $secondKey = null, $localKey = null, $secondLocalKey = null) - { - $through = $this->newRelatedThroughInstance($through); - - $firstKey = $firstKey ?: $this->getForeignKey(); - - $secondKey = $secondKey ?: $through->getForeignKey(); - - return $this->newHasOneThrough( - $this->newRelatedInstance($related)->newQuery(), - $this, - $through, - $firstKey, - $secondKey, - $localKey ?: $this->getKeyName(), - $secondLocalKey ?: $through->getKeyName(), - ); - } - - /** - * Instantiate a new HasOneThrough relationship. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TIntermediateModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param TDeclaringModel $farParent - * @param TIntermediateModel $throughParent - * @param string $firstKey - * @param string $secondKey - * @param string $localKey - * @param string $secondLocalKey - * @return \Illuminate\Database\Eloquent\Relations\HasOneThrough - */ - protected function newHasOneThrough(Builder $query, Model $farParent, Model $throughParent, $firstKey, $secondKey, $localKey, $secondLocalKey) - { - return new HasOneThrough($query, $farParent, $throughParent, $firstKey, $secondKey, $localKey, $secondLocalKey); - } - - /** - * Define a polymorphic one-to-one relationship. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param class-string $related - * @param string $name - * @param string|null $type - * @param string|null $id - * @param string|null $localKey - * @return \Illuminate\Database\Eloquent\Relations\MorphOne - */ - public function morphOne($related, $name, $type = null, $id = null, $localKey = null) - { - $instance = $this->newRelatedInstance($related); - - [$type, $id] = $this->getMorphs($name, $type, $id); - - $localKey = $localKey ?: $this->getKeyName(); - - return $this->newMorphOne($instance->newQuery(), $this, $instance->qualifyColumn($type), $instance->qualifyColumn($id), $localKey); - } - - /** - * Instantiate a new MorphOne relationship. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param TDeclaringModel $parent - * @param string $type - * @param string $id - * @param string $localKey - * @return \Illuminate\Database\Eloquent\Relations\MorphOne - */ - protected function newMorphOne(Builder $query, Model $parent, $type, $id, $localKey) - { - return new MorphOne($query, $parent, $type, $id, $localKey); - } - - /** - * Define an inverse one-to-one or many relationship. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param class-string $related - * @param string|null $foreignKey - * @param string|null $ownerKey - * @param string|null $relation - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function belongsTo($related, $foreignKey = null, $ownerKey = null, $relation = null) - { - // If no relation name was given, we will use this debug backtrace to extract - // the calling method's name and use that as the relationship name as most - // of the time this will be what we desire to use for the relationships. - if (is_null($relation)) { - $relation = $this->guessBelongsToRelation(); - } - - $instance = $this->newRelatedInstance($related); - - // If no foreign key was supplied, we can use a backtrace to guess the proper - // foreign key name by using the name of the relationship function, which - // when combined with an "_id" should conventionally match the columns. - if (is_null($foreignKey)) { - $foreignKey = Str::snake($relation).'_'.$instance->getKeyName(); - } - - // Once we have the foreign key names we'll just create a new Eloquent query - // for the related models and return the relationship instance which will - // actually be responsible for retrieving and hydrating every relation. - $ownerKey = $ownerKey ?: $instance->getKeyName(); - - return $this->newBelongsTo( - $instance->newQuery(), $this, $foreignKey, $ownerKey, $relation - ); - } - - /** - * Instantiate a new BelongsTo relationship. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param TDeclaringModel $child - * @param string $foreignKey - * @param string $ownerKey - * @param string $relation - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - protected function newBelongsTo(Builder $query, Model $child, $foreignKey, $ownerKey, $relation) - { - return new BelongsTo($query, $child, $foreignKey, $ownerKey, $relation); - } - - /** - * Define a polymorphic, inverse one-to-one or many relationship. - * - * @param string|null $name - * @param string|null $type - * @param string|null $id - * @param string|null $ownerKey - * @return \Illuminate\Database\Eloquent\Relations\MorphTo<\Illuminate\Database\Eloquent\Model, $this> - */ - public function morphTo($name = null, $type = null, $id = null, $ownerKey = null) - { - // If no name is provided, we will use the backtrace to get the function name - // since that is most likely the name of the polymorphic interface. We can - // use that to get both the class and foreign key that will be utilized. - $name = $name ?: $this->guessBelongsToRelation(); - - [$type, $id] = $this->getMorphs( - Str::snake($name), $type, $id - ); - - // If the type value is null it is probably safe to assume we're eager loading - // the relationship. In this case we'll just pass in a dummy query where we - // need to remove any eager loads that may already be defined on a model. - return is_null($class = $this->getAttributeFromArray($type)) || $class === '' - ? $this->morphEagerTo($name, $type, $id, $ownerKey) - : $this->morphInstanceTo($class, $name, $type, $id, $ownerKey); - } - - /** - * Define a polymorphic, inverse one-to-one or many relationship. - * - * @param string $name - * @param string $type - * @param string $id - * @param string|null $ownerKey - * @return \Illuminate\Database\Eloquent\Relations\MorphTo<\Illuminate\Database\Eloquent\Model, $this> - */ - protected function morphEagerTo($name, $type, $id, $ownerKey) - { - return $this->newMorphTo( - $this->newQuery()->setEagerLoads([]), $this, $id, $ownerKey, $type, $name - ); - } - - /** - * Define a polymorphic, inverse one-to-one or many relationship. - * - * @param string $target - * @param string $name - * @param string $type - * @param string $id - * @param string|null $ownerKey - * @return \Illuminate\Database\Eloquent\Relations\MorphTo<\Illuminate\Database\Eloquent\Model, $this> - */ - protected function morphInstanceTo($target, $name, $type, $id, $ownerKey) - { - $instance = $this->newRelatedInstance( - static::getActualClassNameForMorph($target) - ); - - return $this->newMorphTo( - $instance->newQuery(), $this, $id, $ownerKey ?? $instance->getKeyName(), $type, $name - ); - } - - /** - * Instantiate a new MorphTo relationship. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param TDeclaringModel $parent - * @param string $foreignKey - * @param string|null $ownerKey - * @param string $type - * @param string $relation - * @return \Illuminate\Database\Eloquent\Relations\MorphTo - */ - protected function newMorphTo(Builder $query, Model $parent, $foreignKey, $ownerKey, $type, $relation) - { - return new MorphTo($query, $parent, $foreignKey, $ownerKey, $type, $relation); - } - - /** - * Retrieve the actual class name for a given morph class. - * - * @param string $class - * @return string - */ - public static function getActualClassNameForMorph($class) - { - return Arr::get(Relation::morphMap() ?: [], $class, $class); - } - - /** - * Guess the "belongs to" relationship name. - * - * @return string - */ - protected function guessBelongsToRelation() - { - [, , $caller] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); - - return $caller['function']; - } - - /** - * Create a pending has-many-through or has-one-through relationship. - * - * @template TIntermediateModel of \Illuminate\Database\Eloquent\Model - * - * @param string|\Illuminate\Database\Eloquent\Relations\HasMany|\Illuminate\Database\Eloquent\Relations\HasOne $relationship - * @return ( - * $relationship is string - * ? \Illuminate\Database\Eloquent\PendingHasThroughRelationship<\Illuminate\Database\Eloquent\Model, $this> - * : ( - * $relationship is \Illuminate\Database\Eloquent\Relations\HasMany - * ? \Illuminate\Database\Eloquent\PendingHasThroughRelationship> - * : \Illuminate\Database\Eloquent\PendingHasThroughRelationship> - * ) - * ) - */ - public function through($relationship) - { - if (is_string($relationship)) { - $relationship = $this->{$relationship}(); - } - - return new PendingHasThroughRelationship($this, $relationship); - } - - /** - * Define a one-to-many relationship. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param class-string $related - * @param string|null $foreignKey - * @param string|null $localKey - * @return \Illuminate\Database\Eloquent\Relations\HasMany - */ - public function hasMany($related, $foreignKey = null, $localKey = null) - { - $instance = $this->newRelatedInstance($related); - - $foreignKey = $foreignKey ?: $this->getForeignKey(); - - $localKey = $localKey ?: $this->getKeyName(); - - return $this->newHasMany( - $instance->newQuery(), $this, $instance->qualifyColumn($foreignKey), $localKey - ); - } - - /** - * Instantiate a new HasMany relationship. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param TDeclaringModel $parent - * @param string $foreignKey - * @param string $localKey - * @return \Illuminate\Database\Eloquent\Relations\HasMany - */ - protected function newHasMany(Builder $query, Model $parent, $foreignKey, $localKey) - { - return new HasMany($query, $parent, $foreignKey, $localKey); - } - - /** - * Define a has-many-through relationship. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TIntermediateModel of \Illuminate\Database\Eloquent\Model - * - * @param class-string $related - * @param class-string $through - * @param string|null $firstKey - * @param string|null $secondKey - * @param string|null $localKey - * @param string|null $secondLocalKey - * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough - */ - public function hasManyThrough($related, $through, $firstKey = null, $secondKey = null, $localKey = null, $secondLocalKey = null) - { - $through = $this->newRelatedThroughInstance($through); - - $firstKey = $firstKey ?: $this->getForeignKey(); - - $secondKey = $secondKey ?: $through->getForeignKey(); - - return $this->newHasManyThrough( - $this->newRelatedInstance($related)->newQuery(), - $this, - $through, - $firstKey, - $secondKey, - $localKey ?: $this->getKeyName(), - $secondLocalKey ?: $through->getKeyName() - ); - } - - /** - * Instantiate a new HasManyThrough relationship. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TIntermediateModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param TDeclaringModel $farParent - * @param TIntermediateModel $throughParent - * @param string $firstKey - * @param string $secondKey - * @param string $localKey - * @param string $secondLocalKey - * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough - */ - protected function newHasManyThrough(Builder $query, Model $farParent, Model $throughParent, $firstKey, $secondKey, $localKey, $secondLocalKey) - { - return new HasManyThrough($query, $farParent, $throughParent, $firstKey, $secondKey, $localKey, $secondLocalKey); - } - - /** - * Define a polymorphic one-to-many relationship. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param class-string $related - * @param string $name - * @param string|null $type - * @param string|null $id - * @param string|null $localKey - * @return \Illuminate\Database\Eloquent\Relations\MorphMany - */ - public function morphMany($related, $name, $type = null, $id = null, $localKey = null) - { - $instance = $this->newRelatedInstance($related); - - // Here we will gather up the morph type and ID for the relationship so that we - // can properly query the intermediate table of a relation. Finally, we will - // get the table and create the relationship instances for the developers. - [$type, $id] = $this->getMorphs($name, $type, $id); - - $localKey = $localKey ?: $this->getKeyName(); - - return $this->newMorphMany($instance->newQuery(), $this, $instance->qualifyColumn($type), $instance->qualifyColumn($id), $localKey); - } - - /** - * Instantiate a new MorphMany relationship. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param TDeclaringModel $parent - * @param string $type - * @param string $id - * @param string $localKey - * @return \Illuminate\Database\Eloquent\Relations\MorphMany - */ - protected function newMorphMany(Builder $query, Model $parent, $type, $id, $localKey) - { - return new MorphMany($query, $parent, $type, $id, $localKey); - } - - /** - * Define a many-to-many relationship. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param class-string $related - * @param string|class-string<\Illuminate\Database\Eloquent\Model>|null $table - * @param string|null $foreignPivotKey - * @param string|null $relatedPivotKey - * @param string|null $parentKey - * @param string|null $relatedKey - * @param string|null $relation - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany - */ - public function belongsToMany( - $related, - $table = null, - $foreignPivotKey = null, - $relatedPivotKey = null, - $parentKey = null, - $relatedKey = null, - $relation = null, - ) { - // If no relationship name was passed, we will pull backtraces to get the - // name of the calling function. We will use that function name as the - // title of this relation since that is a great convention to apply. - if (is_null($relation)) { - $relation = $this->guessBelongsToManyRelation(); - } - - // First, we'll need to determine the foreign key and "other key" for the - // relationship. Once we have determined the keys we'll make the query - // instances as well as the relationship instances we need for this. - $instance = $this->newRelatedInstance($related); - - $foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey(); - - $relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey(); - - // If no table name was provided, we can guess it by concatenating the two - // models using underscores in alphabetical order. The two model names - // are transformed to snake case from their default CamelCase also. - if (is_null($table)) { - $table = $this->joiningTable($related, $instance); - } - - return $this->newBelongsToMany( - $instance->newQuery(), - $this, - $table, - $foreignPivotKey, - $relatedPivotKey, - $parentKey ?: $this->getKeyName(), - $relatedKey ?: $instance->getKeyName(), - $relation, - ); - } - - /** - * Instantiate a new BelongsToMany relationship. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param TDeclaringModel $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 \Illuminate\Database\Eloquent\Relations\BelongsToMany - */ - protected function newBelongsToMany( - Builder $query, - Model $parent, - $table, - $foreignPivotKey, - $relatedPivotKey, - $parentKey, - $relatedKey, - $relationName = null, - ) { - return new BelongsToMany($query, $parent, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $relationName); - } - - /** - * Define a polymorphic many-to-many relationship. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param class-string $related - * @param string $name - * @param string|null $table - * @param string|null $foreignPivotKey - * @param string|null $relatedPivotKey - * @param string|null $parentKey - * @param string|null $relatedKey - * @param string|null $relation - * @param bool $inverse - * @return \Illuminate\Database\Eloquent\Relations\MorphToMany - */ - public function morphToMany( - $related, - $name, - $table = null, - $foreignPivotKey = null, - $relatedPivotKey = null, - $parentKey = null, - $relatedKey = null, - $relation = null, - $inverse = false, - ) { - $relation = $relation ?: $this->guessBelongsToManyRelation(); - - // First, we will need to determine the foreign key and "other key" for the - // relationship. Once we have determined the keys we will make the query - // instances, as well as the relationship instances we need for these. - $instance = $this->newRelatedInstance($related); - - $foreignPivotKey = $foreignPivotKey ?: $name.'_id'; - - $relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey(); - - // Now we're ready to create a new query builder for the related model and - // the relationship instances for this relation. This relation will set - // appropriate query constraints then entirely manage the hydrations. - if (! $table) { - $words = preg_split('/(_)/u', $name, -1, PREG_SPLIT_DELIM_CAPTURE); - - $lastWord = array_pop($words); - - $table = implode('', $words).Str::plural($lastWord); - } - - return $this->newMorphToMany( - $instance->newQuery(), - $this, - $name, - $table, - $foreignPivotKey, - $relatedPivotKey, - $parentKey ?: $this->getKeyName(), - $relatedKey ?: $instance->getKeyName(), - $relation, - $inverse, - ); - } - - /** - * Instantiate a new MorphToMany relationship. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param TDeclaringModel $parent - * @param string $name - * @param string $table - * @param string $foreignPivotKey - * @param string $relatedPivotKey - * @param string $parentKey - * @param string $relatedKey - * @param string|null $relationName - * @param bool $inverse - * @return \Illuminate\Database\Eloquent\Relations\MorphToMany - */ - protected function newMorphToMany( - Builder $query, - Model $parent, - $name, - $table, - $foreignPivotKey, - $relatedPivotKey, - $parentKey, - $relatedKey, - $relationName = null, - $inverse = false, - ) { - return new MorphToMany( - $query, - $parent, - $name, - $table, - $foreignPivotKey, - $relatedPivotKey, - $parentKey, - $relatedKey, - $relationName, - $inverse, - ); - } - - /** - * Define a polymorphic, inverse many-to-many relationship. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param class-string $related - * @param string $name - * @param string|null $table - * @param string|null $foreignPivotKey - * @param string|null $relatedPivotKey - * @param string|null $parentKey - * @param string|null $relatedKey - * @param string|null $relation - * @return \Illuminate\Database\Eloquent\Relations\MorphToMany - */ - public function morphedByMany( - $related, - $name, - $table = null, - $foreignPivotKey = null, - $relatedPivotKey = null, - $parentKey = null, - $relatedKey = null, - $relation = null, - ) { - $foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey(); - - // For the inverse of the polymorphic many-to-many relations, we will change - // the way we determine the foreign and other keys, as it is the opposite - // of the morph-to-many method since we're figuring out these inverses. - $relatedPivotKey = $relatedPivotKey ?: $name.'_id'; - - return $this->morphToMany( - $related, - $name, - $table, - $foreignPivotKey, - $relatedPivotKey, - $parentKey, - $relatedKey, - $relation, - true, - ); - } - - /** - * Get the relationship name of the belongsToMany relationship. - * - * @return string|null - */ - protected function guessBelongsToManyRelation() - { - $caller = Arr::first(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), function ($trace) { - return ! in_array( - $trace['function'], - array_merge(static::$manyMethods, ['guessBelongsToManyRelation']) - ); - }); - - return $caller['function'] ?? null; - } - - /** - * Get the joining table name for a many-to-many relation. - * - * @param string $related - * @param \Illuminate\Database\Eloquent\Model|null $instance - * @return string - */ - public function joiningTable($related, $instance = null) - { - // The joining table name, by convention, is simply the snake cased models - // sorted alphabetically and concatenated with an underscore, so we can - // just sort the models and join them together to get the table name. - $segments = [ - $instance - ? $instance->joiningTableSegment() - : Str::snake(class_basename($related)), - $this->joiningTableSegment(), - ]; - - // Now that we have the model names in an array we can just sort them and - // use the implode function to join them together with an underscores, - // which is typically used by convention within the database system. - sort($segments); - - return strtolower(implode('_', $segments)); - } - - /** - * Get this model's half of the intermediate table name for belongsToMany relationships. - * - * @return string - */ - public function joiningTableSegment() - { - return Str::snake(class_basename($this)); - } - - /** - * Determine if the model touches a given relation. - * - * @param string $relation - * @return bool - */ - public function touches($relation) - { - return in_array($relation, $this->getTouchedRelations()); - } - - /** - * Touch the owning relations of the model. - * - * @return void - */ - public function touchOwners() - { - $this->withoutRecursion(function () { - foreach ($this->getTouchedRelations() as $relation) { - $this->$relation()->touch(); - - if ($this->$relation instanceof self) { - $this->$relation->fireModelEvent('saved', false); - - $this->$relation->touchOwners(); - } elseif ($this->$relation instanceof EloquentCollection) { - $this->$relation->each->touchOwners(); - } - } - }); - } - - /** - * Get the polymorphic relationship columns. - * - * @param string $name - * @param string $type - * @param string $id - * @return array - */ - protected function getMorphs($name, $type, $id) - { - return [$type ?: $name.'_type', $id ?: $name.'_id']; - } - - /** - * Get the class name for polymorphic relations. - * - * @return string - * - * @throws \Illuminate\Database\ClassMorphViolationException - */ - public function getMorphClass() - { - $morphMap = Relation::morphMap(); - - if (! empty($morphMap) && in_array(static::class, $morphMap)) { - return array_search(static::class, $morphMap, true); - } - - if (static::class === Pivot::class) { - return static::class; - } - - if (Relation::requiresMorphMap()) { - throw new ClassMorphViolationException($this); - } - - return static::class; - } - - /** - * Create a new model instance for a related model. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param class-string $class - * @return TRelatedModel - */ - protected function newRelatedInstance($class) - { - return tap(new $class, function ($instance) { - if (! $instance->getConnectionName()) { - $instance->setConnection($this->getConnectionName()); - } - }); - } - - /** - * Create a new model instance for a related "through" model. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param class-string $class - * @return TRelatedModel - */ - protected function newRelatedThroughInstance($class) - { - return new $class; - } - - /** - * Get all the loaded relations for the instance. - * - * @return array - */ - public function getRelations() - { - return $this->relations; - } - - /** - * Get a specified relationship. - * - * @param string $relation - * @return mixed - */ - public function getRelation($relation) - { - return $this->relations[$relation]; - } - - /** - * Determine if the given relation is loaded. - * - * @param string $key - * @return bool - */ - public function relationLoaded($key) - { - return array_key_exists($key, $this->relations); - } - - /** - * Set the given relationship on the model. - * - * @param string $relation - * @param mixed $value - * @return $this - */ - public function setRelation($relation, $value) - { - $this->relations[$relation] = $value; - - $this->propagateRelationAutoloadCallbackToRelation($relation, $value); - - return $this; - } - - /** - * Unset a loaded relationship. - * - * @param string $relation - * @return $this - */ - public function unsetRelation($relation) - { - unset($this->relations[$relation]); - - return $this; - } - - /** - * Set the entire relations array on the model. - * - * @param array $relations - * @return $this - */ - public function setRelations(array $relations) - { - $this->relations = $relations; - - return $this; - } - - /** - * Enable relationship autoloading for this model. - * - * @return $this - */ - public function withRelationshipAutoloading() - { - $this->newCollection([$this])->withRelationshipAutoloading(); - - return $this; - } - - /** - * Duplicate the instance and unset all the loaded relations. - * - * @return $this - */ - public function withoutRelations() - { - $model = clone $this; - - return $model->unsetRelations(); - } - - /** - * Unset all the loaded relations for the instance. - * - * @return $this - */ - public function unsetRelations() - { - $this->relations = []; - - return $this; - } - - /** - * Get the relationships that are touched on save. - * - * @return array - */ - public function getTouchedRelations() - { - return $this->touches; - } - - /** - * Set the relationships that are touched on save. - * - * @param array $touches - * @return $this - */ - public function setTouchedRelations(array $touches) - { - $this->touches = $touches; - - return $this; - } -} diff --git a/Eloquent/Concerns/HasTimestamps.php b/Eloquent/Concerns/HasTimestamps.php deleted file mode 100644 index b16fe685c6..0000000000 --- a/Eloquent/Concerns/HasTimestamps.php +++ /dev/null @@ -1,249 +0,0 @@ -timestamps === true) { - if (($table = static::resolveClassAttribute(Table::class)) && $table->timestamps !== null) { - $this->timestamps = $table->timestamps; - } - } - } - - /** - * Update the model's update timestamp. - * - * @param string|null $attribute - * @return bool - */ - public function touch($attribute = null) - { - if ($attribute) { - $this->$attribute = $this->freshTimestamp(); - - return $this->save(); - } - - if (! $this->usesTimestamps()) { - return false; - } - - $this->updateTimestamps(); - - return $this->save(); - } - - /** - * Update the model's update timestamp without raising any events. - * - * @param string|null $attribute - * @return bool - */ - public function touchQuietly($attribute = null) - { - return static::withoutEvents(fn () => $this->touch($attribute)); - } - - /** - * Update the creation and update timestamps. - * - * @return $this - */ - public function updateTimestamps() - { - $time = $this->freshTimestamp(); - - $updatedAtColumn = $this->getUpdatedAtColumn(); - - if (! is_null($updatedAtColumn) && ! $this->isDirty($updatedAtColumn)) { - $this->setUpdatedAt($time); - } - - $createdAtColumn = $this->getCreatedAtColumn(); - - if (! $this->exists && ! is_null($createdAtColumn) && ! $this->isDirty($createdAtColumn)) { - $this->setCreatedAt($time); - } - - return $this; - } - - /** - * Set the value of the "created at" attribute. - * - * @param mixed $value - * @return $this - */ - public function setCreatedAt($value) - { - $this->{$this->getCreatedAtColumn()} = $value; - - return $this; - } - - /** - * Set the value of the "updated at" attribute. - * - * @param mixed $value - * @return $this - */ - public function setUpdatedAt($value) - { - $this->{$this->getUpdatedAtColumn()} = $value; - - return $this; - } - - /** - * Get a fresh timestamp for the model. - * - * @return \Illuminate\Support\Carbon - */ - public function freshTimestamp() - { - return Date::now(); - } - - /** - * Get a fresh timestamp for the model. - * - * @return string - */ - public function freshTimestampString() - { - return $this->fromDateTime($this->freshTimestamp()); - } - - /** - * Determine if the model uses timestamps. - * - * @return bool - */ - public function usesTimestamps() - { - return $this->timestamps && ! static::isIgnoringTimestamps($this::class); - } - - /** - * Get the name of the "created at" column. - * - * @return string|null - */ - public function getCreatedAtColumn() - { - return static::CREATED_AT; - } - - /** - * Get the name of the "updated at" column. - * - * @return string|null - */ - public function getUpdatedAtColumn() - { - return static::UPDATED_AT; - } - - /** - * Get the fully-qualified "created at" column. - * - * @return string|null - */ - public function getQualifiedCreatedAtColumn() - { - $column = $this->getCreatedAtColumn(); - - return $column ? $this->qualifyColumn($column) : null; - } - - /** - * Get the fully-qualified "updated at" column. - * - * @return string|null - */ - public function getQualifiedUpdatedAtColumn() - { - $column = $this->getUpdatedAtColumn(); - - return $column ? $this->qualifyColumn($column) : null; - } - - /** - * Disable timestamps for the current class during the given callback scope. - * - * @param callable $callback - * @return mixed - */ - public static function withoutTimestamps(callable $callback) - { - return static::withoutTimestampsOn([static::class], $callback); - } - - /** - * Disable timestamps for the given model classes during the given callback scope. - * - * @param array $models - * @param callable $callback - * @return mixed - */ - public static function withoutTimestampsOn($models, $callback) - { - static::$ignoreTimestampsOn = array_values(array_merge(static::$ignoreTimestampsOn, $models)); - - try { - return $callback(); - } finally { - foreach ($models as $model) { - if (($key = array_search($model, static::$ignoreTimestampsOn, true)) !== false) { - unset(static::$ignoreTimestampsOn[$key]); - } - } - } - } - - /** - * Determine if the given model is ignoring timestamps / touches. - * - * @param string|null $class - * @return bool - */ - public static function isIgnoringTimestamps($class = null) - { - $class ??= static::class; - - foreach (static::$ignoreTimestampsOn as $ignoredClass) { - if ($class === $ignoredClass || is_subclass_of($class, $ignoredClass)) { - return true; - } - } - - return false; - } -} diff --git a/Eloquent/Concerns/HasUlids.php b/Eloquent/Concerns/HasUlids.php deleted file mode 100644 index 344f97338a..0000000000 --- a/Eloquent/Concerns/HasUlids.php +++ /dev/null @@ -1,31 +0,0 @@ -usesUniqueIds; - } - - /** - * Generate unique keys for the model. - * - * @return void - */ - public function setUniqueIds() - { - foreach ($this->uniqueIds() as $column) { - if (empty($this->{$column})) { - $this->{$column} = $this->newUniqueId(); - } - } - } - - /** - * Generate a new key for the model. - * - * @return string - */ - public function newUniqueId() - { - return null; - } - - /** - * Get the columns that should receive a unique identifier. - * - * @return array - */ - public function uniqueIds() - { - return []; - } -} diff --git a/Eloquent/Concerns/HasUniqueStringIds.php b/Eloquent/Concerns/HasUniqueStringIds.php deleted file mode 100644 index 324961f2b2..0000000000 --- a/Eloquent/Concerns/HasUniqueStringIds.php +++ /dev/null @@ -1,108 +0,0 @@ -usesUniqueIds = true; - } - - /** - * Get the columns that should receive a unique identifier. - * - * @return array - */ - public function uniqueIds() - { - return $this->usesUniqueIds() ? [$this->getKeyName()] : parent::uniqueIds(); - } - - /** - * Retrieve the model for a bound value. - * - * @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Relations\Relation<*, *, *> $query - * @param mixed $value - * @param string|null $field - * @return \Illuminate\Contracts\Database\Eloquent\Builder - * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - */ - public function resolveRouteBindingQuery($query, $value, $field = null) - { - if ($field && in_array($field, $this->uniqueIds()) && ! $this->isValidUniqueId($value)) { - $this->handleInvalidUniqueId($value, $field); - } - - if (! $field && in_array($this->getRouteKeyName(), $this->uniqueIds()) && ! $this->isValidUniqueId($value)) { - $this->handleInvalidUniqueId($value, $field); - } - - return parent::resolveRouteBindingQuery($query, $value, $field); - } - - /** - * Get the auto-incrementing key type. - * - * @return string - */ - public function getKeyType() - { - if (in_array($this->getKeyName(), $this->uniqueIds())) { - return 'string'; - } - - return parent::getKeyType(); - } - - /** - * Get the value indicating whether the IDs are incrementing. - * - * @return bool - */ - public function getIncrementing() - { - if (in_array($this->getKeyName(), $this->uniqueIds())) { - return false; - } - - return parent::getIncrementing(); - } - - /** - * Throw an exception for the given invalid unique ID. - * - * @param mixed $value - * @param string|null $field - * @return never - * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - */ - protected function handleInvalidUniqueId($value, $field) - { - throw (new ModelNotFoundException)->setModel(get_class($this), $value); - } -} diff --git a/Eloquent/Concerns/HasUuids.php b/Eloquent/Concerns/HasUuids.php deleted file mode 100644 index 89d40f829a..0000000000 --- a/Eloquent/Concerns/HasUuids.php +++ /dev/null @@ -1,31 +0,0 @@ - - */ - protected $hidden = []; - - /** - * The attributes that should be visible in serialization. - * - * @var array - */ - protected $visible = []; - - /** - * Initialize the HidesAttributes trait. - * - * @return void - */ - #[Initialize] - public function initializeHidesAttributes() - { - if (empty($this->hidden)) { - $this->hidden = static::resolveClassAttribute(Hidden::class, 'columns') ?? []; - } - - if (empty($this->visible)) { - $this->visible = static::resolveClassAttribute(Visible::class, 'columns') ?? []; - } - } - - /** - * Get the hidden attributes for the model. - * - * @return array - */ - public function getHidden() - { - return $this->hidden; - } - - /** - * Set the hidden attributes for the model. - * - * @param array $hidden - * @return $this - */ - public function setHidden(array $hidden) - { - $this->hidden = $hidden; - - return $this; - } - - /** - * Merge new hidden attributes with existing hidden attributes on the model. - * - * @param array $hidden - * @return $this - */ - public function mergeHidden(array $hidden) - { - $this->hidden = array_values(array_unique(array_merge($this->hidden, $hidden))); - - return $this; - } - - /** - * Get the visible attributes for the model. - * - * @return array - */ - public function getVisible() - { - return $this->visible; - } - - /** - * Set the visible attributes for the model. - * - * @param array $visible - * @return $this - */ - public function setVisible(array $visible) - { - $this->visible = $visible; - - return $this; - } - - /** - * Merge new visible attributes with existing visible attributes on the model. - * - * @param array $visible - * @return $this - */ - public function mergeVisible(array $visible) - { - $this->visible = array_values(array_unique(array_merge($this->visible, $visible))); - - return $this; - } - - /** - * Make the given, typically hidden, attributes visible. - * - * @param array|string|null $attributes - * @return $this - */ - public function makeVisible($attributes) - { - $attributes = is_array($attributes) ? $attributes : func_get_args(); - - $this->hidden = array_diff($this->hidden, $attributes); - - if (! empty($this->visible)) { - $this->visible = array_values(array_unique(array_merge($this->visible, $attributes))); - } - - return $this; - } - - /** - * Make the given, typically hidden, attributes visible if the given truth test passes. - * - * @param bool|\Closure $condition - * @param array|string|null $attributes - * @return $this - */ - public function makeVisibleIf($condition, $attributes) - { - return value($condition, $this) ? $this->makeVisible($attributes) : $this; - } - - /** - * Make the given, typically visible, attributes hidden. - * - * @param array|string|null $attributes - * @return $this - */ - public function makeHidden($attributes) - { - $this->hidden = array_values(array_unique(array_merge( - $this->hidden, is_array($attributes) ? $attributes : func_get_args() - ))); - - return $this; - } - - /** - * Make the given, typically visible, attributes hidden if the given truth test passes. - * - * @param bool|\Closure $condition - * @param array|string|null $attributes - * @return $this - */ - public function makeHiddenIf($condition, $attributes) - { - return value($condition, $this) ? $this->makeHidden($attributes) : $this; - } -} diff --git a/Eloquent/Concerns/PreventsCircularRecursion.php b/Eloquent/Concerns/PreventsCircularRecursion.php deleted file mode 100644 index 85aa66d9ce..0000000000 --- a/Eloquent/Concerns/PreventsCircularRecursion.php +++ /dev/null @@ -1,107 +0,0 @@ -> - */ - protected static $recursionCache; - - /** - * Prevent a method from being called multiple times on the same object within the same call stack. - * - * @param callable $callback - * @param mixed $default - * @return mixed - */ - protected function withoutRecursion($callback, $default = null) - { - $trace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 2); - - $onceable = Onceable::tryFromTrace($trace, $callback); - - if (is_null($onceable)) { - return call_user_func($callback); - } - - $stack = static::getRecursiveCallStack($this); - - if (array_key_exists($onceable->hash, $stack)) { - return is_callable($stack[$onceable->hash]) - ? static::setRecursiveCallValue($this, $onceable->hash, call_user_func($stack[$onceable->hash])) - : $stack[$onceable->hash]; - } - - try { - static::setRecursiveCallValue($this, $onceable->hash, $default); - - return call_user_func($onceable->callable); - } finally { - static::clearRecursiveCallValue($this, $onceable->hash); - } - } - - /** - * Remove an entry from the recursion cache for an object. - * - * @param object $object - * @param string $hash - */ - protected static function clearRecursiveCallValue($object, string $hash) - { - if ($stack = Arr::except(static::getRecursiveCallStack($object), $hash)) { - static::getRecursionCache()->offsetSet($object, $stack); - } elseif (static::getRecursionCache()->offsetExists($object)) { - static::getRecursionCache()->offsetUnset($object); - } - } - - /** - * Get the stack of methods being called recursively for the current object. - * - * @param object $object - * @return array - */ - protected static function getRecursiveCallStack($object): array - { - return static::getRecursionCache()->offsetExists($object) - ? static::getRecursionCache()->offsetGet($object) - : []; - } - - /** - * Get the current recursion cache being used by the model. - * - * @return WeakMap - */ - protected static function getRecursionCache() - { - return static::$recursionCache ??= new WeakMap(); - } - - /** - * Set a value in the recursion cache for the given object and method. - * - * @param object $object - * @param string $hash - * @param mixed $value - * @return mixed - */ - protected static function setRecursiveCallValue($object, string $hash, $value) - { - static::getRecursionCache()->offsetSet( - $object, - tap(static::getRecursiveCallStack($object), fn (&$stack) => $stack[$hash] = $value), - ); - - return static::getRecursiveCallStack($object)[$hash]; - } -} diff --git a/Eloquent/Concerns/QueriesRelationships.php b/Eloquent/Concerns/QueriesRelationships.php deleted file mode 100644 index 6072dcff3d..0000000000 --- a/Eloquent/Concerns/QueriesRelationships.php +++ /dev/null @@ -1,1137 +0,0 @@ -|string $relation - * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|int $count - * @param string $boolean - * @param (\Closure(\Illuminate\Database\Eloquent\Builder): mixed)|null $callback - * @return $this - * - * @throws \RuntimeException - */ - public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', ?Closure $callback = null) - { - if (is_string($relation)) { - if (str_contains($relation, '.')) { - return $this->hasNested($relation, $operator, $count, $boolean, $callback); - } - - $relation = $this->getRelationWithoutConstraints($relation); - } - - if ($relation instanceof MorphTo) { - return $this->hasMorph($relation, ['*'], $operator, $count, $boolean, $callback); - } - - // If we only need to check for the existence of the relation, then we can optimize - // the subquery to only run a "where exists" clause instead of this full "count" - // clause. This will make these queries run much faster compared with a count. - $method = $this->canUseExistsForExistenceCheck($operator, $count) - ? 'getRelationExistenceQuery' - : 'getRelationExistenceCountQuery'; - - $hasQuery = $relation->{$method}( - $relation->getRelated()->newQueryWithoutRelationships(), $this - ); - - // Next we will call any given callback as an "anonymous" scope so they can get the - // proper logical grouping of the where clauses if needed by this Eloquent query - // builder. Then, we will be ready to finalize and return this query instance. - if ($callback) { - $hasQuery->callScope($callback); - } - - return $this->addHasWhere( - $hasQuery, $relation, $operator, $count, $boolean - ); - } - - /** - * Add nested relationship count / exists conditions to the query. - * - * Sets up recursive call to whereHas until we finish the nested relation. - * - * @param string $relations - * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|int $count - * @param string $boolean - * @param (\Closure(\Illuminate\Database\Eloquent\Builder<*>): mixed)|null $callback - * @return $this - */ - protected function hasNested($relations, $operator = '>=', $count = 1, $boolean = 'and', $callback = null) - { - $relations = explode('.', $relations); - - $initialRelations = [...$relations]; - - $doesntHave = $operator === '<' && $count === 1; - - if ($doesntHave) { - $operator = '>='; - $count = 1; - } - - $closure = function ($q) use (&$closure, &$relations, $operator, $count, $callback, $initialRelations) { - // If the same closure is called multiple times, reset the relation array to loop through them again... - if ($count === 1 && empty($relations)) { - $relations = [...$initialRelations]; - - array_shift($relations); - } - - // In order to nest "has", we need to add count relation constraints on the - // callback Closure. We'll do this by simply passing the Closure its own - // reference to itself so it calls itself recursively on each segment. - count($relations) > 1 - ? $q->whereHas(array_shift($relations), $closure) - : $q->has(array_shift($relations), $operator, $count, 'and', $callback); - }; - - return $this->has(array_shift($relations), $doesntHave ? '<' : '>=', 1, $boolean, $closure); - } - - /** - * Add a relationship count / exists condition to the query with an "or". - * - * @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation - * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|int $count - * @return $this - */ - public function orHas($relation, $operator = '>=', $count = 1) - { - return $this->has($relation, $operator, $count, 'or'); - } - - /** - * Add a relationship count / exists condition to the query. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Relations\Relation|string $relation - * @param string $boolean - * @param (\Closure(\Illuminate\Database\Eloquent\Builder): mixed)|null $callback - * @return $this - */ - public function doesntHave($relation, $boolean = 'and', ?Closure $callback = null) - { - return $this->has($relation, '<', 1, $boolean, $callback); - } - - /** - * Add a relationship count / exists condition to the query with an "or". - * - * @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation - * @return $this - */ - public function orDoesntHave($relation) - { - return $this->doesntHave($relation, 'or'); - } - - /** - * Add a relationship count / exists condition to the query with where clauses. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Relations\Relation|string $relation - * @param (\Closure(\Illuminate\Database\Eloquent\Builder): mixed)|null $callback - * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|int $count - * @return $this - */ - public function whereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1) - { - return $this->has($relation, $operator, $count, 'and', $callback); - } - - /** - * Add a relationship count / exists condition to the query with where clauses. - * - * Also load the relationship with the same condition. - * - * @param string $relation - * @param (\Closure(\Illuminate\Database\Eloquent\Builder<*>|\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): mixed)|null $callback - * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|int $count - * @return $this - */ - public function withWhereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1) - { - return $this->whereHas(Str::before($relation, ':'), $callback, $operator, $count) - ->with($callback ? [$relation => fn ($query) => $callback($query)] : $relation); - } - - /** - * Add a relationship count / exists condition to the query with where clauses and an "or". - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Relations\Relation|string $relation - * @param (\Closure(\Illuminate\Database\Eloquent\Builder): mixed)|null $callback - * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|int $count - * @return $this - */ - public function orWhereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1) - { - return $this->has($relation, $operator, $count, 'or', $callback); - } - - /** - * Add a relationship count / exists condition to the query with where clauses. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Relations\Relation|string $relation - * @param (\Closure(\Illuminate\Database\Eloquent\Builder): mixed)|null $callback - * @return $this - */ - public function whereDoesntHave($relation, ?Closure $callback = null) - { - return $this->doesntHave($relation, 'and', $callback); - } - - /** - * Add a relationship count / exists condition to the query with where clauses and an "or". - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Relations\Relation|string $relation - * @param (\Closure(\Illuminate\Database\Eloquent\Builder): mixed)|null $callback - * @return $this - */ - public function orWhereDoesntHave($relation, ?Closure $callback = null) - { - return $this->doesntHave($relation, 'or', $callback); - } - - /** - * Add a polymorphic relationship count / exists condition to the query. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation - * @param string|array $types - * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|int $count - * @param string $boolean - * @param (\Closure(\Illuminate\Database\Eloquent\Builder, string): mixed)|null $callback - * @return $this - */ - public function hasMorph($relation, $types, $operator = '>=', $count = 1, $boolean = 'and', ?Closure $callback = null) - { - if (is_string($relation)) { - $relation = $this->getRelationWithoutConstraints($relation); - } - - $types = (array) $types; - - $checkMorphNull = $types === ['*'] - && (($operator === '<' && $count >= 1) - || ($operator === '<=' && $count >= 0) - || ($operator === '=' && $count === 0) - || ($operator === '!=' && $count >= 1)); - - if ($types === ['*']) { - $types = $this->model->newModelQuery()->distinct()->pluck($relation->getMorphType()) - ->filter() - ->map(fn ($item) => enum_value($item)) - ->all(); - } - - if (empty($types)) { - return $this->where(new Expression('0'), $operator, $count, $boolean); - } - - foreach ($types as &$type) { - $type = Relation::getMorphedModel($type) ?? $type; - } - - return $this->where(function ($query) use ($relation, $callback, $operator, $count, $types, $checkMorphNull) { - foreach ($types as $type) { - $query->orWhere(function ($query) use ($relation, $callback, $operator, $count, $type) { - $belongsTo = $this->getBelongsToRelation($relation, $type); - - if ($callback) { - $callback = function ($query) use ($callback, $type) { - return $callback($query, $type); - }; - } - - $query->where($this->qualifyColumn($relation->getMorphType()), '=', (new $type)->getMorphClass()) - ->whereHas($belongsTo, $callback, $operator, $count); - }); - } - - $query->when($checkMorphNull, fn (self $query) => $query->orWhereMorphedTo($relation, null)); - }, null, null, $boolean); - } - - /** - * Get the BelongsTo relationship for a single polymorphic type. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, TDeclaringModel> $relation - * @param class-string $type - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - protected function getBelongsToRelation(MorphTo $relation, $type) - { - $belongsTo = Relation::noConstraints(function () use ($relation, $type) { - return $this->model->belongsTo( - $type, - $relation->getForeignKeyName(), - $relation->getOwnerKeyName() - ); - }); - - $belongsTo->getQuery()->mergeConstraintsFrom($relation->getQuery()); - - return $belongsTo; - } - - /** - * Add a polymorphic relationship count / exists condition to the query with an "or". - * - * @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation - * @param string|array $types - * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|int $count - * @return $this - */ - public function orHasMorph($relation, $types, $operator = '>=', $count = 1) - { - return $this->hasMorph($relation, $types, $operator, $count, 'or'); - } - - /** - * Add a polymorphic relationship count / exists condition to the query. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation - * @param string|array $types - * @param string $boolean - * @param (\Closure(\Illuminate\Database\Eloquent\Builder, string): mixed)|null $callback - * @return $this - */ - public function doesntHaveMorph($relation, $types, $boolean = 'and', ?Closure $callback = null) - { - return $this->hasMorph($relation, $types, '<', 1, $boolean, $callback); - } - - /** - * Add a polymorphic relationship count / exists condition to the query with an "or". - * - * @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation - * @param string|array $types - * @return $this - */ - public function orDoesntHaveMorph($relation, $types) - { - return $this->doesntHaveMorph($relation, $types, 'or'); - } - - /** - * Add a polymorphic relationship count / exists condition to the query with where clauses. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation - * @param string|array $types - * @param (\Closure(\Illuminate\Database\Eloquent\Builder, string): mixed)|null $callback - * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|int $count - * @return $this - */ - public function whereHasMorph($relation, $types, ?Closure $callback = null, $operator = '>=', $count = 1) - { - return $this->hasMorph($relation, $types, $operator, $count, 'and', $callback); - } - - /** - * Add a polymorphic relationship count / exists condition to the query with where clauses and an "or". - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation - * @param string|array $types - * @param (\Closure(\Illuminate\Database\Eloquent\Builder, string): mixed)|null $callback - * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|int $count - * @return $this - */ - public function orWhereHasMorph($relation, $types, ?Closure $callback = null, $operator = '>=', $count = 1) - { - return $this->hasMorph($relation, $types, $operator, $count, 'or', $callback); - } - - /** - * Add a polymorphic relationship count / exists condition to the query with where clauses. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation - * @param string|array $types - * @param (\Closure(\Illuminate\Database\Eloquent\Builder, string): mixed)|null $callback - * @return $this - */ - public function whereDoesntHaveMorph($relation, $types, ?Closure $callback = null) - { - return $this->doesntHaveMorph($relation, $types, 'and', $callback); - } - - /** - * Add a polymorphic relationship count / exists condition to the query with where clauses and an "or". - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation - * @param string|array $types - * @param (\Closure(\Illuminate\Database\Eloquent\Builder, string): mixed)|null $callback - * @return $this - */ - public function orWhereDoesntHaveMorph($relation, $types, ?Closure $callback = null) - { - return $this->doesntHaveMorph($relation, $types, 'or', $callback); - } - - /** - * Add a basic where clause to a relationship query. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Relations\Relation|string $relation - * @param (\Closure(\Illuminate\Database\Eloquent\Builder): mixed)|string|array|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @return $this - */ - public function whereRelation($relation, $column, $operator = null, $value = null) - { - return $this->whereHas($relation, function ($query) use ($column, $operator, $value) { - if ($column instanceof Closure) { - $column($query); - } else { - $query->where($column, $operator, $value); - } - }); - } - - /** - * Add a basic where clause to a relationship query and eager-load the relationship with the same conditions. - * - * @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation - * @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @return $this - */ - public function withWhereRelation($relation, $column, $operator = null, $value = null) - { - return $this->whereRelation($relation, $column, $operator, $value) - ->with([ - $relation => fn ($query) => $column instanceof Closure - ? $column($query) - : $query->where($column, $operator, $value), - ]); - } - - /** - * Add an "or where" clause to a relationship query. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Relations\Relation|string $relation - * @param (\Closure(\Illuminate\Database\Eloquent\Builder): mixed)|string|array|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @return $this - */ - public function orWhereRelation($relation, $column, $operator = null, $value = null) - { - return $this->orWhereHas($relation, function ($query) use ($column, $operator, $value) { - if ($column instanceof Closure) { - $column($query); - } else { - $query->where($column, $operator, $value); - } - }); - } - - /** - * Add a basic count / exists condition to a relationship query. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Relations\Relation|string $relation - * @param (\Closure(\Illuminate\Database\Eloquent\Builder): mixed)|string|array|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @return $this - */ - public function whereDoesntHaveRelation($relation, $column, $operator = null, $value = null) - { - return $this->whereDoesntHave($relation, function ($query) use ($column, $operator, $value) { - if ($column instanceof Closure) { - $column($query); - } else { - $query->where($column, $operator, $value); - } - }); - } - - /** - * Add an "or where" clause to a relationship query. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Relations\Relation|string $relation - * @param (\Closure(\Illuminate\Database\Eloquent\Builder): mixed)|string|array|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @return $this - */ - public function orWhereDoesntHaveRelation($relation, $column, $operator = null, $value = null) - { - return $this->orWhereDoesntHave($relation, function ($query) use ($column, $operator, $value) { - if ($column instanceof Closure) { - $column($query); - } else { - $query->where($column, $operator, $value); - } - }); - } - - /** - * Add a polymorphic relationship condition to the query with a where clause. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation - * @param string|array $types - * @param (\Closure(\Illuminate\Database\Eloquent\Builder): mixed)|string|array|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @return $this - */ - public function whereMorphRelation($relation, $types, $column, $operator = null, $value = null) - { - return $this->whereHasMorph($relation, $types, function ($query) use ($column, $operator, $value) { - $query->where($column, $operator, $value); - }); - } - - /** - * Add a polymorphic relationship condition to the query with an "or where" clause. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation - * @param string|array $types - * @param (\Closure(\Illuminate\Database\Eloquent\Builder): mixed)|string|array|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @return $this - */ - public function orWhereMorphRelation($relation, $types, $column, $operator = null, $value = null) - { - return $this->orWhereHasMorph($relation, $types, function ($query) use ($column, $operator, $value) { - $query->where($column, $operator, $value); - }); - } - - /** - * Add a polymorphic relationship condition to the query with a doesn't have clause. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation - * @param string|array $types - * @param (\Closure(\Illuminate\Database\Eloquent\Builder): mixed)|string|array|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @return $this - */ - public function whereMorphDoesntHaveRelation($relation, $types, $column, $operator = null, $value = null) - { - return $this->whereDoesntHaveMorph($relation, $types, function ($query) use ($column, $operator, $value) { - $query->where($column, $operator, $value); - }); - } - - /** - * Add a polymorphic relationship condition to the query with an "or doesn't have" clause. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation - * @param string|array $types - * @param (\Closure(\Illuminate\Database\Eloquent\Builder): mixed)|string|array|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @return $this - */ - public function orWhereMorphDoesntHaveRelation($relation, $types, $column, $operator = null, $value = null) - { - return $this->orWhereDoesntHaveMorph($relation, $types, function ($query) use ($column, $operator, $value) { - $query->where($column, $operator, $value); - }); - } - - /** - * Add a morph-to relationship condition to the query. - * - * @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation - * @param \Illuminate\Database\Eloquent\Model|iterable|string|null $model - * @return $this - * - * @throws \InvalidArgumentException - */ - public function whereMorphedTo($relation, $model, $boolean = 'and') - { - if (is_string($relation)) { - $relation = $this->getRelationWithoutConstraints($relation); - } - - if (is_null($model)) { - return $this->whereNull($relation->qualifyColumn($relation->getMorphType()), $boolean); - } - - if (is_string($model)) { - $morphMap = Relation::morphMap(); - - if (! empty($morphMap) && in_array($model, $morphMap)) { - $model = array_search($model, $morphMap, true); - } - - return $this->where($relation->qualifyColumn($relation->getMorphType()), $model, null, $boolean); - } - - $models = BaseCollection::wrap($model); - - if ($models->isEmpty()) { - throw new InvalidArgumentException('Collection given to whereMorphedTo method may not be empty.'); - } - - return $this->where(function ($query) use ($relation, $models) { - $models->groupBy(fn ($model) => $model->getMorphClass())->each(function ($models) use ($query, $relation) { - $query->orWhere(function ($query) use ($relation, $models) { - $query->where($relation->qualifyColumn($relation->getMorphType()), $models->first()->getMorphClass()) - ->whereIn($relation->qualifyColumn($relation->getForeignKeyName()), $models->map->getKey()); - }); - }); - }, null, null, $boolean); - } - - /** - * Add a not morph-to relationship condition to the query. - * - * @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation - * @param \Illuminate\Database\Eloquent\Model|iterable|string $model - * @return $this - * - * @throws \InvalidArgumentException - */ - public function whereNotMorphedTo($relation, $model, $boolean = 'and') - { - if (is_string($relation)) { - $relation = $this->getRelationWithoutConstraints($relation); - } - - if (is_string($model)) { - $morphMap = Relation::morphMap(); - - if (! empty($morphMap) && in_array($model, $morphMap)) { - $model = array_search($model, $morphMap, true); - } - - return $this->whereNot($relation->qualifyColumn($relation->getMorphType()), '<=>', $model, $boolean); - } - - $models = BaseCollection::wrap($model); - - if ($models->isEmpty()) { - throw new InvalidArgumentException('Collection given to whereNotMorphedTo method may not be empty.'); - } - - return $this->whereNot(function ($query) use ($relation, $models) { - $models->groupBy(fn ($model) => $model->getMorphClass())->each(function ($models) use ($query, $relation) { - $query->orWhere(function ($query) use ($relation, $models) { - $query->where($relation->qualifyColumn($relation->getMorphType()), '<=>', $models->first()->getMorphClass()) - ->whereIn($relation->qualifyColumn($relation->getForeignKeyName()), $models->map->getKey()); - }); - }); - }, null, null, $boolean); - } - - /** - * Add a morph-to relationship condition to the query with an "or where" clause. - * - * @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation - * @param \Illuminate\Database\Eloquent\Model|iterable|string|null $model - * @return $this - */ - public function orWhereMorphedTo($relation, $model) - { - return $this->whereMorphedTo($relation, $model, 'or'); - } - - /** - * Add a not morph-to relationship condition to the query with an "or where" clause. - * - * @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation - * @param \Illuminate\Database\Eloquent\Model|iterable|string $model - * @return $this - */ - public function orWhereNotMorphedTo($relation, $model) - { - return $this->whereNotMorphedTo($relation, $model, 'or'); - } - - /** - * Add a "belongs to" relationship where clause to the query. - * - * @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection $related - * @param string|null $relationshipName - * @param string $boolean - * @return $this - * - * @throws \InvalidArgumentException - * @throws \Illuminate\Database\Eloquent\RelationNotFoundException - */ - public function whereBelongsTo($related, $relationshipName = null, $boolean = 'and') - { - if (! $related instanceof EloquentCollection) { - $relatedCollection = $related->newCollection([$related]); - } else { - $relatedCollection = $related; - - $related = $relatedCollection->first(); - } - - if ($relatedCollection->isEmpty()) { - throw new InvalidArgumentException('Collection given to whereBelongsTo method may not be empty.'); - } - - if ($relationshipName === null) { - $relationshipName = Str::camel(class_basename($related)); - } - - try { - $relationship = $this->model->{$relationshipName}(); - } catch (BadMethodCallException) { - throw RelationNotFoundException::make($this->model, $relationshipName); - } - - if (! $relationship instanceof BelongsTo) { - throw RelationNotFoundException::make($this->model, $relationshipName, BelongsTo::class); - } - - $this->whereIn( - $relationship->getQualifiedForeignKeyName(), - $relatedCollection->pluck($relationship->getOwnerKeyName())->toArray(), - $boolean, - ); - - return $this; - } - - /** - * Add a "BelongsTo" relationship with an "or where" clause to the query. - * - * @param \Illuminate\Database\Eloquent\Model $related - * @param string|null $relationshipName - * @return $this - */ - public function orWhereBelongsTo($related, $relationshipName = null) - { - return $this->whereBelongsTo($related, $relationshipName, 'or'); - } - - /** - * Add a "belongs to many" relationship where clause to the query. - * - * @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection $related - * @param string|null $relationshipName - * @param string $boolean - * @return $this - * - * @throws \InvalidArgumentException - * @throws \Illuminate\Database\Eloquent\RelationNotFoundException - */ - public function whereAttachedTo($related, $relationshipName = null, $boolean = 'and') - { - $relatedCollection = $related instanceof EloquentCollection ? $related : $related->newCollection([$related]); - - $related = $relatedCollection->first(); - - if ($relatedCollection->isEmpty()) { - throw new InvalidArgumentException('Collection given to whereAttachedTo method may not be empty.'); - } - - if ($relationshipName === null) { - $relationshipName = Str::plural(Str::camel(class_basename($related))); - } - - try { - $relationship = $this->model->{$relationshipName}(); - } catch (BadMethodCallException) { - throw RelationNotFoundException::make($this->model, $relationshipName); - } - - if (! $relationship instanceof BelongsToMany) { - throw RelationNotFoundException::make($this->model, $relationshipName, BelongsToMany::class); - } - - $this->has( - $relationshipName, - boolean: $boolean, - callback: fn (Builder $query) => $query->whereKey($relatedCollection->pluck($related->getKeyName())), - ); - - return $this; - } - - /** - * Add a "belongs to many" relationship with an "or where" clause to the query. - * - * @param \Illuminate\Database\Eloquent\Model $related - * @param string|null $relationshipName - * @return $this - * - * @throws \RuntimeException - */ - public function orWhereAttachedTo($related, $relationshipName = null) - { - return $this->whereAttachedTo($related, $relationshipName, 'or'); - } - - /** - * Add subselect queries to include an aggregate value for a relationship. - * - * @param mixed $relations - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string|null $function - * @return $this - */ - public function withAggregate($relations, $column, $function = null) - { - if (empty($relations)) { - return $this; - } - - if (is_null($this->query->columns)) { - $this->query->select([$this->query->from.'.*']); - } - - $relations = is_array($relations) ? $relations : [$relations]; - - foreach ($this->parseWithRelations($relations) as $name => $constraints) { - // First we will determine if the name has been aliased using an "as" clause on the name - // and if it has we will extract the actual relationship name and the desired name of - // the resulting column. This allows multiple aggregates on the same relationships. - $segments = explode(' ', $name); - - unset($alias); - - if (count($segments) === 3 && Str::lower($segments[1]) === 'as') { - [$name, $alias] = [$segments[0], $segments[2]]; - } - - $relation = $this->getRelationWithoutConstraints($name); - - if ($function) { - if ($this->getQuery()->getGrammar()->isExpression($column)) { - $aggregateColumn = $this->getQuery()->getGrammar()->getValue($column); - } else { - $hashedColumn = $this->getRelationHashedColumn($column, $relation); - - $aggregateColumn = $this->getQuery()->getGrammar()->wrap( - $column === '*' ? $column : $relation->getRelated()->qualifyColumn($hashedColumn) - ); - } - - $expression = $function === 'exists' ? $aggregateColumn : sprintf('%s(%s)', $function, $aggregateColumn); - } else { - $expression = $this->getQuery()->getGrammar()->getValue($column); - } - - // Here, we will grab the relationship sub-query and prepare to add it to the main query - // as a sub-select. First, we'll get the "has" query and use that to get the relation - // sub-query. We'll format this relationship name and append this column if needed. - $query = $relation->getRelationExistenceQuery( - $relation->getRelated()->newQuery(), $this, new Expression($expression) - )->setBindings([], 'select'); - - $query->callScope($constraints); - - $query = $query->mergeConstraintsFrom($relation->getQuery())->toBase(); - - // If the query contains certain elements like orderings / more than one column selected - // then we will remove those elements from the query so that it will execute properly - // when given to the database. Otherwise, we may receive SQL errors or poor syntax. - $query->orders = null; - $query->setBindings([], 'order'); - - if (count($query->columns) > 1) { - $query->columns = [$query->columns[0]]; - $query->bindings['select'] = []; - } - - // Finally, we will make the proper column alias to the query and run this sub-select on - // the query builder. Then, we will return the builder instance back to the developer - // for further constraint chaining that needs to take place on the query as needed. - $alias ??= Str::snake( - preg_replace( - '/[^[:alnum:][:space:]_]/u', - '', - sprintf('%s %s %s', $name, $function, strtolower($this->getQuery()->getGrammar()->getValue($column))) - ) - ); - - if ($function === 'exists') { - $this->selectRaw( - sprintf('exists(%s) as %s', $query->toSql(), $this->getQuery()->grammar->wrap($alias)), - $query->getBindings() - )->withCasts([$alias => 'bool']); - } else { - $this->selectSub( - $function ? $query : $query->limit(1), - $alias - ); - } - } - - return $this; - } - - /** - * Get the relation hashed column name for the given column and relation. - * - * @param string $column - * @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *> $relation - * @return string - */ - protected function getRelationHashedColumn($column, $relation) - { - if (str_contains($column, '.')) { - return $column; - } - - return $this->getQuery()->from === $relation->getQuery()->getQuery()->from - ? "{$relation->getRelationCountHash(false)}.$column" - : $column; - } - - /** - * Add subselect queries to count the relations. - * - * @param mixed $relations - * @return $this - */ - public function withCount($relations) - { - return $this->withAggregate(is_array($relations) ? $relations : func_get_args(), '*', 'count'); - } - - /** - * Add subselect queries to include the max of the relation's column. - * - * @param string|array $relation - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @return $this - */ - public function withMax($relation, $column) - { - return $this->withAggregate($relation, $column, 'max'); - } - - /** - * Add subselect queries to include the min of the relation's column. - * - * @param string|array $relation - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @return $this - */ - public function withMin($relation, $column) - { - return $this->withAggregate($relation, $column, 'min'); - } - - /** - * Add subselect queries to include the sum of the relation's column. - * - * @param string|array $relation - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @return $this - */ - public function withSum($relation, $column) - { - return $this->withAggregate($relation, $column, 'sum'); - } - - /** - * Add subselect queries to include the average of the relation's column. - * - * @param string|array $relation - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @return $this - */ - public function withAvg($relation, $column) - { - return $this->withAggregate($relation, $column, 'avg'); - } - - /** - * Add subselect queries to include the existence of related models. - * - * @param string|array $relation - * @return $this - */ - public function withExists($relation) - { - return $this->withAggregate($relation, '*', 'exists'); - } - - /** - * Add the "has" condition where clause to the query. - * - * @param \Illuminate\Database\Eloquent\Builder<*> $hasQuery - * @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *> $relation - * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|int $count - * @param string $boolean - * @return $this - */ - protected function addHasWhere(Builder $hasQuery, Relation $relation, $operator, $count, $boolean) - { - $hasQuery->mergeConstraintsFrom($relation->getQuery()); - - return $this->canUseExistsForExistenceCheck($operator, $count) - ? $this->addWhereExistsQuery($hasQuery->toBase(), $boolean, $operator === '<' && $count === 1) - : $this->addWhereCountQuery($hasQuery->toBase(), $operator, $count, $boolean); - } - - /** - * Merge the where constraints from another query to the current query. - * - * @param \Illuminate\Database\Eloquent\Builder<*> $from - * @return $this - */ - public function mergeConstraintsFrom(Builder $from) - { - $whereBindings = $from->getQuery()->getRawBindings()['where'] ?? []; - - $wheres = $from->getQuery()->from !== $this->getQuery()->from - ? $this->requalifyWhereTables( - $from->getQuery()->wheres, - $from->getQuery()->grammar->getValue($from->getQuery()->from), - $this->getModel()->getTable() - ) : $from->getQuery()->wheres; - - // Here we have some other query that we want to merge the where constraints from. We will - // copy over any where constraints on the query as well as remove any global scopes the - // query might have removed. Then we will return ourselves with the finished merging. - return $this->withoutGlobalScopes( - $from->removedScopes() - )->mergeWheres( - $wheres, $whereBindings - ); - } - - /** - * Updates the table name for any columns with a new qualified name. - * - * @param array $wheres - * @param string $from - * @param string $to - * @return array - */ - protected function requalifyWhereTables(array $wheres, string $from, string $to): array - { - return (new BaseCollection($wheres))->map(function ($where) use ($from, $to) { - return (new BaseCollection($where))->map(function ($value) use ($from, $to) { - return is_string($value) && str_starts_with($value, $from.'.') - ? $to.'.'.Str::afterLast($value, '.') - : $value; - }); - })->toArray(); - } - - /** - * Add a sub-query count clause to this query. - * - * @param \Illuminate\Database\Query\Builder $query - * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|int $count - * @param string $boolean - * @return $this - */ - protected function addWhereCountQuery(QueryBuilder $query, $operator = '>=', $count = 1, $boolean = 'and') - { - $this->query->addBinding($query->getBindings(), 'where'); - - return $this->where( - new Expression('('.$query->toSql().')'), - $operator, - is_numeric($count) ? new Expression($count) : $count, - $boolean - ); - } - - /** - * Get the "has relation" base query instance. - * - * @param string $relation - * @return \Illuminate\Database\Eloquent\Relations\Relation<*, *, *> - */ - protected function getRelationWithoutConstraints($relation) - { - return Relation::noConstraints(function () use ($relation) { - return $this->getModel()->{$relation}(); - }); - } - - /** - * Check if we can run an "exists" query to optimize performance. - * - * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|int $count - * @return bool - */ - protected function canUseExistsForExistenceCheck($operator, $count) - { - return ($operator === '>=' || $operator === '<') && $count === 1; - } -} diff --git a/Eloquent/Concerns/TransformsToResource.php b/Eloquent/Concerns/TransformsToResource.php deleted file mode 100644 index 6006c35509..0000000000 --- a/Eloquent/Concerns/TransformsToResource.php +++ /dev/null @@ -1,99 +0,0 @@ -|null $resourceClass - * @return \Illuminate\Http\Resources\Json\JsonResource - */ - public function toResource(?string $resourceClass = null): JsonResource - { - if ($resourceClass === null) { - return $this->guessResource(); - } - - return $resourceClass::make($this); - } - - /** - * Guess the resource class for the model. - * - * @return \Illuminate\Http\Resources\Json\JsonResource - * - * @throws \LogicException - */ - protected function guessResource(): JsonResource - { - $resourceClass = $this->resolveResourceFromAttribute(static::class); - - if ($resourceClass !== null && class_exists($resourceClass)) { - return $resourceClass::make($this); - } - - foreach (static::guessResourceName() as $resourceClass) { - if (is_string($resourceClass) && class_exists($resourceClass)) { - return $resourceClass::make($this); - } - } - - throw new LogicException(sprintf('Failed to find resource class for model [%s].', get_class($this))); - } - - /** - * Guess the resource class name for the model. - * - * @return array{class-string<\Illuminate\Http\Resources\Json\JsonResource>, class-string<\Illuminate\Http\Resources\Json\JsonResource>} - */ - public static function guessResourceName(): array - { - $modelClass = static::class; - - if (! Str::contains($modelClass, '\\Models\\')) { - return []; - } - - $relativeNamespace = Str::after($modelClass, '\\Models\\'); - - $relativeNamespace = Str::contains($relativeNamespace, '\\') - ? Str::before($relativeNamespace, '\\'.class_basename($modelClass)) - : ''; - - $potentialResource = sprintf( - '%s\\Http\\Resources\\%s%s', - Str::before($modelClass, '\\Models'), - strlen($relativeNamespace) > 0 ? $relativeNamespace.'\\' : '', - class_basename($modelClass) - ); - - return [$potentialResource.'Resource', $potentialResource]; - } - - /** - * Get the resource class from the class attribute. - * - * @param class-string<\Illuminate\Http\Resources\Json\JsonResource> $class - * @return class-string<*>|null - */ - protected function resolveResourceFromAttribute(string $class): ?string - { - if (! class_exists($class)) { - return null; - } - - $attributes = (new ReflectionClass($class))->getAttributes(UseResource::class); - - return $attributes !== [] - ? $attributes[0]->newInstance()->class - : null; - } -} diff --git a/Eloquent/Factories/Attributes/UseModel.php b/Eloquent/Factories/Attributes/UseModel.php deleted file mode 100644 index 0f7c10db10..0000000000 --- a/Eloquent/Factories/Attributes/UseModel.php +++ /dev/null @@ -1,19 +0,0 @@ -factory = $factory; - $this->pivot = $pivot; - $this->relationship = $relationship; - } - - /** - * Create the attached relationship for the given model. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return void - */ - public function createFor(Model $model) - { - $factoryInstance = $this->factory instanceof Factory; - - if ($factoryInstance) { - $relationship = $model->{$this->relationship}(); - } - - Collection::wrap($factoryInstance ? $this->factory->prependState($relationship->getQuery()->pendingAttributes)->create([], $model) : $this->factory)->each(function ($attachable) use ($model) { - $model->{$this->relationship}()->attach( - $attachable, - is_callable($this->pivot) ? call_user_func($this->pivot, $model) : $this->pivot - ); - }); - } - - /** - * Specify the model instances to always use when creating relationships. - * - * @param \Illuminate\Support\Collection $recycle - * @return $this - */ - public function recycle($recycle) - { - if ($this->factory instanceof Factory) { - $this->factory = $this->factory->recycle($recycle); - } - - return $this; - } -} diff --git a/Eloquent/Factories/BelongsToRelationship.php b/Eloquent/Factories/BelongsToRelationship.php deleted file mode 100644 index 5979183d92..0000000000 --- a/Eloquent/Factories/BelongsToRelationship.php +++ /dev/null @@ -1,96 +0,0 @@ -factory = $factory; - $this->relationship = $relationship; - } - - /** - * Get the parent model attributes and resolvers for the given child model. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return array - */ - public function attributesFor(Model $model) - { - $relationship = $model->{$this->relationship}(); - - return $relationship instanceof MorphTo ? [ - $relationship->getMorphType() => $this->factory instanceof Factory ? $this->factory->newModel()->getMorphClass() : $this->factory->getMorphClass(), - $relationship->getForeignKeyName() => $this->resolver($relationship->getOwnerKeyName()), - ] : [ - $relationship->getForeignKeyName() => $this->resolver($relationship->getOwnerKeyName()), - ]; - } - - /** - * Get the deferred resolver for this relationship's parent ID. - * - * @param string|null $key - * @return \Closure - */ - protected function resolver($key) - { - return function () use ($key) { - if (! $this->resolved) { - $instance = $this->factory instanceof Factory - ? ($this->factory->getRandomRecycledModel($this->factory->modelName()) ?? $this->factory->create()) - : $this->factory; - - return $this->resolved = $key ? $instance->{$key} : $instance->getKey(); - } - - return $this->resolved; - }; - } - - /** - * Specify the model instances to always use when creating relationships. - * - * @param \Illuminate\Support\Collection $recycle - * @return $this - */ - public function recycle($recycle) - { - if ($this->factory instanceof Factory) { - $this->factory = $this->factory->recycle($recycle); - } - - return $this; - } -} diff --git a/Eloquent/Factories/CrossJoinSequence.php b/Eloquent/Factories/CrossJoinSequence.php deleted file mode 100644 index 594120b385..0000000000 --- a/Eloquent/Factories/CrossJoinSequence.php +++ /dev/null @@ -1,25 +0,0 @@ - - */ - protected $model; - - /** - * The number of models that should be generated. - * - * @var int|null - */ - protected $count; - - /** - * The state transformations that will be applied to the model. - * - * @var \Illuminate\Support\Collection - */ - protected $states; - - /** - * The parent relationships that will be applied to the model. - * - * @var \Illuminate\Support\Collection - */ - protected $has; - - /** - * The child relationships that will be applied to the model. - * - * @var \Illuminate\Support\Collection - */ - protected $for; - - /** - * The model instances to always use when creating relationships. - * - * @var \Illuminate\Support\Collection - */ - protected $recycle; - - /** - * The "after making" callbacks that will be applied to the model. - * - * @var \Illuminate\Support\Collection - */ - protected $afterMaking; - - /** - * The "after creating" callbacks that will be applied to the model. - * - * @var \Illuminate\Support\Collection - */ - protected $afterCreating; - - /** - * Whether relationships should not be automatically created. - * - * @var bool - */ - protected $expandRelationships = true; - - /** - * The relationships that should not be automatically created. - * - * @var array - */ - protected $excludeRelationships = []; - - /** - * The name of the database connection that will be used to create the models. - * - * @var \UnitEnum|string|null - */ - protected $connection; - - /** - * The current Faker instance. - * - * @var \Faker\Generator - */ - protected $faker; - - /** - * The default namespace where factories reside. - * - * @var string - */ - public static $namespace = 'Database\\Factories\\'; - - /** - * @deprecated use $modelNameResolvers - * - * @var callable(self): class-string - */ - protected static $modelNameResolver; - - /** - * The default model name resolvers. - * - * @var array> - */ - protected static $modelNameResolvers = []; - - /** - * The factory name resolver. - * - * @var callable - */ - protected static $factoryNameResolver; - - /** - * Whether to expand relationships by default. - * - * @var bool - */ - protected static $expandRelationshipsByDefault = true; - - /** - * The cached model class names resolved from attributes. - * - * @var array|false> - */ - protected static $cachedModelAttributes = []; - - /** - * Create a new factory instance. - * - * @param int|null $count - * @param \Illuminate\Support\Collection|null $states - * @param \Illuminate\Support\Collection|null $has - * @param \Illuminate\Support\Collection|null $for - * @param \Illuminate\Support\Collection|null $afterMaking - * @param \Illuminate\Support\Collection|null $afterCreating - * @param \UnitEnum|string|null $connection - * @param \Illuminate\Support\Collection|null $recycle - * @param bool|null $expandRelationships - * @param array $excludeRelationships - */ - public function __construct( - $count = null, - ?Collection $states = null, - ?Collection $has = null, - ?Collection $for = null, - ?Collection $afterMaking = null, - ?Collection $afterCreating = null, - $connection = null, - ?Collection $recycle = null, - ?bool $expandRelationships = null, - array $excludeRelationships = [], - ) { - $this->count = $count; - $this->states = $states ?? new Collection; - $this->has = $has ?? new Collection; - $this->for = $for ?? new Collection; - $this->afterMaking = $afterMaking ?? new Collection; - $this->afterCreating = $afterCreating ?? new Collection; - $this->connection = $connection; - $this->recycle = $recycle ?? new Collection; - $this->faker = $this->withFaker(); - $this->expandRelationships = $expandRelationships ?? self::$expandRelationshipsByDefault; - $this->excludeRelationships = $excludeRelationships; - } - - /** - * Define the model's default state. - * - * @return array - */ - abstract public function definition(); - - /** - * Get a new factory instance for the given attributes. - * - * @param (callable(array): array)|array $attributes - * @return static - */ - public static function new($attributes = []) - { - return (new static)->state($attributes)->configure(); - } - - /** - * Get a new factory instance for the given number of models. - * - * @param int $count - * @return static - */ - public static function times(int $count) - { - return static::new()->count($count); - } - - /** - * Configure the factory. - * - * @return static - */ - public function configure() - { - return $this; - } - - /** - * Get the raw attributes generated by the factory. - * - * @param (callable(array): array)|array $attributes - * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return array - */ - public function raw($attributes = [], ?Model $parent = null) - { - if ($this->count === null) { - return $this->state($attributes)->getExpandedAttributes($parent); - } - - return array_map(function () use ($attributes, $parent) { - return $this->state($attributes)->getExpandedAttributes($parent); - }, range(1, $this->count)); - } - - /** - * Create a single model and persist it to the database. - * - * @param (callable(array): array)|array $attributes - * @return TModel - */ - public function createOne($attributes = []) - { - return $this->count(null)->create($attributes); - } - - /** - * Create a single model and persist it to the database without dispatching any model events. - * - * @param (callable(array): array)|array $attributes - * @return TModel - */ - public function createOneQuietly($attributes = []) - { - return $this->count(null)->createQuietly($attributes); - } - - /** - * Create a collection of models and persist them to the database. - * - * @param int|null|iterable> $records - * @return \Illuminate\Database\Eloquent\Collection - */ - public function createMany(int|iterable|null $records = null) - { - $records ??= ($this->count ?? 1); - - $this->count = null; - - if (is_numeric($records)) { - $records = array_fill(0, $records, []); - } - - return new EloquentCollection( - (new Collection($records))->map(function ($record) { - return $this->state($record)->create(); - }) - ); - } - - /** - * Create a collection of models and persist them to the database without dispatching any model events. - * - * @param int|null|iterable> $records - * @return \Illuminate\Database\Eloquent\Collection - */ - public function createManyQuietly(int|iterable|null $records = null) - { - return Model::withoutEvents(fn () => $this->createMany($records)); - } - - /** - * Create a collection of models and persist them to the database. - * - * @param (callable(array): array)|array $attributes - * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return \Illuminate\Database\Eloquent\Collection|TModel - */ - public function create($attributes = [], ?Model $parent = null) - { - if (! empty($attributes)) { - return $this->state($attributes)->create([], $parent); - } - - $results = $this->make($attributes, $parent); - - if ($results instanceof Model) { - $this->store(new Collection([$results])); - - $this->callAfterCreating(new Collection([$results]), $parent); - } else { - $this->store($results); - - $this->callAfterCreating($results, $parent); - } - - return $results; - } - - /** - * Create a collection of models and persist them to the database without dispatching any model events. - * - * @param (callable(array): array)|array $attributes - * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return \Illuminate\Database\Eloquent\Collection|TModel - */ - public function createQuietly($attributes = [], ?Model $parent = null) - { - return Model::withoutEvents(fn () => $this->create($attributes, $parent)); - } - - /** - * Create a callback that persists a model in the database when invoked. - * - * @param array $attributes - * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return \Closure(): (\Illuminate\Database\Eloquent\Collection|TModel) - */ - public function lazy(array $attributes = [], ?Model $parent = null) - { - return fn () => $this->create($attributes, $parent); - } - - /** - * Set the connection name on the results and store them. - * - * @param \Illuminate\Support\Collection $results - * @return void - */ - protected function store(Collection $results) - { - $results->each(function ($model) { - if (! isset($this->connection)) { - $model->setConnection($model->newQueryWithoutScopes()->getConnection()->getName()); - } - - $model->save(); - - foreach ($model->getRelations() as $name => $items) { - if ($items instanceof Enumerable && $items->isEmpty()) { - $model->unsetRelation($name); - } - } - - $this->createChildren($model); - }); - } - - /** - * Create the children for the given model. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return void - */ - protected function createChildren(Model $model) - { - Model::unguarded(function () use ($model) { - $this->has->each(function ($has) use ($model) { - $has->recycle($this->recycle)->createFor($model); - }); - }); - } - - /** - * Make a single instance of the model. - * - * @param (callable(array): array)|array $attributes - * @return TModel - */ - public function makeOne($attributes = []) - { - return $this->count(null)->make($attributes); - } - - /** - * Create a collection of models. - * - * @param (callable(array): array)|array $attributes - * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return \Illuminate\Database\Eloquent\Collection|TModel - */ - public function make($attributes = [], ?Model $parent = null) - { - $autoEagerLoadingEnabled = Model::isAutomaticallyEagerLoadingRelationships(); - - if ($autoEagerLoadingEnabled) { - Model::automaticallyEagerLoadRelationships(false); - } - - try { - if (! empty($attributes)) { - return $this->state($attributes)->make([], $parent); - } - - if ($this->count === null) { - return tap($this->makeInstance($parent), function ($instance) { - $this->callAfterMaking(new Collection([$instance])); - }); - } - - if ($this->count < 1) { - return $this->newModel()->newCollection(); - } - - $instances = $this->newModel()->newCollection(array_map(function () use ($parent) { - return $this->makeInstance($parent); - }, range(1, $this->count))); - - $this->callAfterMaking($instances); - - return $instances; - } finally { - Model::automaticallyEagerLoadRelationships($autoEagerLoadingEnabled); - } - } - - /** - * Create a collection of models. - * - * @param iterable>|int|null $records - * @return \Illuminate\Database\Eloquent\Collection - */ - public function makeMany(iterable|int|null $records = null) - { - $records ??= ($this->count ?? 1); - - $this->count = null; - - if (is_numeric($records)) { - $records = array_fill(0, $records, []); - } - - return new EloquentCollection( - (new Collection($records))->map(function ($record) { - return $this->state($record)->make(); - }) - ); - } - - /** - * Insert the model records in bulk. No model events are emitted. - * - * @param array $attributes - * @param Model|null $parent - * @return void - */ - public function insert(array $attributes = [], ?Model $parent = null): void - { - $made = $this->make($attributes, $parent); - - $madeCollection = $made instanceof Collection - ? $made - : $this->newModel()->newCollection([$made]); - - $model = $madeCollection->first(); - - if (isset($this->connection)) { - $model->setConnection($this->connection); - } - - $query = $model->newQueryWithoutScopes(); - - $query->fillAndInsert( - $madeCollection->withoutAppends() - ->setHidden([]) - ->map(static fn (Model $model) => $model->attributesToArray()) - ->all() - ); - } - - /** - * Make an instance of the model with the given attributes. - * - * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return \Illuminate\Database\Eloquent\Model - */ - protected function makeInstance(?Model $parent) - { - return Model::unguarded(function () use ($parent) { - return tap($this->newModel($this->getExpandedAttributes($parent)), function ($instance) { - if (isset($this->connection)) { - $instance->setConnection($this->connection); - } - }); - }); - } - - /** - * Get a raw attributes array for the model. - * - * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return mixed - */ - protected function getExpandedAttributes(?Model $parent) - { - return $this->expandAttributes($this->getRawAttributes($parent)); - } - - /** - * Get the raw attributes for the model as an array. - * - * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return array - */ - protected function getRawAttributes(?Model $parent) - { - return $this->states->pipe(function ($states) { - return $this->for->isEmpty() ? $states : new Collection(array_merge([function () { - return $this->parentResolvers(); - }], $states->all())); - })->reduce(function ($carry, $state) use ($parent) { - if ($state instanceof Closure) { - $state = $state->bindTo($this); - } - - return array_merge($carry, $state($carry, $parent)); - }, $this->definition()); - } - - /** - * Create the parent relationship resolvers (as deferred Closures). - * - * @return array - */ - protected function parentResolvers() - { - return $this->for - ->map(fn (BelongsToRelationship $for) => $for->recycle($this->recycle)->attributesFor($this->newModel())) - ->collapse() - ->all(); - } - - /** - * Expand all attributes to their underlying values. - * - * @param array $definition - * @return array - */ - protected function expandAttributes(array $definition) - { - return (new Collection($definition)) - ->map($evaluateRelations = function ($attribute, $key) { - if (! $this->expandRelationships && $attribute instanceof self) { - $attribute = null; - } elseif ($attribute instanceof self && - array_intersect([$attribute->modelName(), $key], $this->excludeRelationships)) { - $attribute = null; - } elseif ($attribute instanceof self) { - $attribute = $this->getRandomRecycledModel($attribute->modelName())?->getKey() - ?? $attribute->recycle($this->recycle)->create()->getKey(); - } elseif ($attribute instanceof Model) { - $attribute = $attribute->getKey(); - } - - return $attribute; - }) - ->map(function ($attribute, $key) use (&$definition, $evaluateRelations) { - if (is_callable($attribute) && ! is_string($attribute) && ! is_array($attribute)) { - $attribute = $attribute($definition); - } - - $attribute = $evaluateRelations($attribute, $key); - - $definition[$key] = $attribute; - - return $attribute; - }) - ->all(); - } - - /** - * Add a new state transformation to the model definition. - * - * @param (callable(array, Model|null): array)|array $state - * @return static - */ - public function state($state) - { - return $this->newInstance([ - 'states' => $this->states->concat([ - is_callable($state) ? $state : fn () => $state, - ]), - ]); - } - - /** - * Prepend a new state transformation to the model definition. - * - * @param (callable(array, Model|null): array)|array $state - * @return static - */ - public function prependState($state) - { - return $this->newInstance([ - 'states' => $this->states->prepend( - is_callable($state) ? $state : fn () => $state, - ), - ]); - } - - /** - * Set a single model attribute. - * - * @param string|int $key - * @param mixed $value - * @return static - */ - public function set($key, $value) - { - return $this->state([$key => $value]); - } - - /** - * Add a new sequenced state transformation to the model definition. - * - * @param mixed ...$sequence - * @return static - */ - public function sequence(...$sequence) - { - return $this->state(new Sequence(...$sequence)); - } - - /** - * Add a new sequenced state transformation to the model definition and update the pending creation count to the size of the sequence. - * - * @param array ...$sequence - * @return static - */ - public function forEachSequence(...$sequence) - { - return $this->state(new Sequence(...$sequence))->count(count($sequence)); - } - - /** - * Add a new cross joined sequenced state transformation to the model definition. - * - * @param array ...$sequence - * @return static - */ - public function crossJoinSequence(...$sequence) - { - return $this->state(new CrossJoinSequence(...$sequence)); - } - - /** - * Define a child relationship for the model. - * - * @param \Illuminate\Database\Eloquent\Factories\Factory $factory - * @param string|null $relationship - * @return static - */ - public function has(self $factory, $relationship = null) - { - return $this->newInstance([ - 'has' => $this->has->concat([new Relationship( - $factory, $relationship ?? $this->guessRelationship($factory->modelName()) - )]), - ]); - } - - /** - * Attempt to guess the relationship name for a "has" relationship. - * - * @param string $related - * @return string - */ - protected function guessRelationship(string $related) - { - $guess = Str::camel(Str::plural(class_basename($related))); - - return method_exists($this->modelName(), $guess) ? $guess : Str::singular($guess); - } - - /** - * Define an attached relationship for the model. - * - * @param \Illuminate\Database\Eloquent\Factories\Factory|\Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $factory - * @param (callable(): array)|array $pivot - * @param string|null $relationship - * @return static - */ - public function hasAttached($factory, $pivot = [], $relationship = null) - { - return $this->newInstance([ - 'has' => $this->has->concat([new BelongsToManyRelationship( - $factory, - $pivot, - $relationship ?? Str::camel(Str::plural(class_basename( - $factory instanceof Factory - ? $factory->modelName() - : Collection::wrap($factory)->first() - ))) - )]), - ]); - } - - /** - * Define a parent relationship for the model. - * - * @param \Illuminate\Database\Eloquent\Factories\Factory|\Illuminate\Database\Eloquent\Model $factory - * @param string|null $relationship - * @return static - */ - public function for($factory, $relationship = null) - { - return $this->newInstance(['for' => $this->for->concat([new BelongsToRelationship( - $factory, - $relationship ?? Str::camel(class_basename( - $factory instanceof Factory ? $factory->modelName() : $factory - )) - )])]); - } - - /** - * Provide model instances to use instead of any nested factory calls when creating relationships. - * - * @param \Illuminate\Database\Eloquent\Model|\Illuminate\Support\Collection|array $model - * @return static - */ - public function recycle($model) - { - // Group provided models by the type and merge them into existing recycle collection - return $this->newInstance([ - 'recycle' => $this->recycle - ->flatten() - ->merge( - Collection::wrap($model instanceof Model ? func_get_args() : $model) - ->flatten() - )->groupBy(fn ($model) => get_class($model)), - ]); - } - - /** - * Retrieve a random model of a given type from previously provided models to recycle. - * - * @template TClass of \Illuminate\Database\Eloquent\Model - * - * @param class-string $modelClassName - * @return TClass|null - */ - public function getRandomRecycledModel($modelClassName) - { - return $this->recycle->get($modelClassName)?->random(); - } - - /** - * Add a new "after making" callback to the model definition. - * - * @param \Closure(TModel): mixed $callback - * @return static - */ - public function afterMaking(Closure $callback) - { - return $this->newInstance(['afterMaking' => $this->afterMaking->concat([$callback])]); - } - - /** - * Add a new "after creating" callback to the model definition. - * - * @param \Closure(TModel, \Illuminate\Database\Eloquent\Model|null): mixed $callback - * @return static - */ - public function afterCreating(Closure $callback) - { - return $this->newInstance(['afterCreating' => $this->afterCreating->concat([$callback])]); - } - - /** - * Remove the "after making" callbacks from the factory. - * - * @return static - */ - public function withoutAfterMaking() - { - return $this->newInstance(['afterMaking' => new Collection]); - } - - /** - * Remove the "after creating" callbacks from the factory. - * - * @return static - */ - public function withoutAfterCreating() - { - return $this->newInstance(['afterCreating' => new Collection]); - } - - /** - * Call the "after making" callbacks for the given model instances. - * - * @param \Illuminate\Support\Collection $instances - * @return void - */ - protected function callAfterMaking(Collection $instances) - { - $instances->each(function ($model) { - $this->afterMaking->each(function ($callback) use ($model) { - $callback($model); - }); - }); - } - - /** - * Call the "after creating" callbacks for the given model instances. - * - * @param \Illuminate\Support\Collection $instances - * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return void - */ - protected function callAfterCreating(Collection $instances, ?Model $parent = null) - { - $instances->each(function ($model) use ($parent) { - $this->afterCreating->each(function ($callback) use ($model, $parent) { - $callback($model, $parent); - }); - }); - } - - /** - * Specify how many models should be generated. - * - * @param int|null $count - * @return static - */ - public function count(?int $count) - { - return $this->newInstance(['count' => $count]); - } - - /** - * Indicate that related parent models should not be created. - * - * @param array> $parents - * @return static - */ - public function withoutParents($parents = []) - { - return $this->newInstance(! $parents ? ['expandRelationships' => false] : ['excludeRelationships' => $parents]); - } - - /** - * Get the name of the database connection that is used to generate models. - * - * @return string - */ - public function getConnectionName() - { - return enum_value($this->connection); - } - - /** - * Specify the database connection that should be used to generate models. - * - * @param \UnitEnum|string|null $connection - * @return static - */ - public function connection(UnitEnum|string|null $connection) - { - return $this->newInstance(['connection' => $connection]); - } - - /** - * Create a new instance of the factory builder with the given mutated properties. - * - * @param array $arguments - * @return static - */ - protected function newInstance(array $arguments = []) - { - return new static(...array_values(array_merge([ - 'count' => $this->count, - 'states' => $this->states, - 'has' => $this->has, - 'for' => $this->for, - 'afterMaking' => $this->afterMaking, - 'afterCreating' => $this->afterCreating, - 'connection' => $this->connection, - 'recycle' => $this->recycle, - 'expandRelationships' => $this->expandRelationships, - 'excludeRelationships' => $this->excludeRelationships, - ], $arguments))); - } - - /** - * Get a new model instance. - * - * @param array $attributes - * @return TModel - */ - public function newModel(array $attributes = []) - { - $model = $this->modelName(); - - return new $model($attributes); - } - - /** - * Get the name of the model that is generated by the factory. - * - * @return class-string - */ - public function modelName() - { - if (! array_key_exists(static::class, static::$cachedModelAttributes)) { - $attribute = (new ReflectionClass($this))->getAttributes(UseModel::class); - - static::$cachedModelAttributes[static::class] = count($attribute) > 0 - ? $attribute[0]->newInstance()->class - : false; - } - - if (static::$cachedModelAttributes[static::class]) { - return static::$cachedModelAttributes[static::class]; - } - - if ($this->model !== null) { - return $this->model; - } - - $resolver = static::$modelNameResolvers[static::class] ?? static::$modelNameResolvers[self::class] ?? static::$modelNameResolver ?? function (self $factory) { - $namespacedFactoryBasename = Str::replaceLast( - 'Factory', '', Str::replaceFirst(static::$namespace, '', $factory::class) - ); - - $factoryBasename = Str::replaceLast('Factory', '', class_basename($factory)); - - $appNamespace = static::appNamespace(); - - return class_exists($appNamespace.'Models\\'.$namespacedFactoryBasename) - ? $appNamespace.'Models\\'.$namespacedFactoryBasename - : $appNamespace.$factoryBasename; - }; - - return $resolver($this); - } - - /** - * Specify the callback that should be invoked to guess model names based on factory names. - * - * @param callable(self): class-string $callback - * @return void - */ - public static function guessModelNamesUsing(callable $callback) - { - static::$modelNameResolvers[static::class] = $callback; - } - - /** - * Specify the default namespace that contains the application's model factories. - * - * @param string $namespace - * @return void - */ - public static function useNamespace(string $namespace) - { - static::$namespace = $namespace; - } - - /** - * Get a new factory instance for the given model name. - * - * @template TClass of \Illuminate\Database\Eloquent\Model - * - * @param class-string $modelName - * @return \Illuminate\Database\Eloquent\Factories\Factory - */ - public static function factoryForModel(string $modelName) - { - $factory = static::resolveFactoryName($modelName); - - return $factory::new(); - } - - /** - * Specify the callback that should be invoked to guess factory names based on dynamic relationship names. - * - * @param callable(class-string<\Illuminate\Database\Eloquent\Model>): class-string<\Illuminate\Database\Eloquent\Factories\Factory> $callback - * @return void - */ - public static function guessFactoryNamesUsing(callable $callback) - { - static::$factoryNameResolver = $callback; - } - - /** - * Specify that relationships should create parent relationships by default. - * - * @return void - */ - public static function expandRelationshipsByDefault() - { - static::$expandRelationshipsByDefault = true; - } - - /** - * Specify that relationships should not create parent relationships by default. - * - * @return void - */ - public static function dontExpandRelationshipsByDefault() - { - static::$expandRelationshipsByDefault = false; - } - - /** - * Get a new Faker instance. - * - * @return \Faker\Generator|null - */ - protected function withFaker() - { - if (! class_exists(Generator::class)) { - return; - } - - return Container::getInstance()->make(Generator::class); - } - - /** - * Get the factory name for the given model name. - * - * @template TClass of \Illuminate\Database\Eloquent\Model - * - * @param class-string $modelName - * @return class-string<\Illuminate\Database\Eloquent\Factories\Factory> - */ - public static function resolveFactoryName(string $modelName) - { - $resolver = static::$factoryNameResolver ?? function (string $modelName) { - $appNamespace = static::appNamespace(); - - $modelName = Str::startsWith($modelName, $appNamespace.'Models\\') - ? Str::after($modelName, $appNamespace.'Models\\') - : Str::after($modelName, $appNamespace); - - return static::$namespace.$modelName.'Factory'; - }; - - return $resolver($modelName); - } - - /** - * Get the application namespace for the application. - * - * @return string - */ - protected static function appNamespace() - { - try { - return Container::getInstance() - ->make(Application::class) - ->getNamespace(); - } catch (Throwable) { - return 'App\\'; - } - } - - /** - * Flush the factory's global state. - * - * @return void - */ - public static function flushState() - { - static::$modelNameResolver = null; - static::$modelNameResolvers = []; - static::$factoryNameResolver = null; - static::$namespace = 'Database\\Factories\\'; - static::$expandRelationshipsByDefault = true; - } - - /** - * Proxy dynamic factory methods onto their proper methods. - * - * @param string $method - * @param array $parameters - * @return mixed - */ - public function __call($method, $parameters) - { - if (static::hasMacro($method)) { - return $this->macroCall($method, $parameters); - } - - if ($method === 'trashed' && $this->modelName()::isSoftDeletable()) { - return $this->state([ - $this->newModel()->getDeletedAtColumn() => $parameters[0] ?? Carbon::now()->subDay(), - ]); - } - - if (! Str::startsWith($method, ['for', 'has'])) { - static::throwBadMethodCallException($method); - } - - $relationship = Str::camel(Str::substr($method, 3)); - - $relatedModel = get_class($this->newModel()->{$relationship}()->getRelated()); - - if (method_exists($relatedModel, 'newFactory')) { - $factory = $relatedModel::newFactory() ?? static::factoryForModel($relatedModel); - } else { - $factory = static::factoryForModel($relatedModel); - } - - if (str_starts_with($method, 'for')) { - return $this->for($factory->state($parameters[0] ?? []), $relationship); - } elseif (str_starts_with($method, 'has')) { - return $this->has( - $factory - ->count(is_numeric($parameters[0] ?? null) ? $parameters[0] : 1) - ->state((is_callable($parameters[0] ?? null) || is_array($parameters[0] ?? null)) ? $parameters[0] : ($parameters[1] ?? [])), - $relationship - ); - } - } -} diff --git a/Eloquent/Factories/HasFactory.php b/Eloquent/Factories/HasFactory.php deleted file mode 100644 index d2747cc93c..0000000000 --- a/Eloquent/Factories/HasFactory.php +++ /dev/null @@ -1,62 +0,0 @@ -, static|null): array)|array|int|null $count - * @param (callable(array, static|null): array)|array $state - * @return TFactory - */ - public static function factory($count = null, $state = []) - { - $factory = static::newFactory() ?? Factory::factoryForModel(static::class); - - return $factory - ->count(is_numeric($count) ? $count : null) - ->state(is_callable($count) || is_array($count) ? $count : $state); - } - - /** - * Create a new factory instance for the model. - * - * @return TFactory|null - */ - protected static function newFactory() - { - if (isset(static::$factory)) { - return static::$factory::new(); - } - - return static::getUseFactoryAttribute() ?? null; - } - - /** - * Get the factory from the UseFactory class attribute. - * - * @return TFactory|null - */ - protected static function getUseFactoryAttribute() - { - $attributes = (new \ReflectionClass(static::class)) - ->getAttributes(UseFactory::class); - - if ($attributes !== []) { - $useFactory = $attributes[0]->newInstance(); - - $factory = $useFactory->factoryClass::new(); - - $factory->guessModelNamesUsing(fn () => static::class); - - return $factory; - } - } -} diff --git a/Eloquent/Factories/Relationship.php b/Eloquent/Factories/Relationship.php deleted file mode 100644 index e23bc99d78..0000000000 --- a/Eloquent/Factories/Relationship.php +++ /dev/null @@ -1,76 +0,0 @@ -factory = $factory; - $this->relationship = $relationship; - } - - /** - * Create the child relationship for the given parent model. - * - * @param \Illuminate\Database\Eloquent\Model $parent - * @return void - */ - public function createFor(Model $parent) - { - $relationship = $parent->{$this->relationship}(); - - if ($relationship instanceof MorphOneOrMany) { - $this->factory->state([ - $relationship->getMorphType() => $relationship->getMorphClass(), - $relationship->getForeignKeyName() => $relationship->getParentKey(), - ])->prependState($relationship->getQuery()->pendingAttributes)->create([], $parent); - } elseif ($relationship instanceof HasOneOrMany) { - $this->factory->state([ - $relationship->getForeignKeyName() => $relationship->getParentKey(), - ])->prependState($relationship->getQuery()->pendingAttributes)->create([], $parent); - } elseif ($relationship instanceof BelongsToMany) { - $relationship->attach( - $this->factory->prependState($relationship->getQuery()->pendingAttributes)->create([], $parent) - ); - } - } - - /** - * Specify the model instances to always use when creating relationships. - * - * @param \Illuminate\Support\Collection $recycle - * @return $this - */ - public function recycle($recycle) - { - $this->factory = $this->factory->recycle($recycle); - - return $this; - } -} diff --git a/Eloquent/Factories/Sequence.php b/Eloquent/Factories/Sequence.php deleted file mode 100644 index 4d350d2ad1..0000000000 --- a/Eloquent/Factories/Sequence.php +++ /dev/null @@ -1,64 +0,0 @@ -sequence = $sequence; - $this->count = count($sequence); - } - - /** - * Get the current count of the sequence items. - * - * @return int - */ - public function count(): int - { - return $this->count; - } - - /** - * Get the next value in the sequence. - * - * @param array $attributes - * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return mixed - */ - public function __invoke($attributes = [], $parent = null) - { - return tap(value($this->sequence[$this->index % $this->count], $this, $attributes, $parent), function () { - $this->index = $this->index + 1; - }); - } -} diff --git a/Eloquent/Factory.php b/Eloquent/Factory.php new file mode 100644 index 0000000000..099f4b4f8e --- /dev/null +++ b/Eloquent/Factory.php @@ -0,0 +1,229 @@ +faker = $faker; + } + + /** + * The model definitions in the container. + * + * @var array + */ + protected $definitions = []; + + /** + * Create a new factory container. + * + * @param \Faker\Generator $faker + * @param string|null $pathToFactories + * @return static + */ + public static function construct(Faker $faker, $pathToFactories = null) + { + $pathToFactories = $pathToFactories ?: database_path('factories'); + + return (new static($faker))->load($pathToFactories); + } + + /** + * Define a class with a given short-name. + * + * @param string $class + * @param string $name + * @param callable $attributes + * @return void + */ + public function defineAs($class, $name, callable $attributes) + { + return $this->define($class, $attributes, $name); + } + + /** + * Define a class with a given set of attributes. + * + * @param string $class + * @param callable $attributes + * @param string $name + * @return void + */ + public function define($class, callable $attributes, $name = 'default') + { + $this->definitions[$class][$name] = $attributes; + } + + /** + * Create an instance of the given model and persist it to the database. + * + * @param string $class + * @param array $attributes + * @return mixed + */ + public function create($class, array $attributes = []) + { + return $this->of($class)->create($attributes); + } + + /** + * Create an instance of the given model and type and persist it to the database. + * + * @param string $class + * @param string $name + * @param array $attributes + * @return mixed + */ + public function createAs($class, $name, array $attributes = []) + { + return $this->of($class, $name)->create($attributes); + } + + /** + * Load factories from path. + * + * @param string $path + * @return $this + */ + public function load($path) + { + $factory = $this; + + if (is_dir($path)) { + foreach (Finder::create()->files()->in($path) as $file) { + require $file->getRealPath(); + } + } + + return $factory; + } + + /** + * Create an instance of the given model. + * + * @param string $class + * @param array $attributes + * @return mixed + */ + public function make($class, array $attributes = []) + { + return $this->of($class)->make($attributes); + } + + /** + * Create an instance of the given model and type. + * + * @param string $class + * @param string $name + * @param array $attributes + * @return mixed + */ + public function makeAs($class, $name, array $attributes = []) + { + return $this->of($class, $name)->make($attributes); + } + + /** + * Get the raw attribute array for a given named model. + * + * @param string $class + * @param string $name + * @param array $attributes + * @return array + */ + public function rawOf($class, $name, array $attributes = []) + { + return $this->raw($class, $attributes, $name); + } + + /** + * Get the raw attribute array for a given model. + * + * @param string $class + * @param array $attributes + * @param string $name + * @return array + */ + public function raw($class, array $attributes = [], $name = 'default') + { + $raw = call_user_func($this->definitions[$class][$name], $this->faker); + + return array_merge($raw, $attributes); + } + + /** + * Create a builder for the given model. + * + * @param string $class + * @param string $name + * @return \Illuminate\Database\Eloquent\FactoryBuilder + */ + public function of($class, $name = 'default') + { + return new FactoryBuilder($class, $name, $this->definitions, $this->faker); + } + + /** + * Determine if the given offset exists. + * + * @param string $offset + * @return bool + */ + public function offsetExists($offset) + { + return isset($this->definitions[$offset]); + } + + /** + * Get the value of the given offset. + * + * @param string $offset + * @return mixed + */ + public function offsetGet($offset) + { + return $this->make($offset); + } + + /** + * Set the given offset to the given value. + * + * @param string $offset + * @param callable $value + * @return void + */ + public function offsetSet($offset, $value) + { + return $this->define($offset, $value); + } + + /** + * Unset the value at the given offset. + * + * @param string $offset + * @return void + */ + public function offsetUnset($offset) + { + unset($this->definitions[$offset]); + } +} diff --git a/Eloquent/FactoryBuilder.php b/Eloquent/FactoryBuilder.php new file mode 100644 index 0000000000..3fdb4b0b2e --- /dev/null +++ b/Eloquent/FactoryBuilder.php @@ -0,0 +1,161 @@ +name = $name; + $this->class = $class; + $this->faker = $faker; + $this->definitions = $definitions; + } + + /** + * Set the amount of models you wish to create / make. + * + * @param int $amount + * @return $this + */ + public function times($amount) + { + $this->amount = $amount; + + return $this; + } + + /** + * Create a collection of models and persist them to the database. + * + * @param array $attributes + * @return mixed + */ + public function create(array $attributes = []) + { + $results = $this->make($attributes); + + if ($this->amount === 1) { + $results->save(); + } else { + foreach ($results as $result) { + $result->save(); + } + } + + return $results; + } + + /** + * Create a collection of models. + * + * @param array $attributes + * @return mixed + */ + public function make(array $attributes = []) + { + if ($this->amount === 1) { + return $this->makeInstance($attributes); + } else { + $results = []; + + for ($i = 0; $i < $this->amount; $i++) { + $results[] = $this->makeInstance($attributes); + } + + return new Collection($results); + } + } + + /** + * Make an instance of the model with the given attributes. + * + * @param array $attributes + * @return \Illuminate\Database\Eloquent\Model + * + * @throws \InvalidArgumentException + */ + protected function makeInstance(array $attributes = []) + { + return Model::unguarded(function () use ($attributes) { + if (! isset($this->definitions[$this->class][$this->name])) { + throw new InvalidArgumentException("Unable to locate factory with name [{$this->name}] [{$this->class}]."); + } + + $definition = call_user_func( + $this->definitions[$this->class][$this->name], + $this->faker, $attributes + ); + + $evaluated = $this->callClosureAttributes( + array_merge($definition, $attributes) + ); + + return new $this->class($evaluated); + }); + } + + /** + * Evaluate any Closure attributes on the attribute array. + * + * @param array $attributes + * @return array + */ + protected function callClosureAttributes(array $attributes) + { + foreach ($attributes as &$attribute) { + $attribute = $attribute instanceof Closure + ? $attribute($attributes) : $attribute; + } + + return $attributes; + } +} diff --git a/Eloquent/HasBuilder.php b/Eloquent/HasBuilder.php deleted file mode 100644 index 9431bb46f7..0000000000 --- a/Eloquent/HasBuilder.php +++ /dev/null @@ -1,124 +0,0 @@ -, class-string> - */ - protected static array $resolvedCollectionClasses = []; - - /** - * Create a new Eloquent Collection instance. - * - * @param array $models - * @return TCollection - */ - public function newCollection(array $models = []) - { - static::$resolvedCollectionClasses[static::class] ??= ($this->resolveCollectionFromAttribute() ?? static::$collectionClass); - - $collection = new static::$resolvedCollectionClasses[static::class]($models); - - if (Model::isAutomaticallyEagerLoadingRelationships()) { - $collection->withRelationshipAutoloading(); - } - - return $collection; - } - - /** - * Resolve the collection class name from the CollectedBy attribute. - * - * @return class-string|null - */ - public function resolveCollectionFromAttribute() - { - $reflectionClass = new ReflectionClass(static::class); - - $attributes = $reflectionClass->getAttributes(CollectedBy::class); - - if (! isset($attributes[0]) || ! isset($attributes[0]->getArguments()[0])) { - return; - } - - return $attributes[0]->getArguments()[0]; - } -} diff --git a/Eloquent/HigherOrderBuilderProxy.php b/Eloquent/HigherOrderBuilderProxy.php deleted file mode 100644 index dfcbbd6774..0000000000 --- a/Eloquent/HigherOrderBuilderProxy.php +++ /dev/null @@ -1,49 +0,0 @@ - - */ - protected $builder; - - /** - * The method being proxied. - * - * @var string - */ - protected $method; - - /** - * Create a new proxy instance. - * - * @param \Illuminate\Database\Eloquent\Builder<*> $builder - * @param string $method - */ - public function __construct(Builder $builder, $method) - { - $this->method = $method; - $this->builder = $builder; - } - - /** - * Proxy a scope call onto the query builder. - * - * @param string $method - * @param array $parameters - * @return mixed - */ - public function __call($method, $parameters) - { - return $this->builder->{$this->method}(function ($value) use ($method, $parameters) { - return $value->{$method}(...$parameters); - }); - } -} diff --git a/Eloquent/InvalidCastException.php b/Eloquent/InvalidCastException.php deleted file mode 100644 index f37672c0b9..0000000000 --- a/Eloquent/InvalidCastException.php +++ /dev/null @@ -1,47 +0,0 @@ -model = $class; - $this->column = $column; - $this->castType = $castType; - } -} diff --git a/Eloquent/JsonEncodingException.php b/Eloquent/JsonEncodingException.php deleted file mode 100644 index f62abd4695..0000000000 --- a/Eloquent/JsonEncodingException.php +++ /dev/null @@ -1,49 +0,0 @@ -getKey().'] to JSON: '.$message); - } - - /** - * Create a new JSON encoding exception for the resource. - * - * @param \Illuminate\Http\Resources\Json\JsonResource $resource - * @param string $message - * @return static - */ - public static function forResource($resource, $message) - { - $model = $resource->resource; - - return new static('Error encoding resource ['.get_class($resource).'] with model ['.get_class($model).'] with ID ['.$model->getKey().'] to JSON: '.$message); - } - - /** - * Create a new JSON encoding exception for an attribute. - * - * @param mixed $model - * @param mixed $key - * @param string $message - * @return static - */ - public static function forAttribute($model, $key, $message) - { - $class = get_class($model); - - return new static("Unable to encode attribute [{$key}] for model [{$class}] to JSON: {$message}."); - } -} diff --git a/Eloquent/MassPrunable.php b/Eloquent/MassPrunable.php deleted file mode 100644 index 3edb605ace..0000000000 --- a/Eloquent/MassPrunable.php +++ /dev/null @@ -1,52 +0,0 @@ -prunable(), function ($query) use ($chunkSize) { - $query->when(! $query->getQuery()->limit, function ($query) use ($chunkSize) { - $query->limit($chunkSize); - }); - }); - - $total = 0; - - $softDeletable = static::isSoftDeletable(); - - do { - $total += $count = $softDeletable - ? $query->forceDelete() - : $query->delete(); - - if ($count > 0) { - event(new ModelsPruned(static::class, $total)); - } - } while ($count > 0); - - return $total; - } - - /** - * Get the prunable model query. - * - * @return \Illuminate\Database\Eloquent\Builder - * - * @throws \LogicException - */ - public function prunable() - { - throw new LogicException('Please implement the prunable method on your model.'); - } -} diff --git a/Eloquent/MissingAttributeException.php b/Eloquent/MissingAttributeException.php deleted file mode 100755 index ef05109927..0000000000 --- a/Eloquent/MissingAttributeException.php +++ /dev/null @@ -1,22 +0,0 @@ -> */ - use HasCollection; - /** * The connection name for the model. * - * @var \UnitEnum|string|null + * @var string */ protected $connection; /** * The table associated with the model. * - * @var string|null + * @var string */ protected $table; @@ -76,11 +57,11 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt protected $primaryKey = 'id'; /** - * The "type" of the primary key ID. + * The number of models to return for pagination. * - * @var string + * @var int */ - protected $keyType = 'int'; + protected $perPage = 15; /** * Indicates if the IDs are auto-incrementing. @@ -90,233 +71,213 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt public $incrementing = true; /** - * The relations to eager load on every query. + * Indicates if the model should be timestamped. * - * @var array + * @var bool */ - protected $with = []; + public $timestamps = true; /** - * The relationship counts that should be eager loaded on every query. + * The model's attributes. * * @var array */ - protected $withCount = []; + protected $attributes = []; /** - * Indicates whether lazy loading will be prevented on this model. + * The model attribute's original state. * - * @var bool + * @var array */ - public $preventsLazyLoading = false; + protected $original = []; /** - * The number of models to return for pagination. + * The loaded relationships for the model. * - * @var int + * @var array */ - protected $perPage = 15; + protected $relations = []; /** - * Indicates if the model exists. + * The attributes that should be hidden for arrays. * - * @var bool + * @var array */ - public $exists = false; + protected $hidden = []; /** - * Indicates if the model was inserted during the object's lifecycle. + * The attributes that should be visible in arrays. * - * @var bool + * @var array */ - public $wasRecentlyCreated = false; + protected $visible = []; /** - * Indicates that the object's string representation should be escaped when __toString is invoked. + * The accessors to append to the model's array form. * - * @var bool + * @var array */ - protected $escapeWhenCastingToString = false; + protected $appends = []; /** - * The connection resolver instance. + * The attributes that are mass assignable. * - * @var \Illuminate\Database\ConnectionResolverInterface + * @var array */ - protected static $resolver; + protected $fillable = []; /** - * The event dispatcher instance. + * The attributes that aren't mass assignable. * - * @var \Illuminate\Contracts\Events\Dispatcher|null + * @var array */ - protected static $dispatcher; + protected $guarded = ['*']; /** - * The models that are currently being booted. + * The attributes that should be mutated to dates. * * @var array */ - protected static $booting = []; + protected $dates = []; /** - * The array of booted models. + * The storage format of the model's date columns. * - * @var array + * @var string */ - protected static $booted = []; + protected $dateFormat; /** - * The callbacks that should be executed after the model has booted. + * The attributes that should be cast to native types. * * @var array */ - protected static $bootedCallbacks = []; + protected $casts = []; /** - * The array of trait initializers that will be called on each new instance. + * The relationships that should be touched on save. * * @var array */ - protected static $traitInitializers = []; + protected $touches = []; /** - * The array of global scopes on the model. + * User exposed observable events. * * @var array */ - protected static $globalScopes = []; + protected $observables = []; /** - * The list of models classes that should not be affected with touch. + * The relations to eager load on every query. * * @var array */ - protected static $ignoreOnTouch = []; + protected $with = []; /** - * Indicates whether lazy loading should be restricted on all models. + * The class name to be used in polymorphic relations. * - * @var bool + * @var string */ - protected static $modelsShouldPreventLazyLoading = false; + protected $morphClass; /** - * Indicates whether relations should be automatically loaded on all models when they are accessed. + * Indicates if the model exists. * * @var bool */ - protected static $modelsShouldAutomaticallyEagerLoadRelationships = false; - - /** - * The callback that is responsible for handling lazy loading violations. - * - * @var (callable(self, string))|null - */ - protected static $lazyLoadingViolationCallback; + public $exists = false; /** - * Indicates if an exception should be thrown instead of silently discarding non-fillable attributes. + * Indicates if the model was inserted during the current request lifecycle. * * @var bool */ - protected static $modelsShouldPreventSilentlyDiscardingAttributes = false; - - /** - * The callback that is responsible for handling discarded attribute violations. - * - * @var (callable(self, array))|null - */ - protected static $discardedAttributeViolationCallback; + public $wasRecentlyCreated = false; /** - * Indicates if an exception should be thrown when trying to access a missing attribute on a retrieved model. + * Indicates whether attributes are snake cased on arrays. * * @var bool */ - protected static $modelsShouldPreventAccessingMissingAttributes = false; - - /** - * The callback that is responsible for handling missing attribute violations. - * - * @var (callable(self, string))|null - */ - protected static $missingAttributeViolationCallback; + public static $snakeAttributes = true; /** - * Indicates if broadcasting is currently enabled. + * The connection resolver instance. * - * @var bool + * @var \Illuminate\Database\ConnectionResolverInterface */ - protected static $isBroadcasting = true; + protected static $resolver; /** - * The Eloquent query builder class to use for the model. + * The event dispatcher instance. * - * @var class-string<\Illuminate\Database\Eloquent\Builder<*>> + * @var \Illuminate\Contracts\Events\Dispatcher */ - protected static string $builder = Builder::class; + protected static $dispatcher; /** - * The Eloquent collection class to use for the model. + * The array of booted models. * - * @var class-string<\Illuminate\Database\Eloquent\Collection<*, *>> + * @var array */ - protected static string $collectionClass = Collection::class; + protected static $booted = []; /** - * Cache of soft deletable models. + * The array of global scopes on the model. * - * @var array, bool> + * @var array */ - protected static array $isSoftDeletable; + protected static $globalScopes = []; /** - * Cache of prunable models. + * Indicates if all mass assignment is enabled. * - * @var array, bool> + * @var bool */ - protected static array $isPrunable; + protected static $unguarded = false; /** - * Cache of mass prunable models. + * The cache of the mutated attributes for each class. * - * @var array, bool> + * @var array */ - protected static array $isMassPrunable; + protected static $mutatorCache = []; /** - * Cache of resolved class attributes. + * The many to many relationship methods. * - * @var array, array> + * @var array */ - protected static array $classAttributes = []; + public static $manyMethods = ['belongsToMany', 'morphToMany', 'morphedByMany']; /** * The name of the "created at" column. * - * @var string|null + * @var string */ const CREATED_AT = 'created_at'; /** * The name of the "updated at" column. * - * @var string|null + * @var string */ const UPDATED_AT = 'updated_at'; /** * Create a new Eloquent model instance. * - * @param array $attributes + * @param array $attributes + * @return void */ public function __construct(array $attributes = []) { $this->bootIfNotBooted(); - $this->initializeTraits(); - $this->initializeModelAttributes(); + $this->syncOriginal(); + $this->fill($attributes); } @@ -324,50 +285,22 @@ public function __construct(array $attributes = []) * Check if the model needs to be booted and if so, do it. * * @return void - * - * @throws \LogicException */ protected function bootIfNotBooted() { if (! isset(static::$booted[static::class])) { - if (isset(static::$booting[static::class])) { - throw new LogicException('The ['.__METHOD__.'] method may not be called on model ['.static::class.'] while it is being booted.'); - } - - static::$booting[static::class] = true; + static::$booted[static::class] = true; $this->fireModelEvent('booting', false); - static::booting(); static::boot(); - static::$booted[static::class] = true; - unset(static::$booting[static::class]); - - static::booted(); - - static::$bootedCallbacks[static::class] ??= []; - - foreach (static::$bootedCallbacks[static::class] as $callback) { - $callback(); - } - $this->fireModelEvent('booted', false); } } /** - * Perform any actions required before the model boots. - * - * @return void - */ - protected static function booting() - { - // - } - - /** - * Bootstrap the model and its traits. + * The "booting" method of the model. * * @return void */ @@ -385,882 +318,1126 @@ protected static function bootTraits() { $class = static::class; - $booted = []; - - static::$traitInitializers[$class] = []; - - $uses = class_uses_recursive($class); - - $conventionalBootMethods = array_map(static fn ($trait) => 'boot'.class_basename($trait), $uses); - $conventionalInitMethods = array_map(static fn ($trait) => 'initialize'.class_basename($trait), $uses); - - foreach ((new ReflectionClass($class))->getMethods() as $method) { - if (! in_array($method->getName(), $booted) && - $method->isStatic() && - (in_array($method->getName(), $conventionalBootMethods) || - $method->getAttributes(Boot::class) !== [])) { - $method->invoke(null); - - $booted[] = $method->getName(); - } - - if (in_array($method->getName(), $conventionalInitMethods) || - $method->getAttributes(Initialize::class) !== []) { - static::$traitInitializers[$class][] = $method->getName(); + foreach (class_uses_recursive($class) as $trait) { + if (method_exists($class, $method = 'boot'.class_basename($trait))) { + forward_static_call([$class, $method]); } } - - static::$traitInitializers[$class] = array_unique(static::$traitInitializers[$class]); } /** - * Initialize any initializable traits on the model. + * Clear the list of booted models so they will be re-booted. * * @return void */ - protected function initializeTraits() + public static function clearBootedModels() { - foreach (static::$traitInitializers[static::class] as $method) { - $this->{$method}(); - } + static::$booted = []; + static::$globalScopes = []; } /** - * Initialize the model attributes from class attributes. + * Register a new global scope on the model. * - * @return void + * @param \Illuminate\Database\Eloquent\Scope|\Closure|string $scope + * @param \Closure|null $implementation + * @return mixed + * + * @throws \InvalidArgumentException */ - public function initializeModelAttributes() + public static function addGlobalScope($scope, Closure $implementation = null) { - $table = static::resolveClassAttribute(Table::class); - - $this->table ??= $table->name ?? null; - $this->connection ??= static::resolveClassAttribute(Connection::class, 'name'); - - if ($this->primaryKey === 'id' && $table && $table->key !== null) { - $this->primaryKey = $table->key; + if (is_string($scope) && $implementation !== null) { + return static::$globalScopes[static::class][$scope] = $implementation; } - if ($this->keyType === 'int' && $table && $table->keyType !== null) { - $this->keyType = $table->keyType; + if ($scope instanceof Closure) { + return static::$globalScopes[static::class][spl_object_hash($scope)] = $scope; } - if ($this->incrementing === true && $table && $table->incrementing !== null) { - $this->incrementing = $table->incrementing; + if ($scope instanceof Scope) { + return static::$globalScopes[static::class][get_class($scope)] = $scope; } + + throw new InvalidArgumentException('Global scope must be an instance of Closure or Scope.'); } /** - * Perform any actions required after the model boots. + * Determine if a model has a global scope. * - * @return void + * @param \Illuminate\Database\Eloquent\Scope|string $scope + * @return bool */ - protected static function booted() + public static function hasGlobalScope($scope) { - // + return ! is_null(static::getGlobalScope($scope)); } /** - * Register a closure to be executed after the model has booted. + * Get a global scope registered with the model. * - * @param \Closure $callback - * @return void + * @param \Illuminate\Database\Eloquent\Scope|string $scope + * @return \Illuminate\Database\Eloquent\Scope|\Closure|null */ - protected static function whenBooted(Closure $callback) + public static function getGlobalScope($scope) { - static::$bootedCallbacks[static::class] ??= []; + $modelScopes = Arr::get(static::$globalScopes, static::class, []); + + if (is_string($scope)) { + return isset($modelScopes[$scope]) ? $modelScopes[$scope] : null; + } - static::$bootedCallbacks[static::class][] = $callback; + return Arr::first($modelScopes, function ($value) use ($scope) { + return $scope instanceof $value; + }); } /** - * Clear the list of booted models so they will be re-booted. + * Get the global scopes for this class instance. * - * @return void + * @return array */ - public static function clearBootedModels() + public function getGlobalScopes() { - static::$booted = []; - static::$bootedCallbacks = []; - static::$classAttributes = []; - static::$globalScopes = []; + return Arr::get(static::$globalScopes, static::class, []); } /** - * Disables relationship model touching for the current class during given callback scope. + * Register an observer with the Model. * - * @param callable $callback + * @param object|string $class + * @param int $priority * @return void */ - public static function withoutTouching(callable $callback) + public static function observe($class, $priority = 0) { - static::withoutTouchingOn([static::class], $callback); + $instance = new static; + + $className = is_string($class) ? $class : get_class($class); + + // When registering a model observer, we will spin through the possible events + // and determine if this observer has that method. If it does, we will hook + // it into the model's event system, making it convenient to watch these. + foreach ($instance->getObservableEvents() as $event) { + if (method_exists($class, $event)) { + static::registerModelEvent($event, $className.'@'.$event, $priority); + } + } } /** - * Disables relationship model touching for the given model classes during given callback scope. + * Fill the model with an array of attributes. * - * @param array $models - * @param callable $callback - * @return void + * @param array $attributes + * @return $this + * + * @throws \Illuminate\Database\Eloquent\MassAssignmentException */ - public static function withoutTouchingOn(array $models, callable $callback) + public function fill(array $attributes) { - static::$ignoreOnTouch = array_values(array_merge(static::$ignoreOnTouch, $models)); + $totallyGuarded = $this->totallyGuarded(); - try { - $callback(); - } finally { - static::$ignoreOnTouch = array_values(array_diff(static::$ignoreOnTouch, $models)); + foreach ($this->fillableFromArray($attributes) as $key => $value) { + $key = $this->removeTableFromKey($key); + + // The developers may choose to place some attributes in the "fillable" + // array, which means only those attributes may be set through mass + // assignment to the model, and all others will just be ignored. + if ($this->isFillable($key)) { + $this->setAttribute($key, $value); + } elseif ($totallyGuarded) { + throw new MassAssignmentException($key); + } } + + return $this; } /** - * Determine if the given model is ignoring touches. + * Fill the model with an array of attributes. Force mass assignment. * - * @param string|null $class - * @return bool + * @param array $attributes + * @return $this */ - public static function isIgnoringTouch($class = null) + public function forceFill(array $attributes) { - $class = $class ?: static::class; + return static::unguarded(function () use ($attributes) { + return $this->fill($attributes); + }); + } - if (! $class::UPDATED_AT) { - return true; + /** + * Get the fillable attributes of a given array. + * + * @param array $attributes + * @return array + */ + protected function fillableFromArray(array $attributes) + { + if (count($this->getFillable()) > 0 && ! static::$unguarded) { + return array_intersect_key($attributes, array_flip($this->getFillable())); } - $timestamps = static::resolveClassAttribute(Table::class, 'timestamps', $class) - ?? get_class_vars($class)['timestamps']; + return $attributes; + } - if (! $timestamps) { - return true; - } + /** + * Create a new instance of the given model. + * + * @param array $attributes + * @param bool $exists + * @return static + */ + public function newInstance($attributes = [], $exists = false) + { + // This method just provides a convenient way for us to generate fresh model + // instances of this current model. It is particularly useful during the + // hydration of new objects via the Eloquent query builder instances. + $model = new static((array) $attributes); - foreach (static::$ignoreOnTouch as $ignoredClass) { - if ($class === $ignoredClass || is_subclass_of($class, $ignoredClass)) { - return true; - } - } + $model->exists = $exists; - return false; + return $model; } /** - * Indicate that models should prevent lazy loading, silently discarding attributes, and accessing missing attributes. + * Create a new model instance that is existing. * - * @param bool $shouldBeStrict - * @return void + * @param array $attributes + * @param string|null $connection + * @return static */ - public static function shouldBeStrict(bool $shouldBeStrict = true) + public function newFromBuilder($attributes = [], $connection = null) { - static::preventLazyLoading($shouldBeStrict); - static::preventSilentlyDiscardingAttributes($shouldBeStrict); - static::preventAccessingMissingAttributes($shouldBeStrict); + $model = $this->newInstance([], true); + + $model->setRawAttributes((array) $attributes, true); + + $model->setConnection($connection ?: $this->connection); + + return $model; } /** - * Prevent model relationships from being lazy loaded. + * Create a collection of models from plain arrays. * - * @param bool $value - * @return void + * @param array $items + * @param string|null $connection + * @return \Illuminate\Database\Eloquent\Collection */ - public static function preventLazyLoading($value = true) + public static function hydrate(array $items, $connection = null) { - static::$modelsShouldPreventLazyLoading = $value; + $instance = (new static)->setConnection($connection); + + $items = array_map(function ($item) use ($instance) { + return $instance->newFromBuilder($item); + }, $items); + + return $instance->newCollection($items); } /** - * Determine if model relationships should be automatically eager loaded when accessed. + * Create a collection of models from a raw query. * - * @param bool $value - * @return void + * @param string $query + * @param array $bindings + * @param string|null $connection + * @return \Illuminate\Database\Eloquent\Collection */ - public static function automaticallyEagerLoadRelationships($value = true) + public static function hydrateRaw($query, $bindings = [], $connection = null) { - static::$modelsShouldAutomaticallyEagerLoadRelationships = $value; + $instance = (new static)->setConnection($connection); + + $items = $instance->getConnection()->select($query, $bindings); + + return static::hydrate($items, $connection); } /** - * Register a callback that is responsible for handling lazy loading violations. + * Save a new model and return the instance. * - * @param (callable(self, string))|null $callback - * @return void + * @param array $attributes + * @return static */ - public static function handleLazyLoadingViolationUsing(?callable $callback) + public static function create(array $attributes = []) { - static::$lazyLoadingViolationCallback = $callback; + $model = new static($attributes); + + $model->save(); + + return $model; } /** - * Prevent non-fillable attributes from being silently discarded. + * Save a new model and return the instance. Allow mass-assignment. * - * @param bool $value - * @return void + * @param array $attributes + * @return static */ - public static function preventSilentlyDiscardingAttributes($value = true) + public static function forceCreate(array $attributes) { - static::$modelsShouldPreventSilentlyDiscardingAttributes = $value; + return static::unguarded(function () use ($attributes) { + return (new static)->create($attributes); + }); } /** - * Register a callback that is responsible for handling discarded attribute violations. + * Begin querying the model. * - * @param (callable(self, array))|null $callback - * @return void + * @return \Illuminate\Database\Eloquent\Builder */ - public static function handleDiscardedAttributeViolationUsing(?callable $callback) + public static function query() { - static::$discardedAttributeViolationCallback = $callback; + return (new static)->newQuery(); } /** - * Prevent accessing missing attributes on retrieved models. + * Begin querying the model on a given connection. * - * @param bool $value - * @return void + * @param string|null $connection + * @return \Illuminate\Database\Eloquent\Builder */ - public static function preventAccessingMissingAttributes($value = true) + public static function on($connection = null) { - static::$modelsShouldPreventAccessingMissingAttributes = $value; + // First we will just create a fresh instance of this model, and then we can + // set the connection on the model so that it is be used for the queries + // we execute, as well as being set on each relationship we retrieve. + $instance = new static; + + $instance->setConnection($connection); + + return $instance->newQuery(); } /** - * Register a callback that is responsible for handling missing attribute violations. + * Begin querying the model on the write connection. * - * @param (callable(self, string))|null $callback - * @return void + * @return \Illuminate\Database\Query\Builder */ - public static function handleMissingAttributeViolationUsing(?callable $callback) + public static function onWriteConnection() { - static::$missingAttributeViolationCallback = $callback; + $instance = new static; + + return $instance->newQuery()->useWritePdo(); } /** - * Execute a callback without broadcasting any model events for all model types. + * Get all of the models from the database. * - * @param callable $callback - * @return mixed + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Collection|static[] */ - public static function withoutBroadcasting(callable $callback) + public static function all($columns = ['*']) { - $isBroadcasting = static::$isBroadcasting; + $columns = is_array($columns) ? $columns : func_get_args(); - static::$isBroadcasting = false; + $instance = new static; - try { - return $callback(); - } finally { - static::$isBroadcasting = $isBroadcasting; - } + return $instance->newQuery()->get($columns); } /** - * Fill the model with an array of attributes. - * - * @param array $attributes - * @return $this + * Reload a fresh model instance from the database. * - * @throws \Illuminate\Database\Eloquent\MassAssignmentException + * @param array|string $with + * @return $this|null */ - public function fill(array $attributes) + public function fresh($with = []) { - $totallyGuarded = $this->totallyGuarded(); + if (! $this->exists) { + return; + } - $fillable = $this->fillableFromArray($attributes); + $key = $this->getKeyName(); - foreach ($fillable as $key => $value) { - // The developers may choose to place some attributes in the "fillable" array - // which means only those attributes may be set through mass assignment to - // the model, and all others will just get ignored for security reasons. - if ($this->isFillable($key)) { - $this->setAttribute($key, $value); - } elseif ($totallyGuarded || static::preventsSilentlyDiscardingAttributes()) { - if (isset(static::$discardedAttributeViolationCallback)) { - call_user_func(static::$discardedAttributeViolationCallback, $this, [$key]); - } else { - throw new MassAssignmentException(sprintf( - 'Add [%s] to fillable property to allow mass assignment on [%s].', - $key, get_class($this) - )); - } - } + return static::with($with)->where($key, $this->getKey())->first(); + } + + /** + * Eager load relations on the model. + * + * @param array|string $relations + * @return $this + */ + public function load($relations) + { + if (is_string($relations)) { + $relations = func_get_args(); } - if (count($attributes) !== count($fillable) && - static::preventsSilentlyDiscardingAttributes()) { - $keys = array_diff(array_keys($attributes), array_keys($fillable)); + $query = $this->newQuery()->with($relations); - if (isset(static::$discardedAttributeViolationCallback)) { - call_user_func(static::$discardedAttributeViolationCallback, $this, $keys); - } else { - throw new MassAssignmentException(sprintf( - 'Add fillable property [%s] to allow mass assignment on [%s].', - implode(', ', $keys), - get_class($this) - )); - } - } + $query->eagerLoadRelations([$this]); return $this; } /** - * Fill the model with an array of attributes. Force mass assignment. + * Begin querying a model with eager loading. * - * @param array $attributes - * @return $this + * @param array|string $relations + * @return \Illuminate\Database\Eloquent\Builder|static */ - public function forceFill(array $attributes) + public static function with($relations) { - return static::unguarded(fn () => $this->fill($attributes)); + if (is_string($relations)) { + $relations = func_get_args(); + } + + $instance = new static; + + return $instance->newQuery()->with($relations); } /** - * Qualify the given column name by the model's table. + * Append attributes to query when building a query. * - * @param string $column - * @return string + * @param array|string $attributes + * @return $this */ - public function qualifyColumn($column) + public function append($attributes) { - if (str_contains($column, '.')) { - return $column; + if (is_string($attributes)) { + $attributes = func_get_args(); } - return $this->getTable().'.'.$column; + $this->appends = array_unique( + array_merge($this->appends, $attributes) + ); + + return $this; } /** - * Qualify the given columns with the model's table. + * Define a one-to-one relationship. * - * @param array $columns - * @return array + * @param string $related + * @param string $foreignKey + * @param string $localKey + * @return \Illuminate\Database\Eloquent\Relations\HasOne */ - public function qualifyColumns($columns) + public function hasOne($related, $foreignKey = null, $localKey = null) { - return (new BaseCollection($columns)) - ->map(fn ($column) => $this->qualifyColumn($column)) - ->all(); + $foreignKey = $foreignKey ?: $this->getForeignKey(); + + $instance = new $related; + + $localKey = $localKey ?: $this->getKeyName(); + + return new HasOne($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey); } /** - * Create a new instance of the given model. + * Define a polymorphic one-to-one relationship. * - * @param array $attributes - * @param bool $exists - * @return static + * @param string $related + * @param string $name + * @param string $type + * @param string $id + * @param string $localKey + * @return \Illuminate\Database\Eloquent\Relations\MorphOne */ - public function newInstance($attributes = [], $exists = false) + public function morphOne($related, $name, $type = null, $id = null, $localKey = null) { - // This method just provides a convenient way for us to generate fresh model - // instances of this current model. It is particularly useful during the - // hydration of new objects via the Eloquent query builder instances. - $model = new static; + $instance = new $related; - $model->exists = $exists; - - $model->setConnection( - $this->getConnectionName() - ); - - $model->setTable($this->getTable()); + list($type, $id) = $this->getMorphs($name, $type, $id); - $model->mergeCasts($this->casts); + $table = $instance->getTable(); - $model->fill((array) $attributes); + $localKey = $localKey ?: $this->getKeyName(); - return $model; + return new MorphOne($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $localKey); } /** - * Create a new model instance that is existing. + * Define an inverse one-to-one or many relationship. * - * @param array $attributes - * @param \UnitEnum|string|null $connection - * @return static + * @param string $related + * @param string $foreignKey + * @param string $otherKey + * @param string $relation + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ - public function newFromBuilder($attributes = [], $connection = null) + public function belongsTo($related, $foreignKey = null, $otherKey = null, $relation = null) { - $model = $this->newInstance([], true); + // If no relation name was given, we will use this debug backtrace to extract + // the calling method's name and use that as the relationship name as most + // of the time this will be what we desire to use for the relationships. + if (is_null($relation)) { + list($current, $caller) = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); - $model->setRawAttributes((array) $attributes, true); + $relation = $caller['function']; + } - $model->setConnection($connection ?? $this->getConnectionName()); + // If no foreign key was supplied, we can use a backtrace to guess the proper + // foreign key name by using the name of the relationship function, which + // when combined with an "_id" should conventionally match the columns. + if (is_null($foreignKey)) { + $foreignKey = Str::snake($relation).'_id'; + } - $model->fireModelEvent('retrieved', false); + $instance = new $related; - return $model; + // Once we have the foreign key names, we'll just create a new Eloquent query + // for the related models and returns the relationship instance which will + // actually be responsible for retrieving and hydrating every relations. + $query = $instance->newQuery(); + + $otherKey = $otherKey ?: $instance->getKeyName(); + + return new BelongsTo($query, $this, $foreignKey, $otherKey, $relation); } /** - * Begin querying the model on a given connection. + * Define a polymorphic, inverse one-to-one or many relationship. * - * @param \UnitEnum|string|null $connection - * @return \Illuminate\Database\Eloquent\Builder + * @param string $name + * @param string $type + * @param string $id + * @return \Illuminate\Database\Eloquent\Relations\MorphTo */ - public static function on($connection = null) + public function morphTo($name = null, $type = null, $id = null) { - // First we will just create a fresh instance of this model, and then we can set the - // connection on the model so that it is used for the queries we execute, as well - // as being set on every relation we retrieve without a custom connection name. - return (new static)->setConnection($connection)->newQuery(); + // If no name is provided, we will use the backtrace to get the function name + // since that is most likely the name of the polymorphic interface. We can + // use that to get both the class and foreign key that will be utilized. + if (is_null($name)) { + list($current, $caller) = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); + + $name = Str::snake($caller['function']); + } + + list($type, $id) = $this->getMorphs($name, $type, $id); + + // If the type value is null it is probably safe to assume we're eager loading + // the relationship. When that is the case we will pass in a dummy query as + // there are multiple types in the morph and we can't use single queries. + if (empty($class = $this->$type)) { + return new MorphTo( + $this->newQuery(), $this, $id, null, $type, $name + ); + } + + // If we are not eager loading the relationship we will essentially treat this + // as a belongs-to style relationship since morph-to extends that class and + // we will pass in the appropriate values so that it behaves as expected. + else { + $class = $this->getActualClassNameForMorph($class); + + $instance = new $class; + + return new MorphTo( + $instance->newQuery(), $this, $id, $instance->getKeyName(), $type, $name + ); + } } /** - * Begin querying the model on the write connection. + * Retrieve the fully qualified class name from a slug. * - * @return \Illuminate\Database\Eloquent\Builder + * @param string $class + * @return string */ - public static function onWriteConnection() + public function getActualClassNameForMorph($class) { - return static::query()->useWritePdo(); + return Arr::get(Relation::morphMap(), $class, $class); } /** - * Get all of the models from the database. + * Define a one-to-many relationship. * - * @param array|string $columns - * @return \Illuminate\Database\Eloquent\Collection + * @param string $related + * @param string $foreignKey + * @param string $localKey + * @return \Illuminate\Database\Eloquent\Relations\HasMany */ - public static function all($columns = ['*']) + public function hasMany($related, $foreignKey = null, $localKey = null) { - return static::query()->get( - is_array($columns) ? $columns : func_get_args() - ); + $foreignKey = $foreignKey ?: $this->getForeignKey(); + + $instance = new $related; + + $localKey = $localKey ?: $this->getKeyName(); + + return new HasMany($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey); } /** - * Begin querying a model with eager loading. + * Define a has-many-through relationship. * - * @param array|string $relations - * @return \Illuminate\Database\Eloquent\Builder + * @param string $related + * @param string $through + * @param string|null $firstKey + * @param string|null $secondKey + * @param string|null $localKey + * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough */ - public static function with($relations) + public function hasManyThrough($related, $through, $firstKey = null, $secondKey = null, $localKey = null) { - return static::query()->with( - is_string($relations) ? func_get_args() : $relations - ); + $through = new $through; + + $firstKey = $firstKey ?: $this->getForeignKey(); + + $secondKey = $secondKey ?: $through->getForeignKey(); + + $localKey = $localKey ?: $this->getKeyName(); + + return new HasManyThrough((new $related)->newQuery(), $this, $through, $firstKey, $secondKey, $localKey); } /** - * Eager load relations on the model. + * Define a polymorphic one-to-many relationship. * - * @param array|string $relations - * @return $this + * @param string $related + * @param string $name + * @param string $type + * @param string $id + * @param string $localKey + * @return \Illuminate\Database\Eloquent\Relations\MorphMany */ - public function load($relations) + public function morphMany($related, $name, $type = null, $id = null, $localKey = null) { - $query = $this->newQueryWithoutRelationships()->with( - is_string($relations) ? func_get_args() : $relations - ); + $instance = new $related; - $query->eagerLoadRelations([$this]); + // Here we will gather up the morph type and ID for the relationship so that we + // can properly query the intermediate table of a relation. Finally, we will + // get the table and create the relationship instances for the developers. + list($type, $id) = $this->getMorphs($name, $type, $id); - return $this; + $table = $instance->getTable(); + + $localKey = $localKey ?: $this->getKeyName(); + + return new MorphMany($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $localKey); } /** - * Eager load relationships on the polymorphic relation of a model. + * Define a many-to-many relationship. * + * @param string $related + * @param string $table + * @param string $foreignKey + * @param string $otherKey * @param string $relation - * @param array $relations - * @return $this + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ - public function loadMorph($relation, $relations) + public function belongsToMany($related, $table = null, $foreignKey = null, $otherKey = null, $relation = null) { - if (! $this->{$relation}) { - return $this; + // If no relationship name was passed, we will pull backtraces to get the + // name of the calling function. We will use that function name as the + // title of this relation since that is a great convention to apply. + if (is_null($relation)) { + $relation = $this->getBelongsToManyCaller(); } - $className = get_class($this->{$relation}); + // First, we'll need to determine the foreign key and "other key" for the + // relationship. Once we have determined the keys we'll make the query + // instances as well as the relationship instances we need for this. + $foreignKey = $foreignKey ?: $this->getForeignKey(); - $this->{$relation}->load($relations[$className] ?? []); + $instance = new $related; - return $this; + $otherKey = $otherKey ?: $instance->getForeignKey(); + + // If no table name was provided, we can guess it by concatenating the two + // models using underscores in alphabetical order. The two model names + // are transformed to snake case from their default CamelCase also. + if (is_null($table)) { + $table = $this->joiningTable($related); + } + + // Now we're ready to create a new query builder for the related model and + // the relationship instances for the relation. The relations will set + // appropriate query constraint and entirely manages the hydrations. + $query = $instance->newQuery(); + + return new BelongsToMany($query, $this, $table, $foreignKey, $otherKey, $relation); } /** - * Eager load relations on the model if they are not already eager loaded. + * Define a polymorphic many-to-many relationship. * - * @param array|string $relations - * @return $this + * @param string $related + * @param string $name + * @param string $table + * @param string $foreignKey + * @param string $otherKey + * @param bool $inverse + * @return \Illuminate\Database\Eloquent\Relations\MorphToMany */ - public function loadMissing($relations) + public function morphToMany($related, $name, $table = null, $foreignKey = null, $otherKey = null, $inverse = false) { - $relations = is_string($relations) ? func_get_args() : $relations; + $caller = $this->getBelongsToManyCaller(); - $this->newCollection([$this])->loadMissing($relations); + // First, we will need to determine the foreign key and "other key" for the + // relationship. Once we have determined the keys we will make the query + // instances, as well as the relationship instances we need for these. + $foreignKey = $foreignKey ?: $name.'_id'; - return $this; + $instance = new $related; + + $otherKey = $otherKey ?: $instance->getForeignKey(); + + // Now we're ready to create a new query builder for this related model and + // the relationship instances for this relation. This relations will set + // appropriate query constraints then entirely manages the hydrations. + $query = $instance->newQuery(); + + $table = $table ?: Str::plural($name); + + return new MorphToMany( + $query, $this, $name, $table, $foreignKey, + $otherKey, $caller, $inverse + ); } /** - * Eager load relation's column aggregations on the model. + * Define a polymorphic, inverse many-to-many relationship. * - * @param array|string $relations - * @param string $column - * @param string|null $function - * @return $this + * @param string $related + * @param string $name + * @param string $table + * @param string $foreignKey + * @param string $otherKey + * @return \Illuminate\Database\Eloquent\Relations\MorphToMany */ - public function loadAggregate($relations, $column, $function = null) + public function morphedByMany($related, $name, $table = null, $foreignKey = null, $otherKey = null) { - $this->newCollection([$this])->loadAggregate($relations, $column, $function); + $foreignKey = $foreignKey ?: $this->getForeignKey(); - return $this; + // For the inverse of the polymorphic many-to-many relations, we will change + // the way we determine the foreign and other keys, as it is the opposite + // of the morph-to-many method since we're figuring out these inverses. + $otherKey = $otherKey ?: $name.'_id'; + + return $this->morphToMany($related, $name, $table, $foreignKey, $otherKey, true); } /** - * Eager load relation counts on the model. + * Get the relationship name of the belongs to many. * - * @param array|string $relations - * @return $this + * @return string */ - public function loadCount($relations) + protected function getBelongsToManyCaller() { - $relations = is_string($relations) ? func_get_args() : $relations; + $self = __FUNCTION__; + + $caller = Arr::first(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), function ($trace) use ($self) { + $caller = $trace['function']; + + return ! in_array($caller, Model::$manyMethods) && $caller != $self; + }); - return $this->loadAggregate($relations, '*', 'count'); + return ! is_null($caller) ? $caller['function'] : null; } /** - * Eager load relation max column values on the model. + * Get the joining table name for a many-to-many relation. * - * @param array|string $relations - * @param string $column - * @return $this + * @param string $related + * @return string */ - public function loadMax($relations, $column) + public function joiningTable($related) { - return $this->loadAggregate($relations, $column, 'max'); + // The joining table name, by convention, is simply the snake cased models + // sorted alphabetically and concatenated with an underscore, so we can + // just sort the models and join them together to get the table name. + $base = Str::snake(class_basename($this)); + + $related = Str::snake(class_basename($related)); + + $models = [$related, $base]; + + // Now that we have the model names in an array we can just sort them and + // use the implode function to join them together with an underscores, + // which is typically used by convention within the database system. + sort($models); + + return strtolower(implode('_', $models)); } /** - * Eager load relation min column values on the model. + * Destroy the models for the given IDs. * - * @param array|string $relations - * @param string $column - * @return $this + * @param array|int $ids + * @return int */ - public function loadMin($relations, $column) + public static function destroy($ids) { - return $this->loadAggregate($relations, $column, 'min'); + // We'll initialize a count here so we will return the total number of deletes + // for the operation. The developers can then check this number as a boolean + // type value or get this total count of records deleted for logging, etc. + $count = 0; + + $ids = is_array($ids) ? $ids : func_get_args(); + + $instance = new static; + + // We will actually pull the models from the database table and call delete on + // each of them individually so that their events get fired properly with a + // correct set of attributes in case the developers wants to check these. + $key = $instance->getKeyName(); + + foreach ($instance->whereIn($key, $ids)->get() as $model) { + if ($model->delete()) { + $count++; + } + } + + return $count; } /** - * Eager load relation's column summations on the model. + * Delete the model from the database. * - * @param array|string $relations - * @param string $column - * @return $this + * @return bool|null + * + * @throws \Exception */ - public function loadSum($relations, $column) + public function delete() { - return $this->loadAggregate($relations, $column, 'sum'); + if (is_null($this->getKeyName())) { + throw new Exception('No primary key defined on model.'); + } + + if ($this->exists) { + if ($this->fireModelEvent('deleting') === false) { + return false; + } + + // Here, we'll touch the owning models, verifying these timestamps get updated + // for the models. This will allow any caching to get broken on the parents + // by the timestamp. Then we will go ahead and delete the model instance. + $this->touchOwners(); + + $this->performDeleteOnModel(); + + $this->exists = false; + + // Once the model has been deleted, we will fire off the deleted event so that + // the developers may hook into post-delete operations. We will then return + // a boolean true as the delete is presumably successful on the database. + $this->fireModelEvent('deleted', false); + + return true; + } } /** - * Eager load relation average column values on the model. + * Force a hard delete on a soft deleted model. * - * @param array|string $relations - * @param string $column - * @return $this + * This method protects developers from running forceDelete when trait is missing. + * + * @return bool|null */ - public function loadAvg($relations, $column) + public function forceDelete() { - return $this->loadAggregate($relations, $column, 'avg'); + return $this->delete(); } /** - * Eager load related model existence values on the model. + * Perform the actual delete query on this model instance. * - * @param array|string $relations - * @return $this + * @return void */ - public function loadExists($relations) + protected function performDeleteOnModel() { - return $this->loadAggregate($relations, '*', 'exists'); + $this->setKeysForSaveQuery($this->newQueryWithoutScopes())->delete(); } /** - * Eager load relationship column aggregation on the polymorphic relation of a model. + * Register a saving model event with the dispatcher. * - * @param string $relation - * @param array $relations - * @param string $column - * @param string|null $function - * @return $this + * @param \Closure|string $callback + * @param int $priority + * @return void */ - public function loadMorphAggregate($relation, $relations, $column, $function = null) + public static function saving($callback, $priority = 0) { - if (! $this->{$relation}) { - return $this; - } - - $className = get_class($this->{$relation}); - - $this->{$relation}->loadAggregate($relations[$className] ?? [], $column, $function); - - return $this; + static::registerModelEvent('saving', $callback, $priority); } /** - * Eager load relationship counts on the polymorphic relation of a model. + * Register a saved model event with the dispatcher. * - * @param string $relation - * @param array $relations - * @return $this + * @param \Closure|string $callback + * @param int $priority + * @return void */ - public function loadMorphCount($relation, $relations) + public static function saved($callback, $priority = 0) { - return $this->loadMorphAggregate($relation, $relations, '*', 'count'); + static::registerModelEvent('saved', $callback, $priority); } /** - * Eager load relationship max column values on the polymorphic relation of a model. + * Register an updating model event with the dispatcher. * - * @param string $relation - * @param array $relations - * @param string $column - * @return $this + * @param \Closure|string $callback + * @param int $priority + * @return void */ - public function loadMorphMax($relation, $relations, $column) + public static function updating($callback, $priority = 0) { - return $this->loadMorphAggregate($relation, $relations, $column, 'max'); + static::registerModelEvent('updating', $callback, $priority); } /** - * Eager load relationship min column values on the polymorphic relation of a model. + * Register an updated model event with the dispatcher. * - * @param string $relation - * @param array $relations - * @param string $column - * @return $this + * @param \Closure|string $callback + * @param int $priority + * @return void */ - public function loadMorphMin($relation, $relations, $column) + public static function updated($callback, $priority = 0) { - return $this->loadMorphAggregate($relation, $relations, $column, 'min'); + static::registerModelEvent('updated', $callback, $priority); } /** - * Eager load relationship column summations on the polymorphic relation of a model. + * Register a creating model event with the dispatcher. * - * @param string $relation - * @param array $relations - * @param string $column - * @return $this + * @param \Closure|string $callback + * @param int $priority + * @return void */ - public function loadMorphSum($relation, $relations, $column) + public static function creating($callback, $priority = 0) { - return $this->loadMorphAggregate($relation, $relations, $column, 'sum'); + static::registerModelEvent('creating', $callback, $priority); } /** - * Eager load relationship average column values on the polymorphic relation of a model. + * Register a created model event with the dispatcher. * - * @param string $relation - * @param array $relations - * @param string $column - * @return $this + * @param \Closure|string $callback + * @param int $priority + * @return void */ - public function loadMorphAvg($relation, $relations, $column) + public static function created($callback, $priority = 0) { - return $this->loadMorphAggregate($relation, $relations, $column, 'avg'); + static::registerModelEvent('created', $callback, $priority); } /** - * Increment a column's value by a given amount. + * Register a deleting model event with the dispatcher. * - * @param string $column - * @param float|int $amount - * @param array $extra - * @return int + * @param \Closure|string $callback + * @param int $priority + * @return void */ - protected function increment($column, $amount = 1, array $extra = []) + public static function deleting($callback, $priority = 0) { - return $this->incrementOrDecrement($column, $amount, $extra, 'increment'); + static::registerModelEvent('deleting', $callback, $priority); } /** - * Decrement a column's value by a given amount. + * Register a deleted model event with the dispatcher. * - * @param string $column - * @param float|int $amount - * @param array $extra - * @return int + * @param \Closure|string $callback + * @param int $priority + * @return void */ - protected function decrement($column, $amount = 1, array $extra = []) + public static function deleted($callback, $priority = 0) { - return $this->incrementOrDecrement($column, $amount, $extra, 'decrement'); + static::registerModelEvent('deleted', $callback, $priority); } /** - * Run the increment or decrement method on the model. + * Remove all of the event listeners for the model. * - * @param string $column - * @param float|int $amount - * @param array $extra - * @param string $method - * @return int + * @return void */ - protected function incrementOrDecrement($column, $amount, $extra, $method) + public static function flushEventListeners() { - if (! $this->exists) { - return $this->newQueryWithoutRelationships()->{$method}($column, $amount, $extra); + if (! isset(static::$dispatcher)) { + return; } - $this->{$column} = $this->isClassDeviable($column) - ? $this->deviateClassCastableAttribute($method, $column, $amount) - : $this->{$column} + ($method === 'increment' ? $amount : $amount * -1); - - $this->forceFill($extra); + $instance = new static; - if ($this->fireModelEvent('updating') === false) { - return false; - } - - if ($this->isClassDeviable($column)) { - $amount = (clone $this)->setAttribute($column, $amount)->getAttributeFromArray($column); + foreach ($instance->getObservableEvents() as $event) { + static::$dispatcher->forget("eloquent.{$event}: ".static::class); } + } - return tap($this->setKeysForSaveQuery($this->newQueryWithoutScopes())->{$method}($column, $amount, $extra), function () use ($column) { - $this->syncChanges(); + /** + * Register a model event with the dispatcher. + * + * @param string $event + * @param \Closure|string $callback + * @param int $priority + * @return void + */ + protected static function registerModelEvent($event, $callback, $priority = 0) + { + if (isset(static::$dispatcher)) { + $name = static::class; - $this->fireModelEvent('updated', false); + static::$dispatcher->listen("eloquent.{$event}: {$name}", $callback, $priority); + } + } - $this->syncOriginalAttribute($column); - }); + /** + * Get the observable event names. + * + * @return array + */ + public function getObservableEvents() + { + return array_merge( + [ + 'creating', 'created', 'updating', 'updated', + 'deleting', 'deleted', 'saving', 'saved', + 'restoring', 'restored', + ], + $this->observables + ); } /** - * Update the model in the database. + * Set the observable event names. * - * @param array $attributes - * @param array $options - * @return bool + * @param array $observables + * @return $this */ - public function update(array $attributes = [], array $options = []) + public function setObservableEvents(array $observables) { - if (! $this->exists) { - return false; - } + $this->observables = $observables; - return $this->fill($attributes)->save($options); + return $this; } /** - * Update the model in the database within a transaction. - * - * @param array $attributes - * @param array $options - * @return bool + * Add an observable event name. * - * @throws \Throwable + * @param array|mixed $observables + * @return void */ - public function updateOrFail(array $attributes = [], array $options = []) + public function addObservableEvents($observables) { - if (! $this->exists) { - return false; - } + $observables = is_array($observables) ? $observables : func_get_args(); - return $this->fill($attributes)->saveOrFail($options); + $this->observables = array_unique(array_merge($this->observables, $observables)); } /** - * Update the model in the database without raising any events. + * Remove an observable event name. * - * @param array $attributes - * @param array $options - * @return bool + * @param array|mixed $observables + * @return void */ - public function updateQuietly(array $attributes = [], array $options = []) + public function removeObservableEvents($observables) { - if (! $this->exists) { - return false; - } + $observables = is_array($observables) ? $observables : func_get_args(); - return $this->fill($attributes)->saveQuietly($options); + $this->observables = array_diff($this->observables, $observables); } /** - * Increment a column's value by a given amount without raising any events. + * Increment a column's value by a given amount. * * @param string $column - * @param float|int $amount + * @param int $amount * @param array $extra * @return int */ - protected function incrementQuietly($column, $amount = 1, array $extra = []) + protected function increment($column, $amount = 1, array $extra = []) { - return static::withoutEvents( - fn () => $this->incrementOrDecrement($column, $amount, $extra, 'increment') - ); + return $this->incrementOrDecrement($column, $amount, $extra, 'increment'); } /** - * Decrement a column's value by a given amount without raising any events. + * Decrement a column's value by a given amount. * * @param string $column - * @param float|int $amount + * @param int $amount * @param array $extra * @return int */ - protected function decrementQuietly($column, $amount = 1, array $extra = []) + protected function decrement($column, $amount = 1, array $extra = []) { - return static::withoutEvents( - fn () => $this->incrementOrDecrement($column, $amount, $extra, 'decrement') - ); + return $this->incrementOrDecrement($column, $amount, $extra, 'decrement'); } /** - * Save the model and all of its relationships. + * Run the increment or decrement method on the model. * - * @return bool + * @param string $column + * @param int $amount + * @param array $extra + * @param string $method + * @return int */ - public function push() + protected function incrementOrDecrement($column, $amount, $extra, $method) { - return $this->withoutRecursion(function () { - if (! $this->save()) { - return false; - } + $query = $this->newQuery(); - // To sync all of the relationships to the database, we will simply spin through - // the relationships and save each model via this "push" method, which allows - // us to recurse into all of these nested relations for the model instance. - foreach ($this->relations as $models) { - $models = $models instanceof Collection - ? $models->all() - : [$models]; - - foreach (array_filter($models) as $model) { - if (! $model->push()) { - return false; - } - } - } + if (! $this->exists) { + return $query->{$method}($column, $amount, $extra); + } - return true; - }, true); + $this->incrementOrDecrementAttributeValue($column, $amount, $method); + + return $query->where($this->getKeyName(), $this->getKey())->{$method}($column, $amount, $extra); } /** - * Save the model and all of its relationships without raising any events to the parent model. + * Increment the underlying attribute value and sync with original. * - * @return bool + * @param string $column + * @param int $amount + * @param string $method + * @return void */ - public function pushQuietly() + protected function incrementOrDecrementAttributeValue($column, $amount, $method) { - return static::withoutEvents(fn () => $this->push()); + $this->{$column} = $this->{$column} + ($method == 'increment' ? $amount : $amount * -1); + + $this->syncOriginalAttribute($column); } /** - * Save the model to the database without raising any events. + * Update the model in the database. * + * @param array $attributes * @param array $options + * @return bool|int + */ + public function update(array $attributes = [], array $options = []) + { + if (! $this->exists) { + return false; + } + + return $this->fill($attributes)->save($options); + } + + /** + * Save the model and all of its relationships. + * * @return bool */ - public function saveQuietly(array $options = []) + public function push() { - return static::withoutEvents(fn () => $this->save($options)); + if (! $this->save()) { + return false; + } + + // To sync all of the relationships to the database, we will simply spin through + // the relationships and save each model via this "push" method, which allows + // us to recurse into all of these nested relations for the model instance. + foreach ($this->relations as $models) { + $models = $models instanceof Collection + ? $models->all() : [$models]; + + foreach (array_filter($models) as $model) { + if (! $model->push()) { + return false; + } + } + } + + return true; } /** @@ -1271,9 +1448,7 @@ public function saveQuietly(array $options = []) */ public function save(array $options = []) { - $this->mergeAttributesFromCachedCasts(); - - $query = $this->newModelQuery(); + $query = $this->newQueryWithoutScopes(); // If the "saving" event returns false we'll bail out of the save and return // false, indicating that the save failed. This provides a chance for any @@ -1286,8 +1461,7 @@ public function save(array $options = []) // that is already in this database using the current IDs in this "where" // clause to only update this model. Otherwise, we'll just insert them. if ($this->exists) { - $saved = $this->isDirty() ? - $this->performUpdate($query) : true; + $saved = $this->performUpdate($query); } // If the model is brand new, we'll insert it into our database and set the @@ -1295,16 +1469,8 @@ public function save(array $options = []) // which is typically an auto-increment value managed by the database. else { $saved = $this->performInsert($query); - - if (! $this->getConnectionName() && - $connection = $query->getConnection()) { - $this->setConnection($connection->getName()); - } } - // If the model is successfully saved, we need to do a few more things once - // that is done. We will call the "saved" method here to run any actions - // we need to happen after a model gets successfully saved right here. if ($saved) { $this->finishSave($options); } @@ -1313,7 +1479,7 @@ public function save(array $options = []) } /** - * Save the model to the database within a transaction. + * Save the model to the database using transaction. * * @param array $options * @return bool @@ -1322,11 +1488,13 @@ public function save(array $options = []) */ public function saveOrFail(array $options = []) { - return $this->getConnection()->transaction(fn () => $this->save($options)); + return $this->getConnection()->transaction(function () use ($options) { + return $this->save($options); + }); } /** - * Perform any actions that are necessary after the model is saved. + * Finish processing on a successful save operation. * * @param array $options * @return void @@ -1335,109 +1503,61 @@ protected function finishSave(array $options) { $this->fireModelEvent('saved', false); - if ($this->isDirty() && ($options['touch'] ?? true)) { + $this->syncOriginal(); + + if (Arr::get($options, 'touch', true)) { $this->touchOwners(); } - - $this->syncOriginal(); } /** * Perform a model update operation. * - * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $query * @return bool */ protected function performUpdate(Builder $query) { - // If the updating event returns false, we will cancel the update operation so - // developers can hook Validation systems into their models and cancel this - // operation if the model does not pass validation. Otherwise, we update. - if ($this->fireModelEvent('updating') === false) { - return false; - } + $dirty = $this->getDirty(); - // First we need to create a fresh query instance and touch the creation and - // update timestamp on the model which are maintained by us for developer - // convenience. Then we will just continue saving the model instances. - if ($this->usesTimestamps()) { - $this->updateTimestamps(); - } + if (count($dirty) > 0) { + // If the updating event returns false, we will cancel the update operation so + // developers can hook Validation systems into their models and cancel this + // operation if the model does not pass validation. Otherwise, we update. + if ($this->fireModelEvent('updating') === false) { + return false; + } - // Once we have run the update operation, we will fire the "updated" event for - // this model instance. This will allow developers to hook into these after - // models are updated, giving them a chance to do any special processing. - $dirty = $this->getDirtyForUpdate(); + // First we need to create a fresh query instance and touch the creation and + // update timestamp on the model which are maintained by us for developer + // convenience. Then we will just continue saving the model instances. + if ($this->timestamps) { + $this->updateTimestamps(); + } - if (count($dirty) > 0) { - $this->setKeysForSaveQuery($query)->update($dirty); + // Once we have run the update operation, we will fire the "updated" event for + // this model instance. This will allow developers to hook into these after + // models are updated, giving them a chance to do any special processing. + $dirty = $this->getDirty(); - $this->syncChanges(); + if (count($dirty) > 0) { + $numRows = $this->setKeysForSaveQuery($query)->update($dirty); - $this->fireModelEvent('updated', false); + $this->fireModelEvent('updated', false); + } } return true; } - /** - * Set the keys for a select query. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @return \Illuminate\Database\Eloquent\Builder - */ - protected function setKeysForSelectQuery($query) - { - $query->where($this->getKeyName(), '=', $this->getKeyForSelectQuery()); - - return $query; - } - - /** - * Get the primary key value for a select query. - * - * @return mixed - */ - protected function getKeyForSelectQuery() - { - return $this->original[$this->getKeyName()] ?? $this->getKey(); - } - - /** - * Set the keys for a save update query. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @return \Illuminate\Database\Eloquent\Builder - */ - protected function setKeysForSaveQuery($query) - { - $query->where($this->getKeyName(), '=', $this->getKeyForSaveQuery()); - - return $query; - } - - /** - * Get the primary key value for a save query. - * - * @return mixed - */ - protected function getKeyForSaveQuery() - { - return $this->original[$this->getKeyName()] ?? $this->getKey(); - } - /** * Perform a model insert operation. * - * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $query * @return bool */ protected function performInsert(Builder $query) { - if ($this->usesUniqueIds()) { - $this->setUniqueIds(); - } - if ($this->fireModelEvent('creating') === false) { return false; } @@ -1445,14 +1565,14 @@ protected function performInsert(Builder $query) // First we'll need to create a fresh query instance and touch the creation and // update timestamps on this model, which are maintained by us for developer // convenience. After, we will just continue saving these model instances. - if ($this->usesTimestamps()) { + if ($this->timestamps) { $this->updateTimestamps(); } // If the model has an incrementing key, we can use the "insertGetId" method on // the query builder, which will give us back the final inserted ID for this // table from the database. Not all tables have to be incrementing though. - $attributes = $this->getAttributesForInsert(); + $attributes = $this->attributes; if ($this->getIncrementing()) { $this->insertAndSetId($query, $attributes); @@ -1462,10 +1582,6 @@ protected function performInsert(Builder $query) // are. These attribute arrays must contain an "id" column previously placed // there by the developer as the manually determined key for these models. else { - if (empty($attributes)) { - return true; - } - $query->insert($attributes); } @@ -1484,8 +1600,8 @@ protected function performInsert(Builder $query) /** * Insert the given attributes and set the ID on the model. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param array $attributes + * @param \Illuminate\Database\Eloquent\Builder $query + * @param array $attributes * @return void */ protected function insertAndSetId(Builder $query, $attributes) @@ -1496,1016 +1612,1751 @@ protected function insertAndSetId(Builder $query, $attributes) } /** - * Destroy the models for the given IDs. + * Touch the owning relations of the model. * - * @param \Illuminate\Support\Collection|array|int|string $ids - * @return int + * @return void */ - public static function destroy($ids) + public function touchOwners() { - if ($ids instanceof EloquentCollection) { - $ids = $ids->modelKeys(); - } + foreach ($this->touches as $relation) { + $this->$relation()->touch(); + + if ($this->$relation instanceof self) { + $this->$relation->fireModelEvent('saved', false); - if ($ids instanceof BaseCollection) { - $ids = $ids->all(); + $this->$relation->touchOwners(); + } elseif ($this->$relation instanceof Collection) { + $this->$relation->each(function (Model $relation) { + $relation->touchOwners(); + }); + } } + } - $ids = is_array($ids) ? $ids : func_get_args(); + /** + * Determine if the model touches a given relation. + * + * @param string $relation + * @return bool + */ + public function touches($relation) + { + return in_array($relation, $this->touches); + } - if (count($ids) === 0) { - return 0; + /** + * Fire the given event for the model. + * + * @param string $event + * @param bool $halt + * @return mixed + */ + protected function fireModelEvent($event, $halt = true) + { + if (! isset(static::$dispatcher)) { + return true; } - // We will actually pull the models from the database table and call delete on - // each of them individually so that their events get fired properly with a - // correct set of attributes in case the developers wants to check these. - $key = ($instance = new static)->getKeyName(); + // We will append the names of the class to the event to distinguish it from + // other model events that are fired, allowing us to listen on each model + // event set individually instead of catching event for all the models. + $event = "eloquent.{$event}: ".static::class; - $count = 0; + $method = $halt ? 'until' : 'fire'; - foreach ($instance->whereIn($key, $ids)->get() as $model) { - if ($model->delete()) { - $count++; - } + return static::$dispatcher->$method($event, $this); + } + + /** + * Set the keys for a save update query. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + protected function setKeysForSaveQuery(Builder $query) + { + $query->where($this->getKeyName(), '=', $this->getKeyForSaveQuery()); + + return $query; + } + + /** + * Get the primary key value for a save query. + * + * @return mixed + */ + protected function getKeyForSaveQuery() + { + if (isset($this->original[$this->getKeyName()])) { + return $this->original[$this->getKeyName()]; } - return $count; + return $this->getAttribute($this->getKeyName()); } /** - * Delete the model from the database. + * Update the model's update timestamp. * - * @return bool|null + * @return bool + */ + public function touch() + { + if (! $this->timestamps) { + return false; + } + + $this->updateTimestamps(); + + return $this->save(); + } + + /** + * Update the creation and update timestamps. * - * @throws \LogicException + * @return void */ - public function delete() + protected function updateTimestamps() { - $this->mergeAttributesFromCachedCasts(); + $time = $this->freshTimestamp(); - if (is_null($this->getKeyName())) { - throw new LogicException('No primary key defined on model.'); + if (! $this->isDirty(static::UPDATED_AT)) { + $this->setUpdatedAt($time); } - // If the model doesn't exist, there is nothing to delete so we'll just return - // immediately and not do anything else. Otherwise, we will continue with a - // deletion process on the model, firing the proper events, and so forth. - if (! $this->exists) { - return; + if (! $this->exists && ! $this->isDirty(static::CREATED_AT)) { + $this->setCreatedAt($time); } + } - if ($this->fireModelEvent('deleting') === false) { - return false; - } + /** + * Set the value of the "created at" attribute. + * + * @param mixed $value + * @return $this + */ + public function setCreatedAt($value) + { + $this->{static::CREATED_AT} = $value; - // Here, we'll touch the owning models, verifying these timestamps get updated - // for the models. This will allow any caching to get broken on the parents - // by the timestamp. Then we will go ahead and delete the model instance. - $this->touchOwners(); + return $this; + } + + /** + * Set the value of the "updated at" attribute. + * + * @param mixed $value + * @return $this + */ + public function setUpdatedAt($value) + { + $this->{static::UPDATED_AT} = $value; - $this->performDeleteOnModel(); + return $this; + } - // Once the model has been deleted, we will fire off the deleted event so that - // the developers may hook into post-delete operations. We will then return - // a boolean true as the delete is presumably successful on the database. - $this->fireModelEvent('deleted', false); + /** + * Get the name of the "created at" column. + * + * @return string + */ + public function getCreatedAtColumn() + { + return static::CREATED_AT; + } - return true; + /** + * Get the name of the "updated at" column. + * + * @return string + */ + public function getUpdatedAtColumn() + { + return static::UPDATED_AT; + } + + /** + * Get a fresh timestamp for the model. + * + * @return \Carbon\Carbon + */ + public function freshTimestamp() + { + return new Carbon; + } + + /** + * Get a fresh timestamp for the model. + * + * @return string + */ + public function freshTimestampString() + { + return $this->fromDateTime($this->freshTimestamp()); + } + + /** + * Get a new query builder for the model's table. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function newQuery() + { + $builder = $this->newQueryWithoutScopes(); + + foreach ($this->getGlobalScopes() as $identifier => $scope) { + $builder->withGlobalScope($identifier, $scope); + } + + return $builder; + } + + /** + * Get a new query instance without a given scope. + * + * @param \Illuminate\Database\Eloquent\Scope|string $scope + * @return \Illuminate\Database\Eloquent\Builder + */ + public function newQueryWithoutScope($scope) + { + $builder = $this->newQuery(); + + return $builder->withoutGlobalScope($scope); + } + + /** + * Get a new query builder that doesn't have any global scopes. + * + * @return \Illuminate\Database\Eloquent\Builder|static + */ + public function newQueryWithoutScopes() + { + $builder = $this->newEloquentBuilder( + $this->newBaseQueryBuilder() + ); + + // Once we have the query builders, we will set the model instances so the + // builder can easily access any information it may need from the model + // while it is constructing and executing various queries against it. + return $builder->setModel($this)->with($this->with); + } + + /** + * Create a new Eloquent query builder for the model. + * + * @param \Illuminate\Database\Query\Builder $query + * @return \Illuminate\Database\Eloquent\Builder|static + */ + public function newEloquentBuilder($query) + { + return new Builder($query); + } + + /** + * Get a new query builder instance for the connection. + * + * @return \Illuminate\Database\Query\Builder + */ + protected function newBaseQueryBuilder() + { + $conn = $this->getConnection(); + + $grammar = $conn->getQueryGrammar(); + + return new QueryBuilder($conn, $grammar, $conn->getPostProcessor()); + } + + /** + * Create a new Eloquent Collection instance. + * + * @param array $models + * @return \Illuminate\Database\Eloquent\Collection + */ + public function newCollection(array $models = []) + { + return new Collection($models); + } + + /** + * Create a new pivot model instance. + * + * @param \Illuminate\Database\Eloquent\Model $parent + * @param array $attributes + * @param string $table + * @param bool $exists + * @return \Illuminate\Database\Eloquent\Relations\Pivot + */ + public function newPivot(Model $parent, array $attributes, $table, $exists) + { + return new Pivot($parent, $attributes, $table, $exists); + } + + /** + * Get the table associated with the model. + * + * @return string + */ + public function getTable() + { + if (isset($this->table)) { + return $this->table; + } + + return str_replace('\\', '', Str::snake(Str::plural(class_basename($this)))); + } + + /** + * Set the table associated with the model. + * + * @param string $table + * @return $this + */ + public function setTable($table) + { + $this->table = $table; + + return $this; + } + + /** + * Get the value of the model's primary key. + * + * @return mixed + */ + public function getKey() + { + return $this->getAttribute($this->getKeyName()); + } + + /** + * Get the queueable identity for the entity. + * + * @return mixed + */ + public function getQueueableId() + { + return $this->getKey(); + } + + /** + * Get the primary key for the model. + * + * @return string + */ + public function getKeyName() + { + return $this->primaryKey; + } + + /** + * Set the primary key for the model. + * + * @param string $key + * @return $this + */ + public function setKeyName($key) + { + $this->primaryKey = $key; + + return $this; + } + + /** + * Get the table qualified key name. + * + * @return string + */ + public function getQualifiedKeyName() + { + return $this->getTable().'.'.$this->getKeyName(); + } + + /** + * Get the value of the model's route key. + * + * @return mixed + */ + public function getRouteKey() + { + return $this->getAttribute($this->getRouteKeyName()); + } + + /** + * Get the route key for the model. + * + * @return string + */ + public function getRouteKeyName() + { + return $this->getKeyName(); + } + + /** + * Determine if the model uses timestamps. + * + * @return bool + */ + public function usesTimestamps() + { + return $this->timestamps; + } + + /** + * Get the polymorphic relationship columns. + * + * @param string $name + * @param string $type + * @param string $id + * @return array + */ + protected function getMorphs($name, $type, $id) + { + $type = $type ?: $name.'_type'; + + $id = $id ?: $name.'_id'; + + return [$type, $id]; + } + + /** + * Get the class name for polymorphic relations. + * + * @return string + */ + public function getMorphClass() + { + $morphMap = Relation::morphMap(); + + $class = static::class; + + if (! empty($morphMap) && in_array($class, $morphMap)) { + return array_search($class, $morphMap, true); + } + + return $this->morphClass ?: $class; + } + + /** + * Get the number of models to return per page. + * + * @return int + */ + public function getPerPage() + { + return $this->perPage; + } + + /** + * Set the number of models to return per page. + * + * @param int $perPage + * @return $this + */ + public function setPerPage($perPage) + { + $this->perPage = $perPage; + + return $this; + } + + /** + * Get the default foreign key name for the model. + * + * @return string + */ + public function getForeignKey() + { + return Str::snake(class_basename($this)).'_id'; + } + + /** + * Get the hidden attributes for the model. + * + * @return array + */ + public function getHidden() + { + return $this->hidden; + } + + /** + * Set the hidden attributes for the model. + * + * @param array $hidden + * @return $this + */ + public function setHidden(array $hidden) + { + $this->hidden = $hidden; + + return $this; + } + + /** + * Add hidden attributes for the model. + * + * @param array|string|null $attributes + * @return void + */ + public function addHidden($attributes = null) + { + $attributes = is_array($attributes) ? $attributes : func_get_args(); + + $this->hidden = array_merge($this->hidden, $attributes); + } + + /** + * Make the given, typically hidden, attributes visible. + * + * @param array|string $attributes + * @return $this + */ + public function makeVisible($attributes) + { + $this->hidden = array_diff($this->hidden, (array) $attributes); + + return $this; + } + + /** + * Get the visible attributes for the model. + * + * @return array + */ + public function getVisible() + { + return $this->visible; + } + + /** + * Set the visible attributes for the model. + * + * @param array $visible + * @return $this + */ + public function setVisible(array $visible) + { + $this->visible = $visible; + + return $this; + } + + /** + * Add visible attributes for the model. + * + * @param array|string|null $attributes + * @return void + */ + public function addVisible($attributes = null) + { + $attributes = is_array($attributes) ? $attributes : func_get_args(); + + $this->visible = array_merge($this->visible, $attributes); + } + + /** + * Set the accessors to append to model arrays. + * + * @param array $appends + * @return $this + */ + public function setAppends(array $appends) + { + $this->appends = $appends; + + return $this; + } + + /** + * Get the fillable attributes for the model. + * + * @return array + */ + public function getFillable() + { + return $this->fillable; } /** - * Delete the model from the database without raising any events. + * Set the fillable attributes for the model. * - * @return bool + * @param array $fillable + * @return $this */ - public function deleteQuietly() + public function fillable(array $fillable) { - return static::withoutEvents(fn () => $this->delete()); + $this->fillable = $fillable; + + return $this; } /** - * Delete the model from the database within a transaction. + * Get the guarded attributes for the model. * - * @return bool|null - * - * @throws \Throwable + * @return array */ - public function deleteOrFail() + public function getGuarded() { - if (! $this->exists) { - return false; - } - - return $this->getConnection()->transaction(fn () => $this->delete()); + return $this->guarded; } /** - * Force a hard delete on a soft deleted model. - * - * This method protects developers from running forceDelete when the trait is missing. + * Set the guarded attributes for the model. * - * @return bool|null + * @param array $guarded + * @return $this */ - public function forceDelete() + public function guard(array $guarded) { - return $this->delete(); + $this->guarded = $guarded; + + return $this; } /** - * Force a hard destroy on a soft deleted model. + * Disable all mass assignable restrictions. * - * This method protects developers from running forceDestroy when the trait is missing. - * - * @param \Illuminate\Support\Collection|array|int|string $ids - * @return bool|null + * @param bool $state + * @return void */ - public static function forceDestroy($ids) + public static function unguard($state = true) { - return static::destroy($ids); + static::$unguarded = $state; } /** - * Perform the actual delete query on this model instance. + * Enable the mass assignment restrictions. * * @return void */ - protected function performDeleteOnModel() + public static function reguard() { - $this->setKeysForSaveQuery($this->newModelQuery())->delete(); - - $this->exists = false; + static::$unguarded = false; } /** - * Begin querying the model. + * Determine if current state is "unguarded". * - * @return \Illuminate\Database\Eloquent\Builder + * @return bool */ - public static function query() + public static function isUnguarded() { - return (new static)->newQuery(); + return static::$unguarded; } /** - * Get a new query builder for the model's table. + * Run the given callable while being unguarded. * - * @return \Illuminate\Database\Eloquent\Builder + * @param callable $callback + * @return mixed */ - public function newQuery() + public static function unguarded(callable $callback) { - return $this->registerGlobalScopes($this->newQueryWithoutScopes()); + if (static::$unguarded) { + return $callback(); + } + + static::unguard(); + + try { + return $callback(); + } finally { + static::reguard(); + } } /** - * Get a new query builder that doesn't have any global scopes or eager loading. + * Determine if the given attribute may be mass assigned. * - * @return \Illuminate\Database\Eloquent\Builder + * @param string $key + * @return bool */ - public function newModelQuery() + public function isFillable($key) { - return $this->newEloquentBuilder( - $this->newBaseQueryBuilder() - )->setModel($this); + if (static::$unguarded) { + return true; + } + + // If the key is in the "fillable" array, we can of course assume that it's + // a fillable attribute. Otherwise, we will check the guarded array when + // we need to determine if the attribute is black-listed on the model. + if (in_array($key, $this->getFillable())) { + return true; + } + + if ($this->isGuarded($key)) { + return false; + } + + return empty($this->getFillable()) && ! Str::startsWith($key, '_'); } /** - * Get a new query builder with no relationships loaded. + * Determine if the given key is guarded. * - * @return \Illuminate\Database\Eloquent\Builder + * @param string $key + * @return bool */ - public function newQueryWithoutRelationships() + public function isGuarded($key) { - return $this->registerGlobalScopes($this->newModelQuery()); + return in_array($key, $this->getGuarded()) || $this->getGuarded() == ['*']; } /** - * Register the global scopes for this builder instance. + * Determine if the model is totally guarded. * - * @param \Illuminate\Database\Eloquent\Builder $builder - * @return \Illuminate\Database\Eloquent\Builder + * @return bool */ - public function registerGlobalScopes($builder) + public function totallyGuarded() { - foreach ($this->getGlobalScopes() as $identifier => $scope) { - $builder->withGlobalScope($identifier, $scope); - } - - return $builder; + return count($this->getFillable()) == 0 && $this->getGuarded() == ['*']; } /** - * Get a new query builder that doesn't have any global scopes. + * Remove the table name from a given key. * - * @return \Illuminate\Database\Eloquent\Builder + * @param string $key + * @return string */ - public function newQueryWithoutScopes() + protected function removeTableFromKey($key) { - return $this->newModelQuery() - ->with($this->with) - ->withCount($this->withCount); + if (! Str::contains($key, '.')) { + return $key; + } + + return last(explode('.', $key)); } /** - * Get a new query instance without a given scope. + * Get the relationships that are touched on save. * - * @param \Illuminate\Database\Eloquent\Scope|string $scope - * @return \Illuminate\Database\Eloquent\Builder + * @return array */ - public function newQueryWithoutScope($scope) + public function getTouchedRelations() { - return $this->newQuery()->withoutGlobalScope($scope); + return $this->touches; } /** - * Get a new query to restore one or more models by their queueable IDs. + * Set the relationships that are touched on save. * - * @param array|int $ids - * @return \Illuminate\Database\Eloquent\Builder + * @param array $touches + * @return $this */ - public function newQueryForRestoration($ids) + public function setTouchedRelations(array $touches) { - return $this->newQueryWithoutScopes()->whereKey($ids); + $this->touches = $touches; + + return $this; } /** - * Create a new Eloquent query builder for the model. + * Get the value indicating whether the IDs are incrementing. * - * @param \Illuminate\Database\Query\Builder $query - * @return \Illuminate\Database\Eloquent\Builder<*> + * @return bool */ - public function newEloquentBuilder($query) + public function getIncrementing() { - $builderClass = $this->resolveCustomBuilderClass(); - - if ($builderClass && is_subclass_of($builderClass, Builder::class)) { - return new $builderClass($query); - } - - return new static::$builder($query); + return $this->incrementing; } /** - * Resolve the custom Eloquent builder class from the model attributes. + * Set whether IDs are incrementing. * - * @return class-string<\Illuminate\Database\Eloquent\Builder>|false + * @param bool $value + * @return $this */ - protected function resolveCustomBuilderClass() + public function setIncrementing($value) { - $attributes = (new ReflectionClass($this)) - ->getAttributes(UseEloquentBuilder::class); + $this->incrementing = $value; - return ! empty($attributes) - ? $attributes[0]->newInstance()->builderClass - : false; + return $this; } /** - * Get a new query builder instance for the connection. + * Convert the model instance to JSON. * - * @return \Illuminate\Database\Query\Builder + * @param int $options + * @return string */ - protected function newBaseQueryBuilder() + public function toJson($options = 0) { - return $this->getConnection()->query(); + return json_encode($this->jsonSerialize(), $options); } /** - * Create a new pivot model instance. + * Convert the object into something JSON serializable. * - * @param \Illuminate\Database\Eloquent\Model $parent - * @param array $attributes - * @param string $table - * @param bool $exists - * @param string|null $using - * @return \Illuminate\Database\Eloquent\Relations\Pivot + * @return array */ - public function newPivot(self $parent, array $attributes, $table, $exists, $using = null) + public function jsonSerialize() { - return $using ? $using::fromRawAttributes($parent, $attributes, $table, $exists) - : Pivot::fromAttributes($parent, $attributes, $table, $exists); + return $this->toArray(); } /** - * Determine if the model has a given scope. + * Convert the model instance to an array. * - * @param string $scope - * @return bool + * @return array */ - public function hasNamedScope($scope) + public function toArray() { - return method_exists($this, 'scope'.ucfirst($scope)) || - static::isScopeMethodWithAttribute($scope); + $attributes = $this->attributesToArray(); + + return array_merge($attributes, $this->relationsToArray()); } /** - * Apply the given named scope if possible. + * Convert the model's attributes to an array. * - * @param string $scope - * @param array $parameters - * @return mixed + * @return array */ - public function callNamedScope($scope, array $parameters = []) + public function attributesToArray() { - if ($this->isScopeMethodWithAttribute($scope)) { - return $this->{$scope}(...$parameters); + $attributes = $this->getArrayableAttributes(); + + // If an attribute is a date, we will cast it to a string after converting it + // to a DateTime / Carbon instance. This is so we will get some consistent + // formatting while accessing attributes vs. arraying / JSONing a model. + foreach ($this->getDates() as $key) { + if (! isset($attributes[$key])) { + continue; + } + + $attributes[$key] = $this->serializeDate( + $this->asDateTime($attributes[$key]) + ); + } + + $mutatedAttributes = $this->getMutatedAttributes(); + + // We want to spin through all the mutated attributes for this model and call + // the mutator for the attribute. We cache off every mutated attributes so + // we don't have to constantly check on attributes that actually change. + foreach ($mutatedAttributes as $key) { + if (! array_key_exists($key, $attributes)) { + continue; + } + + $attributes[$key] = $this->mutateAttributeForArray( + $key, $attributes[$key] + ); + } + + // Next we will handle any casts that have been setup for this model and cast + // the values to their appropriate type. If the attribute has a mutator we + // will not perform the cast on those attributes to avoid any confusion. + foreach ($this->getCasts() as $key => $value) { + if (! array_key_exists($key, $attributes) || + in_array($key, $mutatedAttributes)) { + continue; + } + + $attributes[$key] = $this->castAttribute( + $key, $attributes[$key] + ); + + if ($attributes[$key] && ($value === 'date' || $value === 'datetime')) { + $attributes[$key] = $this->serializeDate($attributes[$key]); + } + } + + // Here we will grab all of the appended, calculated attributes to this model + // as these attributes are not really in the attributes array, but are run + // when we need to array or JSON the model for convenience to the coder. + foreach ($this->getArrayableAppends() as $key) { + $attributes[$key] = $this->mutateAttributeForArray($key, null); } - return $this->{'scope'.ucfirst($scope)}(...$parameters); + return $attributes; } /** - * Determine if the given method has a scope attribute. + * Get an attribute array of all arrayable attributes. * - * @param string $method - * @return bool + * @return array */ - protected static function isScopeMethodWithAttribute(string $method) + protected function getArrayableAttributes() { - return method_exists(static::class, $method) && - (new ReflectionMethod(static::class, $method)) - ->getAttributes(LocalScope::class) !== []; + return $this->getArrayableItems($this->attributes); } /** - * Convert the model instance to an array. + * Get all of the appendable values that are arrayable. * * @return array */ - public function toArray() + protected function getArrayableAppends() { - return $this->withoutRecursion( - fn () => array_merge($this->attributesToArray(), $this->relationsToArray()), - fn () => $this->attributesToArray(), + if (! count($this->appends)) { + return []; + } + + return $this->getArrayableItems( + array_combine($this->appends, $this->appends) ); } /** - * Convert the model instance to JSON. - * - * @param int $options - * @return string + * Get the model's relationships in array form. * - * @throws \Illuminate\Database\Eloquent\JsonEncodingException + * @return array */ - public function toJson($options = 0) + public function relationsToArray() { - try { - $json = json_encode($this->jsonSerialize(), $options | JSON_THROW_ON_ERROR); - } catch (JsonException $e) { - throw JsonEncodingException::forModel($this, $e->getMessage()); + $attributes = []; + + foreach ($this->getArrayableRelations() as $key => $value) { + // If the values implements the Arrayable interface we can just call this + // toArray method on the instances which will convert both models and + // collections to their proper array form and we'll set the values. + if ($value instanceof Arrayable) { + $relation = $value->toArray(); + } + + // If the value is null, we'll still go ahead and set it in this list of + // attributes since null is used to represent empty relationships if + // if it a has one or belongs to type relationships on the models. + elseif (is_null($value)) { + $relation = $value; + } + + // If the relationships snake-casing is enabled, we will snake case this + // key so that the relation attribute is snake cased in this returned + // array to the developers, making this consistent with attributes. + if (static::$snakeAttributes) { + $key = Str::snake($key); + } + + // If the relation value has been set, we will set it on this attributes + // list for returning. If it was not arrayable or null, we'll not set + // the value on the array because it is some type of invalid value. + if (isset($relation) || is_null($value)) { + $attributes[$key] = $relation; + } + + unset($relation); } - return $json; + return $attributes; } /** - * Convert the model instance to pretty print formatted JSON. - * - * @param int $options - * @return string + * Get an attribute array of all arrayable relations. * - * @throws \Illuminate\Database\Eloquent\JsonEncodingException + * @return array */ - public function toPrettyJson(int $options = 0) + protected function getArrayableRelations() { - return $this->toJson(JSON_PRETTY_PRINT | $options); + return $this->getArrayableItems($this->relations); } /** - * Convert the object into something JSON serializable. + * Get an attribute array of all arrayable values. * - * @return mixed + * @param array $values + * @return array */ - public function jsonSerialize(): mixed + protected function getArrayableItems(array $values) { - return $this->toArray(); + if (count($this->getVisible()) > 0) { + return array_intersect_key($values, array_flip($this->getVisible())); + } + + return array_diff_key($values, array_flip($this->getHidden())); } /** - * Reload a fresh model instance from the database. + * Get an attribute from the model. * - * @param array|string $with - * @return static|null + * @param string $key + * @return mixed */ - public function fresh($with = []) + public function getAttribute($key) { - if (! $this->exists) { - return; + if (array_key_exists($key, $this->attributes) || $this->hasGetMutator($key)) { + return $this->getAttributeValue($key); } - return $this->setKeysForSelectQuery($this->newQueryWithoutScopes()) - ->useWritePdo() - ->with(is_string($with) ? func_get_args() : $with) - ->first(); + return $this->getRelationValue($key); } /** - * Reload the current model instance with fresh attributes from the database. + * Get a plain attribute (not a relationship). * - * @return $this + * @param string $key + * @return mixed */ - public function refresh() + public function getAttributeValue($key) { - if (! $this->exists) { - return $this; - } + $value = $this->getAttributeFromArray($key); - $this->setRawAttributes( - $this->setKeysForSelectQuery($this->newQueryWithoutScopes()) - ->useWritePdo() - ->firstOrFail() - ->attributes - ); + // If the attribute has a get mutator, we will call that then return what + // it returns as the value, which is useful for transforming values on + // retrieval from the model to a form that is more useful for usage. + if ($this->hasGetMutator($key)) { + return $this->mutateAttribute($key, $value); + } - $this->load((new BaseCollection($this->relations))->reject( - fn ($relation) => $relation instanceof Pivot - || (is_object($relation) && in_array(AsPivot::class, class_uses_recursive($relation), true)) - )->keys()->all()); + // If the attribute exists within the cast array, we will convert it to + // an appropriate native PHP type dependant upon the associated value + // given with the key in the pair. Dayle made this comment line up. + if ($this->hasCast($key)) { + return $this->castAttribute($key, $value); + } - $this->syncOriginal(); + // If the attribute is listed as a date, we will convert it to a DateTime + // instance on retrieval, which makes it quite convenient to work with + // date fields without having to create a mutator for each property. + if (in_array($key, $this->getDates()) && ! is_null($value)) { + return $this->asDateTime($value); + } - return $this; + return $value; } /** - * Clone the model into a new, non-existing instance. + * Get a relationship. * - * @param array|null $except - * @return static + * @param string $key + * @return mixed */ - public function replicate(?array $except = null) + public function getRelationValue($key) { - $defaults = array_values(array_filter([ - $this->getKeyName(), - $this->getCreatedAtColumn(), - $this->getUpdatedAtColumn(), - ...$this->uniqueIds(), - 'laravel_through_key', - ])); - - $attributes = Arr::except( - $this->getAttributes(), $except ? array_unique(array_merge($except, $defaults)) : $defaults - ); - - return tap(new static, function ($instance) use ($attributes) { - $instance->setRawAttributes($attributes); - - $instance->setRelations($this->relations); + // If the key already exists in the relationships array, it just means the + // relationship has already been loaded, so we'll just return it out of + // here because there is no need to query within the relations twice. + if ($this->relationLoaded($key)) { + return $this->relations[$key]; + } - $instance->fireModelEvent('replicating', false); - }); + // 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)) { + return $this->getRelationshipFromMethod($key); + } } /** - * Clone the model into a new, non-existing instance without raising any events. + * Get an attribute from the $attributes array. * - * @param array|null $except - * @return static + * @param string $key + * @return mixed */ - public function replicateQuietly(?array $except = null) + protected function getAttributeFromArray($key) { - return static::withoutEvents(fn () => $this->replicate($except)); + if (array_key_exists($key, $this->attributes)) { + return $this->attributes[$key]; + } } /** - * Determine if two models have the same ID and belong to the same table. + * Get a relationship value from a method. * - * @param \Illuminate\Database\Eloquent\Model|null $model - * @return bool + * @param string $method + * @return mixed + * + * @throws \LogicException */ - public function is($model) + protected function getRelationshipFromMethod($method) { - return ! is_null($model) && - $this->getKey() === $model->getKey() && - $this->getTable() === $model->getTable() && - $this->getConnectionName() === $model->getConnectionName(); + $relations = $this->$method(); + + if (! $relations instanceof Relation) { + throw new LogicException('Relationship method must return an object of type ' + .'Illuminate\Database\Eloquent\Relations\Relation'); + } + + $this->setRelation($method, $results = $relations->getResults()); + + return $results; } /** - * Determine if two models are not the same. + * Determine if a get mutator exists for an attribute. * - * @param \Illuminate\Database\Eloquent\Model|null $model + * @param string $key * @return bool */ - public function isNot($model) + public function hasGetMutator($key) { - return ! $this->is($model); + return method_exists($this, 'get'.Str::studly($key).'Attribute'); } /** - * Get the database connection for the model. + * Get the value of an attribute using its mutator. * - * @return \Illuminate\Database\Connection + * @param string $key + * @param mixed $value + * @return mixed */ - public function getConnection() + protected function mutateAttribute($key, $value) { - return static::resolveConnection($this->getConnectionName()); + return $this->{'get'.Str::studly($key).'Attribute'}($value); } /** - * Get the current connection name for the model. + * Get the value of an attribute using its mutator for array conversion. * - * @return string|null + * @param string $key + * @param mixed $value + * @return mixed */ - public function getConnectionName() + protected function mutateAttributeForArray($key, $value) { - return enum_value($this->connection); + $value = $this->mutateAttribute($key, $value); + + return $value instanceof Arrayable ? $value->toArray() : $value; } /** - * Set the connection associated with the model. + * Determine whether an attribute should be cast to a native type. * - * @param \UnitEnum|string|null $name - * @return $this + * @param string $key + * @param array|string|null $types + * @return bool */ - public function setConnection($name) + public function hasCast($key, $types = null) { - $this->connection = $name; + if (array_key_exists($key, $this->getCasts())) { + return $types ? in_array($this->getCastType($key), (array) $types, true) : true; + } - return $this; + return false; } /** - * Resolve a connection instance. + * Get the casts array. * - * @param \UnitEnum|string|null $connection - * @return \Illuminate\Database\Connection + * @return array */ - public static function resolveConnection($connection = null) + public function getCasts() { - return static::$resolver->connection($connection); + if ($this->getIncrementing()) { + return array_merge([ + $this->getKeyName() => 'int', + ], $this->casts); + } + + return $this->casts; } /** - * Get the connection resolver instance. + * Determine whether a value is Date / DateTime castable for inbound manipulation. * - * @return \Illuminate\Database\ConnectionResolverInterface|null + * @param string $key + * @return bool */ - public static function getConnectionResolver() + protected function isDateCastable($key) { - return static::$resolver; + return $this->hasCast($key, ['date', 'datetime']); } /** - * Set the connection resolver instance. + * Determine whether a value is JSON castable for inbound manipulation. * - * @param \Illuminate\Database\ConnectionResolverInterface $resolver - * @return void + * @param string $key + * @return bool */ - public static function setConnectionResolver(Resolver $resolver) + protected function isJsonCastable($key) { - static::$resolver = $resolver; + return $this->hasCast($key, ['array', 'json', 'object', 'collection']); } /** - * Unset the connection resolver for models. + * Get the type of cast for a model attribute. * - * @return void + * @param string $key + * @return string */ - public static function unsetConnectionResolver() + protected function getCastType($key) { - static::$resolver = null; + return trim(strtolower($this->getCasts()[$key])); } /** - * Get the table associated with the model. + * Cast an attribute to a native PHP type. * - * @return string + * @param string $key + * @param mixed $value + * @return mixed */ - public function getTable() + protected function castAttribute($key, $value) { - return $this->table ?? Str::snake(Str::pluralStudly(class_basename($this))); + if (is_null($value)) { + return $value; + } + + switch ($this->getCastType($key)) { + case 'int': + case 'integer': + return (int) $value; + case 'real': + case 'float': + case 'double': + return (float) $value; + case 'string': + return (string) $value; + case 'bool': + case 'boolean': + return (bool) $value; + case 'object': + return $this->fromJson($value, true); + case 'array': + case 'json': + return $this->fromJson($value); + case 'collection': + return new BaseCollection($this->fromJson($value)); + case 'date': + case 'datetime': + return $this->asDateTime($value); + case 'timestamp': + return $this->asTimeStamp($value); + default: + return $value; + } } /** - * Set the table associated with the model. + * Set a given attribute on the model. * - * @param string $table + * @param string $key + * @param mixed $value * @return $this */ - public function setTable($table) + public function setAttribute($key, $value) { - $this->table = $table; + // First we will check for the presence of a mutator for the set operation + // which simply lets the developers tweak the attribute as it is set on + // the model, such as "json_encoding" an listing of data for storage. + if ($this->hasSetMutator($key)) { + $method = 'set'.Str::studly($key).'Attribute'; + + return $this->{$method}($value); + } + + // If an attribute is listed as a "date", we'll convert it from a DateTime + // instance into a form proper for storage on the database tables using + // the connection grammar's date format. We will auto set the values. + elseif ($value && (in_array($key, $this->getDates()) || $this->isDateCastable($key))) { + $value = $this->fromDateTime($value); + } + + if ($this->isJsonCastable($key) && ! is_null($value)) { + $value = $this->asJson($value); + } + + $this->attributes[$key] = $value; return $this; } /** - * Get the primary key for the model. + * Determine if a set mutator exists for an attribute. * - * @return string + * @param string $key + * @return bool */ - public function getKeyName() + public function hasSetMutator($key) { - return $this->primaryKey; + return method_exists($this, 'set'.Str::studly($key).'Attribute'); } /** - * Set the primary key for the model. + * Get the attributes that should be converted to dates. * - * @param string $key - * @return $this + * @return array */ - public function setKeyName($key) + public function getDates() { - $this->primaryKey = $key; + $defaults = [static::CREATED_AT, static::UPDATED_AT]; - return $this; + return $this->timestamps ? array_merge($this->dates, $defaults) : $this->dates; } /** - * Get the table qualified key name. + * Convert a DateTime to a storable string. * + * @param \DateTime|int $value * @return string */ - public function getQualifiedKeyName() + public function fromDateTime($value) { - return $this->qualifyColumn($this->getKeyName()); + $format = $this->getDateFormat(); + + $value = $this->asDateTime($value); + + return $value->format($format); } /** - * Get the auto-incrementing key type. + * Return a timestamp as DateTime object. * - * @return string + * @param mixed $value + * @return \Carbon\Carbon */ - public function getKeyType() + protected function asDateTime($value) { - return $this->keyType; + // If this value is already a Carbon instance, we shall just return it as is. + // This prevents us having to re-instantiate a Carbon instance when we know + // it already is one, which wouldn't be fulfilled by the DateTime check. + if ($value instanceof Carbon) { + return $value; + } + + // If the value is already a DateTime instance, we will just skip the rest of + // these checks since they will be a waste of time, and hinder performance + // when checking the field. We will just return the DateTime right away. + if ($value instanceof DateTimeInterface) { + return new Carbon( + $value->format('Y-m-d H:i:s.u'), $value->getTimeZone() + ); + } + + // If this value is an integer, we will assume it is a UNIX timestamp's value + // and format a Carbon object from this timestamp. This allows flexibility + // when defining your date fields as they might be UNIX timestamps here. + if (is_numeric($value)) { + return Carbon::createFromTimestamp($value); + } + + // If the value is in simply year, month, day format, we will instantiate the + // Carbon instances from that format. Again, this provides for simple date + // fields on the database, while still supporting Carbonized conversion. + if (preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $value)) { + return Carbon::createFromFormat('Y-m-d', $value)->startOfDay(); + } + + // Finally, we will just assume this date is in the format used by default on + // the database connection and use that format to create the Carbon object + // that is returned back out to the developers after we convert it here. + return Carbon::createFromFormat($this->getDateFormat(), $value); } /** - * Set the data type for the primary key. + * Return a timestamp as unix timestamp. * - * @param string $type - * @return $this + * @param mixed $value + * @return int */ - public function setKeyType($type) + protected function asTimeStamp($value) { - $this->keyType = $type; + return $this->asDateTime($value)->getTimestamp(); + } - return $this; + /** + * Prepare a date for array / JSON serialization. + * + * @param \DateTime $date + * @return string + */ + protected function serializeDate(DateTime $date) + { + return $date->format($this->getDateFormat()); } /** - * Get the value indicating whether the IDs are incrementing. + * Get the format for database stored dates. * - * @return bool + * @return string */ - public function getIncrementing() + protected function getDateFormat() { - return $this->incrementing; + return $this->dateFormat ?: $this->getConnection()->getQueryGrammar()->getDateFormat(); } /** - * Set whether IDs are incrementing. + * Set the date format used by the model. * - * @param bool $value + * @param string $format * @return $this */ - public function setIncrementing($value) + public function setDateFormat($format) { - $this->incrementing = $value; + $this->dateFormat = $format; return $this; } /** - * Get the value of the model's primary key. + * Encode the given value as JSON. * - * @return mixed + * @param mixed $value + * @return string */ - public function getKey() + protected function asJson($value) { - return $this->getAttribute($this->getKeyName()); + return json_encode($value); } /** - * Get the queueable identity for the entity. + * Decode the given JSON back into an array or object. * + * @param string $value + * @param bool $asObject * @return mixed */ - public function getQueueableId() + public function fromJson($value, $asObject = false) { - return $this->getKey(); + return json_decode($value, ! $asObject); } /** - * Get the queueable relationships for the entity. + * Clone the model into a new, non-existing instance. * - * @return array + * @param array|null $except + * @return \Illuminate\Database\Eloquent\Model */ - public function getQueueableRelations() + public function replicate(array $except = null) { - return $this->withoutRecursion(function () { - $relations = []; - - foreach ($this->getRelations() as $key => $relation) { - if (! method_exists($this, $key)) { - continue; - } + $except = $except ?: [ + $this->getKeyName(), + $this->getCreatedAtColumn(), + $this->getUpdatedAtColumn(), + ]; - $relations[] = $key; + $attributes = Arr::except($this->attributes, $except); - if ($relation instanceof QueueableCollection) { - foreach ($relation->getQueueableRelations() as $collectionValue) { - $relations[] = $key.'.'.$collectionValue; - } - } + $instance = new static; - if ($relation instanceof QueueableEntity) { - foreach ($relation->getQueueableRelations() as $entityValue) { - $relations[] = $key.'.'.$entityValue; - } - } - } + $instance->setRawAttributes($attributes); - return array_unique($relations); - }, []); + return $instance->setRelations($this->relations); } /** - * Get the queueable connection for the entity. + * Get all of the current attributes on the model. * - * @return string|null + * @return array */ - public function getQueueableConnection() + public function getAttributes() { - return $this->getConnectionName(); + return $this->attributes; } /** - * Get the value of the model's route key. + * Set the array of model attributes. No checking is done. * - * @return mixed + * @param array $attributes + * @param bool $sync + * @return $this */ - public function getRouteKey() + public function setRawAttributes(array $attributes, $sync = false) { - return $this->getAttribute($this->getRouteKeyName()); + $this->attributes = $attributes; + + if ($sync) { + $this->syncOriginal(); + } + + return $this; } /** - * Get the route key for the model. + * Get the model's original attribute values. * - * @return string + * @param string|null $key + * @param mixed $default + * @return mixed|array */ - public function getRouteKeyName() + public function getOriginal($key = null, $default = null) { - return $this->getKeyName(); + return Arr::get($this->original, $key, $default); } /** - * Retrieve the model for a bound value. + * Sync the original attributes with the current. * - * @param mixed $value - * @param string|null $field - * @return \Illuminate\Database\Eloquent\Model|null + * @return $this */ - public function resolveRouteBinding($value, $field = null) + public function syncOriginal() { - return $this->resolveRouteBindingQuery($this, $value, $field)->first(); + $this->original = $this->attributes; + + return $this; } /** - * Retrieve the model for a bound value. + * Sync a single original attribute with its current value. * - * @param mixed $value - * @param string|null $field - * @return \Illuminate\Database\Eloquent\Model|null + * @param string $attribute + * @return $this */ - public function resolveSoftDeletableRouteBinding($value, $field = null) + public function syncOriginalAttribute($attribute) { - return $this->resolveRouteBindingQuery($this, $value, $field)->withTrashed()->first(); + $this->original[$attribute] = $this->attributes[$attribute]; + + return $this; } /** - * Retrieve the child model for a bound value. + * Determine if the model or given attribute(s) have been modified. * - * @param string $childType - * @param mixed $value - * @param string|null $field - * @return \Illuminate\Database\Eloquent\Model|null + * @param array|string|null $attributes + * @return bool */ - public function resolveChildRouteBinding($childType, $value, $field) + public function isDirty($attributes = null) { - return $this->resolveChildRouteBindingQuery($childType, $value, $field)->first(); + $dirty = $this->getDirty(); + + if (is_null($attributes)) { + return count($dirty) > 0; + } + + if (! is_array($attributes)) { + $attributes = func_get_args(); + } + + foreach ($attributes as $attribute) { + if (array_key_exists($attribute, $dirty)) { + return true; + } + } + + return false; } /** - * Retrieve the child model for a bound value. + * Get the attributes that have been changed since last sync. * - * @param string $childType - * @param mixed $value - * @param string|null $field - * @return \Illuminate\Database\Eloquent\Model|null + * @return array */ - public function resolveSoftDeletableChildRouteBinding($childType, $value, $field) + public function getDirty() { - return $this->resolveChildRouteBindingQuery($childType, $value, $field)->withTrashed()->first(); + $dirty = []; + + foreach ($this->attributes as $key => $value) { + if (! array_key_exists($key, $this->original)) { + $dirty[$key] = $value; + } elseif ($value !== $this->original[$key] && + ! $this->originalIsNumericallyEquivalent($key)) { + $dirty[$key] = $value; + } + } + + return $dirty; } /** - * Retrieve the child model query for a bound value. + * Determine if the new and old values for a given key are numerically equivalent. * - * @param string $childType - * @param mixed $value - * @param string|null $field - * @return \Illuminate\Database\Eloquent\Relations\Relation<\Illuminate\Database\Eloquent\Model, $this, *> + * @param string $key + * @return bool */ - protected function resolveChildRouteBindingQuery($childType, $value, $field) + protected function originalIsNumericallyEquivalent($key) { - $relationship = $this->{$this->childRouteBindingRelationshipName($childType)}(); + $current = $this->attributes[$key]; - $field = $field ?: $relationship->getRelated()->getRouteKeyName(); + $original = $this->original[$key]; - if ($relationship instanceof HasManyThrough || - $relationship instanceof BelongsToMany) { - $field = $relationship->getRelated()->qualifyColumn($field); - } - - return $relationship instanceof Model - ? $relationship->resolveRouteBindingQuery($relationship, $value, $field) - : $relationship->getRelated()->resolveRouteBindingQuery($relationship, $value, $field); + return is_numeric($current) && is_numeric($original) && strcmp((string) $current, (string) $original) === 0; } /** - * Retrieve the child route model binding relationship name for the given child type. + * Get all the loaded relations for the instance. * - * @param string $childType - * @return string + * @return array */ - protected function childRouteBindingRelationshipName($childType) + public function getRelations() { - return Str::plural(Str::camel($childType)); + return $this->relations; } /** - * Retrieve the model for a bound value. + * Get a specified relationship. * - * @param \Illuminate\Database\Eloquent\Model|\Illuminate\Contracts\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query - * @param mixed $value - * @param string|null $field - * @return \Illuminate\Contracts\Database\Eloquent\Builder + * @param string $relation + * @return mixed */ - public function resolveRouteBindingQuery($query, $value, $field = null) + public function getRelation($relation) { - return $query->where($field ?? $this->getRouteKeyName(), $value); + return $this->relations[$relation]; } /** - * Get the default foreign key name for the model. + * Determine if the given relation is loaded. * - * @return string + * @param string $key + * @return bool */ - public function getForeignKey() + public function relationLoaded($key) { - return Str::snake(class_basename($this)).'_'.$this->getKeyName(); + return array_key_exists($key, $this->relations); } /** - * Get the number of models to return per page. + * Set the specific relationship in the model. * - * @return int + * @param string $relation + * @param mixed $value + * @return $this */ - public function getPerPage() + public function setRelation($relation, $value) { - return $this->perPage; + $this->relations[$relation] = $value; + + return $this; } /** - * Set the number of models to return per page. + * Set the entire relations array on the model. * - * @param int $perPage + * @param array $relations * @return $this */ - public function setPerPage($perPage) + public function setRelations(array $relations) { - $this->perPage = $perPage; + $this->relations = $relations; return $this; } /** - * Determine if the model is soft deletable. + * Get the database connection for the model. + * + * @return \Illuminate\Database\Connection */ - public static function isSoftDeletable(): bool + public function getConnection() { - return static::$isSoftDeletable[static::class] ??= in_array(SoftDeletes::class, class_uses_recursive(static::class)); + return static::resolveConnection($this->getConnectionName()); } /** - * Determine if the model is prunable. + * Get the current connection name for the model. + * + * @return string */ - protected function isPrunable(): bool + public function getConnectionName() { - return self::$isPrunable[static::class] ??= in_array(Prunable::class, class_uses_recursive(static::class)) || static::isMassPrunable(); + return $this->connection; } /** - * Determine if the model is mass prunable. + * Set the connection associated with the model. + * + * @param string $name + * @return $this */ - protected function isMassPrunable(): bool + public function setConnection($name) { - return self::$isMassPrunable[static::class] ??= in_array(MassPrunable::class, class_uses_recursive(static::class)); + $this->connection = $name; + + return $this; } /** - * Determine if lazy loading is disabled. + * Resolve a connection instance. * - * @return bool + * @param string|null $connection + * @return \Illuminate\Database\Connection */ - public static function preventsLazyLoading() + public static function resolveConnection($connection = null) { - return static::$modelsShouldPreventLazyLoading; + return static::$resolver->connection($connection); } /** - * Determine if relationships are being automatically eager loaded when accessed. + * Get the connection resolver instance. * - * @return bool + * @return \Illuminate\Database\ConnectionResolverInterface */ - public static function isAutomaticallyEagerLoadingRelationships() + public static function getConnectionResolver() { - return static::$modelsShouldAutomaticallyEagerLoadRelationships; + return static::$resolver; } /** - * Determine if discarding guarded attribute fills is disabled. + * Set the connection resolver instance. * - * @return bool + * @param \Illuminate\Database\ConnectionResolverInterface $resolver + * @return void */ - public static function preventsSilentlyDiscardingAttributes() + public static function setConnectionResolver(Resolver $resolver) { - return static::$modelsShouldPreventSilentlyDiscardingAttributes; + static::$resolver = $resolver; } /** - * Determine if accessing missing attributes is disabled. + * Unset the connection resolver for models. * - * @return bool + * @return void */ - public static function preventsAccessingMissingAttributes() + public static function unsetConnectionResolver() { - return static::$modelsShouldPreventAccessingMissingAttributes; + static::$resolver = null; } /** - * Get the broadcast channel route definition that is associated with the given entity. + * Get the event dispatcher instance. * - * @return string + * @return \Illuminate\Contracts\Events\Dispatcher */ - public function broadcastChannelRoute() + public static function getEventDispatcher() { - return str_replace('\\', '.', get_class($this)).'.{'.Str::camel(class_basename($this)).'}'; + return static::$dispatcher; } /** - * Get the broadcast channel name that is associated with the given entity. + * Set the event dispatcher instance. * - * @return string + * @param \Illuminate\Contracts\Events\Dispatcher $dispatcher + * @return void */ - public function broadcastChannel() + public static function setEventDispatcher(Dispatcher $dispatcher) { - return str_replace('\\', '.', get_class($this)).'.'.$this->getKey(); + static::$dispatcher = $dispatcher; } /** - * Resolve a class attribute value from the model. - * - * @template TAttribute of object + * Unset the event dispatcher for models. * - * @param class-string $attributeClass - * @param string|null $property - * @param string|null $class - * @return mixed + * @return void */ - protected static function resolveClassAttribute(string $attributeClass, ?string $property = null, ?string $class = null) + public static function unsetEventDispatcher() { - $class = $class ?? static::class; + static::$dispatcher = null; + } - $cacheKey = $class.'@'.$attributeClass; + /** + * Get the mutated attributes for a given instance. + * + * @return array + */ + public function getMutatedAttributes() + { + $class = static::class; - if (array_key_exists($cacheKey, static::$classAttributes)) { - return static::$classAttributes[$cacheKey]; + if (! isset(static::$mutatorCache[$class])) { + static::cacheMutatedAttributes($class); } - try { - $reflection = new ReflectionClass($class); - - do { - $attributes = $reflection->getAttributes($attributeClass); + return static::$mutatorCache[$class]; + } - if (count($attributes) > 0) { - $instance = $attributes[0]->newInstance(); + /** + * Extract and cache all the mutated attributes of a class. + * + * @param string $class + * @return void + */ + public static function cacheMutatedAttributes($class) + { + $mutatedAttributes = []; - return static::$classAttributes[$cacheKey] = $property ? $instance->{$property} : $instance; + // Here we will extract all of the mutated attributes so that we can quickly + // spin through them after we export models to their array form, which we + // need to be fast. This'll let us know the attributes that can mutate. + if (preg_match_all('/(?<=^|;)get([^;]+?)Attribute(;|$)/', implode(';', get_class_methods($class)), $matches)) { + foreach ($matches[1] as $match) { + if (static::$snakeAttributes) { + $match = Str::snake($match); } - } while ($reflection = $reflection->getParentClass()); - } catch (Exception) { - // + + $mutatedAttributes[] = lcfirst($match); + } } - return static::$classAttributes[$cacheKey] = null; + static::$mutatorCache[$class] = $mutatedAttributes; } /** @@ -2537,17 +3388,9 @@ public function __set($key, $value) * @param mixed $offset * @return bool */ - public function offsetExists($offset): bool + public function offsetExists($offset) { - $shouldPrevent = static::$modelsShouldPreventAccessingMissingAttributes; - - static::$modelsShouldPreventAccessingMissingAttributes = false; - - try { - return ! is_null($this->getAttribute($offset)); - } finally { - static::$modelsShouldPreventAccessingMissingAttributes = $shouldPrevent; - } + return isset($this->$offset); } /** @@ -2556,9 +3399,9 @@ public function offsetExists($offset): bool * @param mixed $offset * @return mixed */ - public function offsetGet($offset): mixed + public function offsetGet($offset) { - return $this->getAttribute($offset); + return $this->$offset; } /** @@ -2568,9 +3411,9 @@ public function offsetGet($offset): mixed * @param mixed $value * @return void */ - public function offsetSet($offset, $value): void + public function offsetSet($offset, $value) { - $this->setAttribute($offset, $value); + $this->$offset = $value; } /** @@ -2579,25 +3422,21 @@ public function offsetSet($offset, $value): void * @param mixed $offset * @return void */ - public function offsetUnset($offset): void + public function offsetUnset($offset) { - unset( - $this->attributes[$offset], - $this->relations[$offset], - $this->attributeCastCache[$offset], - $this->classCastCache[$offset] - ); + unset($this->$offset); } /** - * Determine if an attribute or relation exists on the model. + * Determine if an attribute exists on the model. * * @param string $key * @return bool */ public function __isset($key) { - return $this->offsetExists($key); + return (isset($this->attributes[$key]) || isset($this->relations[$key])) || + ($this->hasGetMutator($key) && ! is_null($this->getAttributeValue($key))); } /** @@ -2608,7 +3447,7 @@ public function __isset($key) */ public function __unset($key) { - $this->offsetUnset($key); + unset($this->attributes[$key], $this->relations[$key]); } /** @@ -2620,24 +3459,17 @@ public function __unset($key) */ public function __call($method, $parameters) { - if (in_array($method, ['increment', 'decrement', 'incrementQuietly', 'decrementQuietly'])) { - return $this->$method(...$parameters); - } - - if ($resolver = $this->relationResolver(static::class, $method)) { - return $resolver($this); + if (in_array($method, ['increment', 'decrement'])) { + return call_user_func_array([$this, $method], $parameters); } - if (Str::startsWith($method, 'through') && - method_exists($this, $relationMethod = (new SupportStringable($method))->after('through')->lcfirst()->toString())) { - return $this->through($relationMethod); - } + $query = $this->newQuery(); - return $this->forwardCallTo($this->newQuery(), $method, $parameters); + return call_user_func_array([$query, $method], $parameters); } /** - * Handle dynamic static method calls into the model. + * Handle dynamic static method calls into the method. * * @param string $method * @param array $parameters @@ -2645,11 +3477,9 @@ public function __call($method, $parameters) */ public static function __callStatic($method, $parameters) { - if (static::isScopeMethodWithAttribute($method)) { - return static::query()->$method(...$parameters); - } + $instance = new static; - return (new static)->$method(...$parameters); + return call_user_func_array([$instance, $method], $parameters); } /** @@ -2659,49 +3489,7 @@ public static function __callStatic($method, $parameters) */ public function __toString() { - return $this->escapeWhenCastingToString - ? e($this->toJson()) - : $this->toJson(); - } - - /** - * Indicate that the object's string representation should be escaped when __toString is invoked. - * - * @param bool $escape - * @return $this - */ - public function escapeWhenCastingToString($escape = true) - { - $this->escapeWhenCastingToString = $escape; - - return $this; - } - - /** - * Prepare the object for serialization. - * - * @return array - */ - public function __sleep() - { - $this->mergeAttributesFromCachedCasts(); - - $this->classCastCache = []; - $this->attributeCastCache = []; - $this->relationAutoloadCallback = null; - $this->relationAutoloadContext = null; - - $keys = get_object_vars($this); - - if (version_compare(PHP_VERSION, '8.4.0', '>=')) { - foreach ((new ReflectionClass($this))->getProperties() as $property) { - if ($property->hasHooks()) { - unset($keys[$property->getName()]); - } - } - } - - return array_keys($keys); + return $this->toJson(); } /** @@ -2712,11 +3500,5 @@ public function __sleep() public function __wakeup() { $this->bootIfNotBooted(); - $this->initializeTraits(); - $this->initializeModelAttributes(); - - if (static::isAutomaticallyEagerLoadingRelationships()) { - $this->withRelationshipAutoloading(); - } } } diff --git a/Eloquent/ModelInfo.php b/Eloquent/ModelInfo.php deleted file mode 100644 index 9ff740ffda..0000000000 --- a/Eloquent/ModelInfo.php +++ /dev/null @@ -1,97 +0,0 @@ - - */ -class ModelInfo implements Arrayable, ArrayAccess -{ - /** - * @template TModel of \Illuminate\Database\Eloquent\Model - * - * @param class-string $class The model's fully-qualified class. - * @param string $database The database connection name. - * @param string $table The database table name. - * @param class-string|null $policy The policy that applies to the model. - * @param \Illuminate\Support\Collection> $attributes The attributes available on the model. - * @param \Illuminate\Support\Collection}> $relations The relations defined on the model. - * @param \Illuminate\Support\Collection $events The events that the model dispatches. - * @param \Illuminate\Support\Collection}> $observers The observers registered for the model. - * @param class-string<\Illuminate\Database\Eloquent\Collection> $collection The Collection class that collects the models. - * @param class-string<\Illuminate\Database\Eloquent\Builder> $builder The Builder class registered for the model. - * @param \Illuminate\Http\Resources\Json\JsonResource|null $resource The JSON resource that represents the model. - */ - public function __construct( - public $class, - public $database, - public $table, - public $policy, - public $attributes, - public $relations, - public $events, - public $observers, - public $collection, - public $builder, - public $resource - ) { - } - - /** - * Convert the model info to an array. - * - * @return array{ - * "class": class-string<\Illuminate\Database\Eloquent\Model>, - * database: string, - * table: string, - * policy: class-string|null, - * attributes: \Illuminate\Support\Collection>, - * relations: \Illuminate\Support\Collection}>, - * events: \Illuminate\Support\Collection, - * observers: \Illuminate\Support\Collection}>, collection: class-string<\Illuminate\Database\Eloquent\Collection<\Illuminate\Database\Eloquent\Model>>, - * builder: class-string<\Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model>> - * resource: \Illuminate\Http\Resources\Json\JsonResource|null - * } - */ - public function toArray() - { - return [ - 'class' => $this->class, - 'database' => $this->database, - 'table' => $this->table, - 'policy' => $this->policy, - 'attributes' => $this->attributes, - 'relations' => $this->relations, - 'events' => $this->events, - 'observers' => $this->observers, - 'collection' => $this->collection, - 'builder' => $this->builder, - 'resource' => $this->resource, - ]; - } - - public function offsetExists(mixed $offset): bool - { - return property_exists($this, $offset); - } - - public function offsetGet(mixed $offset): mixed - { - return property_exists($this, $offset) ? $this->{$offset} : throw new InvalidArgumentException("Property {$offset} does not exist."); - } - - public function offsetSet(mixed $offset, mixed $value): void - { - throw new LogicException(self::class.' may not be mutated using array access.'); - } - - public function offsetUnset(mixed $offset): void - { - throw new LogicException(self::class.' may not be mutated using array access.'); - } -} diff --git a/Eloquent/ModelInspector.php b/Eloquent/ModelInspector.php deleted file mode 100644 index b89d6bccfb..0000000000 --- a/Eloquent/ModelInspector.php +++ /dev/null @@ -1,414 +0,0 @@ - - */ - protected $relationMethods = [ - 'hasMany', - 'hasManyThrough', - 'hasOneThrough', - 'belongsToMany', - 'hasOne', - 'belongsTo', - 'morphOne', - 'morphTo', - 'morphMany', - 'morphToMany', - 'morphedByMany', - ]; - - /** - * Create a new model inspector instance. - * - * @param \Illuminate\Contracts\Foundation\Application $app The Laravel application instance. - */ - public function __construct(protected Application $app) - { - } - - /** - * Extract model details for the given model. - * - * @param class-string<\Illuminate\Database\Eloquent\Model>|string $model - * @param string|null $connection - * @return \Illuminate\Database\Eloquent\ModelInfo - * - * @throws \Illuminate\Contracts\Container\BindingResolutionException - */ - public function inspect($model, $connection = null) - { - $class = $this->qualifyModel($model); - - /** @var \Illuminate\Database\Eloquent\Model $model */ - $model = $this->app->make($class); - - if ($connection !== null) { - $model->setConnection($connection); - } - - return new ModelInfo( - class: $model::class, - database: $model->getConnection()->getName(), - table: $model->getConnection()->getTablePrefix().$model->getTable(), - policy: $this->getPolicy($model), - attributes: $this->getAttributes($model), - relations: $this->getRelations($model), - events: $this->getEvents($model), - observers: $this->getObservers($model), - collection: $this->getCollectedBy($model), - builder: $this->getBuilder($model), - resource: $this->getResource($model), - ); - } - - /** - * Get the column attributes for the given model. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return \Illuminate\Support\Collection> - */ - protected function getAttributes($model) - { - $connection = $model->getConnection(); - $schema = $connection->getSchemaBuilder(); - $table = $model->getTable(); - $columns = $schema->getColumns($table); - $indexes = $schema->getIndexes($table); - - return (new BaseCollection($columns)) - ->map(fn ($column) => [ - 'name' => $column['name'], - 'type' => $column['type'], - 'increments' => $column['auto_increment'], - 'nullable' => $column['nullable'], - 'default' => $this->getColumnDefault($column, $model), - 'unique' => $this->columnIsUnique($column['name'], $indexes), - 'fillable' => $model->isFillable($column['name']), - 'hidden' => $this->attributeIsHidden($column['name'], $model), - 'appended' => null, - 'cast' => $this->getCastType($column['name'], $model), - ]) - ->merge($this->getVirtualAttributes($model, $columns)); - } - - /** - * Get the virtual (non-column) attributes for the given model. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @param array $columns - * @return \Illuminate\Support\Collection - */ - protected function getVirtualAttributes($model, $columns) - { - $class = new ReflectionClass($model); - - return (new BaseCollection($class->getMethods())) - ->reject( - fn (ReflectionMethod $method) => $method->isStatic() - || $method->isAbstract() - || $method->getDeclaringClass()->getName() === Model::class - ) - ->mapWithKeys(function (ReflectionMethod $method) use ($model) { - if (preg_match('/^get(.+)Attribute$/', $method->getName(), $matches) === 1) { - return [Str::snake($matches[1]) => 'accessor']; - } elseif ($model->hasAttributeMutator($method->getName())) { - return [Str::snake($method->getName()) => 'attribute']; - } else { - return []; - } - }) - ->reject(fn ($cast, $name) => (new BaseCollection($columns))->contains('name', $name)) - ->map(fn ($cast, $name) => [ - 'name' => $name, - 'type' => null, - 'increments' => false, - 'nullable' => null, - 'default' => null, - 'unique' => null, - 'fillable' => $model->isFillable($name), - 'hidden' => $this->attributeIsHidden($name, $model), - 'appended' => $model->hasAppended($name), - 'cast' => $cast, - ]) - ->values(); - } - - /** - * Get the relations from the given model. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return \Illuminate\Support\Collection - */ - protected function getRelations($model) - { - return (new BaseCollection(get_class_methods($model))) - ->map(fn ($method) => new ReflectionMethod($model, $method)) - ->reject( - fn (ReflectionMethod $method) => $method->isStatic() - || $method->isAbstract() - || $method->getDeclaringClass()->getName() === Model::class - || $method->getNumberOfParameters() > 0 - ) - ->filter(function (ReflectionMethod $method) { - if ($method->getReturnType() instanceof ReflectionNamedType - && is_subclass_of($method->getReturnType()->getName(), Relation::class)) { - return true; - } - - $file = new SplFileObject($method->getFileName()); - $file->seek($method->getStartLine() - 1); - $code = ''; - while ($file->key() < $method->getEndLine()) { - $code .= trim($file->current()); - $file->next(); - } - - return (new BaseCollection($this->relationMethods)) - ->contains(fn ($relationMethod) => str_contains($code, '$this->'.$relationMethod.'(')); - }) - ->map(function (ReflectionMethod $method) use ($model) { - $relation = $method->invoke($model); - - if (! $relation instanceof Relation) { - return null; - } - - return [ - 'name' => $method->getName(), - 'type' => Str::afterLast(get_class($relation), '\\'), - 'related' => get_class($relation->getRelated()), - ]; - }) - ->filter() - ->values(); - } - - /** - * Get the first policy associated with this model. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return string|null - */ - protected function getPolicy($model) - { - $policy = Gate::getPolicyFor($model::class); - - return $policy ? $policy::class : null; - } - - /** - * Get the events that the model dispatches. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return \Illuminate\Support\Collection - */ - protected function getEvents($model) - { - return (new BaseCollection($model->dispatchesEvents())) - ->map(fn (string $class, string $event) => [ - 'event' => $event, - 'class' => $class, - ])->values(); - } - - /** - * Get the observers watching this model. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return \Illuminate\Support\Collection - * - * @throws \Illuminate\Contracts\Container\BindingResolutionException - */ - protected function getObservers($model) - { - $listeners = $this->app->make('events')->getRawListeners(); - - // Get the Eloquent observers for this model... - $listeners = array_filter($listeners, function ($v, $key) use ($model) { - return Str::startsWith($key, 'eloquent.') && Str::endsWith($key, $model::class); - }, ARRAY_FILTER_USE_BOTH); - - // Format listeners Eloquent verb => Observer methods... - $extractVerb = function ($key) { - preg_match('/eloquent.([a-zA-Z]+)\: /', $key, $matches); - - return $matches[1] ?? '?'; - }; - - $formatted = []; - - foreach ($listeners as $key => $observerMethods) { - $formatted[] = [ - 'event' => $extractVerb($key), - 'observer' => array_map(fn ($obs) => is_string($obs) ? $obs : 'Closure', $observerMethods), - ]; - } - - return new BaseCollection($formatted); - } - - /** - * Get the collection class being used by the model. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return class-string<\Illuminate\Database\Eloquent\Collection> - */ - protected function getCollectedBy($model) - { - return $model->newCollection()::class; - } - - /** - * Get the builder class being used by the model. - * - * @template TModel of \Illuminate\Database\Eloquent\Model - * - * @param TModel $model - * @return class-string<\Illuminate\Database\Eloquent\Builder> - */ - protected function getBuilder($model) - { - return $model->newQuery()::class; - } - - /** - * Get the class used for JSON response transforming. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return \Illuminate\Http\Resources\Json\JsonResource|null - */ - protected function getResource($model) - { - return rescue(static fn () => $model->toResource()::class, null, false); - } - - /** - * Qualify the given model class base name. - * - * @param string $model - * @return class-string<\Illuminate\Database\Eloquent\Model> - * - * @see \Illuminate\Console\GeneratorCommand - */ - protected function qualifyModel(string $model) - { - if (str_contains($model, '\\') && class_exists($model)) { - return $model; - } - - $model = ltrim($model, '\\/'); - - $model = str_replace('/', '\\', $model); - - $rootNamespace = $this->app->getNamespace(); - - if (Str::startsWith($model, $rootNamespace)) { - return $model; - } - - return is_dir(app_path('Models')) - ? $rootNamespace.'Models\\'.$model - : $rootNamespace.$model; - } - - /** - * Get the cast type for the given column. - * - * @param string $column - * @param \Illuminate\Database\Eloquent\Model $model - * @return string|null - */ - protected function getCastType($column, $model) - { - if ($model->hasGetMutator($column) || $model->hasSetMutator($column)) { - return 'accessor'; - } - - if ($model->hasAttributeMutator($column)) { - return 'attribute'; - } - - return $this->getCastsWithDates($model)->get($column) ?? null; - } - - /** - * Get the model casts, including any date casts. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return \Illuminate\Support\Collection - */ - protected function getCastsWithDates($model) - { - return (new BaseCollection($model->getDates())) - ->filter() - ->flip() - ->map(fn () => 'datetime') - ->merge($model->getCasts()); - } - - /** - * Determine if the given attribute is hidden. - * - * @param string $attribute - * @param \Illuminate\Database\Eloquent\Model $model - * @return bool - */ - protected function attributeIsHidden($attribute, $model) - { - if (count($model->getHidden()) > 0) { - return in_array($attribute, $model->getHidden()); - } - - if (count($model->getVisible()) > 0) { - return ! in_array($attribute, $model->getVisible()); - } - - return false; - } - - /** - * Get the default value for the given column. - * - * @param array $column - * @param \Illuminate\Database\Eloquent\Model $model - * @return mixed - */ - protected function getColumnDefault($column, $model) - { - $attributeDefault = $model->getAttributes()[$column['name']] ?? null; - - return enum_value($attributeDefault) ?? $column['default']; - } - - /** - * Determine if the given attribute is unique. - * - * @param string $column - * @param array $indexes - * @return bool - */ - protected function columnIsUnique($column, $indexes) - { - return (new BaseCollection($indexes))->contains( - fn ($index) => count($index['columns']) === 1 && $index['columns'][0] === $column && $index['unique'] - ); - } -} diff --git a/Eloquent/ModelNotFoundException.php b/Eloquent/ModelNotFoundException.php index 79ae8a39de..102683aa93 100755 --- a/Eloquent/ModelNotFoundException.php +++ b/Eloquent/ModelNotFoundException.php @@ -2,47 +2,28 @@ namespace Illuminate\Database\Eloquent; -use Illuminate\Database\RecordsNotFoundException; -use Illuminate\Support\Arr; +use RuntimeException; -/** - * @template TModel of \Illuminate\Database\Eloquent\Model - */ -class ModelNotFoundException extends RecordsNotFoundException +class ModelNotFoundException extends RuntimeException { /** * Name of the affected Eloquent model. * - * @var class-string + * @var string */ protected $model; /** - * The affected model IDs. + * Set the affected Eloquent model. * - * @var array - */ - protected $ids; - - /** - * Set the affected Eloquent model and instance ids. - * - * @param class-string $model - * @param array|int|string $ids + * @param string $model * @return $this */ - public function setModel($model, $ids = []) + public function setModel($model) { $this->model = $model; - $this->ids = Arr::wrap($ids); - - $this->message = "No query results for model [{$model}]"; - if (count($this->ids) > 0) { - $this->message .= ' '.implode(', ', $this->ids); - } else { - $this->message .= '.'; - } + $this->message = "No query results for model [{$model}]."; return $this; } @@ -50,20 +31,10 @@ public function setModel($model, $ids = []) /** * Get the affected Eloquent model. * - * @return class-string + * @return string */ public function getModel() { return $this->model; } - - /** - * Get the affected Eloquent model IDs. - * - * @return array - */ - public function getIds() - { - return $this->ids; - } } diff --git a/Eloquent/PendingHasThroughRelationship.php b/Eloquent/PendingHasThroughRelationship.php deleted file mode 100644 index 9f3b757fbd..0000000000 --- a/Eloquent/PendingHasThroughRelationship.php +++ /dev/null @@ -1,119 +0,0 @@ - - */ -class PendingHasThroughRelationship -{ - /** - * The root model that the relationship exists on. - * - * @var TDeclaringModel - */ - protected $rootModel; - - /** - * The local relationship. - * - * @var TLocalRelationship - */ - protected $localRelationship; - - /** - * Create a pending has-many-through or has-one-through relationship. - * - * @param TDeclaringModel $rootModel - * @param TLocalRelationship $localRelationship - */ - public function __construct($rootModel, $localRelationship) - { - $this->rootModel = $rootModel; - - $this->localRelationship = $localRelationship; - } - - /** - * Define the distant relationship that this model has. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param string|(callable(TIntermediateModel): (\Illuminate\Database\Eloquent\Relations\HasOne|\Illuminate\Database\Eloquent\Relations\HasMany|\Illuminate\Database\Eloquent\Relations\MorphOneOrMany)) $callback - * @return ( - * $callback is string - * ? \Illuminate\Database\Eloquent\Relations\HasManyThrough<\Illuminate\Database\Eloquent\Model, TIntermediateModel, TDeclaringModel>|\Illuminate\Database\Eloquent\Relations\HasOneThrough<\Illuminate\Database\Eloquent\Model, TIntermediateModel, TDeclaringModel> - * : ( - * TLocalRelationship is \Illuminate\Database\Eloquent\Relations\HasMany - * ? \Illuminate\Database\Eloquent\Relations\HasManyThrough - * : ( - * $callback is callable(TIntermediateModel): \Illuminate\Database\Eloquent\Relations\HasMany - * ? \Illuminate\Database\Eloquent\Relations\HasManyThrough - * : \Illuminate\Database\Eloquent\Relations\HasOneThrough - * ) - * ) - * ) - */ - public function has($callback) - { - if (is_string($callback)) { - $callback = fn () => $this->localRelationship->getRelated()->{$callback}(); - } - - $distantRelation = $callback($this->localRelationship->getRelated()); - - if ($distantRelation instanceof HasMany || $this->localRelationship instanceof HasMany) { - $returnedRelation = $this->rootModel->hasManyThrough( - $distantRelation->getRelated()::class, - $this->localRelationship->getRelated()::class, - $this->localRelationship->getForeignKeyName(), - $distantRelation->getForeignKeyName(), - $this->localRelationship->getLocalKeyName(), - $distantRelation->getLocalKeyName(), - ); - } else { - $returnedRelation = $this->rootModel->hasOneThrough( - $distantRelation->getRelated()::class, - $this->localRelationship->getRelated()::class, - $this->localRelationship->getForeignKeyName(), - $distantRelation->getForeignKeyName(), - $this->localRelationship->getLocalKeyName(), - $distantRelation->getLocalKeyName(), - ); - } - - if ($this->localRelationship instanceof MorphOneOrMany) { - $returnedRelation->where($this->localRelationship->getQualifiedMorphType(), $this->localRelationship->getMorphClass()); - } - - return $returnedRelation; - } - - /** - * Handle dynamic method calls into the model. - * - * @param string $method - * @param array $parameters - * @return mixed - * - * @throws \BadMethodCallException - */ - public function __call($method, $parameters) - { - if (Str::startsWith($method, 'has')) { - return $this->has((new Stringable($method))->after('has')->lcfirst()->toString()); - } - - throw new BadMethodCallException(sprintf( - 'Call to undefined method %s::%s()', static::class, $method - )); - } -} diff --git a/Eloquent/Prunable.php b/Eloquent/Prunable.php deleted file mode 100644 index 15423dd638..0000000000 --- a/Eloquent/Prunable.php +++ /dev/null @@ -1,85 +0,0 @@ -prunable() - ->when(static::isSoftDeletable(), function ($query) { - $query->withTrashed(); - })->chunkById($chunkSize, function ($models) use (&$total) { - $models->each(function ($model) use (&$total) { - try { - $model->prune(); - - $total++; - } catch (Throwable $e) { - $handler = app(ExceptionHandler::class); - - if ($handler) { - $handler->report($e); - } else { - throw $e; - } - } - }); - - event(new ModelsPruned(static::class, $total)); - }); - - return $total; - } - - /** - * Get the prunable model query. - * - * @return \Illuminate\Database\Eloquent\Builder - * - * @throws \LogicException - */ - public function prunable() - { - throw new LogicException('Please implement the prunable method on your model.'); - } - - /** - * Prune the model in the database. - * - * @return bool|null - */ - public function prune() - { - $this->pruning(); - - return static::isSoftDeletable() - ? $this->forceDelete() - : $this->delete(); - } - - /** - * Prepare the model for pruning. - * - * @return void - */ - protected function pruning() - { - // - } -} diff --git a/Eloquent/RelationNotFoundException.php b/Eloquent/RelationNotFoundException.php deleted file mode 100755 index 73257bb101..0000000000 --- a/Eloquent/RelationNotFoundException.php +++ /dev/null @@ -1,46 +0,0 @@ -model = $class; - $instance->relation = $relation; - - return $instance; - } -} diff --git a/Eloquent/Relations/BelongsTo.php b/Eloquent/Relations/BelongsTo.php index a550eac730..9df11e6400 100755 --- a/Eloquent/Relations/BelongsTo.php +++ b/Eloquent/Relations/BelongsTo.php @@ -2,34 +2,13 @@ namespace Illuminate\Database\Eloquent\Relations; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\Concerns\ComparesRelatedModels; -use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary; -use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels; - -use function Illuminate\Support\enum_value; - -/** - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * - * @extends \Illuminate\Database\Eloquent\Relations\Relation - */ +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Query\Expression; +use Illuminate\Database\Eloquent\Collection; + class BelongsTo extends Relation { - use ComparesRelatedModels, - InteractsWithDictionary, - SupportsDefaultModels; - - /** - * The child model instance of the relation. - * - * @var TDeclaringModel - */ - protected $child; - /** * The foreign key of the parent model. * @@ -42,46 +21,42 @@ class BelongsTo extends Relation * * @var string */ - protected $ownerKey; + protected $otherKey; /** * The name of the relationship. * * @var string */ - protected $relationName; + protected $relation; /** * Create a new belongs to relationship instance. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param TDeclaringModel $child + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Model $parent * @param string $foreignKey - * @param string $ownerKey - * @param string $relationName + * @param string $otherKey + * @param string $relation + * @return void */ - public function __construct(Builder $query, Model $child, $foreignKey, $ownerKey, $relationName) + public function __construct(Builder $query, Model $parent, $foreignKey, $otherKey, $relation) { - $this->ownerKey = $ownerKey; - $this->relationName = $relationName; + $this->otherKey = $otherKey; + $this->relation = $relation; $this->foreignKey = $foreignKey; - // In the underlying base relationship class, this variable is referred to as - // the "parent" since most relationships are not inversed. But, since this - // one is we will create a "child" variable for much better readability. - $this->child = $child; - - parent::__construct($query, $child); + parent::__construct($query, $parent); } - /** @inheritDoc */ + /** + * Get the results of the relationship. + * + * @return mixed + */ public function getResults() { - if (is_null($this->getForeignKeyFrom($this->child))) { - return $this->getDefaultFor($this->parent); - } - - return $this->query->first() ?: $this->getDefaultFor($this->parent); + return $this->query->first(); } /** @@ -95,29 +70,84 @@ public function addConstraints() // For belongs to relationships, which are essentially the inverse of has one // or has many relationships, we need to actually query on the primary key // of the related models matching on the foreign key that's on a parent. - $key = $this->getQualifiedOwnerKeyName(); + $table = $this->related->getTable(); - $this->query->where($key, '=', $this->getForeignKeyFrom($this->child)); + $this->query->where($table.'.'.$this->otherKey, '=', $this->parent->{$this->foreignKey}); } } - /** @inheritDoc */ + /** + * Add the constraints for a relationship query. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parent + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder + */ + public function getRelationQuery(Builder $query, Builder $parent, $columns = ['*']) + { + if ($parent->getQuery()->from == $query->getQuery()->from) { + return $this->getRelationQueryForSelfRelation($query, $parent, $columns); + } + + $query->select($columns); + + $otherKey = $this->wrap($query->getModel()->getTable().'.'.$this->otherKey); + + return $query->where($this->getQualifiedForeignKey(), '=', new Expression($otherKey)); + } + + /** + * Add the constraints for a relationship query on the same table. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parent + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder + */ + public function getRelationQueryForSelfRelation(Builder $query, Builder $parent, $columns = ['*']) + { + $query->select($columns); + + $query->from($query->getModel()->getTable().' as '.$hash = $this->getRelationCountHash()); + + $query->getModel()->setTable($hash); + + $key = $this->wrap($this->getQualifiedForeignKey()); + + return $query->where($hash.'.'.$query->getModel()->getKeyName(), '=', new Expression($key)); + } + + /** + * Get a relationship join table hash. + * + * @return string + */ + public function getRelationCountHash() + { + return 'self_'.md5(microtime(true)); + } + + /** + * Set the constraints for an eager load of the relation. + * + * @param array $models + * @return void + */ public function addEagerConstraints(array $models) { // We'll grab the primary key name of the related models since it could be set to // a non-standard name and not "id". We will then construct the constraint for // our eagerly loading query so it returns the proper models from execution. - $key = $this->getQualifiedOwnerKeyName(); - - $whereIn = $this->whereInMethod($this->related, $this->ownerKey); + $key = $this->related->getTable().'.'.$this->otherKey; - $this->whereInEager($whereIn, $key, $this->getEagerModelKeys($models)); + $this->query->whereIn($key, $this->getEagerModelKeys($models)); } /** * Gather the keys from an array of related models. * - * @param array $models + * @param array $models * @return array */ protected function getEagerModelKeys(array $models) @@ -128,48 +158,66 @@ protected function getEagerModelKeys(array $models) // to query for via the eager loading query. We will add them to an array then // execute a "where in" statement to gather up all of those related records. foreach ($models as $model) { - if (! is_null($value = $this->getForeignKeyFrom($model))) { + if (! is_null($value = $model->{$this->foreignKey})) { $keys[] = $value; } } - sort($keys); + // If there are no keys that were not null we will just return an array with either + // null or 0 in (depending on if incrementing keys are in use) so the query wont + // fail plus returns zero results, which should be what the developer expects. + if (count($keys) === 0) { + return [$this->related->incrementing ? 0 : null]; + } return array_values(array_unique($keys)); } - /** @inheritDoc */ + /** + * 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->getDefaultFor($model)); + $model->setRelation($relation, null); } return $models; } - /** @inheritDoc */ - public function match(array $models, EloquentCollection $results, $relation) + /** + * 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) { + $foreign = $this->foreignKey; + + $other = $this->otherKey; + // First we will get to build a dictionary of the child models by their primary // key of the relationship, then we can easily match the children back onto // the parents using that dictionary and the primary key of the children. $dictionary = []; foreach ($results as $result) { - $attribute = $this->getDictionaryKey($this->getRelatedKeyFrom($result)); - - $dictionary[$attribute] = $result; + $dictionary[$result->getAttribute($other)] = $result; } // Once we have the dictionary constructed, we can loop through all the parents // and match back onto their children using these keys of the dictionary and // the primary key of the children to map them onto the correct instances. foreach ($models as $model) { - $attribute = $this->getDictionaryKey($this->getForeignKeyFrom($model)); - - if (isset($dictionary[$attribute ?? ''])) { - $model->setRelation($relation, $dictionary[$attribute ?? '']); + if (isset($dictionary[$model->$foreign])) { + $model->setRelation($relation, $dictionary[$model->$foreign]); } } @@ -179,121 +227,45 @@ public function match(array $models, EloquentCollection $results, $relation) /** * Associate the model instance to the given parent. * - * @param TRelatedModel|int|string|null $model - * @return TDeclaringModel + * @param \Illuminate\Database\Eloquent\Model|int $model + * @return \Illuminate\Database\Eloquent\Model */ public function associate($model) { - $ownerKey = $model instanceof Model ? $model->getAttribute($this->ownerKey) : $model; + $otherKey = ($model instanceof Model ? $model->getAttribute($this->otherKey) : $model); - $this->child->setAttribute($this->foreignKey, $ownerKey); + $this->parent->setAttribute($this->foreignKey, $otherKey); if ($model instanceof Model) { - $this->child->setRelation($this->relationName, $model); - } else { - $this->child->unsetRelation($this->relationName); + $this->parent->setRelation($this->relation, $model); } - return $this->child; + return $this->parent; } /** * Dissociate previously associated model from the given parent. * - * @return TDeclaringModel + * @return \Illuminate\Database\Eloquent\Model */ public function dissociate() { - $this->child->setAttribute($this->foreignKey, null); - - return $this->child->setRelation($this->relationName, null); - } - - /** - * Alias of "dissociate" method. - * - * @return TDeclaringModel - */ - public function disassociate() - { - return $this->dissociate(); - } + $this->parent->setAttribute($this->foreignKey, null); - /** - * Touch all of the related models for the relationship. - * - * @return void - */ - public function touch() - { - if (! is_null($this->getParentKey())) { - parent::touch(); - } - } - - /** @inheritDoc */ - public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) - { - if ($parentQuery->getQuery()->from == $query->getQuery()->from) { - return $this->getRelationExistenceQueryForSelfRelation($query, $parentQuery, $columns); - } - - return $query->select($columns)->whereColumn( - $this->getQualifiedForeignKeyName(), '=', $query->qualifyColumn($this->ownerKey) - ); + return $this->parent->setRelation($this->relation, null); } /** - * Add the constraints for a relationship query on the same table. + * Update the parent model on the relationship. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param \Illuminate\Database\Eloquent\Builder $parentQuery - * @param mixed $columns - * @return \Illuminate\Database\Eloquent\Builder - */ - public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*']) - { - $query->select($columns)->from( - $query->getModel()->getTable().' as '.$hash = $this->getRelationCountHash() - ); - - $query->getModel()->setTable($hash); - - return $query->whereColumn( - $hash.'.'.$this->ownerKey, '=', $this->getQualifiedForeignKeyName() - ); - } - - /** - * Determine if the related model has an auto-incrementing ID. - * - * @return bool - */ - protected function relationHasIncrementingId() - { - return $this->related->getIncrementing() && - in_array($this->related->getKeyType(), ['int', 'integer']); - } - - /** - * Make a new related instance for the given model. - * - * @param TDeclaringModel $parent - * @return TRelatedModel + * @param array $attributes + * @return mixed */ - protected function newRelatedInstanceFor(Model $parent) + public function update(array $attributes) { - return $this->related->newInstance(); - } + $instance = $this->getResults(); - /** - * Get the child of the relationship. - * - * @return TDeclaringModel - */ - public function getChild() - { - return $this->child; + return $instance->fill($attributes)->save(); } /** @@ -301,29 +273,19 @@ public function getChild() * * @return string */ - public function getForeignKeyName() + public function getForeignKey() { return $this->foreignKey; } /** - * Get the fully-qualified foreign key of the relationship. + * Get the fully qualified foreign key of the relationship. * * @return string */ - public function getQualifiedForeignKeyName() + public function getQualifiedForeignKey() { - return $this->child->qualifyColumn($this->foreignKey); - } - - /** - * Get the key value of the child's foreign key. - * - * @return mixed - */ - public function getParentKey() - { - return $this->getForeignKeyFrom($this->child); + return $this->parent->getTable().'.'.$this->foreignKey; } /** @@ -331,52 +293,18 @@ public function getParentKey() * * @return string */ - public function getOwnerKeyName() + public function getOtherKey() { - return $this->ownerKey; - } - - /** - * Get the fully-qualified associated key of the relationship. - * - * @return string - */ - public function getQualifiedOwnerKeyName() - { - return $this->related->qualifyColumn($this->ownerKey); - } - - /** - * Get the value of the model's foreign key. - * - * @param TRelatedModel $model - * @return int|string - */ - protected function getRelatedKeyFrom(Model $model) - { - return $model->{$this->ownerKey}; - } - - /** - * Get the value of the model's foreign key. - * - * @param TDeclaringModel $model - * @return mixed - */ - protected function getForeignKeyFrom(Model $model) - { - $foreignKey = $model->{$this->foreignKey}; - - return enum_value($foreignKey); + return $this->otherKey; } /** - * Get the name of the relationship. + * Get the fully qualified associated key of the relationship. * * @return string */ - public function getRelationName() + public function getQualifiedOtherKeyName() { - return $this->relationName; + return $this->related->getTable().'.'.$this->otherKey; } } diff --git a/Eloquent/Relations/BelongsToMany.php b/Eloquent/Relations/BelongsToMany.php index 2a7e011ddf..194302b949 100755 --- a/Eloquent/Relations/BelongsToMany.php +++ b/Eloquent/Relations/BelongsToMany.php @@ -2,36 +2,15 @@ namespace Illuminate\Database\Eloquent\Relations; -use Closure; -use Illuminate\Contracts\Support\Arrayable; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; -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\Arr; -use Illuminate\Support\Collection as BaseCollection; use Illuminate\Support\Str; -use InvalidArgumentException; - -/** - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * @template TPivotModel of \Illuminate\Database\Eloquent\Relations\Pivot = \Illuminate\Database\Eloquent\Relations\Pivot - * @template TAccessor of string = 'pivot' - * - * @extends \Illuminate\Database\Eloquent\Relations\Relation> - * - * @todo use TAccessor when PHPStan bug is fixed: https://github.com/phpstan/phpstan/issues/12756 - */ +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Collection; +use Illuminate\Database\Eloquent\ModelNotFoundException; + class BelongsToMany extends Relation { - use InteractsWithDictionary, InteractsWithPivotTable; - /** * The intermediate table for the relation. * @@ -44,28 +23,14 @@ class BelongsToMany extends Relation * * @var string */ - protected $foreignPivotKey; + protected $foreignKey; /** * 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; + protected $otherKey; /** * The "name" of the relationship. @@ -77,640 +42,571 @@ class BelongsToMany extends Relation /** * The pivot table columns to retrieve. * - * @var array + * @var array */ protected $pivotColumns = []; /** - * Any pivot table restrictions for where clauses. + * Any pivot table restrictions. * * @var array */ protected $pivotWheres = []; /** - * Any pivot table restrictions for whereIn clauses. + * The custom pivot table column for the created_at timestamp. * - * @var array + * @var string */ - protected $pivotWhereIns = []; + protected $pivotCreatedAt; /** - * Any pivot table restrictions for whereNull clauses. + * The custom pivot table column for the updated_at timestamp. * - * @var array + * @var string */ - protected $pivotWhereNulls = []; + protected $pivotUpdatedAt; /** - * The default values for the pivot columns. + * Create a new belongs to many relationship instance. * - * @var array + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Model $parent + * @param string $table + * @param string $foreignKey + * @param string $otherKey + * @param string $relationName + * @return void */ - protected $pivotValues = []; + public function __construct(Builder $query, Model $parent, $table, $foreignKey, $otherKey, $relationName = null) + { + $this->table = $table; + $this->otherKey = $otherKey; + $this->foreignKey = $foreignKey; + $this->relationName = $relationName; + + parent::__construct($query, $parent); + } /** - * Indicates if timestamps are available on the pivot table. + * Get the results of the relationship. * - * @var bool + * @return mixed */ - public $withTimestamps = false; + public function getResults() + { + return $this->get(); + } /** - * The custom pivot table column for the created_at timestamp. + * Set a where clause for a pivot table column. * - * @var string|null + * @param string $column + * @param string $operator + * @param mixed $value + * @param string $boolean + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ - protected $pivotCreatedAt; + public function wherePivot($column, $operator = null, $value = null, $boolean = 'and') + { + $this->pivotWheres[] = func_get_args(); + + return $this->where($this->table.'.'.$column, $operator, $value, $boolean); + } /** - * The custom pivot table column for the updated_at timestamp. + * Set a "where in" clause for a pivot table column. * - * @var string|null + * @param string $column + * @param mixed $values + * @param string $boolean + * @param bool $not + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ - protected $pivotUpdatedAt; + public function wherePivotIn($column, $values, $boolean = 'and', $not = false) + { + $this->pivotWheres[] = func_get_args(); + + return $this->whereIn($this->table.'.'.$column, $values, $boolean, $not); + } /** - * The class name of the custom pivot model to use for the relationship. + * Set an "or where" clause for a pivot table column. * - * @var class-string + * @param string $column + * @param string $operator + * @param mixed $value + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ - protected $using; + public function orWherePivot($column, $operator = null, $value = null) + { + return $this->wherePivot($column, $operator, $value, 'or'); + } /** - * The name of the accessor to use for the "pivot" relationship. + * Set an "or where in" clause for a pivot table column. * - * @var TAccessor + * @param string $column + * @param mixed $values + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ - protected $accessor = 'pivot'; + public function orWherePivotIn($column, $values) + { + return $this->wherePivotIn($column, $values, 'or'); + } /** - * Create a new belongs to many relationship instance. + * Execute the query and get the first result. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param TDeclaringModel $parent - * @param string|class-string $table - * @param string $foreignPivotKey - * @param string $relatedPivotKey - * @param string $parentKey - * @param string $relatedKey - * @param string|null $relationName - */ - 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); + * @param array $columns + * @return mixed + */ + public function first($columns = ['*']) + { + $results = $this->take(1)->get($columns); - parent::__construct($query, $parent); + return count($results) > 0 ? $results->first() : null; } /** - * Attempt to resolve the intermediate table name from the given string. + * Execute the query and get the first result or throw an exception. * - * @param string $table - * @return string + * @param array $columns + * @return \Illuminate\Database\Eloquent\Model|static + * + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException */ - protected function resolveTableName($table) + public function firstOrFail($columns = ['*']) { - 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); + if (! is_null($model = $this->first($columns))) { + return $model; } - return $model->getTable(); + throw new ModelNotFoundException; } /** - * Set the base constraints on the relation query. + * Execute the query as a "select" statement. * - * @return void + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection */ - public function addConstraints() + public function get($columns = ['*']) { - $this->performJoin(); + // 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 out pivot + // models with the result of those columns as a separate model relation. + $columns = $this->query->getQuery()->columns ? [] : $columns; - if (static::$constraints) { - $this->addWhereConstraints(); + $select = $this->getSelectColumns($columns); + + $builder = $this->query->applyScopes(); + + $models = $builder->addSelect($select)->getModels(); + + $this->hydratePivotRelation($models); + + // If we actually found models we will also eager load any relationships that + // have been specified as needing to be eager loaded. This will solve the + // n + 1 query problem for the developer and also increase performance. + if (count($models) > 0) { + $models = $builder->eagerLoadRelations($models); } + + return $this->related->newCollection($models); } /** - * Set the join clause for the relation query. + * Get a paginator for the "select" statement. * - * @param \Illuminate\Database\Eloquent\Builder|null $query - * @return $this + * @param int $perPage + * @param array $columns + * @param string $pageName + * @param int|null $page + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator */ - protected function performJoin($query = null) + public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) { - $query = $query ?: $this->query; + $this->query->addSelect($this->getSelectColumns($columns)); - // 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() - ); + $paginator = $this->query->paginate($perPage, $columns, $pageName, $page); - return $this; + $this->hydratePivotRelation($paginator->items()); + + return $paginator; } /** - * Set the where clause for the relation query. + * Paginate the given query into a simple paginator. * - * @return $this + * @param int $perPage + * @param array $columns + * @param string $pageName + * @return \Illuminate\Contracts\Pagination\Paginator */ - protected function addWhereConstraints() + public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page') { - $this->query->where( - $this->getQualifiedForeignPivotKeyName(), '=', $this->parent->{$this->parentKey} - ); + $this->query->addSelect($this->getSelectColumns($columns)); - return $this; - } + $paginator = $this->query->simplePaginate($perPage, $columns, $pageName); - /** @inheritDoc */ - public function addEagerConstraints(array $models) - { - $whereIn = $this->whereInMethod($this->parent, $this->parentKey); + $this->hydratePivotRelation($paginator->items()); - $this->whereInEager( - $whereIn, - $this->getQualifiedForeignPivotKeyName(), - $this->getKeys($models, $this->parentKey) - ); + return $paginator; } - /** @inheritDoc */ - public function initRelation(array $models, $relation) + /** + * Chunk the results of the query. + * + * @param int $count + * @param callable $callback + * @return bool + */ + public function chunk($count, callable $callback) { - foreach ($models as $model) { - $model->setRelation($relation, $this->related->newCollection()); - } + $this->query->addSelect($this->getSelectColumns()); - return $models; + return $this->query->chunk($count, function ($results) use ($callback) { + $this->hydratePivotRelation($results->all()); + + return $callback($results); + }); } - /** @inheritDoc */ - public function match(array $models, EloquentCollection $results, $relation) + /** + * Hydrate the pivot table relationship on the models. + * + * @param array $models + * @return void + */ + protected function hydratePivotRelation(array $models) { - $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. + // 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 so it they be used. foreach ($models as $model) { - $key = $this->getDictionaryKey($model->{$this->parentKey}); + $pivot = $this->newExistingPivot($this->cleanPivotAttributes($model)); - if (isset($dictionary[$key])) { - $model->setRelation( - $relation, $this->related->newCollection($dictionary[$key]) - ); - } + $model->setRelation('pivot', $pivot); } - - return $models; } /** - * Build model dictionary keyed by the relation's foreign key. + * Get the pivot attributes from a model. * - * @param \Illuminate\Database\Eloquent\Collection $results - * @return array> + * @param \Illuminate\Database\Eloquent\Model $model + * @return array */ - protected function buildDictionary(EloquentCollection $results) + protected function cleanPivotAttributes(Model $model) { - // 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 = []; - - $isAssociative = Arr::isAssoc($results->all()); + $values = []; - foreach ($results as $key => $result) { - $value = $this->getDictionaryKey($result->{$this->accessor}->{$this->foreignPivotKey}); + 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 (strpos($key, 'pivot_') === 0) { + $values[substr($key, 6)] = $value; - if ($isAssociative) { - $dictionary[$value][$key] = $result; - } else { - $dictionary[$value][] = $result; + unset($model->$key); } } - return $dictionary; + return $values; } /** - * Get the class being used for pivot models. + * Set the base constraints on the relation query. * - * @return class-string + * @return void */ - public function getPivotClass() + public function addConstraints() { - return $this->using ?? Pivot::class; + $this->setJoin(); + + if (static::$constraints) { + $this->setWhere(); + } } /** - * Specify the custom pivot model to use for the relationship. + * Add the constraints for a relationship query. * - * @template TNewPivotModel of \Illuminate\Database\Eloquent\Relations\Pivot - * - * @param class-string $class - * @return $this - * - * @phpstan-this-out static + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parent + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder */ - public function using($class) + public function getRelationQuery(Builder $query, Builder $parent, $columns = ['*']) { - $this->using = $class; + if ($parent->getQuery()->from == $query->getQuery()->from) { + return $this->getRelationQueryForSelfJoin($query, $parent, $columns); + } - return $this; + $this->setJoin($query); + + return parent::getRelationQuery($query, $parent, $columns); } /** - * Specify the custom pivot accessor to use for the relationship. - * - * @template TNewAccessor of string - * - * @param TNewAccessor $accessor - * @return $this + * Add the constraints for a relationship query on the same table. * - * @phpstan-this-out static + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parent + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder */ - public function as($accessor) + public function getRelationQueryForSelfJoin(Builder $query, Builder $parent, $columns = ['*']) { - $this->accessor = $accessor; + $query->select($columns); - return $this; + $query->from($this->related->getTable().' as '.$hash = $this->getRelationCountHash()); + + $this->related->setTable($hash); + + $this->setJoin($query); + + return parent::getRelationQuery($query, $parent, $columns); } /** - * Set a where clause for a pivot table column. + * Get a relationship join table hash. * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @param string $boolean - * @return $this + * @return string */ - public function wherePivot($column, $operator = null, $value = null, $boolean = 'and') + public function getRelationCountHash() { - $this->pivotWheres[] = func_get_args(); - - return $this->where($this->qualifyPivotColumn($column), $operator, $value, $boolean); + return 'self_'.md5(microtime(true)); } /** - * Set a "where between" clause for a pivot table column. + * Set the select clause for the relation query. * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param array $values - * @param string $boolean - * @param bool $not - * @return $this + * @param array $columns + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ - public function wherePivotBetween($column, array $values, $boolean = 'and', $not = false) + protected function getSelectColumns(array $columns = ['*']) { - return $this->whereBetween($this->qualifyPivotColumn($column), $values, $boolean, $not); + if ($columns == ['*']) { + $columns = [$this->related->getTable().'.*']; + } + + return array_merge($columns, $this->getAliasedPivotColumns()); } /** - * Set a "or where between" clause for a pivot table column. + * Get the pivot columns for the relation. * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param array $values - * @return $this + * @return array */ - public function orWherePivotBetween($column, array $values) + protected function getAliasedPivotColumns() { - return $this->wherePivotBetween($column, $values, 'or'); + $defaults = [$this->foreignKey, $this->otherKey]; + + // We need to alias all of the pivot columns with the "pivot_" prefix so we + // can easily extract them out of the models and put them into the pivot + // relationships when they are retrieved and hydrated into the models. + $columns = []; + + foreach (array_merge($defaults, $this->pivotColumns) as $column) { + $columns[] = $this->table.'.'.$column.' as pivot_'.$column; + } + + return array_unique($columns); } /** - * Set a "where pivot not between" clause for a pivot table column. + * Determine whether the given column is defined as a pivot column. * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param array $values - * @param string $boolean - * @return $this + * @param string $column + * @return bool */ - public function wherePivotNotBetween($column, array $values, $boolean = 'and') + protected function hasPivotColumn($column) { - return $this->wherePivotBetween($column, $values, $boolean, true); + return in_array($column, $this->pivotColumns); } /** - * Set a "or where not between" clause for a pivot table column. + * Set the join clause for the relation query. * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param array $values + * @param \Illuminate\Database\Eloquent\Builder|null $query * @return $this */ - public function orWherePivotNotBetween($column, array $values) + protected function setJoin($query = null) { - return $this->wherePivotBetween($column, $values, 'or', true); + $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. + $baseTable = $this->related->getTable(); + + $key = $baseTable.'.'.$this->related->getKeyName(); + + $query->join($this->table, $key, '=', $this->getOtherKey()); + + return $this; } /** - * Set a "where in" clause for a pivot table column. + * Set the where clause for the relation query. * - * @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) + protected function setWhere() { - $this->pivotWhereIns[] = func_get_args(); + $foreign = $this->getForeignKey(); - return $this->whereIn($this->qualifyPivotColumn($column), $values, $boolean, $not); + $this->query->where($foreign, '=', $this->parent->getKey()); + + return $this; } /** - * Set an "or where" clause for a pivot table column. + * Set the constraints for an eager load of the relation. * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @return $this + * @param array $models + * @return void */ - public function orWherePivot($column, $operator = null, $value = null) + public function addEagerConstraints(array $models) { - return $this->wherePivot($column, $operator, $value, 'or'); + $this->query->whereIn($this->getForeignKey(), $this->getKeys($models)); } /** - * 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 $column - * @param mixed $value - * @return $this + * Initialize the relation on a set of models. * - * @throws \InvalidArgumentException + * @param array $models + * @param string $relation + * @return array */ - public function withPivotValue($column, $value = null) + public function initRelation(array $models, $relation) { - 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.'); + foreach ($models as $model) { + $model->setRelation($relation, $this->related->newCollection()); } - $this->pivotValues[] = compact('column', 'value'); - - return $this->wherePivot($column, '=', $value); + return $models; } /** - * Set an "or where in" clause for a pivot table column. + * Match the eagerly loaded results to their parents. * - * @param string $column - * @param mixed $values - * @return $this + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results + * @param string $relation + * @return array */ - public function orWherePivotIn($column, $values) + public function match(array $models, Collection $results, $relation) { - return $this->wherePivotIn($column, $values, 'or'); - } + $dictionary = $this->buildDictionary($results); - /** - * 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); - } + // 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 + // the parent models. Then we will return the hydrated models back out. + foreach ($models as $model) { + if (isset($dictionary[$key = $model->getKey()])) { + $collection = $this->related->newCollection($dictionary[$key]); - /** - * 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'); + $model->setRelation($relation, $collection); + } + } + + return $models; } /** - * Set a "where null" clause for a pivot table column. + * Build model dictionary keyed by the relation's foreign key. * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param string $boolean - * @param bool $not - * @return $this + * @param \Illuminate\Database\Eloquent\Collection $results + * @return array */ - public function wherePivotNull($column, $boolean = 'and', $not = false) + protected function buildDictionary(Collection $results) { - $this->pivotWhereNulls[] = func_get_args(); + $foreign = $this->foreignKey; - return $this->whereNull($this->qualifyPivotColumn($column), $boolean, $not); - } + // First we will build a dictionary of child models keyed by the foreign key + // of the relation so that we will easily and quickly match them to their + // parents without having a possibly slow inner loops for every models. + $dictionary = []; - /** - * 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); - } + foreach ($results as $result) { + $dictionary[$result->pivot->$foreign][] = $result; + } - /** - * 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); + return $dictionary; } /** - * Set a "or where not null" clause for a pivot table column. + * Touch all of the related models for the relationship. * - * @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. + * E.g.: Touch all roles associated with this user. * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param string $direction - * @return $this + * @return void */ - public function orderByPivot($column, $direction = 'asc') + public function touch() { - return $this->orderBy($this->qualifyPivotColumn($column), $direction); - } + $key = $this->getRelated()->getKeyName(); - /** - * Add an "order by desc" clause for a pivot table column. - * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @return $this - */ - public function orderByPivotDesc($column) - { - return $this->orderBy($this->qualifyPivotColumn($column), 'desc'); - } + $columns = $this->getRelatedFreshUpdate(); - /** - * Find a related model by its primary key or return a new instance of the related model. - * - * @param mixed $id - * @param array $columns - * @return ( - * $id is (\Illuminate\Contracts\Support\Arrayable|array) - * ? \Illuminate\Database\Eloquent\Collection - * : TRelatedModel&object{pivot: TPivotModel} - * ) - */ - public function findOrNew($id, $columns = ['*']) - { - if (is_null($instance = $this->find($id, $columns))) { - $instance = $this->related->newInstance(); - } + // 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. + $ids = $this->getRelatedIds(); - return $instance; + if (count($ids) > 0) { + $this->getRelated()->newQuery()->whereIn($key, $ids)->update($columns); + } } /** - * Get the first related model record matching the attributes or instantiate it. + * Get all of the IDs for the related models. * - * @param array $attributes - * @param array $values - * @return TRelatedModel&object{pivot: TPivotModel} + * @return \Illuminate\Support\Collection */ - public function firstOrNew(array $attributes = [], array $values = []) + public function getRelatedIds() { - if (is_null($instance = $this->related->where($attributes)->first())) { - $instance = $this->related->newInstance(array_merge($attributes, $values)); - } + $related = $this->getRelated(); - return $instance; - } + $fullKey = $related->getQualifiedKeyName(); - /** - * Get the first record matching the attributes. If the record is not found, create it. - * - * @param array $attributes - * @param (\Closure(): array)|array $values - * @param array $joining - * @param bool $touch - * @return TRelatedModel&object{pivot: TPivotModel} - */ - public function firstOrCreate(array $attributes = [], Closure|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; + return $this->getQuery()->select($fullKey)->pluck($related->getKeyName()); } /** - * Attempt to create the record. If a unique constraint violation occurs, attempt to find the matching record. + * Save a new model and attach it to the parent model. * - * @param array $attributes - * @param (\Closure(): array)|array $values + * @param \Illuminate\Database\Eloquent\Model $model * @param array $joining - * @param bool $touch - * @return TRelatedModel&object{pivot: TPivotModel} - * - * @throws \Illuminate\Database\UniqueConstraintViolationException + * @param bool $touch + * @return \Illuminate\Database\Eloquent\Model */ - public function createOrFirst(array $attributes = [], Closure|array $values = [], array $joining = [], $touch = true) + public function save(Model $model, array $joining = [], $touch = true) { - try { - return $this->getQuery()->withSavepointIfNeeded(fn () => $this->create(array_merge($attributes, value($values)), $joining, $touch)); - } catch (UniqueConstraintViolationException $e) { - // ... - } + $model->save(['touch' => false]); - 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; - } + $this->attach($model->getKey(), $joining, $touch); + + return $model; } /** - * Create or update a related record matching the attributes, and fill it with values. + * Save an array of new models and attach them to the parent model. * - * @param array $attributes - * @param array $values - * @param array $joining - * @param bool $touch - * @return TRelatedModel&object{pivot: TPivotModel} + * @param \Illuminate\Support\Collection|array $models + * @param array $joinings + * @return array */ - public function updateOrCreate(array $attributes, array $values = [], array $joining = [], $touch = true) + public function saveMany($models, array $joinings = []) { - return tap($this->firstOrCreate($attributes, $values, $joining, $touch), function ($instance) use ($values) { - if (! $instance->wasRecentlyCreated) { - $instance->fill($values); + foreach ($models as $key => $model) { + $this->save($model, (array) Arr::get($joinings, $key), false); + } - $instance->save(['touch' => false]); - } - }); + $this->touchIfTouching(); + + return $models; } /** @@ -718,58 +614,35 @@ public function updateOrCreate(array $attributes, array $values = [], array $joi * * @param mixed $id * @param array $columns - * @return ( - * $id is (\Illuminate\Contracts\Support\Arrayable|array) - * ? \Illuminate\Database\Eloquent\Collection - * : (TRelatedModel&object{pivot: TPivotModel})|null - * ) + * @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)) { + if (is_array($id)) { return $this->findMany($id, $columns); } - return $this->where( - $this->getRelated()->getQualifiedKeyName(), '=', $this->parseId($id) - )->first($columns); - } + $this->where($this->getRelated()->getQualifiedKeyName(), '=', $id); - /** - * Find a sole related model by its primary key. - * - * @param mixed $id - * @param array $columns - * @return TRelatedModel&object{pivot: TPivotModel} - * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - * @throws \Illuminate\Database\MultipleRecordsFoundException - */ - public function findSole($id, $columns = ['*']) - { - return $this->where( - $this->getRelated()->getQualifiedKeyName(), '=', $this->parseId($id) - )->sole($columns); + return $this->first($columns); } /** * Find multiple related models by their primary keys. * - * @param \Illuminate\Contracts\Support\Arrayable|array $ids + * @param mixed $ids * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection + * @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); + $this->whereIn($this->getRelated()->getQualifiedKeyName(), $ids); + + return $this->get($columns); } /** @@ -777,876 +650,622 @@ public function findMany($ids, $columns = ['*']) * * @param mixed $id * @param array $columns - * @return ( - * $id is (\Illuminate\Contracts\Support\Arrayable|array) - * ? \Illuminate\Database\Eloquent\Collection - * : TRelatedModel&object{pivot: TPivotModel} - * ) + * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException */ 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))) { + if (count($result) == count(array_unique($id))) { return $result; } } elseif (! is_null($result)) { return $result; } - throw (new ModelNotFoundException)->setModel(get_class($this->related), $id); + throw (new ModelNotFoundException)->setModel(get_class($this->parent)); } /** - * Find a related model by its primary key or call a callback. - * - * @template TValue + * Find a related model by its primary key or return new instance of the related model. * * @param mixed $id - * @param (\Closure(): TValue)|list|string $columns - * @param (\Closure(): TValue)|null $callback - * @return ( - * $id is (\Illuminate\Contracts\Support\Arrayable|array) - * ? \Illuminate\Database\Eloquent\Collection|TValue - * : (TRelatedModel&object{pivot: TPivotModel})|TValue - * ) - */ - 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 (TRelatedModel&object{pivot: TPivotModel})|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 (TRelatedModel&object{pivot: TPivotModel})|null + * @return \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model */ - public function first($columns = ['*']) - { - $results = $this->limit(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 TRelatedModel&object{pivot: TPivotModel} - * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - */ - public function firstOrFail($columns = ['*']) + public function findOrNew($id, $columns = ['*']) { - if (! is_null($model = $this->first($columns))) { - return $model; + if (is_null($instance = $this->find($id, $columns))) { + $instance = $this->getRelated()->newInstance(); } - throw (new ModelNotFoundException)->setModel(get_class($this->related)); + return $instance; } /** - * Execute the query and get the first result or call a callback. - * - * @template TValue + * Get the first related model record matching the attributes or instantiate it. * - * @param (\Closure(): TValue)|list $columns - * @param (\Closure(): TValue)|null $callback - * @return (TRelatedModel&object{pivot: TPivotModel})|TValue + * @param array $attributes + * @return \Illuminate\Database\Eloquent\Model */ - public function firstOr($columns = ['*'], ?Closure $callback = null) + public function firstOrNew(array $attributes) { - if ($columns instanceof Closure) { - $callback = $columns; - - $columns = ['*']; + if (is_null($instance = $this->where($attributes)->first())) { + $instance = $this->related->newInstance($attributes); } - if (! is_null($model = $this->first($columns))) { - return $model; - } - - return $callback(); - } - - /** @inheritDoc */ - public function getResults() - { - return ! is_null($this->parent->{$this->parentKey}) - ? $this->get() - : $this->related->newCollection(); - } - - /** @inheritDoc */ - 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) - ); + return $instance; } /** - * Get the select columns for the relation query. + * Get the first related record matching the attributes or create it. * - * @param array $columns - * @return array + * @param array $attributes + * @param array $joining + * @param bool $touch + * @return \Illuminate\Database\Eloquent\Model */ - protected function shouldSelect(array $columns = ['*']) + public function firstOrCreate(array $attributes, array $joining = [], $touch = true) { - if ($columns == ['*']) { - $columns = [$this->related->qualifyColumn('*')]; + if (is_null($instance = $this->where($attributes)->first())) { + $instance = $this->create($attributes, $joining, $touch); } - 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() - { - return (new BaseCollection([ - $this->foreignPivotKey, - $this->relatedPivotKey, - ...$this->pivotColumns, - ])) - ->map(fn ($column) => $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\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; - }); + return $instance; } /** - * Query lazily, by chunking the results of a query by comparing IDs. + * Create or update a related record matching the attributes, and fill it with values. * - * @param int $chunkSize - * @param string|null $column - * @param string|null $alias - * @return \Illuminate\Support\LazyCollection + * @param array $attributes + * @param array $values + * @param array $joining + * @param bool $touch + * @return \Illuminate\Database\Eloquent\Model */ - public function lazyById($chunkSize = 1000, $column = null, $alias = null) + public function updateOrCreate(array $attributes, array $values = [], array $joining = [], $touch = true) { - $column ??= $this->getRelated()->qualifyColumn( - $this->getRelatedKeyName() - ); + if (is_null($instance = $this->where($attributes)->first())) { + return $this->create($values, $joining, $touch); + } - $alias ??= $this->getRelatedKeyName(); + $instance->fill($values); - return $this->prepareQueryBuilder()->lazyById($chunkSize, $column, $alias)->map(function ($model) { - $this->hydratePivotRelation([$model]); + $instance->save(['touch' => false]); - return $model; - }); + return $instance; } /** - * Query lazily, by chunking the results of a query by comparing IDs in descending order. + * Create a new instance of the related model. * - * @param int $chunkSize - * @param string|null $column - * @param string|null $alias - * @return \Illuminate\Support\LazyCollection + * @param array $attributes + * @param array $joining + * @param bool $touch + * @return \Illuminate\Database\Eloquent\Model */ - public function lazyByIdDesc($chunkSize = 1000, $column = null, $alias = null) + public function create(array $attributes, array $joining = [], $touch = true) { - $column ??= $this->getRelated()->qualifyColumn( - $this->getRelatedKeyName() - ); + $instance = $this->related->newInstance($attributes); - $alias ??= $this->getRelatedKeyName(); + // 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]); - return $this->prepareQueryBuilder()->lazyByIdDesc($chunkSize, $column, $alias)->map(function ($model) { - $this->hydratePivotRelation([$model]); + $this->attach($instance->getKey(), $joining, $touch); - return $model; - }); + return $instance; } /** - * Get a lazy collection for the given query. + * Create an array of new instances of the related models. * - * @return \Illuminate\Support\LazyCollection + * @param array $records + * @param array $joinings + * @return array */ - public function cursor() + public function createMany(array $records, array $joinings = []) { - return $this->prepareQueryBuilder()->cursor()->map(function ($model) { - $this->hydratePivotRelation([$model]); + $instances = []; - return $model; - }); + foreach ($records as $key => $record) { + $instances[] = $this->create($record, (array) Arr::get($joinings, $key), false); + } + + $this->touchIfTouching(); + + return $instances; } /** - * Prepare the query builder for query execution. + * Sync the intermediate tables with a list of IDs or collection of models. * - * @return \Illuminate\Database\Eloquent\Builder + * @param \Illuminate\Database\Eloquent\Collection|array $ids + * @param bool $detaching + * @return array */ - protected function prepareQueryBuilder() + public function sync($ids, $detaching = true) { - return $this->query->addSelect($this->shouldSelect()); + $changes = [ + 'attached' => [], 'detached' => [], 'updated' => [], + ]; + + if ($ids instanceof Collection) { + $ids = $ids->modelKeys(); + } + + // First we need to attach any of the associated models that are not currently + // in this joining table. We'll spin through the given IDs, checking to see + // if they exist in the array of current ones, and if not we will insert. + $current = $this->newPivotQuery()->pluck($this->otherKey)->all(); + + $records = $this->formatSyncList($ids); + + $detach = array_diff($current, array_keys($records)); + + // Next, we will take the differences of the currents and given IDs and detach + // all of the entities that exist in the "current" array but are not in the + // the array of the IDs given to the method which will complete the sync. + if ($detaching && count($detach) > 0) { + $this->detach($detach); + + $changes['detached'] = (array) array_map(function ($v) { + return is_numeric($v) ? (int) $v : (string) $v; + }, $detach); + } + + // Now we are finally ready to attach the new records. Note that we'll disable + // touching until after the entire operation is complete so we don't fire a + // ton of touch operations until we are totally done syncing the records. + $changes = array_merge( + $changes, $this->attachNew($records, $current, false) + ); + + if (count($changes['attached']) || count($changes['updated'])) { + $this->touchIfTouching(); + } + + return $changes; } /** - * Hydrate the pivot table relationship on the models. + * Format the sync list so that it is keyed by ID. * - * @param array $models - * @return void + * @param array $records + * @return array */ - protected function hydratePivotRelation(array $models) + protected function formatSyncList(array $records) { - // 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) - )); + $results = []; + + foreach ($records as $id => $attributes) { + if (! is_array($attributes)) { + list($id, $attributes) = [$attributes, []]; + } + + $results[$id] = $attributes; } + + return $results; } /** - * Get the pivot attributes from a model. + * Attach all of the IDs that aren't in the current array. * - * @param TRelatedModel $model + * @param array $records + * @param array $current + * @param bool $touch * @return array */ - protected function migratePivotAttributes(Model $model) + protected function attachNew(array $records, array $current, $touch = true) { - $values = []; + $changes = ['attached' => [], 'updated' => []]; - 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; + foreach ($records as $id => $attributes) { + // If the ID is not in the list of existing pivot IDs, we will insert a new pivot + // record, otherwise, we will just update this existing record on this joining + // table, so that the developers will easily update these records pain free. + if (! in_array($id, $current)) { + $this->attach($id, $attributes, $touch); - unset($model->$key); + $changes['attached'][] = is_numeric($id) ? (int) $id : (string) $id; + } + + // Now we'll try to update an existing pivot record with the attributes that were + // given to the method. If the model is actually updated we will add it to the + // list of updated pivot records so we return them back out to the consumer. + elseif (count($attributes) > 0 && + $this->updateExistingPivot($id, $attributes, $touch)) { + $changes['updated'][] = is_numeric($id) ? (int) $id : (string) $id; } } - return $values; + return $changes; } /** - * If we're touching the parent model, touch. + * Update an existing pivot record on the table. * - * @return void + * @param mixed $id + * @param array $attributes + * @param bool $touch + * @return int */ - public function touchIfTouching() + public function updateExistingPivot($id, array $attributes, $touch = true) { - if ($this->touchingParent()) { - $this->getParent()->touch(); + if (in_array($this->updatedAt(), $this->pivotColumns)) { + $attributes = $this->setTimestampsOnAttach($attributes, true); } - if ($this->getParent()->touches($this->relationName)) { - $this->touch(); - } - } + $updated = $this->newPivotStatementForId($id)->update($attributes); - /** - * Determine if we should touch the parent on sync. - * - * @return bool - */ - protected function touchingParent() - { - return $this->getRelated()->touches($this->guessInverseRelation()); - } + if ($touch) { + $this->touchIfTouching(); + } - /** - * 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()))); + return $updated; } /** - * Touch all of the related models for the relationship. - * - * E.g.: Touch all roles associated with this user. + * Attach a model to the parent. * + * @param mixed $id + * @param array $attributes + * @param bool $touch * @return void */ - public function touch() + public function attach($id, array $attributes = [], $touch = true) { - if ($this->related->isIgnoringTouch()) { - return; + if ($id instanceof Model) { + $id = $id->getKey(); } - $columns = [ - $this->related->getUpdatedAtColumn() => $this->related->freshTimestampString(), - ]; + if ($id instanceof Collection) { + $id = $id->modelKeys(); + } - // 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); + $query = $this->newPivotStatement(); + + $query->insert($this->createAttachRecords((array) $id, $attributes)); + + if ($touch) { + $this->touchIfTouching(); } } /** - * Get all of the IDs for the related models. + * Create an array of records to insert into the pivot table. * - * @return \Illuminate\Support\Collection + * @param array $ids + * @param array $attributes + * @return array */ - public function allRelatedIds() + protected function createAttachRecords($ids, array $attributes) { - return $this->newPivotQuery()->pluck($this->relatedPivotKey); + $records = []; + + $timed = ($this->hasPivotColumn($this->createdAt()) || + $this->hasPivotColumn($this->updatedAt())); + + // To create the attachment records, we will simply spin through the IDs given + // and create a new record to insert for each ID. Each ID may actually be a + // key in the array, with extra attributes to be placed in other columns. + foreach ($ids as $key => $value) { + $records[] = $this->attacher($key, $value, $attributes, $timed); + } + + return $records; } /** - * Save a new model and attach it to the parent model. + * Create a full attachment record payload. * - * @param TRelatedModel $model - * @param array $pivotAttributes - * @param bool $touch - * @return TRelatedModel&object{pivot: TPivotModel} + * @param int $key + * @param mixed $value + * @param array $attributes + * @param bool $timed + * @return array */ - public function save(Model $model, array $pivotAttributes = [], $touch = true) + protected function attacher($key, $value, $attributes, $timed) { - $model->save(['touch' => false]); + list($id, $extra) = $this->getAttachId($key, $value, $attributes); - $this->attach($model, $pivotAttributes, $touch); + // To create the attachment records, we will simply spin through the IDs given + // and create a new record to insert for each ID. Each ID may actually be a + // key in the array, with extra attributes to be placed in other columns. + $record = $this->createAttachRecord($id, $timed); - return $model; + return array_merge($record, $extra); } /** - * Save a new model without raising any events and attach it to the parent model. + * Get the attach record ID and extra attributes. * - * @param TRelatedModel $model - * @param array $pivotAttributes - * @param bool $touch - * @return TRelatedModel&object{pivot: TPivotModel} + * @param mixed $key + * @param mixed $value + * @param array $attributes + * @return array */ - public function saveQuietly(Model $model, array $pivotAttributes = [], $touch = true) + protected function getAttachId($key, $value, array $attributes) { - return Model::withoutEvents(function () use ($model, $pivotAttributes, $touch) { - return $this->save($model, $pivotAttributes, $touch); - }); + if (is_array($value)) { + return [$key, array_merge($value, $attributes)]; + } + + return [$value, $attributes]; } /** - * Save an array of new models and attach them to the parent model. + * Create a new pivot attachment record. * - * @template TContainer of \Illuminate\Support\Collection|array - * - * @param TContainer $models - * @param array $pivotAttributes - * @return TContainer + * @param int $id + * @param bool $timed + * @return array */ - public function saveMany($models, array $pivotAttributes = []) + protected function createAttachRecord($id, $timed) { - foreach ($models as $key => $model) { - $this->save($model, (array) ($pivotAttributes[$key] ?? []), false); - } + $record[$this->foreignKey] = $this->parent->getKey(); - $this->touchIfTouching(); + $record[$this->otherKey] = $id; - return $models; + // If the record needs to have creation and update timestamps, we will make + // them by calling the parent model's "freshTimestamp" method which will + // provide us with a fresh timestamp in this model's preferred format. + if ($timed) { + $record = $this->setTimestampsOnAttach($record); + } + + return $record; } /** - * Save an array of new models without raising any events and attach them to the parent model. + * Set the creation and update timestamps on an attach record. * - * @template TContainer of \Illuminate\Support\Collection|array - * - * @param TContainer $models - * @param array $pivotAttributes - * @return TContainer + * @param array $record + * @param bool $exists + * @return array */ - public function saveManyQuietly($models, array $pivotAttributes = []) + protected function setTimestampsOnAttach(array $record, $exists = false) { - return Model::withoutEvents(function () use ($models, $pivotAttributes) { - return $this->saveMany($models, $pivotAttributes); - }); + $fresh = $this->parent->freshTimestamp(); + + if (! $exists && $this->hasPivotColumn($this->createdAt())) { + $record[$this->createdAt()] = $fresh; + } + + if ($this->hasPivotColumn($this->updatedAt())) { + $record[$this->updatedAt()] = $fresh; + } + + return $record; } /** - * Create a new instance of the related model. + * Detach models from the relationship. * - * @param array $attributes - * @param array $joining + * @param int|array $ids * @param bool $touch - * @return TRelatedModel&object{pivot: TPivotModel} + * @return int */ - public function create(array $attributes = [], array $joining = [], $touch = true) + public function detach($ids = [], $touch = true) { - $attributes = array_merge($this->getQuery()->pendingAttributes, $attributes); + if ($ids instanceof Model) { + $ids = (array) $ids->getKey(); + } - $instance = $this->related->newInstance($attributes); + $query = $this->newPivotQuery(); - // 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]); + // If associated IDs were passed to the method we will only delete those + // associations, otherwise all of the association ties will be broken. + // We'll return the numbers of affected rows when we do the deletes. + $ids = (array) $ids; + + if (count($ids) > 0) { + $query->whereIn($this->otherKey, (array) $ids); + } - $this->attach($instance, $joining, $touch); + // Once we have all of the conditions set on the statement, we are ready + // to run the delete on the pivot table. Then, if the touch parameter + // is true, we will go ahead and touch all related models to sync. + $results = $query->delete(); - return $instance; + if ($touch) { + $this->touchIfTouching(); + } + + return $results; } /** - * Create an array of new instances of the related models. + * If we're touching the parent model, touch. * - * @param iterable $records - * @param array $joinings - * @return array + * @return void */ - public function createMany(iterable $records, array $joinings = []) + public function touchIfTouching() { - $instances = []; - - foreach ($records as $key => $record) { - $instances[] = $this->create($record, (array) ($joinings[$key] ?? []), false); + if ($this->touchingParent()) { + $this->getParent()->touch(); } - $this->touchIfTouching(); - - return $instances; - } - - /** @inheritDoc */ - public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) - { - if ($parentQuery->getQuery()->from == $query->getQuery()->from) { - return $this->getRelationExistenceQueryForSelfJoin($query, $parentQuery, $columns); + if ($this->getParent()->touches($this->relationName)) { + $this->touch(); } - - $this->performJoin($query); - - return parent::getRelationExistenceQuery($query, $parentQuery, $columns); } /** - * Add the constraints for a relationship query on the same table. + * Determine if we should touch the parent on sync. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param \Illuminate\Database\Eloquent\Builder $parentQuery - * @param mixed $columns - * @return \Illuminate\Database\Eloquent\Builder + * @return bool */ - public function getRelationExistenceQueryForSelfJoin(Builder $query, Builder $parentQuery, $columns = ['*']) + protected function touchingParent() { - $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); + return $this->getRelated()->touches($this->guessInverseRelation()); } /** - * Alias to set the "limit" value of the query. + * Attempt to guess the name of the inverse of the relation. * - * @param int $value - * @return $this + * @return string */ - public function take($value) + protected function guessInverseRelation() { - return $this->limit($value); + return Str::camel(Str::plural(class_basename($this->getParent()))); } /** - * Set the "limit" value of the query. + * Create a new query builder for the pivot table. * - * @param int $value - * @return $this + * @return \Illuminate\Database\Query\Builder */ - public function limit($value) + protected function newPivotQuery() { - if ($this->parent->exists) { - $this->query->limit($value); - } else { - $column = $this->getExistenceCompareKey(); - - $grammar = $this->query->getQuery()->getGrammar(); + $query = $this->newPivotStatement(); - if ($grammar instanceof MySqlGrammar && $grammar->useLegacyGroupLimit($this->query->getQuery())) { - $column = 'pivot_'.last(explode('.', $column)); - } - - $this->query->groupLimit($value, $column); + foreach ($this->pivotWheres as $whereArgs) { + call_user_func_array([$query, 'where'], $whereArgs); } - return $this; + return $query->where($this->foreignKey, $this->parent->getKey()); } /** - * Get the key for comparing against the parent key in "has" query. + * Get a new plain query builder for the pivot table. * - * @return string + * @return \Illuminate\Database\Query\Builder */ - public function getExistenceCompareKey() + public function newPivotStatement() { - return $this->getQualifiedForeignPivotKeyName(); + return $this->query->getQuery()->newQuery()->from($this->table); } /** - * Specify that the pivot table has creation and update timestamps. + * Get a new pivot statement for a given "other" ID. * - * @param string|null|false $createdAt - * @param string|null|false $updatedAt - * @return $this + * @param mixed $id + * @return \Illuminate\Database\Query\Builder */ - public function withTimestamps($createdAt = null, $updatedAt = null) + public function newPivotStatementForId($id) { - $this->pivotCreatedAt = $createdAt !== false ? $createdAt : null; - $this->pivotUpdatedAt = $updatedAt !== false ? $updatedAt : null; - - $pivots = array_filter([ - $createdAt !== false ? $this->createdAt() : null, - $updatedAt !== false ? $this->updatedAt() : null, - ]); - - $this->withTimestamps = ! empty($pivots); - - return $this->withTimestamps ? $this->withPivot($pivots) : $this; + return $this->newPivotQuery()->where($this->otherKey, $id); } /** - * Get the name of the "created at" column. + * Create a new pivot model instance. * - * @return string + * @param array $attributes + * @param bool $exists + * @return \Illuminate\Database\Eloquent\Relations\Pivot */ - public function createdAt() + public function newPivot(array $attributes = [], $exists = false) { - return $this->pivotCreatedAt ?? $this->parent->getCreatedAtColumn() ?? Model::CREATED_AT; + $pivot = $this->related->newPivot($this->parent, $attributes, $this->table, $exists); + + return $pivot->setPivotKeys($this->foreignKey, $this->otherKey); } /** - * Get the name of the "updated at" column. + * Create a new existing pivot model instance. * - * @return string + * @param array $attributes + * @return \Illuminate\Database\Eloquent\Relations\Pivot */ - public function updatedAt() + public function newExistingPivot(array $attributes = []) { - return $this->pivotUpdatedAt ?? $this->parent->getUpdatedAtColumn() ?? Model::UPDATED_AT; + return $this->newPivot($attributes, true); } /** - * Get the foreign key for the relation. + * Set the columns on the pivot table to retrieve. * - * @return string + * @param array|mixed $columns + * @return $this */ - public function getForeignPivotKeyName() + public function withPivot($columns) { - return $this->foreignPivotKey; + $columns = is_array($columns) ? $columns : func_get_args(); + + $this->pivotColumns = array_merge($this->pivotColumns, $columns); + + return $this; } /** - * Get the fully-qualified foreign key for the relation. + * Specify that the pivot table has creation and update timestamps. * - * @return string + * @param mixed $createdAt + * @param mixed $updatedAt + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ - public function getQualifiedForeignPivotKeyName() + public function withTimestamps($createdAt = null, $updatedAt = null) { - return $this->qualifyPivotColumn($this->foreignPivotKey); + $this->pivotCreatedAt = $createdAt; + $this->pivotUpdatedAt = $updatedAt; + + return $this->withPivot($this->createdAt(), $this->updatedAt()); } /** - * Get the "related key" for the relation. + * Get the name of the "created at" column. * * @return string */ - public function getRelatedPivotKeyName() + public function createdAt() { - return $this->relatedPivotKey; + return $this->pivotCreatedAt ?: $this->parent->getCreatedAtColumn(); } /** - * Get the fully-qualified "related key" for the relation. + * Get the name of the "updated at" column. * * @return string */ - public function getQualifiedRelatedPivotKeyName() + public function updatedAt() { - return $this->qualifyPivotColumn($this->relatedPivotKey); + return $this->pivotUpdatedAt ?: $this->parent->getUpdatedAtColumn(); } /** - * Get the parent key for the relationship. + * Get the related model's updated at column name. * * @return string */ - public function getParentKeyName() + public function getRelatedFreshUpdate() { - return $this->parentKey; + return [$this->related->getUpdatedAtColumn() => $this->related->freshTimestamp()]; } /** - * Get the fully-qualified parent key name for the relation. + * Get the key for comparing against the parent key in "has" query. * * @return string */ - public function getQualifiedParentKeyName() + public function getHasCompareKey() { - return $this->parent->qualifyColumn($this->parentKey); + return $this->getForeignKey(); } /** - * Get the related key for the relationship. + * Get the fully qualified foreign key for the relation. * * @return string */ - public function getRelatedKeyName() + public function getForeignKey() { - return $this->relatedKey; + return $this->table.'.'.$this->foreignKey; } /** - * Get the fully-qualified related key name for the relation. + * Get the fully qualified "other key" for the relation. * * @return string */ - public function getQualifiedRelatedKeyName() + public function getOtherKey() { - return $this->related->qualifyColumn($this->relatedKey); + return $this->table.'.'.$this->otherKey; } /** @@ -1668,41 +1287,4 @@ public function getRelationName() { return $this->relationName; } - - /** - * Get the name of the pivot accessor for this relationship. - * - * @return TAccessor - */ - 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; - } } diff --git a/Eloquent/Relations/Concerns/AsPivot.php b/Eloquent/Relations/Concerns/AsPivot.php deleted file mode 100644 index 610592e5a3..0000000000 --- a/Eloquent/Relations/Concerns/AsPivot.php +++ /dev/null @@ -1,356 +0,0 @@ -timestamps = $instance->hasTimestampAttributes($attributes); - - // The pivot model is a "dynamic" model since we will set the tables dynamically - // for the instance. This allows it work for any intermediate tables for the - // many to many relationship that are defined by this developer's classes. - $instance->setConnection($parent->getConnectionName()) - ->setTable($table) - ->forceFill($attributes) - ->syncOriginal(); - - // We store off the parent instance so we will access the timestamp column names - // for the model, since the pivot model timestamps aren't easily configurable - // from the developer's point of view. We can use the parents to get these. - $instance->pivotParent = $parent; - - $instance->exists = $exists; - - return $instance; - } - - /** - * Create a new pivot model from raw values returned from a query. - * - * @param \Illuminate\Database\Eloquent\Model $parent - * @param array $attributes - * @param string $table - * @param bool $exists - * @return static - */ - public static function fromRawAttributes(Model $parent, $attributes, $table, $exists = false) - { - $instance = static::fromAttributes($parent, [], $table, $exists); - - $instance->timestamps = $instance->hasTimestampAttributes($attributes); - - $instance->setRawAttributes( - array_merge($instance->getRawOriginal(), $attributes), $exists - ); - - return $instance; - } - - /** - * Set the keys for a select query. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @return \Illuminate\Database\Eloquent\Builder - */ - protected function setKeysForSelectQuery($query) - { - if (isset($this->attributes[$this->getKeyName()])) { - return parent::setKeysForSelectQuery($query); - } - - $query->where($this->foreignKey, $this->getOriginal( - $this->foreignKey, $this->getAttribute($this->foreignKey) - )); - - return $query->where($this->relatedKey, $this->getOriginal( - $this->relatedKey, $this->getAttribute($this->relatedKey) - )); - } - - /** - * Set the keys for a save update query. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @return \Illuminate\Database\Eloquent\Builder - */ - protected function setKeysForSaveQuery($query) - { - return $this->setKeysForSelectQuery($query); - } - - /** - * Delete the pivot model record from the database. - * - * @return int - */ - public function delete() - { - if (isset($this->attributes[$this->getKeyName()])) { - return (int) parent::delete(); - } - - if ($this->fireModelEvent('deleting') === false) { - return 0; - } - - $this->touchOwners(); - - return tap($this->getDeleteQuery()->delete(), function () { - $this->exists = false; - - $this->fireModelEvent('deleted', false); - }); - } - - /** - * Get the query builder for a delete operation on the pivot. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - protected function getDeleteQuery() - { - return $this->newQueryWithoutRelationships()->where([ - $this->foreignKey => $this->getOriginal($this->foreignKey, $this->getAttribute($this->foreignKey)), - $this->relatedKey => $this->getOriginal($this->relatedKey, $this->getAttribute($this->relatedKey)), - ]); - } - - /** - * Get the table associated with the model. - * - * @return string - */ - public function getTable() - { - if (! isset($this->table) && (! $this instanceof MorphPivot)) { - $this->setTable(str_replace( - '\\', '', Str::snake(Str::singular(class_basename($this))) - )); - } - - return parent::getTable(); - } - - /** - * Get the foreign key column name. - * - * @return string - */ - public function getForeignKey() - { - return $this->foreignKey; - } - - /** - * Get the "related key" column name. - * - * @return string - */ - public function getRelatedKey() - { - return $this->relatedKey; - } - - /** - * Get the "related key" column name. - * - * @return string - */ - public function getOtherKey() - { - return $this->getRelatedKey(); - } - - /** - * Set the key names for the pivot model instance. - * - * @param string $foreignKey - * @param string $relatedKey - * @return $this - */ - public function setPivotKeys($foreignKey, $relatedKey) - { - $this->foreignKey = $foreignKey; - - $this->relatedKey = $relatedKey; - - return $this; - } - - /** - * Set the related model of the relationship. - * - * @param \Illuminate\Database\Eloquent\Model|null $related - * @return $this - */ - public function setRelatedModel(?Model $related = null) - { - $this->pivotRelated = $related; - - return $this; - } - - /** - * Determine if the pivot model or given attributes has timestamp attributes. - * - * @param array|null $attributes - * @return bool - */ - public function hasTimestampAttributes($attributes = null) - { - return ($createdAt = $this->getCreatedAtColumn()) !== null - && array_key_exists($createdAt, $attributes ?? $this->attributes); - } - - /** - * Get the name of the "created at" column. - * - * @return string - */ - public function getCreatedAtColumn() - { - return $this->pivotParent - ? $this->pivotParent->getCreatedAtColumn() - : parent::getCreatedAtColumn(); - } - - /** - * Get the name of the "updated at" column. - * - * @return string - */ - public function getUpdatedAtColumn() - { - return $this->pivotParent - ? $this->pivotParent->getUpdatedAtColumn() - : parent::getUpdatedAtColumn(); - } - - /** - * Get the queueable identity for the entity. - * - * @return mixed - */ - public function getQueueableId() - { - if (isset($this->attributes[$this->getKeyName()])) { - return $this->getKey(); - } - - return sprintf( - '%s:%s:%s:%s', - $this->foreignKey, $this->getAttribute($this->foreignKey), - $this->relatedKey, $this->getAttribute($this->relatedKey) - ); - } - - /** - * Get a new query to restore one or more models by their queueable IDs. - * - * @param int[]|string[]|string $ids - * @return \Illuminate\Database\Eloquent\Builder - */ - public function newQueryForRestoration($ids) - { - if (is_array($ids)) { - return $this->newQueryForCollectionRestoration($ids); - } - - if (! str_contains($ids, ':')) { - return parent::newQueryForRestoration($ids); - } - - $segments = explode(':', $ids); - - return $this->newQueryWithoutScopes() - ->where($segments[0], $segments[1]) - ->where($segments[2], $segments[3]); - } - - /** - * Get a new query to restore multiple models by their queueable IDs. - * - * @param int[]|string[] $ids - * @return \Illuminate\Database\Eloquent\Builder - */ - protected function newQueryForCollectionRestoration(array $ids) - { - $ids = array_values($ids); - - if (! str_contains($ids[0], ':')) { - return parent::newQueryForRestoration($ids); - } - - $query = $this->newQueryWithoutScopes(); - - foreach ($ids as $id) { - $segments = explode(':', $id); - - $query->orWhere(function ($query) use ($segments) { - return $query->where($segments[0], $segments[1]) - ->where($segments[2], $segments[3]); - }); - } - - return $query; - } - - /** - * Unset all the loaded relations for the instance. - * - * @return $this - */ - public function unsetRelations() - { - $this->pivotParent = null; - $this->pivotRelated = null; - $this->relations = []; - - return $this; - } -} diff --git a/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/Eloquent/Relations/Concerns/CanBeOneOfMany.php deleted file mode 100644 index 800999f86c..0000000000 --- a/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ /dev/null @@ -1,332 +0,0 @@ -|null - */ - protected $oneOfManySubQuery; - - /** - * 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 - */ - abstract public function addOneOfManySubQueryConstraints(Builder $query, $column = null, $aggregate = null); - - /** - * Get the columns the determine the relationship groups. - * - * @return array|string - */ - abstract public function getOneOfManySubQuerySelectColumns(); - - /** - * Add join query constraints for one of many relationships. - * - * @param \Illuminate\Database\Query\JoinClause $join - * @return void - */ - abstract public function addOneOfManyJoinSubQueryConstraints(JoinClause $join); - - /** - * Indicate that the relation is a 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 - * - * @throws \InvalidArgumentException - */ - public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) - { - $this->isOneOfMany = true; - - $this->relationName = $relation ?: $this->getDefaultOneOfManyJoinAlias( - $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->newOneOfManySubQuery( - $this->getOneOfManySubQuerySelectColumns(), - array_merge([$column], $previous['columns'] ?? []), - $aggregate, - ); - - if (isset($previous)) { - $this->addOneOfManyJoinSubQuery( - $subQuery, - $previous['subQuery'], - $previous['columns'], - ); - } - - if (isset($closure)) { - $closure($subQuery); - } - - if (! isset($previous)) { - $this->oneOfManySubQuery = $subQuery; - } - - if (array_key_last($columns) == $column) { - $this->addOneOfManyJoinSubQuery( - $this->query, - $subQuery, - array_merge([$column], $previous['columns'] ?? []), - ); - } - - $previous = [ - 'subQuery' => $subQuery, - 'columns' => array_merge([$column], $previous['columns'] ?? []), - ]; - } - - $this->addConstraints(); - - $columns = $this->query->getQuery()->columns; - - if (is_null($columns) || $columns === ['*']) { - $this->select([$this->qualifyColumn('*')]); - } - - 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|null $relation - * @return $this - */ - public function latestOfMany($column = 'id', $relation = null) - { - return $this->ofMany(Collection::wrap($column)->mapWithKeys(function ($column) { - return [$column => 'MAX']; - })->all(), 'MAX', $relation); - } - - /** - * Indicate that the relation is the oldest single result of a larger one-to-many relationship. - * - * @param string|array|null $column - * @param string|null $relation - * @return $this - */ - public function oldestOfMany($column = 'id', $relation = null) - { - return $this->ofMany(Collection::wrap($column)->mapWithKeys(function ($column) { - return [$column => 'MIN']; - })->all(), 'MIN', $relation); - } - - /** - * 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. - * - * @param string|array $groupBy - * @param array|null $columns - * @param string|null $aggregate - * @return \Illuminate\Database\Eloquent\Builder<*> - */ - protected function newOneOfManySubQuery($groupBy, $columns = null, $aggregate = null) - { - $subQuery = $this->query->getModel() - ->newQuery() - ->withoutGlobalScopes($this->removedScopes()); - - foreach (Arr::wrap($groupBy) as $group) { - $subQuery->groupBy($this->qualifyRelatedColumn($group)); - } - - if (! is_null($columns)) { - foreach ($columns as $key => $column) { - $aggregatedColumn = $subQuery->getQuery()->grammar->wrap($subQuery->qualifyColumn($column)); - - if ($key === 0) { - $aggregatedColumn = "{$aggregate}({$aggregatedColumn})"; - } else { - $aggregatedColumn = "min({$aggregatedColumn})"; - } - - $subQuery->selectRaw($aggregatedColumn.' as '.$subQuery->getQuery()->grammar->wrap($column.'_aggregate')); - } - } - - $this->addOneOfManySubQueryConstraints($subQuery, column: null, aggregate: $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 array $on - * @return void - */ - 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) { - foreach ($on as $onColumn) { - $join->on($this->qualifySubSelectColumn($onColumn.'_aggregate'), '=', $this->qualifyRelatedColumn($onColumn)); - } - - $this->addOneOfManyJoinSubQueryConstraints($join); - }); - }); - } - - /** - * Merge the relationship query joins to the given query builder. - * - * @param \Illuminate\Database\Eloquent\Builder<*> $query - * @return void - */ - protected function mergeOneOfManyJoinsTo(Builder $query) - { - $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; - } - - /** - * Get the one of many inner join subselect builder instance. - * - * @return \Illuminate\Database\Eloquent\Builder<*>|void - */ - public function getOneOfManySubQuery() - { - return $this->oneOfManySubQuery; - } - - /** - * 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 $this->query->getModel()->qualifyColumn($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 deleted file mode 100644 index 3dccf13107..0000000000 --- a/Eloquent/Relations/Concerns/ComparesRelatedModels.php +++ /dev/null @@ -1,77 +0,0 @@ -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; - } - - /** - * Determine if the model is not the related instance of the relationship. - * - * @param \Illuminate\Database\Eloquent\Model|null $model - * @return bool - */ - public function isNot($model) - { - return ! $this->is($model); - } - - /** - * Get the value of the parent model's key. - * - * @return mixed - */ - abstract public function getParentKey(); - - /** - * Get the value of the model's related key. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return mixed - */ - abstract protected function getRelatedKeyFrom(Model $model); - - /** - * Compare the parent key with the related key. - * - * @param mixed $parentKey - * @param mixed $relatedKey - * @return bool - */ - protected function compareKeys($parentKey, $relatedKey) - { - if (empty($parentKey) || empty($relatedKey)) { - return false; - } - - if (is_int($parentKey) || is_int($relatedKey)) { - return (int) $parentKey === (int) $relatedKey; - } - - return $parentKey === $relatedKey; - } -} diff --git a/Eloquent/Relations/Concerns/InteractsWithDictionary.php b/Eloquent/Relations/Concerns/InteractsWithDictionary.php deleted file mode 100644 index d94432e9a4..0000000000 --- a/Eloquent/Relations/Concerns/InteractsWithDictionary.php +++ /dev/null @@ -1,36 +0,0 @@ -__toString(); - } - - if ($attribute instanceof UnitEnum) { - return enum_value($attribute); - } - - throw new InvalidArgumentException('Model attribute value is an object but does not have a __toString method.'); - } - - return $attribute; - } -} diff --git a/Eloquent/Relations/Concerns/InteractsWithPivotTable.php b/Eloquent/Relations/Concerns/InteractsWithPivotTable.php deleted file mode 100644 index dd324d09a6..0000000000 --- a/Eloquent/Relations/Concerns/InteractsWithPivotTable.php +++ /dev/null @@ -1,703 +0,0 @@ - [], 'detached' => [], - ]; - - $records = $this->formatRecordsList($this->parseIds($ids)); - - // Next, we will determine which IDs should get removed from the join table by - // checking which of the given ID/records is in the list of current records - // and removing all of those rows from this "intermediate" joining table. - $detach = array_values(array_intersect( - $this->newPivotQuery()->pluck($this->relatedPivotKey)->all(), - array_keys($records) - )); - - if (count($detach) > 0) { - $this->detach($detach, false); - - $changes['detached'] = $this->castKeys($detach); - } - - // Finally, for all of the records which were not "detached", we'll attach the - // records into the intermediate table. Then, we will add those attaches to - // this change list and get ready to return these results to the callers. - $attach = array_diff_key($records, array_flip($detach)); - - if (count($attach) > 0) { - $this->attach($attach, [], false); - - $changes['attached'] = array_keys($attach); - } - - // Once we have finished attaching or detaching the records, we will see if we - // have done any attaching or detaching, and if we have we will touch these - // relationships if they are configured to touch on any database updates. - if ($touch && (count($changes['attached']) || - count($changes['detached']))) { - $this->touchIfTouching(); - } - - return $changes; - } - - /** - * Sync the intermediate tables with a list of IDs without detaching. - * - * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array|int|string $ids - * @return array{attached: array, detached: array, updated: array} - */ - public function syncWithoutDetaching($ids) - { - return $this->sync($ids, false); - } - - /** - * Sync the intermediate tables with a list of IDs or collection of models. - * - * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array|int|string $ids - * @param bool $detaching - * @return array{attached: array, detached: array, updated: array} - */ - public function sync($ids, $detaching = true) - { - $changes = [ - 'attached' => [], 'detached' => [], 'updated' => [], - ]; - - $records = $this->formatRecordsList($this->parseIds($ids)); - - if (empty($records) && ! $detaching) { - return $changes; - } - - // First we need to attach any of the associated models that are not currently - // in this joining table. We'll spin through the given IDs, checking to see - // if they exist in the array of current ones, and if not we will insert. - $current = $this->getCurrentlyAttachedPivots() - ->pluck($this->relatedPivotKey)->all(); - - // Next, we will take the differences of the currents and given IDs and detach - // all of the entities that exist in the "current" array but are not in the - // array of the new IDs given to the method which will complete the sync. - if ($detaching) { - $detach = array_diff($current, array_keys($records)); - - if (count($detach) > 0) { - $this->detach($detach, false); - - $changes['detached'] = $this->castKeys($detach); - } - } - - // Now we are finally ready to attach the new records. Note that we'll disable - // touching until after the entire operation is complete so we don't fire a - // ton of touch operations until we are totally done syncing the records. - $changes = array_merge( - $changes, $this->attachNew($records, $current, false) - ); - - // Once we have finished attaching or detaching the records, we will see if we - // have done any attaching or detaching, and if we have we will touch these - // relationships if they are configured to touch on any database updates. - if (count($changes['attached']) || - count($changes['updated']) || - count($changes['detached'])) { - $this->touchIfTouching(); - } - - return $changes; - } - - /** - * Sync the intermediate tables with a list of IDs or collection of models with the given pivot values. - * - * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array|int|string $ids - * @param array $values - * @param bool $detaching - * @return array{attached: array, detached: array, updated: array} - */ - public function syncWithPivotValues($ids, array $values, bool $detaching = true) - { - return $this->sync((new BaseCollection($this->parseIds($ids)))->mapWithKeys(function ($id) use ($values) { - return [$id => $values]; - }), $detaching); - } - - /** - * Format the sync / toggle record list so that it is keyed by ID. - * - * @param array $records - * @return array - */ - protected function formatRecordsList(array $records) - { - return (new BaseCollection($records))->mapWithKeys(function ($attributes, $id) { - if (! is_array($attributes)) { - [$id, $attributes] = [$attributes, []]; - } - - if ($id instanceof BackedEnum) { - $id = $id->value; - } - - return [$id => $attributes]; - })->all(); - } - - /** - * Attach all of the records that aren't in the given current records. - * - * @param array $records - * @param array $current - * @param bool $touch - * @return array - */ - protected function attachNew(array $records, array $current, $touch = true) - { - $changes = ['attached' => [], 'updated' => []]; - - foreach ($records as $id => $attributes) { - // If the ID is not in the list of existing pivot IDs, we will insert a new pivot - // record, otherwise, we will just update this existing record on this joining - // table, so that the developers will easily update these records pain free. - if (! in_array($id, $current)) { - $this->attach($id, $attributes, $touch); - - $changes['attached'][] = $this->castKey($id); - } - - // Now we'll try to update an existing pivot record with the attributes that were - // given to the method. If the model is actually updated we will add it to the - // list of updated pivot records so we return them back out to the consumer. - elseif (count($attributes) > 0 && - $this->updateExistingPivot($id, $attributes, $touch)) { - $changes['updated'][] = $this->castKey($id); - } - } - - return $changes; - } - - /** - * Update an existing pivot record on the table. - * - * @param mixed $id - * @param array $attributes - * @param bool $touch - * @return int - */ - public function updateExistingPivot($id, array $attributes, $touch = true) - { - if ($this->using) { - return $this->updateExistingPivotUsingCustomClass($id, $attributes, $touch); - } - - if ($this->hasPivotColumn($this->updatedAt())) { - $attributes = $this->addTimestampsToAttachment($attributes, true); - } - - $updated = $this->newPivotStatementForId($id)->update( - $this->castAttributes($attributes) - ); - - if ($touch) { - $this->touchIfTouching(); - } - - return $updated; - } - - /** - * Update an existing pivot record on the table via a custom class. - * - * @param mixed $id - * @param array $attributes - * @param bool $touch - * @return int - */ - protected function updateExistingPivotUsingCustomClass($id, array $attributes, $touch) - { - $pivot = $this->getCurrentlyAttachedPivotsForIds($id)->first(); - - $updated = $pivot ? $pivot->fill($attributes)->isDirty() : false; - - if ($updated) { - $pivot->save(); - } - - if ($touch) { - $this->touchIfTouching(); - } - - return (int) $updated; - } - - /** - * Attach a model to the parent. - * - * @param mixed $ids - * @param array $attributes - * @param bool $touch - * @return void - */ - public function attach($ids, array $attributes = [], $touch = true) - { - if ($this->using) { - $this->attachUsingCustomClass($ids, $attributes); - } else { - // Here we will insert the attachment records into the pivot table. Once we have - // inserted the records, we will touch the relationships if necessary and the - // function will return. We can parse the IDs before inserting the records. - $this->newPivotStatement()->insert($this->formatAttachRecords( - $this->parseIds($ids), $attributes - )); - } - - if ($touch) { - $this->touchIfTouching(); - } - } - - /** - * Attach a model to the parent using a custom class. - * - * @param mixed $ids - * @param array $attributes - * @return void - */ - protected function attachUsingCustomClass($ids, array $attributes) - { - $records = $this->formatAttachRecords( - $this->parseIds($ids), $attributes - ); - - foreach ($records as $record) { - $this->newPivot($record, false)->save(); - } - } - - /** - * Create an array of records to insert into the pivot table. - * - * @param array $ids - * @param array $attributes - * @return array - */ - protected function formatAttachRecords($ids, array $attributes) - { - $records = []; - - $hasTimestamps = ($this->hasPivotColumn($this->createdAt()) || - $this->hasPivotColumn($this->updatedAt())); - - // To create the attachment records, we will simply spin through the IDs given - // and create a new record to insert for each ID. Each ID may actually be a - // key in the array, with extra attributes to be placed in other columns. - foreach ($ids as $key => $value) { - $records[] = $this->formatAttachRecord( - $key, $value, $attributes, $hasTimestamps - ); - } - - return $records; - } - - /** - * Create a full attachment record payload. - * - * @param int $key - * @param mixed $value - * @param array $attributes - * @param bool $hasTimestamps - * @return array - */ - protected function formatAttachRecord($key, $value, $attributes, $hasTimestamps) - { - [$id, $attributes] = $this->extractAttachIdAndAttributes($key, $value, $attributes); - - return array_merge( - $this->baseAttachRecord($id, $hasTimestamps), $this->castAttributes($attributes) - ); - } - - /** - * Get the attach record ID and extra attributes. - * - * @param mixed $key - * @param mixed $value - * @param array $attributes - * @return array - */ - protected function extractAttachIdAndAttributes($key, $value, array $attributes) - { - return is_array($value) - ? [$key, array_merge($value, $attributes)] - : [$value, $attributes]; - } - - /** - * Create a new pivot attachment record. - * - * @param int $id - * @param bool $timed - * @return array - */ - protected function baseAttachRecord($id, $timed) - { - $record[$this->relatedPivotKey] = $id; - - $record[$this->foreignPivotKey] = $this->parent->{$this->parentKey}; - - // If the record needs to have creation and update timestamps, we will make - // them by calling the parent model's "freshTimestamp" method which will - // provide us with a fresh timestamp in this model's preferred format. - if ($timed) { - $record = $this->addTimestampsToAttachment($record); - } - - foreach ($this->pivotValues as $value) { - $record[$value['column']] = $value['value']; - } - - return $record; - } - - /** - * Set the creation and update timestamps on an attach record. - * - * @param array $record - * @param bool $exists - * @return array - */ - protected function addTimestampsToAttachment(array $record, $exists = false) - { - $fresh = $this->parent->freshTimestamp(); - - if ($this->using) { - $pivotModel = new $this->using; - - $fresh = $pivotModel->fromDateTime($fresh); - } - - if (! $exists && $this->hasPivotColumn($this->createdAt())) { - $record[$this->createdAt()] = $fresh; - } - - if ($this->hasPivotColumn($this->updatedAt())) { - $record[$this->updatedAt()] = $fresh; - } - - return $record; - } - - /** - * Determine whether the given column is defined as a pivot column. - * - * @param string $column - * @return bool - */ - public function hasPivotColumn($column) - { - return in_array($column, $this->pivotColumns); - } - - /** - * Detach models from the relationship. - * - * @param mixed $ids - * @param bool $touch - * @return int - */ - public function detach($ids = null, $touch = true) - { - if ($this->using) { - $results = $this->detachUsingCustomClass($ids); - } else { - $query = $this->newPivotQuery(); - - // If associated IDs were passed to the method we will only delete those - // associations, otherwise all of the association ties will be broken. - // We'll return the numbers of affected rows when we do the deletes. - if (! is_null($ids)) { - $ids = $this->parseIds($ids); - - if (empty($ids)) { - return 0; - } - - $query->whereIn($this->getQualifiedRelatedPivotKeyName(), (array) $ids); - } - - // Once we have all of the conditions set on the statement, we are ready - // to run the delete on the pivot table. Then, if the touch parameter - // is true, we will go ahead and touch all related models to sync. - $results = $query->delete(); - } - - if ($touch) { - $this->touchIfTouching(); - } - - return $results; - } - - /** - * Detach models from the relationship using a custom class. - * - * @param mixed $ids - * @return int - */ - protected function detachUsingCustomClass($ids) - { - $results = 0; - - $records = $this->getCurrentlyAttachedPivotsForIds($ids); - - foreach ($records as $record) { - $results += $record->delete(); - } - - return $results; - } - - /** - * Get the pivot models that are currently attached. - * - * @return \Illuminate\Support\Collection - */ - protected function getCurrentlyAttachedPivots() - { - return $this->getCurrentlyAttachedPivotsForIds(); - } - - /** - * Get the pivot models that are currently attached, filtered by related model keys. - * - * @param mixed $ids - * @return \Illuminate\Support\Collection - */ - protected function getCurrentlyAttachedPivotsForIds($ids = null) - { - return $this->newPivotQuery() - ->when(! is_null($ids), fn ($query) => $query->whereIn( - $this->getQualifiedRelatedPivotKeyName(), $this->parseIds($ids) - )) - ->get() - ->map(function ($record) { - $class = $this->using ?: Pivot::class; - - $pivot = $class::fromRawAttributes($this->parent, (array) $record, $this->getTable(), true); - - return $pivot - ->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey) - ->setRelatedModel($this->related); - }); - } - - /** - * Create a new pivot model instance. - * - * @param array $attributes - * @param bool $exists - * @return \Illuminate\Database\Eloquent\Relations\Pivot - */ - public function newPivot(array $attributes = [], $exists = false) - { - $attributes = array_merge(array_column($this->pivotValues, 'value', 'column'), $attributes); - - $pivot = $this->related->newPivot( - $this->parent, $attributes, $this->table, $exists, $this->using - ); - - return $pivot - ->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey) - ->setRelatedModel($this->related); - } - - /** - * Create a new existing pivot model instance. - * - * @param array $attributes - * @return \Illuminate\Database\Eloquent\Relations\Pivot - */ - public function newExistingPivot(array $attributes = []) - { - return $this->newPivot($attributes, true); - } - - /** - * Get a new plain query builder for the pivot table. - * - * @return \Illuminate\Database\Query\Builder - */ - public function newPivotStatement() - { - return $this->query->getQuery()->newQuery()->from($this->table); - } - - /** - * Get a new pivot statement for a given "other" ID. - * - * @param mixed $id - * @return \Illuminate\Database\Query\Builder - */ - public function newPivotStatementForId($id) - { - return $this->newPivotQuery()->whereIn($this->getQualifiedRelatedPivotKeyName(), $this->parseIds($id)); - } - - /** - * Create a new query builder for the pivot table. - * - * @return \Illuminate\Database\Query\Builder - */ - public function newPivotQuery() - { - $query = $this->newPivotStatement(); - - foreach ($this->pivotWheres as $arguments) { - $query->where(...$arguments); - } - - foreach ($this->pivotWhereIns as $arguments) { - $query->whereIn(...$arguments); - } - - foreach ($this->pivotWhereNulls as $arguments) { - $query->whereNull(...$arguments); - } - - return $query->where($this->getQualifiedForeignPivotKeyName(), $this->parent->{$this->parentKey}); - } - - /** - * Set the columns on the pivot table to retrieve. - * - * @param mixed $columns - * @return $this - */ - public function withPivot($columns) - { - $this->pivotColumns = array_merge( - $this->pivotColumns, is_array($columns) ? $columns : func_get_args() - ); - - return $this; - } - - /** - * Get all of the IDs from the given mixed value. - * - * @param mixed $value - * @return array - */ - protected function parseIds($value) - { - if ($value instanceof Model) { - return [$value->{$this->relatedKey}]; - } - - if ($value instanceof EloquentCollection) { - return $value->pluck($this->relatedKey)->all(); - } - - if ($value instanceof BaseCollection || is_array($value)) { - return (new BaseCollection($value)) - ->map(fn ($item) => $item instanceof Model ? $item->{$this->relatedKey} : $item) - ->all(); - } - - return (array) $value; - } - - /** - * Get the ID from the given mixed value. - * - * @param mixed $value - * @return mixed - */ - protected function parseId($value) - { - return $value instanceof Model ? $value->{$this->relatedKey} : $value; - } - - /** - * Cast the given keys to integers if they are numeric and string otherwise. - * - * @param array $keys - * @return array - */ - protected function castKeys(array $keys) - { - return array_map(function ($v) { - return $this->castKey($v); - }, $keys); - } - - /** - * Cast the given key to convert to primary key type. - * - * @param mixed $key - * @return mixed - */ - protected function castKey($key) - { - return $this->getTypeSwapValue( - $this->related->getKeyType(), - $key - ); - } - - /** - * Cast the given pivot attributes. - * - * @param array $attributes - * @return array - */ - protected function castAttributes($attributes) - { - return $this->using - ? $this->newPivot()->fill($attributes)->getAttributes() - : $attributes; - } - - /** - * Converts a given value to a given type value. - * - * @param string $type - * @param mixed $value - * @return mixed - */ - protected function getTypeSwapValue($type, $value) - { - return match (strtolower($type)) { - 'int', 'integer' => (int) $value, - 'real', 'float', 'double' => (float) $value, - 'string' => (string) $value, - default => $value, - }; - } -} diff --git a/Eloquent/Relations/Concerns/SupportsDefaultModels.php b/Eloquent/Relations/Concerns/SupportsDefaultModels.php deleted file mode 100644 index 74e758f585..0000000000 --- a/Eloquent/Relations/Concerns/SupportsDefaultModels.php +++ /dev/null @@ -1,63 +0,0 @@ -withDefault = $callback; - - return $this; - } - - /** - * Get the default value for this relation. - * - * @param \Illuminate\Database\Eloquent\Model $parent - * @return \Illuminate\Database\Eloquent\Model|null - */ - protected function getDefaultFor(Model $parent) - { - if (! $this->withDefault) { - return; - } - - $instance = $this->newRelatedInstanceFor($parent); - - if (is_callable($this->withDefault)) { - return call_user_func($this->withDefault, $instance, $parent) ?: $instance; - } - - if (is_array($this->withDefault)) { - $instance->forceFill($this->withDefault); - } - - return $instance; - } -} diff --git a/Eloquent/Relations/Concerns/SupportsInverseRelations.php b/Eloquent/Relations/Concerns/SupportsInverseRelations.php deleted file mode 100644 index c7140d0a31..0000000000 --- a/Eloquent/Relations/Concerns/SupportsInverseRelations.php +++ /dev/null @@ -1,157 +0,0 @@ -chaperone($relation); - } - - /** - * Instruct Eloquent to link the related models back to the parent after the relationship query has run. - * - * @param string|null $relation - * @return $this - */ - public function chaperone(?string $relation = null) - { - $relation ??= $this->guessInverseRelation(); - - if (! $relation || ! $this->getModel()->isRelation($relation)) { - throw RelationNotFoundException::make($this->getModel(), $relation ?: 'null'); - } - - if ($this->inverseRelationship === null && $relation) { - $this->query->afterQuery(function ($result) { - return $this->inverseRelationship - ? $this->applyInverseRelationToCollection($result, $this->getParent()) - : $result; - }); - } - - $this->inverseRelationship = $relation; - - return $this; - } - - /** - * Guess the name of the inverse relationship. - * - * @return string|null - */ - protected function guessInverseRelation(): ?string - { - return Arr::first( - $this->getPossibleInverseRelations(), - fn ($relation) => $relation && $this->getModel()->isRelation($relation) - ); - } - - /** - * Get the possible inverse relations for the parent model. - * - * @return array - */ - protected function getPossibleInverseRelations(): array - { - return array_filter(array_unique([ - Str::camel(Str::beforeLast($this->getForeignKeyName(), $this->getParent()->getKeyName())), - Str::camel(Str::beforeLast($this->getParent()->getForeignKey(), $this->getParent()->getKeyName())), - Str::camel(class_basename($this->getParent())), - 'owner', - get_class($this->getParent()) === get_class($this->getModel()) ? 'parent' : null, - ])); - } - - /** - * Set the inverse relation on all models in a collection. - * - * @param \Illuminate\Database\Eloquent\Collection $models - * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return \Illuminate\Database\Eloquent\Collection - */ - protected function applyInverseRelationToCollection($models, ?Model $parent = null) - { - $parent ??= $this->getParent(); - - foreach ($models as $model) { - $model instanceof Model && $this->applyInverseRelationToModel($model, $parent); - } - - return $models; - } - - /** - * Set the inverse relation on a model. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return \Illuminate\Database\Eloquent\Model - */ - protected function applyInverseRelationToModel(Model $model, ?Model $parent = null) - { - if ($inverse = $this->getInverseRelationship()) { - $parent ??= $this->getParent(); - - $model->setRelation($inverse, $parent); - } - - return $model; - } - - /** - * Get the name of the inverse relationship. - * - * @return string|null - */ - public function getInverseRelationship() - { - return $this->inverseRelationship; - } - - /** - * Remove the chaperone / inverse relationship for this query. - * - * Alias of "withoutChaperone". - * - * @return $this - */ - public function withoutInverse() - { - return $this->withoutChaperone(); - } - - /** - * Remove the chaperone / inverse relationship for this query. - * - * @return $this - */ - public function withoutChaperone() - { - $this->inverseRelationship = null; - - return $this; - } -} diff --git a/Eloquent/Relations/HasMany.php b/Eloquent/Relations/HasMany.php index 1337b50246..6149e475ab 100755 --- a/Eloquent/Relations/HasMany.php +++ b/Eloquent/Relations/HasMany.php @@ -2,47 +2,27 @@ namespace Illuminate\Database\Eloquent\Relations; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; +use Illuminate\Database\Eloquent\Collection; -/** - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * - * @extends \Illuminate\Database\Eloquent\Relations\HasOneOrMany> - */ class HasMany extends HasOneOrMany { /** - * Convert the relationship to a "has one" relationship. + * Get the results of the relationship. * - * @return \Illuminate\Database\Eloquent\Relations\HasOne + * @return mixed */ - public function one() - { - return HasOne::noConstraints(fn () => tap( - new HasOne( - $this->getQuery(), - $this->parent, - $this->foreignKey, - $this->localKey - ), - function ($hasOne) { - if ($inverse = $this->getInverseRelationship()) { - $hasOne->inverse($inverse); - } - } - )); - } - - /** @inheritDoc */ public function getResults() { - return ! is_null($this->getParentKey()) - ? $this->query->get() - : $this->related->newCollection(); + return $this->query->get(); } - /** @inheritDoc */ + /** + * 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) { @@ -52,8 +32,15 @@ public function initRelation(array $models, $relation) return $models; } - /** @inheritDoc */ - public function match(array $models, EloquentCollection $results, $relation) + /** + * 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) { return $this->matchMany($models, $results, $relation); } diff --git a/Eloquent/Relations/HasManyThrough.php b/Eloquent/Relations/HasManyThrough.php index 1b94f1c593..52f72ac45c 100644 --- a/Eloquent/Relations/HasManyThrough.php +++ b/Eloquent/Relations/HasManyThrough.php @@ -2,40 +2,152 @@ namespace Illuminate\Database\Eloquent\Relations; +use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; -use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary; - -/** - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TIntermediateModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * - * @extends \Illuminate\Database\Eloquent\Relations\HasOneOrManyThrough> - */ -class HasManyThrough extends HasOneOrManyThrough +use Illuminate\Database\Query\Expression; +use Illuminate\Database\Eloquent\Collection; +use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Database\Eloquent\ModelNotFoundException; + +class HasManyThrough extends Relation { - use InteractsWithDictionary; + /** + * The distance parent model instance. + * + * @var \Illuminate\Database\Eloquent\Model + */ + protected $farParent; + + /** + * The near key on the relationship. + * + * @var string + */ + protected $firstKey; + + /** + * The far key on the relationship. + * + * @var string + */ + protected $secondKey; /** - * Convert the relationship to a "has one through" relationship. + * The local key on the relationship. * - * @return \Illuminate\Database\Eloquent\Relations\HasOneThrough + * @var string */ - public function one() + protected $localKey; + + /** + * Create a new has many through relationship instance. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Model $farParent + * @param \Illuminate\Database\Eloquent\Model $parent + * @param string $firstKey + * @param string $secondKey + * @param string $localKey + * @return void + */ + public function __construct(Builder $query, Model $farParent, Model $parent, $firstKey, $secondKey, $localKey) { - return HasOneThrough::noConstraints(fn () => new HasOneThrough( - tap($this->getQuery(), fn (Builder $query) => $query->getQuery()->joins = []), - $this->farParent, - $this->throughParent, - $this->getFirstKeyName(), - $this->getForeignKeyName(), - $this->getLocalKeyName(), - $this->getSecondLocalKeyName(), - )); + $this->localKey = $localKey; + $this->firstKey = $firstKey; + $this->secondKey = $secondKey; + $this->farParent = $farParent; + + parent::__construct($query, $parent); } - /** @inheritDoc */ + /** + * Set the base constraints on the relation query. + * + * @return void + */ + public function addConstraints() + { + $parentTable = $this->parent->getTable(); + + $localValue = $this->farParent[$this->localKey]; + + $this->setJoin(); + + if (static::$constraints) { + $this->query->where($parentTable.'.'.$this->firstKey, '=', $localValue); + } + } + + /** + * Add the constraints for a relationship query. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parent + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder + */ + public function getRelationQuery(Builder $query, Builder $parent, $columns = ['*']) + { + $parentTable = $this->parent->getTable(); + + $this->setJoin($query); + + $query->select($columns); + + $key = $this->wrap($parentTable.'.'.$this->firstKey); + + return $query->where($this->getHasCompareKey(), '=', new Expression($key)); + } + + /** + * Set the join clause on the query. + * + * @param \Illuminate\Database\Eloquent\Builder|null $query + * @return void + */ + protected function setJoin(Builder $query = null) + { + $query = $query ?: $this->query; + + $foreignKey = $this->related->getTable().'.'.$this->secondKey; + + $query->join($this->parent->getTable(), $this->getQualifiedParentKeyName(), '=', $foreignKey); + + if ($this->parentSoftDeletes()) { + $query->whereNull($this->parent->getQualifiedDeletedAtColumn()); + } + } + + /** + * Determine whether close parent of the relation uses Soft Deletes. + * + * @return bool + */ + public function parentSoftDeletes() + { + return in_array(SoftDeletes::class, class_uses_recursive(get_class($this->parent))); + } + + /** + * Set the constraints for an eager load of the relation. + * + * @param array $models + * @return void + */ + public function addEagerConstraints(array $models) + { + $table = $this->parent->getTable(); + + $this->query->whereIn($table.'.'.$this->firstKey, $this->getKeys($models)); + } + + /** + * 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) { @@ -45,8 +157,15 @@ public function initRelation(array $models, $relation) return $models; } - /** @inheritDoc */ - public function match(array $models, EloquentCollection $results, $relation) + /** + * 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); @@ -54,23 +173,242 @@ public function match(array $models, EloquentCollection $results, $relation) // link them up with their children using the keyed dictionary to make the // matching very convenient and easy work. Then we'll just return them. foreach ($models as $model) { - $key = $this->getDictionaryKey($model->getAttribute($this->localKey)); + $key = $model->getKey(); + + if (isset($dictionary[$key])) { + $value = $this->related->newCollection($dictionary[$key]); - if ($key !== null && isset($dictionary[$key])) { - $model->setRelation( - $relation, $this->related->newCollection($dictionary[$key]) - ); + $model->setRelation($relation, $value); } } return $models; } - /** @inheritDoc */ + /** + * Build model dictionary keyed by the relation's foreign key. + * + * @param \Illuminate\Database\Eloquent\Collection $results + * @return array + */ + protected function buildDictionary(Collection $results) + { + $dictionary = []; + + $foreign = $this->firstKey; + + // First we will create a dictionary of models keyed by the foreign key of the + // relationship as this will allow us to quickly access all of the related + // models without having to do nested looping which will be quite slow. + foreach ($results as $result) { + $dictionary[$result->{$foreign}][] = $result; + } + + return $dictionary; + } + + /** + * Get the results of the relationship. + * + * @return mixed + */ public function getResults() { - return ! is_null($this->farParent->{$this->localKey}) - ? $this->get() - : $this->related->newCollection(); + return $this->get(); + } + + /** + * Execute the query and get the first related model. + * + * @param array $columns + * @return mixed + */ + public function first($columns = ['*']) + { + $results = $this->take(1)->get($columns); + + return count($results) > 0 ? $results->first() : null; + } + + /** + * Execute the query and get the first result or throw an exception. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Model|static + * + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + */ + public function firstOrFail($columns = ['*']) + { + if (! is_null($model = $this->first($columns))) { + return $model; + } + + throw new ModelNotFoundException; + } + + /** + * 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 (is_array($id)) { + return $this->findMany($id, $columns); + } + + $this->where($this->getRelated()->getQualifiedKeyName(), '=', $id); + + return $this->first($columns); + } + + /** + * Find multiple related models by their primary keys. + * + * @param mixed $ids + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function findMany($ids, $columns = ['*']) + { + if (empty($ids)) { + return $this->getRelated()->newCollection(); + } + + $this->whereIn($this->getRelated()->getQualifiedKeyName(), $ids); + + return $this->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 + */ + public function findOrFail($id, $columns = ['*']) + { + $result = $this->find($id, $columns); + + 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->parent)); + } + + /** + * 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 out pivot + // models with the result of those columns as a separate model relation. + $columns = $this->query->getQuery()->columns ? [] : $columns; + + $select = $this->getSelectColumns($columns); + + $builder = $this->query->applyScopes(); + + $models = $builder->addSelect($select)->getModels(); + + // If we actually found models we will also eager load any relationships that + // have been specified as needing to be eager loaded. This will solve the + // n + 1 query problem for the developer and also increase performance. + if (count($models) > 0) { + $models = $builder->eagerLoadRelations($models); + } + + return $this->related->newCollection($models); + } + + /** + * Set the select clause for the relation query. + * + * @param array $columns + * @return array + */ + protected function getSelectColumns(array $columns = ['*']) + { + if ($columns == ['*']) { + $columns = [$this->related->getTable().'.*']; + } + + return array_merge($columns, [$this->parent->getTable().'.'.$this->firstKey]); + } + + /** + * Get a paginator for the "select" statement. + * + * @param int $perPage + * @param array $columns + * @param string $pageName + * @param int $page + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + */ + public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) + { + $this->query->addSelect($this->getSelectColumns($columns)); + + return $this->query->paginate($perPage, $columns, $pageName, $page); + } + + /** + * Paginate the given query into a simple paginator. + * + * @param int $perPage + * @param array $columns + * @param string $pageName + * @return \Illuminate\Contracts\Pagination\Paginator + */ + public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page') + { + $this->query->addSelect($this->getSelectColumns($columns)); + + return $this->query->simplePaginate($perPage, $columns, $pageName); + } + + /** + * Get the key for comparing against the parent key in "has" query. + * + * @return string + */ + public function getHasCompareKey() + { + return $this->farParent->getQualifiedKeyName(); + } + + /** + * Get the qualified foreign key on the related model. + * + * @return string + */ + public function getForeignKey() + { + return $this->related->getTable().'.'.$this->secondKey; + } + + /** + * Get the qualified foreign key on the "through" model. + * + * @return string + */ + public function getThroughKey() + { + return $this->parent->getTable().'.'.$this->firstKey; } } diff --git a/Eloquent/Relations/HasOne.php b/Eloquent/Relations/HasOne.php index 911d4e26c7..52ad2a612c 100755 --- a/Eloquent/Relations/HasOne.php +++ b/Eloquent/Relations/HasOne.php @@ -2,117 +2,46 @@ namespace Illuminate\Database\Eloquent\Relations; -use Illuminate\Contracts\Database\Eloquent\SupportsPartialRelations; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; -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; +use Illuminate\Database\Eloquent\Collection; -/** - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * - * @extends \Illuminate\Database\Eloquent\Relations\HasOneOrMany - */ -class HasOne extends HasOneOrMany implements SupportsPartialRelations +class HasOne extends HasOneOrMany { - use ComparesRelatedModels, CanBeOneOfMany, SupportsDefaultModels; - - /** @inheritDoc */ - public function getResults() - { - if (is_null($this->getParentKey())) { - return $this->getDefaultFor($this->parent); - } - - return $this->query->first() ?: $this->getDefaultFor($this->parent); - } - - /** @inheritDoc */ - public function initRelation(array $models, $relation) - { - foreach ($models as $model) { - $model->setRelation($relation, $this->getDefaultFor($model)); - } - - return $models; - } - - /** @inheritDoc */ - public function match(array $models, EloquentCollection $results, $relation) - { - return $this->matchOne($models, $results, $relation); - } - - /** @inheritDoc */ - public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) - { - if ($this->isOneOfMany()) { - $this->mergeOneOfManyJoinsTo($query); - } - - 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); - } - /** - * Get the columns that should be selected by the one of many subquery. + * Get the results of the relationship. * - * @return array|string + * @return mixed */ - public function getOneOfManySubQuerySelectColumns() + public function getResults() { - return $this->foreignKey; + return $this->query->first(); } /** - * Add join query constraints for one of many relationships. + * Initialize the relation on a set of models. * - * @param \Illuminate\Database\Query\JoinClause $join - * @return void + * @param array $models + * @param string $relation + * @return array */ - public function addOneOfManyJoinSubQueryConstraints(JoinClause $join) + public function initRelation(array $models, $relation) { - $join->on($this->qualifySubSelectColumn($this->foreignKey), '=', $this->qualifyRelatedColumn($this->foreignKey)); - } + foreach ($models as $model) { + $model->setRelation($relation, null); + } - /** - * Make a new related instance for the given model. - * - * @param TDeclaringModel $parent - * @return TRelatedModel - */ - public function newRelatedInstanceFor(Model $parent) - { - return tap($this->related->newInstance(), function ($instance) use ($parent) { - $instance->setAttribute($this->getForeignKeyName(), $parent->{$this->localKey}); - $this->applyInverseRelationToModel($instance, $parent); - }); + return $models; } /** - * Get the value of the model's foreign key. + * Match the eagerly loaded results to their parents. * - * @param TRelatedModel $model - * @return int|string + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results + * @param string $relation + * @return array */ - protected function getRelatedKeyFrom(Model $model) + public function match(array $models, Collection $results, $relation) { - return $model->getAttribute($this->getForeignKeyName()); + return $this->matchOne($models, $results, $relation); } } diff --git a/Eloquent/Relations/HasOneOrMany.php b/Eloquent/Relations/HasOneOrMany.php index ba32c0e3b3..7b33c02a6a 100755 --- a/Eloquent/Relations/HasOneOrMany.php +++ b/Eloquent/Relations/HasOneOrMany.php @@ -2,26 +2,13 @@ namespace Illuminate\Database\Eloquent\Relations; -use Closure; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary; -use Illuminate\Database\Eloquent\Relations\Concerns\SupportsInverseRelations; -use Illuminate\Database\UniqueConstraintViolationException; -use Illuminate\Support\Arr; - -/** - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * @template TResult - * - * @extends \Illuminate\Database\Eloquent\Relations\Relation - */ +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Query\Expression; +use Illuminate\Database\Eloquent\Collection; + abstract class HasOneOrMany extends Relation { - use InteractsWithDictionary, SupportsInverseRelations; - /** * The foreign key of the parent model. * @@ -39,10 +26,11 @@ abstract class HasOneOrMany extends Relation /** * Create a new has one or many relationship instance. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param TDeclaringModel $parent + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Model $parent * @param string $foreignKey * @param string $localKey + * @return void */ public function __construct(Builder $query, Model $parent, $foreignKey, $localKey) { @@ -53,74 +41,87 @@ public function __construct(Builder $query, Model $parent, $foreignKey, $localKe } /** - * Create and return an un-saved instance of the related model. + * Set the base constraints on the relation query. * - * @param array $attributes - * @return TRelatedModel + * @return void */ - public function make(array $attributes = []) + public function addConstraints() { - return tap($this->related->newInstance($attributes), function ($instance) { - $this->setForeignAttributesForCreate($instance); - $this->applyInverseRelationToModel($instance); - }); + if (static::$constraints) { + $this->query->where($this->foreignKey, '=', $this->getParentKey()); + + $this->query->whereNotNull($this->foreignKey); + } } /** - * Create and return an un-saved instance of the related models. + * Add the constraints for a relationship query. * - * @param iterable $records - * @return \Illuminate\Database\Eloquent\Collection + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parent + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder */ - public function makeMany($records) + public function getRelationQuery(Builder $query, Builder $parent, $columns = ['*']) { - $instances = $this->related->newCollection(); - - foreach ($records as $record) { - $instances->push($this->make($record)); + if ($parent->getQuery()->from == $query->getQuery()->from) { + return $this->getRelationQueryForSelfRelation($query, $parent, $columns); } - return $instances; + return parent::getRelationQuery($query, $parent, $columns); } /** - * Set the base constraints on the relation query. + * Add the constraints for a relationship query on the same table. * - * @return void + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parent + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder */ - public function addConstraints() + public function getRelationQueryForSelfRelation(Builder $query, Builder $parent, $columns = ['*']) { - if (static::$constraints) { - $query = $this->getRelationQuery(); + $query->select($columns); + + $query->from($query->getModel()->getTable().' as '.$hash = $this->getRelationCountHash()); + + $query->getModel()->setTable($hash); - $query->where($this->foreignKey, '=', $this->getParentKey()); + $key = $this->wrap($this->getQualifiedParentKeyName()); - $query->whereNotNull($this->foreignKey); - } + return $query->where($hash.'.'.$this->getPlainForeignKey(), '=', new Expression($key)); } - /** @inheritDoc */ + /** + * Get a relationship join table hash. + * + * @return string + */ + public function getRelationCountHash() + { + return 'self_'.md5(microtime(true)); + } + + /** + * 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->localKey); - - $this->whereInEager( - $whereIn, - $this->foreignKey, - $this->getKeys($models, $this->localKey), - $this->getRelationQuery() - ); + $this->query->whereIn($this->foreignKey, $this->getKeys($models, $this->localKey)); } /** * Match the eagerly loaded results to their single parents. * - * @param array $models - * @param \Illuminate\Database\Eloquent\Collection $results + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results * @param string $relation - * @return array + * @return array */ - public function matchOne(array $models, EloquentCollection $results, $relation) + public function matchOne(array $models, Collection $results, $relation) { return $this->matchOneOrMany($models, $results, $relation, 'one'); } @@ -128,12 +129,12 @@ public function matchOne(array $models, EloquentCollection $results, $relation) /** * Match the eagerly loaded results to their many parents. * - * @param array $models - * @param \Illuminate\Database\Eloquent\Collection $results + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results * @param string $relation - * @return array + * @return array */ - public function matchMany(array $models, EloquentCollection $results, $relation) + public function matchMany(array $models, Collection $results, $relation) { return $this->matchOneOrMany($models, $results, $relation, 'many'); } @@ -141,13 +142,13 @@ public function matchMany(array $models, EloquentCollection $results, $relation) /** * Match the eagerly loaded results to their many parents. * - * @param array $models - * @param \Illuminate\Database\Eloquent\Collection $results + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results * @param string $relation * @param string $type - * @return array + * @return array */ - protected function matchOneOrMany(array $models, EloquentCollection $results, $relation, $type) + protected function matchOneOrMany(array $models, Collection $results, $relation, $type) { $dictionary = $this->buildDictionary($results); @@ -155,17 +156,12 @@ protected function matchOneOrMany(array $models, EloquentCollection $results, $r // link them up with their children using the keyed dictionary to make the // matching very convenient and easy work. Then we'll just return them. foreach ($models as $model) { - $key = $this->getDictionaryKey($model->getAttribute($this->localKey)); - - if ($key !== null && isset($dictionary[$key])) { - $related = $this->getRelationValue($dictionary, $key, $type); + $key = $model->getAttribute($this->localKey); - $model->setRelation($relation, $related); + if (isset($dictionary[$key])) { + $value = $this->getRelationValue($dictionary, $key, $type); - // Apply the inverse relation if we have one... - $type === 'one' - ? $this->applyInverseRelationToModel($related, $model) - : $this->applyInverseRelationToCollection($related, $model); + $model->setRelation($relation, $value); } } @@ -175,7 +171,7 @@ protected function matchOneOrMany(array $models, EloquentCollection $results, $r /** * Get the value of a relationship by one or many type. * - * @param array $dictionary + * @param array $dictionary * @param string $key * @param string $type * @return mixed @@ -184,174 +180,49 @@ protected function getRelationValue(array $dictionary, $key, $type) { $value = $dictionary[$key]; - return $type === 'one' ? reset($value) : $this->related->newCollection($value); + return $type == 'one' ? reset($value) : $this->related->newCollection($value); } /** * Build model dictionary keyed by the relation's foreign key. * - * @param \Illuminate\Database\Eloquent\Collection $results - * @return array> + * @param \Illuminate\Database\Eloquent\Collection $results + * @return array */ - protected function buildDictionary(EloquentCollection $results) + protected function buildDictionary(Collection $results) { - $foreign = $this->getForeignKeyName(); - $dictionary = []; - $isAssociative = Arr::isAssoc($results->all()); - - foreach ($results as $key => $item) { - $pairKey = $this->getDictionaryKey($item->{$foreign}); + $foreign = $this->getPlainForeignKey(); - if ($isAssociative) { - $dictionary[$pairKey][$key] = $item; - } else { - $dictionary[$pairKey][] = $item; - } + // First we will create a dictionary of models keyed by the foreign key of the + // relationship as this will allow us to quickly access all of the related + // models without having to do nested looping which will be quite slow. + foreach ($results as $result) { + $dictionary[$result->{$foreign}][] = $result; } return $dictionary; } - /** - * Find a model by its primary key or return a new instance of the related model. - * - * @param mixed $id - * @param array $columns - * @return ($id is (\Illuminate\Contracts\Support\Arrayable|array) ? \Illuminate\Database\Eloquent\Collection : TRelatedModel) - */ - public function findOrNew($id, $columns = ['*']) - { - if (is_null($instance = $this->find($id, $columns))) { - $instance = $this->related->newInstance(); - - $this->setForeignAttributesForCreate($instance); - } - - return $instance; - } - - /** - * Get the first related model record matching the attributes or instantiate it. - * - * @param array $attributes - * @param array $values - * @return TRelatedModel - */ - public function firstOrNew(array $attributes = [], array $values = []) - { - if (is_null($instance = $this->where($attributes)->first())) { - $instance = $this->related->newInstance(array_merge($attributes, $values)); - - $this->setForeignAttributesForCreate($instance); - } - - return $instance; - } - - /** - * Get the first record matching the attributes. If the record is not found, create it. - * - * @param array $attributes - * @param (\Closure(): array)|array $values - * @return TRelatedModel - */ - public function firstOrCreate(array $attributes = [], Closure|array $values = []) - { - if (is_null($instance = (clone $this)->where($attributes)->first())) { - $instance = $this->createOrFirst($attributes, $values); - } - - return $instance; - } - - /** - * Attempt to create the record. If a unique constraint violation occurs, attempt to find the matching record. - * - * @param array $attributes - * @param (\Closure(): array)|array $values - * @return TRelatedModel - * - * @throws \Illuminate\Database\UniqueConstraintViolationException - */ - public function createOrFirst(array $attributes = [], Closure|array $values = []) - { - try { - return $this->getQuery()->withSavepointIfNeeded(fn () => $this->create(array_merge($attributes, value($values)))); - } catch (UniqueConstraintViolationException $e) { - return $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 - * @return TRelatedModel - */ - public function updateOrCreate(array $attributes, array $values = []) - { - return tap($this->firstOrCreate($attributes, $values), function ($instance) use ($values) { - if (! $instance->wasRecentlyCreated) { - $instance->fill($values)->save(); - } - }); - } - - /** - * Insert new records or update the existing ones. - * - * @param array $values - * @param array|string $uniqueBy - * @param array|null $update - * @return int - */ - public function upsert(array $values, $uniqueBy, $update = null) - { - if (! empty($values) && ! is_array(array_first($values))) { - $values = [$values]; - } - - foreach ($values as $key => $value) { - $values[$key][$this->getForeignKeyName()] = $this->getParentKey(); - } - - return $this->getQuery()->upsert($values, $uniqueBy, $update); - } - /** * Attach a model instance to the parent model. * - * @param TRelatedModel $model - * @return TRelatedModel|false + * @param \Illuminate\Database\Eloquent\Model $model + * @return \Illuminate\Database\Eloquent\Model */ public function save(Model $model) { - $this->setForeignAttributesForCreate($model); + $model->setAttribute($this->getPlainForeignKey(), $this->getParentKey()); return $model->save() ? $model : false; } - /** - * Attach a model instance without raising any events to the parent model. - * - * @param TRelatedModel $model - * @return TRelatedModel|false - */ - public function saveQuietly(Model $model) - { - return Model::withoutEvents(function () use ($model) { - return $this->save($model); - }); - } - /** * Attach a collection of models to the parent instance. * - * @param iterable $models - * @return iterable + * @param \Traversable|array $models + * @return \Traversable|array */ public function saveMany($models) { @@ -363,202 +234,123 @@ public function saveMany($models) } /** - * Attach a collection of models to the parent instance without raising any events to the parent model. - * - * @param iterable $models - * @return iterable - */ - public function saveManyQuietly($models) - { - return Model::withoutEvents(function () use ($models) { - return $this->saveMany($models); - }); - } - - /** - * Create a new instance of the related model. + * Find a model by its primary key or return new instance of the related model. * - * @param array $attributes - * @return TRelatedModel + * @param mixed $id + * @param array $columns + * @return \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model */ - public function create(array $attributes = []) + public function findOrNew($id, $columns = ['*']) { - return tap($this->related->newInstance($attributes), function ($instance) { - $this->setForeignAttributesForCreate($instance); + if (is_null($instance = $this->find($id, $columns))) { + $instance = $this->related->newInstance(); - $instance->save(); + $instance->setAttribute($this->getPlainForeignKey(), $this->getParentKey()); + } - $this->applyInverseRelationToModel($instance); - }); + return $instance; } /** - * Create a new instance of the related model without raising any events to the parent model. + * Get the first related model record matching the attributes or instantiate it. * * @param array $attributes - * @return TRelatedModel + * @return \Illuminate\Database\Eloquent\Model */ - public function createQuietly(array $attributes = []) + public function firstOrNew(array $attributes) { - return Model::withoutEvents(fn () => $this->create($attributes)); - } + if (is_null($instance = $this->where($attributes)->first())) { + $instance = $this->related->newInstance($attributes); - /** - * Create a new instance of the related model. Allow mass-assignment. - * - * @param array $attributes - * @return TRelatedModel - */ - public function forceCreate(array $attributes = []) - { - $attributes[$this->getForeignKeyName()] = $this->getParentKey(); + $instance->setAttribute($this->getPlainForeignKey(), $this->getParentKey()); + } - return $this->applyInverseRelationToModel($this->related->forceCreate($attributes)); + return $instance; } /** - * Create a new instance of the related model with mass assignment without raising model events. + * Get the first related record matching the attributes or create it. * * @param array $attributes - * @return TRelatedModel - */ - public function forceCreateQuietly(array $attributes = []) - { - return Model::withoutEvents(fn () => $this->forceCreate($attributes)); - } - - /** - * Create a Collection of new instances of the related model. - * - * @param iterable $records - * @return \Illuminate\Database\Eloquent\Collection + * @return \Illuminate\Database\Eloquent\Model */ - public function createMany(iterable $records) + public function firstOrCreate(array $attributes) { - $instances = $this->related->newCollection(); - - foreach ($records as $record) { - $instances->push($this->create($record)); + if (is_null($instance = $this->where($attributes)->first())) { + $instance = $this->create($attributes); } - return $instances; + return $instance; } /** - * Create a Collection of new instances of the related model without raising any events to the parent model. + * Create or update a related record matching the attributes, and fill it with values. * - * @param iterable $records - * @return \Illuminate\Database\Eloquent\Collection + * @param array $attributes + * @param array $values + * @return \Illuminate\Database\Eloquent\Model */ - public function createManyQuietly(iterable $records) + public function updateOrCreate(array $attributes, array $values = []) { - return Model::withoutEvents(fn () => $this->createMany($records)); - } + $instance = $this->firstOrNew($attributes); - /** - * Create a Collection of new instances of the related model, allowing mass-assignment. - * - * @param iterable $records - * @return \Illuminate\Database\Eloquent\Collection - */ - public function forceCreateMany(iterable $records) - { - $instances = $this->related->newCollection(); + $instance->fill($values); - foreach ($records as $record) { - $instances->push($this->forceCreate($record)); - } + $instance->save(); - return $instances; - } - - /** - * Create a Collection of new instances of the related model, allowing mass-assignment and without raising any events to the parent model. - * - * @param iterable $records - * @return \Illuminate\Database\Eloquent\Collection - */ - public function forceCreateManyQuietly(iterable $records) - { - return Model::withoutEvents(fn () => $this->forceCreateMany($records)); + return $instance; } /** - * Set the foreign ID for creating a related model. + * Create a new instance of the related model. * - * @param TRelatedModel $model - * @return void + * @param array $attributes + * @return \Illuminate\Database\Eloquent\Model */ - protected function setForeignAttributesForCreate(Model $model) + public function create(array $attributes) { - $model->setAttribute($this->getForeignKeyName(), $this->getParentKey()); + // Here we will set the raw attributes to avoid hitting the "fill" method so + // that we do not have to worry about a mass accessor rules blocking sets + // on the models. Otherwise, some of these attributes will not get set. + $instance = $this->related->newInstance($attributes); - foreach ($this->getQuery()->pendingAttributes as $key => $value) { - $attributes ??= $model->getAttributes(); + $instance->setAttribute($this->getPlainForeignKey(), $this->getParentKey()); - if (! array_key_exists($key, $attributes)) { - $model->setAttribute($key, $value); - } - } + $instance->save(); - $this->applyInverseRelationToModel($model); - } - - /** @inheritDoc */ - public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) - { - if ($query->getQuery()->from == $parentQuery->getQuery()->from) { - return $this->getRelationExistenceQueryForSelfRelation($query, $parentQuery, $columns); - } - - return parent::getRelationExistenceQuery($query, $parentQuery, $columns); + return $instance; } /** - * Add the constraints for a relationship query on the same table. + * Create an array of new instances of the related model. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param \Illuminate\Database\Eloquent\Builder $parentQuery - * @param mixed $columns - * @return \Illuminate\Database\Eloquent\Builder + * @param array $records + * @return array */ - public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*']) + public function createMany(array $records) { - $query->from($query->getModel()->getTable().' as '.$hash = $this->getRelationCountHash()); + $instances = []; - $query->getModel()->setTable($hash); - - return $query->select($columns)->whereColumn( - $this->getQualifiedParentKeyName(), '=', $hash.'.'.$this->getForeignKeyName() - ); - } + foreach ($records as $record) { + $instances[] = $this->create($record); + } - /** - * Alias to set the "limit" value of the query. - * - * @param int $value - * @return $this - */ - public function take($value) - { - return $this->limit($value); + return $instances; } /** - * Set the "limit" value of the query. + * Perform an update on all the related models. * - * @param int $value - * @return $this + * @param array $attributes + * @return int */ - public function limit($value) + public function update(array $attributes) { - if ($this->parent->exists) { - $this->query->limit($value); - } else { - $this->query->groupLimit($value, $this->getExistenceCompareKey()); + if ($this->related->usesTimestamps()) { + $attributes[$this->relatedUpdatedAt()] = $this->related->freshTimestampString(); } - return $this; + return $this->query->update($attributes); } /** @@ -566,29 +358,19 @@ public function limit($value) * * @return string */ - public function getExistenceCompareKey() + public function getHasCompareKey() { - return $this->getQualifiedForeignKeyName(); + return $this->getForeignKey(); } /** - * Get the key value of the parent's local key. - * - * @return mixed - */ - public function getParentKey() - { - return $this->parent->getAttribute($this->localKey); - } - - /** - * Get the fully-qualified parent key name. + * Get the foreign key for the relationship. * * @return string */ - public function getQualifiedParentKeyName() + public function getForeignKey() { - return $this->parent->qualifyColumn($this->localKey); + return $this->foreignKey; } /** @@ -596,30 +378,30 @@ public function getQualifiedParentKeyName() * * @return string */ - public function getForeignKeyName() + public function getPlainForeignKey() { - $segments = explode('.', $this->getQualifiedForeignKeyName()); + $segments = explode('.', $this->getForeignKey()); - return array_last($segments); + return $segments[count($segments) - 1]; } /** - * Get the foreign key for the relationship. + * Get the key value of the parent's local key. * - * @return string + * @return mixed */ - public function getQualifiedForeignKeyName() + public function getParentKey() { - return $this->foreignKey; + return $this->parent->getAttribute($this->localKey); } /** - * Get the local key for the relationship. + * Get the fully qualified parent key name. * * @return string */ - public function getLocalKeyName() + public function getQualifiedParentKeyName() { - return $this->localKey; + return $this->parent->getTable().'.'.$this->localKey; } } diff --git a/Eloquent/Relations/HasOneOrManyThrough.php b/Eloquent/Relations/HasOneOrManyThrough.php deleted file mode 100644 index 1636be0640..0000000000 --- a/Eloquent/Relations/HasOneOrManyThrough.php +++ /dev/null @@ -1,872 +0,0 @@ - - */ -abstract class HasOneOrManyThrough extends Relation -{ - use InteractsWithDictionary; - - /** - * The "through" parent model instance. - * - * @var TIntermediateModel - */ - protected $throughParent; - - /** - * The far parent model instance. - * - * @var TDeclaringModel - */ - protected $farParent; - - /** - * The near key on the relationship. - * - * @var string - */ - protected $firstKey; - - /** - * The far key on the relationship. - * - * @var string - */ - protected $secondKey; - - /** - * The local key on the relationship. - * - * @var string - */ - protected $localKey; - - /** - * The local key on the intermediary model. - * - * @var string - */ - protected $secondLocalKey; - - /** - * Create a new has many through relationship instance. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param TDeclaringModel $farParent - * @param TIntermediateModel $throughParent - * @param string $firstKey - * @param string $secondKey - * @param string $localKey - * @param string $secondLocalKey - */ - public function __construct(Builder $query, Model $farParent, Model $throughParent, $firstKey, $secondKey, $localKey, $secondLocalKey) - { - $this->localKey = $localKey; - $this->firstKey = $firstKey; - $this->secondKey = $secondKey; - $this->farParent = $farParent; - $this->throughParent = $throughParent; - $this->secondLocalKey = $secondLocalKey; - - parent::__construct($query, $throughParent); - } - - /** - * Set the base constraints on the relation query. - * - * @return void - */ - public function addConstraints() - { - $query = $this->getRelationQuery(); - - $localValue = $this->farParent[$this->localKey]; - - $this->performJoin($query); - - if (static::$constraints) { - $query->where($this->getQualifiedFirstKeyName(), '=', $localValue); - } - } - - /** - * Set the join clause on the query. - * - * @param \Illuminate\Database\Eloquent\Builder|null $query - * @return void - */ - protected function performJoin(?Builder $query = null) - { - $query ??= $this->query; - - $farKey = $this->getQualifiedFarKeyName(); - - $query->join($this->throughParent->getTable(), $this->getQualifiedParentKeyName(), '=', $farKey); - - if ($this->throughParentSoftDeletes()) { - $query->withGlobalScope('SoftDeletableHasManyThrough', function ($query) { - $query->whereNull($this->throughParent->getQualifiedDeletedAtColumn()); - }); - } - } - - /** - * Get the fully-qualified parent key name. - * - * @return string - */ - public function getQualifiedParentKeyName() - { - return $this->parent->qualifyColumn($this->secondLocalKey); - } - - /** - * Determine whether "through" parent of the relation uses Soft Deletes. - * - * @return bool - */ - public function throughParentSoftDeletes() - { - return $this->throughParent::isSoftDeletable(); - } - - /** - * Indicate that trashed "through" parents should be included in the query. - * - * @return $this - */ - public function withTrashedParents() - { - $this->query->withoutGlobalScope('SoftDeletableHasManyThrough'); - - return $this; - } - - /** @inheritDoc */ - public function addEagerConstraints(array $models) - { - $whereIn = $this->whereInMethod($this->farParent, $this->localKey); - - $this->whereInEager( - $whereIn, - $this->getQualifiedFirstKeyName(), - $this->getKeys($models, $this->localKey), - $this->getRelationQuery(), - ); - } - - /** - * Build model dictionary keyed by the relation's foreign key. - * - * @param \Illuminate\Database\Eloquent\Collection $results - * @return array> - */ - protected function buildDictionary(EloquentCollection $results) - { - $dictionary = []; - - $isAssociative = Arr::isAssoc($results->all()); - - // First we will create a dictionary of models keyed by the foreign key of the - // relationship as this will allow us to quickly access all of the related - // models without having to do nested looping which will be quite slow. - foreach ($results as $key => $result) { - if ($isAssociative) { - $dictionary[$result->laravel_through_key][$key] = $result; - } else { - $dictionary[$result->laravel_through_key][] = $result; - } - } - - return $dictionary; - } - - /** - * Get the first related model record matching the attributes or instantiate it. - * - * @param array $attributes - * @param array $values - * @return TRelatedModel - */ - public function firstOrNew(array $attributes = [], array $values = []) - { - if (! is_null($instance = $this->where($attributes)->first())) { - return $instance; - } - - return $this->related->newInstance(array_merge($attributes, $values)); - } - - /** - * Get the first record matching the attributes. If the record is not found, create it. - * - * @param array $attributes - * @param (\Closure(): array)|array $values - * @return TRelatedModel - */ - public function firstOrCreate(array $attributes = [], Closure|array $values = []) - { - if (! is_null($instance = (clone $this)->where($attributes)->first())) { - return $instance; - } - - return $this->createOrFirst(array_merge($attributes, value($values))); - } - - /** - * Attempt to create the record. If a unique constraint violation occurs, attempt to find the matching record. - * - * @param array $attributes - * @param (\Closure(): array)|array $values - * @return TRelatedModel - * - * @throws \Illuminate\Database\UniqueConstraintViolationException - */ - public function createOrFirst(array $attributes = [], Closure|array $values = []) - { - try { - return $this->getQuery()->withSavepointIfNeeded(fn () => $this->create(array_merge($attributes, value($values)))); - } catch (UniqueConstraintViolationException $exception) { - return $this->where($attributes)->first() ?? throw $exception; - } - } - - /** - * Create or update a related record matching the attributes, and fill it with values. - * - * @param array $attributes - * @param array $values - * @return TRelatedModel - */ - public function updateOrCreate(array $attributes, array $values = []) - { - return tap($this->firstOrCreate($attributes, $values), function ($instance) use ($values) { - if (! $instance->wasRecentlyCreated) { - $instance->fill($values)->save(); - } - }); - } - - /** - * 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 TRelatedModel|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 related model. - * - * @param array $columns - * @return TRelatedModel|null - */ - public function first($columns = ['*']) - { - $results = $this->limit(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 TRelatedModel - * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - */ - 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. - * - * @template TValue - * - * @param (\Closure(): TValue)|list $columns - * @param (\Closure(): TValue)|null $callback - * @return TRelatedModel|TValue - */ - 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(); - } - - /** - * Find a related model by its primary key. - * - * @param mixed $id - * @param array $columns - * @return ($id is (\Illuminate\Contracts\Support\Arrayable|array) ? \Illuminate\Database\Eloquent\Collection : TRelatedModel|null) - */ - public function find($id, $columns = ['*']) - { - if (is_array($id) || $id instanceof Arrayable) { - return $this->findMany($id, $columns); - } - - return $this->where( - $this->getRelated()->getQualifiedKeyName(), '=', $id - )->first($columns); - } - - /** - * Find a sole related model by its primary key. - * - * @param mixed $id - * @param array $columns - * @return TRelatedModel - * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - * @throws \Illuminate\Database\MultipleRecordsFoundException - */ - public function findSole($id, $columns = ['*']) - { - return $this->where( - $this->getRelated()->getQualifiedKeyName(), '=', $id - )->sole($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->whereIn( - $this->getRelated()->getQualifiedKeyName(), $ids - )->get($columns); - } - - /** - * Find a related model by its primary key or throw an exception. - * - * @param mixed $id - * @param array $columns - * @return ($id is (\Illuminate\Contracts\Support\Arrayable|array) ? \Illuminate\Database\Eloquent\Collection : TRelatedModel) - * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - */ - 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. - * - * @template TValue - * - * @param mixed $id - * @param (\Closure(): TValue)|list|string $columns - * @param (\Closure(): TValue)|null $callback - * @return ( - * $id is (\Illuminate\Contracts\Support\Arrayable|array) - * ? \Illuminate\Database\Eloquent\Collection|TValue - * : TRelatedModel|TValue - * ) - */ - 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(); - } - - /** @inheritDoc */ - public function get($columns = ['*']) - { - $builder = $this->prepareQueryBuilder($columns); - - $models = $builder->getModels(); - - // 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 a paginator for the "select" statement. - * - * @param int|null $perPage - * @param array $columns - * @param string $pageName - * @param int|null $page - * @return \Illuminate\Pagination\LengthAwarePaginator - */ - public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) - { - $this->query->addSelect($this->shouldSelect($columns)); - - return $this->query->paginate($perPage, $columns, $pageName, $page); - } - - /** - * 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 $this->query->simplePaginate($perPage, $columns, $pageName, $page); - } - - /** - * 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 $this->query->cursorPaginate($perPage, $columns, $cursorName, $cursor); - } - - /** - * Set the select clause for the relation query. - * - * @param array $columns - * @return array - */ - protected function shouldSelect(array $columns = ['*']) - { - if ($columns == ['*']) { - $columns = [$this->related->qualifyColumn('*')]; - } - - return array_merge($columns, [$this->getQualifiedFirstKeyName().' as laravel_through_key']); - } - - /** - * 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, $callback); - } - - /** - * 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) - { - $column ??= $this->getRelated()->getQualifiedKeyName(); - - $alias ??= $this->getRelated()->getKeyName(); - - return $this->prepareQueryBuilder()->chunkById($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) - { - $column ??= $this->getRelated()->getQualifiedKeyName(); - - $alias ??= $this->getRelated()->getKeyName(); - - return $this->prepareQueryBuilder()->chunkByIdDesc($count, $callback, $column, $alias); - } - - /** - * 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) - { - $column = $column ?? $this->getRelated()->getQualifiedKeyName(); - - $alias = $alias ?? $this->getRelated()->getKeyName(); - - return $this->prepareQueryBuilder()->eachById($callback, $count, $column, $alias); - } - - /** - * Get a generator for the given query. - * - * @return \Illuminate\Support\LazyCollection - */ - public function cursor() - { - return $this->prepareQueryBuilder()->cursor(); - } - - /** - * 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); - } - - /** - * 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()->getQualifiedKeyName(); - - $alias ??= $this->getRelated()->getKeyName(); - - return $this->prepareQueryBuilder()->lazyById($chunkSize, $column, $alias); - } - - /** - * 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()->getQualifiedKeyName(); - - $alias ??= $this->getRelated()->getKeyName(); - - return $this->prepareQueryBuilder()->lazyByIdDesc($chunkSize, $column, $alias); - } - - /** - * Prepare the query builder for query execution. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Builder - */ - protected function prepareQueryBuilder($columns = ['*']) - { - $builder = $this->query->applyScopes(); - - return $builder->addSelect( - $this->shouldSelect($builder->getQuery()->columns ? [] : $columns) - ); - } - - /** @inheritDoc */ - public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) - { - if ($parentQuery->getQuery()->from === $query->getQuery()->from) { - return $this->getRelationExistenceQueryForSelfRelation($query, $parentQuery, $columns); - } - - if ($parentQuery->getQuery()->from === $this->throughParent->getTable()) { - return $this->getRelationExistenceQueryForThroughSelfRelation($query, $parentQuery, $columns); - } - - $this->performJoin($query); - - return $query->select($columns)->whereColumn( - $this->getQualifiedLocalKeyName(), '=', $this->getQualifiedFirstKeyName() - ); - } - - /** - * Add the constraints for a relationship query on the same table. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param \Illuminate\Database\Eloquent\Builder $parentQuery - * @param mixed $columns - * @return \Illuminate\Database\Eloquent\Builder - */ - public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*']) - { - $query->from($query->getModel()->getTable().' as '.$hash = $this->getRelationCountHash()); - - $query->join($this->throughParent->getTable(), $this->getQualifiedParentKeyName(), '=', $hash.'.'.$this->secondKey); - - if ($this->throughParentSoftDeletes()) { - $query->whereNull($this->throughParent->getQualifiedDeletedAtColumn()); - } - - $query->getModel()->setTable($hash); - - return $query->select($columns)->whereColumn( - $parentQuery->getQuery()->from.'.'.$this->localKey, '=', $this->getQualifiedFirstKeyName() - ); - } - - /** - * Add the constraints for a relationship query on the same table as the through parent. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param \Illuminate\Database\Eloquent\Builder $parentQuery - * @param mixed $columns - * @return \Illuminate\Database\Eloquent\Builder - */ - public function getRelationExistenceQueryForThroughSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*']) - { - $table = $this->throughParent->getTable().' as '.$hash = $this->getRelationCountHash(); - - $query->join($table, $hash.'.'.$this->secondLocalKey, '=', $this->getQualifiedFarKeyName()); - - if ($this->throughParentSoftDeletes()) { - $query->whereNull($hash.'.'.$this->throughParent->getDeletedAtColumn()); - } - - return $query->select($columns)->whereColumn( - $parentQuery->getQuery()->from.'.'.$this->localKey, '=', $hash.'.'.$this->firstKey - ); - } - - /** - * 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->farParent->exists) { - $this->query->limit($value); - } else { - $column = $this->getQualifiedFirstKeyName(); - - $grammar = $this->query->getQuery()->getGrammar(); - - if ($grammar instanceof MySqlGrammar && $grammar->useLegacyGroupLimit($this->query->getQuery())) { - $column = 'laravel_through_key'; - } - - $this->query->groupLimit($value, $column); - } - - return $this; - } - - /** - * Get the qualified foreign key on the related model. - * - * @return string - */ - public function getQualifiedFarKeyName() - { - return $this->getQualifiedForeignKeyName(); - } - - /** - * Get the foreign key on the "through" model. - * - * @return string - */ - public function getFirstKeyName() - { - return $this->firstKey; - } - - /** - * Get the qualified foreign key on the "through" model. - * - * @return string - */ - public function getQualifiedFirstKeyName() - { - return $this->throughParent->qualifyColumn($this->firstKey); - } - - /** - * Get the foreign key on the related model. - * - * @return string - */ - public function getForeignKeyName() - { - return $this->secondKey; - } - - /** - * Get the qualified foreign key on the related model. - * - * @return string - */ - public function getQualifiedForeignKeyName() - { - return $this->related->qualifyColumn($this->secondKey); - } - - /** - * Get the local key on the far parent model. - * - * @return string - */ - public function getLocalKeyName() - { - return $this->localKey; - } - - /** - * Get the qualified local key on the far parent model. - * - * @return string - */ - public function getQualifiedLocalKeyName() - { - return $this->farParent->qualifyColumn($this->localKey); - } - - /** - * Get the local key on the intermediary model. - * - * @return string - */ - public function getSecondLocalKeyName() - { - return $this->secondLocalKey; - } -} diff --git a/Eloquent/Relations/HasOneThrough.php b/Eloquent/Relations/HasOneThrough.php deleted file mode 100644 index 27c3a9da5f..0000000000 --- a/Eloquent/Relations/HasOneThrough.php +++ /dev/null @@ -1,124 +0,0 @@ - - */ -class HasOneThrough extends HasOneOrManyThrough implements SupportsPartialRelations -{ - use ComparesRelatedModels, CanBeOneOfMany, InteractsWithDictionary, SupportsDefaultModels; - - /** @inheritDoc */ - public function getResults() - { - if (is_null($this->getParentKey())) { - return $this->getDefaultFor($this->farParent); - } - - return $this->first() ?: $this->getDefaultFor($this->farParent); - } - - /** @inheritDoc */ - public function initRelation(array $models, $relation) - { - foreach ($models as $model) { - $model->setRelation($relation, $this->getDefaultFor($model)); - } - - return $models; - } - - /** @inheritDoc */ - public function match(array $models, EloquentCollection $results, $relation) - { - $dictionary = $this->buildDictionary($results); - - // Once we have the dictionary we can simply spin through the parent models to - // link them up with their children using the keyed dictionary to make the - // matching very convenient and easy work. Then we'll just return them. - foreach ($models as $model) { - $key = $this->getDictionaryKey($model->getAttribute($this->localKey)); - - if ($key !== null && isset($dictionary[$key])) { - $value = $dictionary[$key]; - - $model->setRelation( - $relation, reset($value) - ); - } - } - - return $models; - } - - /** @inheritDoc */ - public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) - { - if ($this->isOneOfMany()) { - $this->mergeOneOfManyJoinsTo($query); - } - - return parent::getRelationExistenceQuery($query, $parentQuery, $columns); - } - - /** @inheritDoc */ - public function addOneOfManySubQueryConstraints(Builder $query, $column = null, $aggregate = null) - { - $query->addSelect([$this->getQualifiedFirstKeyName()]); - - // We need to join subqueries that aren't the inner-most subquery which is joined in the CanBeOneOfMany::ofMany method... - if ($this->getOneOfManySubQuery() !== null) { - $this->performJoin($query); - } - } - - /** @inheritDoc */ - public function getOneOfManySubQuerySelectColumns() - { - return [$this->getQualifiedFirstKeyName()]; - } - - /** @inheritDoc */ - public function addOneOfManyJoinSubQueryConstraints(JoinClause $join) - { - $join->on($this->qualifySubSelectColumn($this->firstKey), '=', $this->getQualifiedFirstKeyName()); - } - - /** - * Make a new related instance for the given model. - * - * @param TDeclaringModel $parent - * @return TRelatedModel - */ - public function newRelatedInstanceFor(Model $parent) - { - return $this->related->newInstance(); - } - - /** @inheritDoc */ - protected function getRelatedKeyFrom(Model $model) - { - return $model->getAttribute($this->getForeignKeyName()); - } - - /** @inheritDoc */ - public function getParentKey() - { - return $this->farParent->getAttribute($this->localKey); - } -} diff --git a/Eloquent/Relations/MorphMany.php b/Eloquent/Relations/MorphMany.php index fd7830956d..e2a5c5a37c 100755 --- a/Eloquent/Relations/MorphMany.php +++ b/Eloquent/Relations/MorphMany.php @@ -2,48 +2,27 @@ namespace Illuminate\Database\Eloquent\Relations; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; +use Illuminate\Database\Eloquent\Collection; -/** - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * - * @extends \Illuminate\Database\Eloquent\Relations\MorphOneOrMany> - */ class MorphMany extends MorphOneOrMany { /** - * Convert the relationship to a "morph one" relationship. + * Get the results of the relationship. * - * @return \Illuminate\Database\Eloquent\Relations\MorphOne + * @return mixed */ - public function one() - { - return MorphOne::noConstraints(fn () => tap( - new MorphOne( - $this->getQuery(), - $this->getParent(), - $this->morphType, - $this->foreignKey, - $this->localKey - ), - function ($morphOne) { - if ($inverse = $this->getInverseRelationship()) { - $morphOne->inverse($inverse); - } - } - )); - } - - /** @inheritDoc */ public function getResults() { - return ! is_null($this->getParentKey()) - ? $this->query->get() - : $this->related->newCollection(); + return $this->query->get(); } - /** @inheritDoc */ + /** + * 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) { @@ -53,17 +32,16 @@ public function initRelation(array $models, $relation) return $models; } - /** @inheritDoc */ - public function match(array $models, EloquentCollection $results, $relation) + /** + * 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) { return $this->matchMany($models, $results, $relation); } - - /** @inheritDoc */ - public function forceCreate(array $attributes = []) - { - $attributes[$this->getMorphType()] = $this->morphClass; - - return parent::forceCreate($attributes); - } } diff --git a/Eloquent/Relations/MorphOne.php b/Eloquent/Relations/MorphOne.php index fa3632efb3..339a68c350 100755 --- a/Eloquent/Relations/MorphOne.php +++ b/Eloquent/Relations/MorphOne.php @@ -2,121 +2,46 @@ namespace Illuminate\Database\Eloquent\Relations; -use Illuminate\Contracts\Database\Eloquent\SupportsPartialRelations; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; -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; +use Illuminate\Database\Eloquent\Collection; -/** - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * - * @extends \Illuminate\Database\Eloquent\Relations\MorphOneOrMany - */ -class MorphOne extends MorphOneOrMany implements SupportsPartialRelations +class MorphOne extends MorphOneOrMany { - use CanBeOneOfMany, ComparesRelatedModels, SupportsDefaultModels; - - /** @inheritDoc */ - public function getResults() - { - if (is_null($this->getParentKey())) { - return $this->getDefaultFor($this->parent); - } - - return $this->query->first() ?: $this->getDefaultFor($this->parent); - } - - /** @inheritDoc */ - public function initRelation(array $models, $relation) - { - foreach ($models as $model) { - $model->setRelation($relation, $this->getDefaultFor($model)); - } - - return $models; - } - - /** @inheritDoc */ - public function match(array $models, EloquentCollection $results, $relation) - { - return $this->matchOne($models, $results, $relation); - } - - /** @inheritDoc */ - public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) - { - if ($this->isOneOfMany()) { - $this->mergeOneOfManyJoinsTo($query); - } - - return parent::getRelationExistenceQuery($query, $parentQuery, $columns); - } - /** - * Add constraints for inner join subselect for one of many relationships. + * Get the results of the relationship. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param string|null $column - * @param string|null $aggregate - * @return void + * @return mixed */ - 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\Query\JoinClause $join - * @return void - */ - public function addOneOfManyJoinSubQueryConstraints(JoinClause $join) + public function getResults() { - $join - ->on($this->qualifySubSelectColumn($this->morphType), '=', $this->qualifyRelatedColumn($this->morphType)) - ->on($this->qualifySubSelectColumn($this->foreignKey), '=', $this->qualifyRelatedColumn($this->foreignKey)); + return $this->query->first(); } /** - * Make a new related instance for the given model. + * Initialize the relation on a set of models. * - * @param TDeclaringModel $parent - * @return TRelatedModel + * @param array $models + * @param string $relation + * @return array */ - public function newRelatedInstanceFor(Model $parent) + public function initRelation(array $models, $relation) { - return tap($this->related->newInstance(), function ($instance) use ($parent) { - $instance->setAttribute($this->getForeignKeyName(), $parent->{$this->localKey}) - ->setAttribute($this->getMorphType(), $this->morphClass); + foreach ($models as $model) { + $model->setRelation($relation, null); + } - $this->applyInverseRelationToModel($instance, $parent); - }); + return $models; } /** - * Get the value of the model's foreign key. + * Match the eagerly loaded results to their parents. * - * @param TRelatedModel $model - * @return int|string + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results + * @param string $relation + * @return array */ - protected function getRelatedKeyFrom(Model $model) + public function match(array $models, Collection $results, $relation) { - return $model->getAttribute($this->getForeignKeyName()); + return $this->matchOne($models, $results, $relation); } } diff --git a/Eloquent/Relations/MorphOneOrMany.php b/Eloquent/Relations/MorphOneOrMany.php index 7c32befcdf..3ba9b11d34 100755 --- a/Eloquent/Relations/MorphOneOrMany.php +++ b/Eloquent/Relations/MorphOneOrMany.php @@ -2,17 +2,9 @@ namespace Illuminate\Database\Eloquent\Relations; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Str; - -/** - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * @template TResult - * - * @extends \Illuminate\Database\Eloquent\Relations\HasOneOrMany - */ +use Illuminate\Database\Eloquent\Builder; + abstract class MorphOneOrMany extends HasOneOrMany { /** @@ -25,18 +17,19 @@ abstract class MorphOneOrMany extends HasOneOrMany /** * The class name of the parent model. * - * @var class-string + * @var string */ protected $morphClass; /** * Create a new morph one or many relationship instance. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param TDeclaringModel $parent + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Model $parent * @param string $type * @param string $id * @param string $localKey + * @return void */ public function __construct(Builder $query, Model $parent, $type, $id, $localKey) { @@ -55,84 +48,158 @@ public function __construct(Builder $query, Model $parent, $type, $id, $localKey public function addConstraints() { if (static::$constraints) { - $this->getRelationQuery()->where($this->morphType, $this->morphClass); - parent::addConstraints(); + + $this->query->where($this->morphType, $this->morphClass); } } - /** @inheritDoc */ + /** + * Get the relationship query. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parent + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder + */ + public function getRelationQuery(Builder $query, Builder $parent, $columns = ['*']) + { + $query = parent::getRelationQuery($query, $parent, $columns); + + return $query->where($this->morphType, $this->morphClass); + } + + /** + * Set the constraints for an eager load of the relation. + * + * @param array $models + * @return void + */ public function addEagerConstraints(array $models) { parent::addEagerConstraints($models); - $this->getRelationQuery()->where($this->morphType, $this->morphClass); + $this->query->where($this->morphType, $this->morphClass); } /** - * Create a new instance of the related model. Allow mass-assignment. + * Attach a model instance to the parent model. * - * @param array $attributes - * @return TRelatedModel + * @param \Illuminate\Database\Eloquent\Model $model + * @return \Illuminate\Database\Eloquent\Model */ - public function forceCreate(array $attributes = []) + public function save(Model $model) { - $attributes[$this->getForeignKeyName()] = $this->getParentKey(); - $attributes[$this->getMorphType()] = $this->morphClass; + $model->setAttribute($this->getPlainMorphType(), $this->morphClass); - return $this->applyInverseRelationToModel($this->related->forceCreate($attributes)); + return parent::save($model); } /** - * Set the foreign ID and type for creating a related model. + * Find a related model by its primary key or return new instance of the related model. * - * @param TRelatedModel $model - * @return void + * @param mixed $id + * @param array $columns + * @return \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model */ - protected function setForeignAttributesForCreate(Model $model) + public function findOrNew($id, $columns = ['*']) { - $model->{$this->getForeignKeyName()} = $this->getParentKey(); + if (is_null($instance = $this->find($id, $columns))) { + $instance = $this->related->newInstance(); - $model->{$this->getMorphType()} = $this->morphClass; + // When saving a polymorphic relationship, we need to set not only the foreign + // key, but also the foreign key type, which is typically the class name of + // the parent model. This makes the polymorphic item unique in the table. + $this->setForeignAttributesForCreate($instance); + } - foreach ($this->getQuery()->pendingAttributes as $key => $value) { - $attributes ??= $model->getAttributes(); + return $instance; + } - if (! array_key_exists($key, $attributes)) { - $model->setAttribute($key, $value); - } + /** + * Get the first related model record matching the attributes or instantiate it. + * + * @param array $attributes + * @return \Illuminate\Database\Eloquent\Model + */ + public function firstOrNew(array $attributes) + { + if (is_null($instance = $this->where($attributes)->first())) { + $instance = $this->related->newInstance($attributes); + + // When saving a polymorphic relationship, we need to set not only the foreign + // key, but also the foreign key type, which is typically the class name of + // the parent model. This makes the polymorphic item unique in the table. + $this->setForeignAttributesForCreate($instance); } - $this->applyInverseRelationToModel($model); + return $instance; } /** - * Insert new records or update the existing ones. + * Get the first related record matching the attributes or create it. * - * @param array $values - * @param array|string $uniqueBy - * @param array|null $update - * @return int + * @param array $attributes + * @return \Illuminate\Database\Eloquent\Model */ - public function upsert(array $values, $uniqueBy, $update = null) + public function firstOrCreate(array $attributes) { - if (! empty($values) && ! is_array(array_first($values))) { - $values = [$values]; + if (is_null($instance = $this->where($attributes)->first())) { + $instance = $this->create($attributes); } - foreach ($values as $key => $value) { - $values[$key][$this->getMorphType()] = $this->getMorphClass(); - } + return $instance; + } - return parent::upsert($values, $uniqueBy, $update); + /** + * Create or update a related record matching the attributes, and fill it with values. + * + * @param array $attributes + * @param array $values + * @return \Illuminate\Database\Eloquent\Model + */ + public function updateOrCreate(array $attributes, array $values = []) + { + $instance = $this->firstOrNew($attributes); + + $instance->fill($values); + + $instance->save(); + + return $instance; + } + + /** + * Create a new instance of the related model. + * + * @param array $attributes + * @return \Illuminate\Database\Eloquent\Model + */ + public function create(array $attributes) + { + $instance = $this->related->newInstance($attributes); + + // When saving a polymorphic relationship, we need to set not only the foreign + // key, but also the foreign key type, which is typically the class name of + // the parent model. This makes the polymorphic item unique in the table. + $this->setForeignAttributesForCreate($instance); + + $instance->save(); + + return $instance; } - /** @inheritDoc */ - public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) + /** + * Set the foreign ID and type for creating a related model. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return void + */ + protected function setForeignAttributesForCreate(Model $model) { - return parent::getRelationExistenceQuery($query, $parentQuery, $columns)->where( - $query->qualifyColumn($this->getMorphType()), $this->morphClass - ); + $model->{$this->getPlainForeignKey()} = $this->getParentKey(); + + $model->{last(explode('.', $this->morphType))} = $this->morphClass; } /** @@ -140,7 +207,7 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, * * @return string */ - public function getQualifiedMorphType() + public function getMorphType() { return $this->morphType; } @@ -150,7 +217,7 @@ public function getQualifiedMorphType() * * @return string */ - public function getMorphType() + public function getPlainMorphType() { return last(explode('.', $this->morphType)); } @@ -158,23 +225,10 @@ public function getMorphType() /** * Get the class name of the parent model. * - * @return class-string + * @return string */ public function getMorphClass() { return $this->morphClass; } - - /** - * Get the possible inverse relations for the parent model. - * - * @return array - */ - protected function getPossibleInverseRelations(): array - { - return array_unique([ - Str::beforeLast($this->getMorphType(), '_type'), - ...parent::getPossibleInverseRelations(), - ]); - } } diff --git a/Eloquent/Relations/MorphPivot.php b/Eloquent/Relations/MorphPivot.php index 01aea33950..b7a2f34195 100644 --- a/Eloquent/Relations/MorphPivot.php +++ b/Eloquent/Relations/MorphPivot.php @@ -2,6 +2,8 @@ namespace Illuminate\Database\Eloquent\Relations; +use Illuminate\Database\Eloquent\Builder; + class MorphPivot extends Pivot { /** @@ -18,36 +20,23 @@ class MorphPivot extends Pivot * * Explicitly define this so it's not included in saved attributes. * - * @var class-string + * @var string */ protected $morphClass; /** * Set the keys for a save update query. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @return \Illuminate\Database\Eloquent\Builder + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder */ - protected function setKeysForSaveQuery($query) + protected function setKeysForSaveQuery(Builder $query) { $query->where($this->morphType, $this->morphClass); return parent::setKeysForSaveQuery($query); } - /** - * Set the keys for a select query. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @return \Illuminate\Database\Eloquent\Builder - */ - protected function setKeysForSelectQuery($query) - { - $query->where($this->morphType, $this->morphClass); - - return parent::setKeysForSelectQuery($query); - } - /** * Delete the pivot model record from the database. * @@ -55,33 +44,11 @@ protected function setKeysForSelectQuery($query) */ public function delete() { - if (isset($this->attributes[$this->getKeyName()])) { - return (int) parent::delete(); - } - - if ($this->fireModelEvent('deleting') === false) { - return 0; - } - $query = $this->getDeleteQuery(); $query->where($this->morphType, $this->morphClass); - return tap($query->delete(), function () { - $this->exists = false; - - $this->fireModelEvent('deleted', false); - }); - } - - /** - * Get the morph type for the pivot. - * - * @return string - */ - public function getMorphType() - { - return $this->morphType; + return $query->delete(); } /** @@ -100,7 +67,7 @@ public function setMorphType($morphType) /** * Set the morph class for the pivot. * - * @param class-string $morphClass + * @param string $morphClass * @return \Illuminate\Database\Eloquent\Relations\MorphPivot */ public function setMorphClass($morphClass) @@ -109,76 +76,4 @@ public function setMorphClass($morphClass) return $this; } - - /** - * Get the queueable identity for the entity. - * - * @return mixed - */ - public function getQueueableId() - { - if (isset($this->attributes[$this->getKeyName()])) { - return $this->getKey(); - } - - return sprintf( - '%s:%s:%s:%s:%s:%s', - $this->foreignKey, $this->getAttribute($this->foreignKey), - $this->relatedKey, $this->getAttribute($this->relatedKey), - $this->morphType, $this->morphClass - ); - } - - /** - * Get a new query to restore one or more models by their queueable IDs. - * - * @param array|int $ids - * @return \Illuminate\Database\Eloquent\Builder - */ - public function newQueryForRestoration($ids) - { - if (is_array($ids)) { - return $this->newQueryForCollectionRestoration($ids); - } - - if (! str_contains($ids, ':')) { - return parent::newQueryForRestoration($ids); - } - - $segments = explode(':', $ids); - - return $this->newQueryWithoutScopes() - ->where($segments[0], $segments[1]) - ->where($segments[2], $segments[3]) - ->where($segments[4], $segments[5]); - } - - /** - * Get a new query to restore multiple models by their queueable IDs. - * - * @param array $ids - * @return \Illuminate\Database\Eloquent\Builder - */ - protected function newQueryForCollectionRestoration(array $ids) - { - $ids = array_values($ids); - - if (! str_contains($ids[0], ':')) { - return parent::newQueryForRestoration($ids); - } - - $query = $this->newQueryWithoutScopes(); - - foreach ($ids as $id) { - $segments = explode(':', $id); - - $query->orWhere(function ($query) use ($segments) { - return $query->where($segments[0], $segments[1]) - ->where($segments[2], $segments[3]) - ->where($segments[4], $segments[5]); - }); - } - - return $query; - } } diff --git a/Eloquent/Relations/MorphTo.php b/Eloquent/Relations/MorphTo.php index 556fcf9cd9..06a488d629 100644 --- a/Eloquent/Relations/MorphTo.php +++ b/Eloquent/Relations/MorphTo.php @@ -2,23 +2,12 @@ namespace Illuminate\Database\Eloquent\Relations; -use BadMethodCallException; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary; -use Illuminate\Support\Arr; - -/** - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * - * @extends \Illuminate\Database\Eloquent\Relations\BelongsTo - */ +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Collection; + class MorphTo extends BelongsTo { - use InteractsWithDictionary; - /** * The type of the polymorphic relation. * @@ -26,17 +15,10 @@ class MorphTo extends BelongsTo */ protected $morphType; - /** - * The associated key on the parent model. - * - * @var string|null - */ - protected $ownerKey; - /** * The models whose relations are being eager loaded. * - * @var \Illuminate\Database\Eloquent\Collection + * @var \Illuminate\Database\Eloquent\Collection */ protected $models; @@ -47,246 +29,193 @@ class MorphTo extends BelongsTo */ protected $dictionary = []; - /** - * A buffer of dynamic calls to query macros. - * - * @var array - */ - protected $macroBuffer = []; - - /** - * A map of relations to load for each individual morph type. - * - * @var array - */ - protected $morphableEagerLoads = []; - - /** - * A map of relationship counts to load for each individual morph type. - * - * @var array - */ - protected $morphableEagerLoadCounts = []; - - /** - * A map of constraints to apply for each individual morph type. + /* + * Indicates if soft-deleted model instances should be fetched. * - * @var array + * @var bool */ - protected $morphableConstraints = []; + protected $withTrashed = false; /** * Create a new morph to relationship instance. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param TDeclaringModel $parent + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Model $parent * @param string $foreignKey - * @param string|null $ownerKey + * @param string $otherKey * @param string $type * @param string $relation + * @return void */ - public function __construct(Builder $query, Model $parent, $foreignKey, $ownerKey, $type, $relation) + public function __construct(Builder $query, Model $parent, $foreignKey, $otherKey, $type, $relation) { $this->morphType = $type; - parent::__construct($query, $parent, $foreignKey, $ownerKey, $relation); + parent::__construct($query, $parent, $foreignKey, $otherKey, $relation); } - /** @inheritDoc */ - #[\Override] + /** + * Get the results of the relationship. + * + * @return mixed + */ + public function getResults() + { + if (! $this->otherKey) { + return; + } + + return $this->query->first(); + } + + /** + * Set the constraints for an eager load of the relation. + * + * @param array $models + * @return void + */ public function addEagerConstraints(array $models) { - $this->buildDictionary($this->models = new EloquentCollection($models)); + $this->buildDictionary($this->models = Collection::make($models)); } /** * Build a dictionary with the models. * - * @param \Illuminate\Database\Eloquent\Collection $models + * @param \Illuminate\Database\Eloquent\Collection $models * @return void */ - protected function buildDictionary(EloquentCollection $models) + protected function buildDictionary(Collection $models) { - $isAssociative = Arr::isAssoc($models->all()); - - foreach ($models as $key => $model) { + foreach ($models as $model) { if ($model->{$this->morphType}) { - $morphTypeKey = $this->getDictionaryKey($model->{$this->morphType}); - $foreignKeyKey = $this->getDictionaryKey($model->{$this->foreignKey}); - - if ($isAssociative) { - $this->dictionary[$morphTypeKey][$foreignKeyKey][$key] = $model; - } else { - $this->dictionary[$morphTypeKey][$foreignKeyKey][] = $model; - } + $this->dictionary[$model->{$this->morphType}][$model->{$this->foreignKey}][] = $model; } } } /** - * Get the results of the relationship. - * - * Called via eager load method of Eloquent query builder. + * Match the eagerly loaded results to their parents. * - * @return \Illuminate\Database\Eloquent\Collection + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results + * @param string $relation + * @return array */ - public function getEager() + public function match(array $models, Collection $results, $relation) { - foreach (array_keys($this->dictionary) as $type) { - $this->matchToMorphParents($type, $this->getResultsByType($type)); - } - - return $this->models; + return $models; } /** - * Get all of the relation results for a type. + * Associate the model instance to the given parent. * - * @param string $type - * @return \Illuminate\Database\Eloquent\Collection + * @param \Illuminate\Database\Eloquent\Model $model + * @return \Illuminate\Database\Eloquent\Model */ - protected function getResultsByType($type) + public function associate($model) { - $instance = $this->createModelByType($type); + $this->parent->setAttribute($this->foreignKey, $model->getKey()); - $ownerKey = $this->ownerKey ?? $instance->getKeyName(); - - $query = $this->replayMacros($instance->newQuery()) - ->mergeConstraintsFrom($this->getQuery()) - ->with(array_merge( - $this->getQuery()->getEagerLoads(), - (array) ($this->morphableEagerLoads[get_class($instance)] ?? []) - )) - ->withCount( - (array) ($this->morphableEagerLoadCounts[get_class($instance)] ?? []) - ); - - if ($callback = ($this->morphableConstraints[get_class($instance)] ?? null)) { - $callback($query); - } + $this->parent->setAttribute($this->morphType, $model->getMorphClass()); - $whereIn = $this->whereInMethod($instance, $ownerKey); - - return $query->{$whereIn}( - $instance->qualifyColumn($ownerKey), $this->gatherKeysByType($type, $instance->getKeyType()) - )->get(); + return $this->parent->setRelation($this->relation, $model); } /** - * Gather all of the foreign keys for a given type. + * Dissociate previously associated model from the given parent. * - * @param string $type - * @param string $keyType - * @return array + * @return \Illuminate\Database\Eloquent\Model */ - protected function gatherKeysByType($type, $keyType) + public function dissociate() { - return $keyType !== 'string' - ? array_keys($this->dictionary[$type]) - : array_map(function ($modelId) { - return (string) $modelId; - }, array_filter(array_keys($this->dictionary[$type]))); + $this->parent->setAttribute($this->foreignKey, null); + + $this->parent->setAttribute($this->morphType, null); + + return $this->parent->setRelation($this->relation, null); } /** - * Create a new model instance by type. + * Get the results of the relationship. * - * @param string $type - * @return TRelatedModel + * Called via eager load method of Eloquent query builder. + * + * @return mixed */ - public function createModelByType($type) + public function getEager() { - $class = Model::getActualClassNameForMorph($type); - - return tap(new $class, function ($instance) { - if (! $instance->getConnectionName()) { - $instance->setConnection($this->getConnection()->getName()); - } - }); - } + foreach (array_keys($this->dictionary) as $type) { + $this->matchToMorphParents($type, $this->getResultsByType($type)); + } - /** @inheritDoc */ - #[\Override] - public function match(array $models, EloquentCollection $results, $relation) - { - return $models; + return $this->models; } /** * Match the results for a given type to their parents. * * @param string $type - * @param \Illuminate\Database\Eloquent\Collection $results + * @param \Illuminate\Database\Eloquent\Collection $results * @return void */ - protected function matchToMorphParents($type, EloquentCollection $results) + protected function matchToMorphParents($type, Collection $results) { foreach ($results as $result) { - $ownerKey = ! is_null($this->ownerKey) ? $this->getDictionaryKey($result->{$this->ownerKey}) : $result->getKey(); - - if (isset($this->dictionary[$type][$ownerKey])) { - foreach ($this->dictionary[$type][$ownerKey] as $model) { - $model->setRelation($this->relationName, $result); + if (isset($this->dictionary[$type][$result->getKey()])) { + foreach ($this->dictionary[$type][$result->getKey()] as $model) { + $model->setRelation($this->relation, $result); } } } } /** - * Associate the model instance to the given parent. + * Get all of the relation results for a type. * - * @param TRelatedModel|null $model - * @return TDeclaringModel + * @param string $type + * @return \Illuminate\Database\Eloquent\Collection */ - #[\Override] - public function associate($model) + protected function getResultsByType($type) { - if ($model instanceof Model) { - $foreignKey = $this->ownerKey && $model->{$this->ownerKey} - ? $this->ownerKey - : $model->getKeyName(); - } + $instance = $this->createModelByType($type); + + $key = $instance->getTable().'.'.$instance->getKeyName(); - $this->parent->setAttribute( - $this->foreignKey, $model instanceof Model ? $model->{$foreignKey} : null - ); + $query = $instance->newQuery(); - $this->parent->setAttribute( - $this->morphType, $model instanceof Model ? $model->getMorphClass() : null - ); + $query = $this->useWithTrashed($query); - return $this->parent->setRelation($this->relationName, $model); + return $query->whereIn($key, $this->gatherKeysByType($type)->all())->get(); } /** - * Dissociate previously associated model from the given parent. + * Gather all of the foreign keys for a given type. * - * @return TDeclaringModel + * @param string $type + * @return array */ - #[\Override] - public function dissociate() + protected function gatherKeysByType($type) { - $this->parent->setAttribute($this->foreignKey, null); + $foreign = $this->foreignKey; - $this->parent->setAttribute($this->morphType, null); + return collect($this->dictionary[$type])->map(function ($models) use ($foreign) { + return head($models)->{$foreign}; - return $this->parent->setRelation($this->relationName, null); + })->values()->unique(); } - /** @inheritDoc */ - #[\Override] - public function touch() + /** + * Create a new model instance by type. + * + * @param string $type + * @return \Illuminate\Database\Eloquent\Model + */ + public function createModelByType($type) { - if (! is_null($this->getParentKey())) { - parent::touch(); - } - } + $class = $this->parent->getActualClassNameForMorph($type); - /** @inheritDoc */ - #[\Override] - protected function newRelatedInstanceFor(Model $parent) - { - return $parent->{$this->getRelationName()}()->getRelated()->newInstance(); + return new $class; } /** @@ -310,153 +239,31 @@ public function getDictionary() } /** - * Specify which relations to load for a given morph type. - * - * @param array $with - * @return $this - */ - public function morphWith(array $with) - { - $this->morphableEagerLoads = array_merge( - $this->morphableEagerLoads, $with - ); - - return $this; - } - - /** - * Specify which relationship counts to load for a given morph type. - * - * @param array $withCount - * @return $this - */ - public function morphWithCount(array $withCount) - { - $this->morphableEagerLoadCounts = array_merge( - $this->morphableEagerLoadCounts, $withCount - ); - - return $this; - } - - /** - * Specify constraints on the query for a given morph type. - * - * @param array $callbacks - * @return $this - */ - public function constrain(array $callbacks) - { - $this->morphableConstraints = array_merge( - $this->morphableConstraints, $callbacks - ); - - return $this; - } - - /** - * Indicate that soft deleted models should be included in the results. + * Fetch soft-deleted model instances with query. * * @return $this */ public function withTrashed() { - $callback = fn ($query) => $query->hasMacro('withTrashed') ? $query->withTrashed() : $query; - - $this->macroBuffer[] = [ - 'method' => 'when', - 'parameters' => [true, $callback], - ]; + $this->withTrashed = true; - return $this->when(true, $callback); - } + $this->query = $this->useWithTrashed($this->query); - /** - * Indicate that soft deleted models should not be included in the results. - * - * @return $this - */ - public function withoutTrashed() - { - $callback = fn ($query) => $query->hasMacro('withoutTrashed') ? $query->withoutTrashed() : $query; - - $this->macroBuffer[] = [ - 'method' => 'when', - 'parameters' => [true, $callback], - ]; - - return $this->when(true, $callback); - } - - /** - * Indicate that only soft deleted models should be included in the results. - * - * @return $this - */ - public function onlyTrashed() - { - $callback = fn ($query) => $query->hasMacro('onlyTrashed') ? $query->onlyTrashed() : $query; - - $this->macroBuffer[] = [ - 'method' => 'when', - 'parameters' => [true, $callback], - ]; - - return $this->when(true, $callback); + return $this; } /** - * Replay stored macro calls on the actual related instance. + * Return trashed models with query if told so. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @return \Illuminate\Database\Eloquent\Builder + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder */ - protected function replayMacros(Builder $query) + protected function useWithTrashed(Builder $query) { - foreach ($this->macroBuffer as $macro) { - $query->{$macro['method']}(...$macro['parameters']); + if ($this->withTrashed && $query->getMacro('withTrashed') !== null) { + return $query->withTrashed(); } return $query; } - - /** @inheritDoc */ - #[\Override] - public function getQualifiedOwnerKeyName() - { - if (is_null($this->ownerKey)) { - return ''; - } - - return parent::getQualifiedOwnerKeyName(); - } - - /** - * Handle dynamic method calls to the relationship. - * - * @param string $method - * @param array $parameters - * @return mixed - */ - public function __call($method, $parameters) - { - try { - $result = parent::__call($method, $parameters); - - if (in_array($method, ['select', 'selectRaw', 'selectSub', 'addSelect', 'withoutGlobalScopes'])) { - $this->macroBuffer[] = compact('method', 'parameters'); - } - - return $result; - } - - // If we tried to call a method that does not exist on the parent Builder instance, - // we'll assume that we want to call a query macro (e.g. withTrashed) that only - // exists on related models. We will just store the call and replay it later. - catch (BadMethodCallException) { - $this->macroBuffer[] = compact('method', 'parameters'); - - return $this; - } - } } diff --git a/Eloquent/Relations/MorphToMany.php b/Eloquent/Relations/MorphToMany.php index dd0680c980..7f124a65be 100644 --- a/Eloquent/Relations/MorphToMany.php +++ b/Eloquent/Relations/MorphToMany.php @@ -2,19 +2,10 @@ namespace Illuminate\Database\Eloquent\Relations; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; -use Illuminate\Support\Collection; - -/** - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * @template TPivotModel of \Illuminate\Database\Eloquent\Relations\Pivot = \Illuminate\Database\Eloquent\Relations\MorphPivot - * @template TAccessor of string = 'pivot' - * - * @extends \Illuminate\Database\Eloquent\Relations\BelongsToMany - */ +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Builder; + class MorphToMany extends BelongsToMany { /** @@ -27,7 +18,7 @@ class MorphToMany extends BelongsToMany /** * The class name of the morph type constraint. * - * @var class-string + * @var string */ protected $morphClass; @@ -43,37 +34,23 @@ class MorphToMany extends BelongsToMany /** * Create a new morph to many relationship instance. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param TDeclaringModel $parent + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Model $parent * @param string $name * @param string $table - * @param string $foreignPivotKey - * @param string $relatedPivotKey - * @param string $parentKey - * @param string $relatedKey - * @param string|null $relationName + * @param string $foreignKey + * @param string $otherKey + * @param string $relationName * @param bool $inverse + * @return void */ - public function __construct( - Builder $query, - Model $parent, - $name, - $table, - $foreignPivotKey, - $relatedPivotKey, - $parentKey, - $relatedKey, - $relationName = null, - $inverse = false, - ) { + public function __construct(Builder $query, Model $parent, $name, $table, $foreignKey, $otherKey, $relationName = null, $inverse = false) + { $this->inverse = $inverse; $this->morphType = $name.'_type'; $this->morphClass = $inverse ? $query->getModel()->getMorphClass() : $parent->getMorphClass(); - parent::__construct( - $query, $parent, $table, $foreignPivotKey, - $relatedPivotKey, $parentKey, $relatedKey, $relationName - ); + parent::__construct($query, $parent, $table, $foreignKey, $otherKey, $relationName); } /** @@ -81,59 +58,55 @@ public function __construct( * * @return $this */ - protected function addWhereConstraints() + protected function setWhere() { - parent::addWhereConstraints(); + parent::setWhere(); - $this->query->where($this->qualifyPivotColumn($this->morphType), $this->morphClass); + $this->query->where($this->table.'.'.$this->morphType, $this->morphClass); return $this; } - /** @inheritDoc */ - public function addEagerConstraints(array $models) + /** + * Add the constraints for a relationship count query. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parent + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder + */ + public function getRelationQuery(Builder $query, Builder $parent, $columns = ['*']) { - parent::addEagerConstraints($models); + $query = parent::getRelationQuery($query, $parent, $columns); - $this->query->where($this->qualifyPivotColumn($this->morphType), $this->morphClass); + return $query->where($this->table.'.'.$this->morphType, $this->morphClass); } /** - * Create a new pivot attachment record. + * Set the constraints for an eager load of the relation. * - * @param int $id - * @param bool $timed - * @return array + * @param array $models + * @return void */ - protected function baseAttachRecord($id, $timed) + public function addEagerConstraints(array $models) { - return Arr::add( - parent::baseAttachRecord($id, $timed), $this->morphType, $this->morphClass - ); - } + parent::addEagerConstraints($models); - /** @inheritDoc */ - public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) - { - return parent::getRelationExistenceQuery($query, $parentQuery, $columns)->where( - $this->qualifyPivotColumn($this->morphType), $this->morphClass - ); + $this->query->where($this->table.'.'.$this->morphType, $this->morphClass); } /** - * Get the pivot models that are currently attached, filtered by related model keys. + * Create a new pivot attachment record. * - * @param mixed $ids - * @return \Illuminate\Support\Collection + * @param int $id + * @param bool $timed + * @return array */ - protected function getCurrentlyAttachedPivotsForIds($ids = null) + protected function createAttachRecord($id, $timed) { - return parent::getCurrentlyAttachedPivotsForIds($ids)->map(function ($record) { - return $record instanceof MorphPivot - ? $record->setMorphType($this->morphType) - ->setMorphClass($this->morphClass) - : $record; - }); + $record = parent::createAttachRecord($id, $timed); + + return Arr::add($record, $this->morphType, $this->morphClass); } /** @@ -141,56 +114,31 @@ protected function getCurrentlyAttachedPivotsForIds($ids = null) * * @return \Illuminate\Database\Query\Builder */ - public function newPivotQuery() + protected function newPivotQuery() { - return parent::newPivotQuery()->where($this->morphType, $this->morphClass); + $query = parent::newPivotQuery(); + + return $query->where($this->morphType, $this->morphClass); } /** * Create a new pivot model instance. * * @param array $attributes - * @param bool $exists - * @return TPivotModel + * @param bool $exists + * @return \Illuminate\Database\Eloquent\Relations\Pivot */ public function newPivot(array $attributes = [], $exists = false) { - $using = $this->using; + $pivot = new MorphPivot($this->parent, $attributes, $this->table, $exists); - $attributes = array_merge([$this->morphType => $this->morphClass], $attributes); - - $pivot = $using - ? $using::fromRawAttributes($this->parent, $attributes, $this->table, $exists) - : MorphPivot::fromAttributes($this->parent, $attributes, $this->table, $exists); - - $pivot->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey) - ->setRelatedModel($this->related) - ->setMorphType($this->morphType) - ->setMorphClass($this->morphClass); + $pivot->setPivotKeys($this->foreignKey, $this->otherKey) + ->setMorphType($this->morphType) + ->setMorphClass($this->morphClass); return $pivot; } - /** - * Get the pivot columns for the relation. - * - * "pivot_" is prefixed at each column for easy removal later. - * - * @return array - */ - protected function aliasedPivotColumns() - { - return (new Collection([ - $this->foreignPivotKey, - $this->relatedPivotKey, - $this->morphType, - ...$this->pivotColumns, - ])) - ->map(fn ($column) => $this->qualifyPivotColumn($column).' as pivot_'.$column) - ->unique() - ->all(); - } - /** * Get the foreign key "type" name. * @@ -201,33 +149,13 @@ public function getMorphType() return $this->morphType; } - /** - * Get the fully-qualified morph type for the relation. - * - * @return string - */ - public function getQualifiedMorphTypeName() - { - return $this->qualifyPivotColumn($this->morphType); - } - /** * Get the class name of the parent model. * - * @return class-string + * @return string */ public function getMorphClass() { return $this->morphClass; } - - /** - * Get the indicator for a reverse relationship. - * - * @return bool - */ - public function getInverse() - { - return $this->inverse; - } } diff --git a/Eloquent/Relations/Pivot.php b/Eloquent/Relations/Pivot.php index 6e1d3f2789..0c4c2740e5 100755 --- a/Eloquent/Relations/Pivot.php +++ b/Eloquent/Relations/Pivot.php @@ -3,23 +3,190 @@ namespace Illuminate\Database\Eloquent\Relations; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\Concerns\AsPivot; +use Illuminate\Database\Eloquent\Builder; class Pivot extends Model { - use AsPivot; + /** + * The parent model of the relationship. + * + * @var \Illuminate\Database\Eloquent\Model + */ + protected $parent; /** - * Indicates if the IDs are auto-incrementing. + * The name of the foreign key column. * - * @var bool + * @var string */ - public $incrementing = false; + protected $foreignKey; + + /** + * The name of the "other key" column. + * + * @var string + */ + protected $otherKey; /** * The attributes that aren't mass assignable. * - * @var array|bool + * @var array */ protected $guarded = []; + + /** + * Create a new pivot model instance. + * + * @param \Illuminate\Database\Eloquent\Model $parent + * @param array $attributes + * @param string $table + * @param bool $exists + * @return void + */ + public function __construct(Model $parent, $attributes, $table, $exists = false) + { + parent::__construct(); + + // The pivot model is a "dynamic" model since we will set the tables dynamically + // for the instance. This allows it work for any intermediate tables for the + // many to many relationship that are defined by this developer's classes. + $this->setTable($table); + + $this->setConnection($parent->getConnectionName()); + + $this->forceFill($attributes); + + $this->syncOriginal(); + + // We store off the parent instance so we will access the timestamp column names + // for the model, since the pivot model timestamps aren't easily configurable + // from the developer's point of view. We can use the parents to get these. + $this->parent = $parent; + + $this->exists = $exists; + + $this->timestamps = $this->hasTimestampAttributes(); + } + + /** + * Create a new pivot model from raw values returned from a query. + * + * @param \Illuminate\Database\Eloquent\Model $parent + * @param array $attributes + * @param string $table + * @param bool $exists + * @return static + */ + public static function fromRawAttributes(Model $parent, $attributes, $table, $exists = false) + { + $instance = new static($parent, $attributes, $table, $exists); + + $instance->setRawAttributes($attributes, true); + + return $instance; + } + + /** + * Set the keys for a save update query. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + protected function setKeysForSaveQuery(Builder $query) + { + $query->where($this->foreignKey, $this->getAttribute($this->foreignKey)); + + return $query->where($this->otherKey, $this->getAttribute($this->otherKey)); + } + + /** + * Delete the pivot model record from the database. + * + * @return int + */ + public function delete() + { + return $this->getDeleteQuery()->delete(); + } + + /** + * Get the query builder for a delete operation on the pivot. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + protected function getDeleteQuery() + { + $foreign = $this->getAttribute($this->foreignKey); + + $query = $this->newQuery()->where($this->foreignKey, $foreign); + + return $query->where($this->otherKey, $this->getAttribute($this->otherKey)); + } + + /** + * Get the foreign key column name. + * + * @return string + */ + public function getForeignKey() + { + return $this->foreignKey; + } + + /** + * Get the "other key" column name. + * + * @return string + */ + public function getOtherKey() + { + return $this->otherKey; + } + + /** + * Set the key names for the pivot model instance. + * + * @param string $foreignKey + * @param string $otherKey + * @return $this + */ + public function setPivotKeys($foreignKey, $otherKey) + { + $this->foreignKey = $foreignKey; + + $this->otherKey = $otherKey; + + return $this; + } + + /** + * Determine if the pivot model has timestamp attributes. + * + * @return bool + */ + public function hasTimestampAttributes() + { + return array_key_exists($this->getCreatedAtColumn(), $this->attributes); + } + + /** + * Get the name of the "created at" column. + * + * @return string + */ + public function getCreatedAtColumn() + { + return $this->parent->getCreatedAtColumn(); + } + + /** + * Get the name of the "updated at" column. + * + * @return string + */ + public function getUpdatedAtColumn() + { + return $this->parent->getUpdatedAtColumn(); + } } diff --git a/Eloquent/Relations/Relation.php b/Eloquent/Relations/Relation.php index 5f0ced5081..a61b578c6b 100755 --- a/Eloquent/Relations/Relation.php +++ b/Eloquent/Relations/Relation.php @@ -3,58 +3,35 @@ namespace Illuminate\Database\Eloquent\Relations; use Closure; -use Illuminate\Contracts\Database\Eloquent\Builder as BuilderContract; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; +use Illuminate\Support\Arr; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\ModelNotFoundException; -use Illuminate\Database\MultipleRecordsFoundException; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Query\Expression; -use Illuminate\Support\Collection as BaseCollection; -use Illuminate\Support\Traits\ForwardsCalls; -use Illuminate\Support\Traits\Macroable; - -/** - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * @template TResult - * - * @mixin \Illuminate\Database\Eloquent\Builder - */ -abstract class Relation implements BuilderContract -{ - use ForwardsCalls, Macroable { - Macroable::__call as macroCall; - } +use Illuminate\Database\Eloquent\Collection; +abstract class Relation +{ /** * The Eloquent query builder instance. * - * @var \Illuminate\Database\Eloquent\Builder + * @var \Illuminate\Database\Eloquent\Builder */ protected $query; /** * The parent model instance. * - * @var TDeclaringModel + * @var \Illuminate\Database\Eloquent\Model */ protected $parent; /** * The related model instance. * - * @var TRelatedModel + * @var \Illuminate\Database\Eloquent\Model */ protected $related; - /** - * Indicates whether the eagerly loaded relation should implicitly return an empty collection. - * - * @var bool - */ - protected $eagerKeysWereEmpty = false; - /** * Indicates if the relation is adding constraints. * @@ -63,31 +40,18 @@ abstract class Relation implements BuilderContract protected static $constraints = true; /** - * An array to map morph names to their class names in the database. - * - * @var array> - */ - public static $morphMap = []; - - /** - * Prevents morph relationships without a morph map. - * - * @var bool - */ - protected static $requireMorphMap = false; - - /** - * The count of self joins. + * An array to map class names to their morph names in database. * - * @var int + * @var array */ - protected static $selfJoinCount = 0; + protected static $morphMap = []; /** * Create a new relation instance. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param TDeclaringModel $parent + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Model $parent + * @return void */ public function __construct(Builder $query, Model $parent) { @@ -98,30 +62,6 @@ public function __construct(Builder $query, Model $parent) $this->addConstraints(); } - /** - * Run a callback with constraints disabled on the relation. - * - * @template TReturn of mixed - * - * @param Closure(): TReturn $callback - * @return TReturn - */ - public static function noConstraints(Closure $callback) - { - $previous = static::$constraints; - - static::$constraints = false; - - // When resetting the relation where clause, we want to shift the first element - // off of the bindings, leaving only the constraints that the developers put - // as "extra" on the relationships, and not original relation constraints. - try { - return $callback(); - } finally { - static::$constraints = $previous; - } - } - /** * Set the base constraints on the relation query. * @@ -132,7 +72,7 @@ abstract public function addConstraints(); /** * Set the constraints for an eager load of the relation. * - * @param array $models + * @param array $models * @return void */ abstract public function addEagerConstraints(array $models); @@ -140,76 +80,37 @@ abstract public function addEagerConstraints(array $models); /** * Initialize the relation on a set of models. * - * @param array $models + * @param array $models * @param string $relation - * @return array + * @return array */ abstract public function initRelation(array $models, $relation); /** * Match the eagerly loaded results to their parents. * - * @param array $models - * @param \Illuminate\Database\Eloquent\Collection $results + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results * @param string $relation - * @return array + * @return array */ - abstract public function match(array $models, EloquentCollection $results, $relation); + abstract public function match(array $models, Collection $results, $relation); /** * Get the results of the relationship. * - * @return TResult + * @return mixed */ abstract public function getResults(); /** * Get the relationship for eager loading. * - * @return \Illuminate\Database\Eloquent\Collection + * @return \Illuminate\Database\Eloquent\Collection */ public function getEager() { - return $this->eagerKeysWereEmpty - ? $this->related->newCollection() - : $this->get(); - } - - /** - * Execute the query and get the first result if it's the sole matching record. - * - * @param array|string $columns - * @return TRelatedModel - * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - * @throws \Illuminate\Database\MultipleRecordsFoundException - */ - public function sole($columns = ['*']) - { - $result = $this->limit(2)->get($columns); - - $count = $result->count(); - - if ($count === 0) { - throw (new ModelNotFoundException)->setModel(get_class($this->related)); - } - - if ($count > 1) { - throw new MultipleRecordsFoundException($count); - } - - return $result->first(); - } - - /** - * Execute the query as a "select" statement. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function get($columns = ['*']) - { - return $this->query->get($columns); + return $this->get(); } /** @@ -219,13 +120,9 @@ public function get($columns = ['*']) */ public function touch() { - $model = $this->getRelated(); + $column = $this->getRelated()->getUpdatedAtColumn(); - if (! $model::isIgnoringTouch()) { - $this->rawUpdate([ - $model->getUpdatedAtColumn() => $model->freshTimestampString(), - ]); - } + $this->rawUpdate([$column => $this->getRelated()->freshTimestampString()]); } /** @@ -236,79 +133,81 @@ public function touch() */ public function rawUpdate(array $attributes = []) { - return $this->query->withoutGlobalScopes()->update($attributes); + return $this->query->update($attributes); } /** * Add the constraints for a relationship count query. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param \Illuminate\Database\Eloquent\Builder $parentQuery - * @return \Illuminate\Database\Eloquent\Builder + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parent + * @return \Illuminate\Database\Eloquent\Builder */ - public function getRelationExistenceCountQuery(Builder $query, Builder $parentQuery) + public function getRelationCountQuery(Builder $query, Builder $parent) { - return $this->getRelationExistenceQuery( - $query, $parentQuery, new Expression('count(*)') - )->setBindings([], 'select'); + return $this->getRelationQuery($query, $parent, new Expression('count(*)')); } /** - * Add the constraints for an internal relationship existence query. + * Add the constraints for a relationship query. * - * Essentially, these queries compare on column names like whereColumn. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param \Illuminate\Database\Eloquent\Builder $parentQuery - * @param mixed $columns - * @return \Illuminate\Database\Eloquent\Builder + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parent + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder */ - public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) + public function getRelationQuery(Builder $query, Builder $parent, $columns = ['*']) { - return $query->select($columns)->whereColumn( - $this->getQualifiedParentKeyName(), '=', $this->getExistenceCompareKey() - ); + $query->select($columns); + + $key = $this->wrap($this->getQualifiedParentKeyName()); + + return $query->where($this->getHasCompareKey(), '=', new Expression($key)); } /** - * Get a relationship join table hash. + * Run a callback with constraints disabled on the relation. * - * @param bool $incrementJoinCount - * @return string + * @param \Closure $callback + * @return mixed */ - public function getRelationCountHash($incrementJoinCount = true) + public static function noConstraints(Closure $callback) { - return 'laravel_reserved_'.($incrementJoinCount ? static::$selfJoinCount++ : static::$selfJoinCount); + $previous = static::$constraints; + + static::$constraints = false; + + // When resetting the relation where clause, we want to shift the first element + // off of the bindings, leaving only the constraints that the developers put + // as "extra" on the relationships, and not original relation constraints. + try { + $results = call_user_func($callback); + } finally { + static::$constraints = $previous; + } + + return $results; } /** * Get all of the primary keys for an array of models. * - * @param array $models - * @param string|null $key - * @return array + * @param array $models + * @param string $key + * @return array */ protected function getKeys(array $models, $key = null) { - return (new BaseCollection($models))->map(function ($value) use ($key) { + return array_unique(array_values(array_map(function ($value) use ($key) { return $key ? $value->getAttribute($key) : $value->getKey(); - })->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; + }, $models))); } /** * Get the underlying query for the relation. * - * @return \Illuminate\Database\Eloquent\Builder + * @return \Illuminate\Database\Eloquent\Builder */ public function getQuery() { @@ -325,20 +224,10 @@ public function getBaseQuery() return $this->query->getQuery(); } - /** - * Get a base query builder instance. - * - * @return \Illuminate\Database\Query\Builder - */ - public function toBase() - { - return $this->query->toBase(); - } - /** * Get the parent model of the relation. * - * @return TDeclaringModel + * @return \Illuminate\Database\Eloquent\Model */ public function getParent() { @@ -346,7 +235,7 @@ public function getParent() } /** - * Get the fully-qualified parent key name. + * Get the fully qualified parent key name. * * @return string */ @@ -358,7 +247,7 @@ public function getQualifiedParentKeyName() /** * Get the related model of the relation. * - * @return TRelatedModel + * @return \Illuminate\Database\Eloquent\Model */ public function getRelated() { @@ -396,88 +285,29 @@ public function relatedUpdatedAt() } /** - * Add a whereIn eager constraint for the given set of model keys to be loaded. - * - * @param string $whereIn - * @param string $key - * @param array $modelKeys - * @param \Illuminate\Database\Eloquent\Builder|null $query - * @return void - */ - protected function whereInEager(string $whereIn, string $key, array $modelKeys, ?Builder $query = null) - { - ($query ?? $this->query)->{$whereIn}($key, $modelKeys); - - if ($modelKeys === []) { - $this->eagerKeysWereEmpty = true; - } - } - - /** - * Get the name of the "where in" method for eager loading. + * Wrap the given value with the parent query's grammar. * - * @param \Illuminate\Database\Eloquent\Model $model - * @param string $key + * @param string $value * @return string */ - protected function whereInMethod(Model $model, $key) + public function wrap($value) { - return $model->getKeyName() === last(explode('.', $key)) - && in_array($model->getKeyType(), ['int', 'integer']) - ? 'whereIntegerInRaw' - : 'whereIn'; - } - - /** - * Prevent polymorphic relationships from being used without model mappings. - * - * @param bool $requireMorphMap - * @return void - */ - public static function requireMorphMap($requireMorphMap = true) - { - static::$requireMorphMap = $requireMorphMap; - } - - /** - * Determine if polymorphic relationships require explicit model mapping. - * - * @return bool - */ - public static function requiresMorphMap() - { - return static::$requireMorphMap; - } - - /** - * Define the morph map for polymorphic relations and require all morphed models to be explicitly mapped. - * - * @param array> $map - * @param bool $merge - * @return array - */ - public static function enforceMorphMap(array $map, $merge = true) - { - static::requireMorphMap(); - - return static::morphMap($map, $merge); + return $this->parent->newQueryWithoutScopes()->getQuery()->getGrammar()->wrap($value); } /** * Set or get the morph map for polymorphic relations. * - * @param array>|null $map + * @param array|null $map * @param bool $merge - * @return array> + * @return array */ - public static function morphMap(?array $map = null, $merge = true) + public static function morphMap(array $map = null, $merge = true) { $map = static::buildMorphMapFromModels($map); if (is_array($map)) { - static::$morphMap = $merge && static::$morphMap - ? $map + static::$morphMap - : $map; + static::$morphMap = $merge ? array_merge(static::$morphMap, $map) : $map; } return static::$morphMap; @@ -486,56 +316,38 @@ public static function morphMap(?array $map = null, $merge = true) /** * Builds a table-keyed array from model class names. * - * @param array>|null $models - * @return array>|null + * @param string[]|null $models + * @return array|null */ - protected static function buildMorphMapFromModels(?array $models = null) + protected static function buildMorphMapFromModels(array $models = null) { - if (is_null($models) || ! array_is_list($models)) { + if (is_null($models) || Arr::isAssoc($models)) { return $models; } - return array_combine(array_map(function ($model) { + $tables = array_map(function ($model) { return (new $model)->getTable(); - }, $models), $models); - } + }, $models); - /** - * Get the model associated with a custom polymorphic type. - * - * @param string $alias - * @return class-string<\Illuminate\Database\Eloquent\Model>|null - */ - public static function getMorphedModel($alias) - { - return static::$morphMap[$alias] ?? null; - } - - /** - * Get the alias associated with a custom polymorphic class. - * - * @param class-string<\Illuminate\Database\Eloquent\Model> $className - * @return int|string - */ - public static function getMorphAlias(string $className) - { - return array_search($className, static::$morphMap, strict: true) ?: $className; + return array_combine($tables, $models); } /** * Handle dynamic method calls to the relationship. * * @param string $method - * @param array $parameters + * @param array $parameters * @return mixed */ public function __call($method, $parameters) { - if (static::hasMacro($method)) { - return $this->macroCall($method, $parameters); + $result = call_user_func_array([$this->query, $method], $parameters); + + if ($result === $this->query) { + return $this; } - return $this->forwardDecoratedCallTo($this->query, $method, $parameters); + return $result; } /** diff --git a/Eloquent/Scope.php b/Eloquent/Scope.php index cfb1d9b97b..63cba6a517 100644 --- a/Eloquent/Scope.php +++ b/Eloquent/Scope.php @@ -7,10 +7,8 @@ interface Scope /** * Apply the scope to a given Eloquent query builder. * - * @template TModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Builder $builder - * @param TModel $model + * @param \Illuminate\Database\Eloquent\Builder $builder + * @param \Illuminate\Database\Eloquent\Model $model * @return void */ public function apply(Builder $builder, Model $model); diff --git a/Eloquent/SoftDeletes.php b/Eloquent/SoftDeletes.php index bdb4972743..25a36fc3f6 100644 --- a/Eloquent/SoftDeletes.php +++ b/Eloquent/SoftDeletes.php @@ -2,16 +2,6 @@ namespace Illuminate\Database\Eloquent; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; -use Illuminate\Support\Collection as BaseCollection; - -/** - * @method static \Illuminate\Database\Eloquent\Builder withTrashed(bool $withTrashed = true) - * @method static \Illuminate\Database\Eloquent\Builder onlyTrashed() - * @method static \Illuminate\Database\Eloquent\Builder withoutTrashed() - * @method static static restoreOrCreate(array $attributes = [], array $values = []) - * @method static static createOrRestore(array $attributes = [], array $values = []) - */ trait SoftDeletes { /** @@ -31,18 +21,6 @@ public static function bootSoftDeletes() static::addGlobalScope(new SoftDeletingScope); } - /** - * Initialize the soft deleting trait for an instance. - * - * @return void - */ - public function initializeSoftDeletes() - { - if (! isset($this->casts[$this->getDeletedAtColumn()])) { - $this->casts[$this->getDeletedAtColumn()] = 'datetime'; - } - } - /** * Force a hard delete on a soft deleted model. * @@ -50,67 +28,13 @@ public function initializeSoftDeletes() */ public function forceDelete() { - if ($this->fireModelEvent('forceDeleting') === false) { - return false; - } - $this->forceDeleting = true; - return tap($this->delete(), function ($deleted) { - $this->forceDeleting = false; - - if ($deleted) { - $this->fireModelEvent('forceDeleted', false); - } - }); - } - - /** - * Force a hard delete on a soft deleted model without raising any events. - * - * @return bool|null - */ - public function forceDeleteQuietly() - { - return static::withoutEvents(fn () => $this->forceDelete()); - } - - /** - * Destroy the models for the given IDs. - * - * @param \Illuminate\Support\Collection|array|int|string $ids - * @return int - */ - public static function forceDestroy($ids) - { - if ($ids instanceof EloquentCollection) { - $ids = $ids->modelKeys(); - } - - if ($ids instanceof BaseCollection) { - $ids = $ids->all(); - } + $deleted = $this->delete(); - $ids = is_array($ids) ? $ids : func_get_args(); + $this->forceDeleting = false; - if (count($ids) === 0) { - return 0; - } - - // We will actually pull the models from the database table and call delete on - // each of them individually so that their events get fired properly with a - // correct set of attributes in case the developers wants to check these. - $key = ($instance = new static)->getKeyName(); - - $count = 0; - - foreach ($instance->withTrashed()->whereIn($key, $ids)->get() as $model) { - if ($model->forceDelete()) { - $count++; - } - } - - return $count; + return $deleted; } /** @@ -121,9 +45,7 @@ public static function forceDestroy($ids) protected function performDeleteOnModel() { if ($this->forceDeleting) { - return tap($this->setKeysForSaveQuery($this->newModelQuery())->forceDelete(), function () { - $this->exists = false; - }); + return $this->newQueryWithoutScopes()->where($this->getKeyName(), $this->getKey())->forceDelete(); } return $this->runSoftDelete(); @@ -136,31 +58,17 @@ protected function performDeleteOnModel() */ protected function runSoftDelete() { - $query = $this->setKeysForSaveQuery($this->newModelQuery()); - - $time = $this->freshTimestamp(); - - $columns = [$this->getDeletedAtColumn() => $this->fromDateTime($time)]; - - $this->{$this->getDeletedAtColumn()} = $time; + $query = $this->newQueryWithoutScopes()->where($this->getKeyName(), $this->getKey()); - if ($this->usesTimestamps() && ! is_null($this->getUpdatedAtColumn())) { - $this->{$this->getUpdatedAtColumn()} = $time; + $this->{$this->getDeletedAtColumn()} = $time = $this->freshTimestamp(); - $columns[$this->getUpdatedAtColumn()] = $this->fromDateTime($time); - } - - $query->update($columns); - - $this->syncOriginalAttributes(array_keys($columns)); - - $this->fireModelEvent('trashed', false); + $query->update([$this->getDeletedAtColumn() => $this->fromDateTime($time)]); } /** * Restore a soft-deleted model instance. * - * @return bool + * @return bool|null */ public function restore() { @@ -185,16 +93,6 @@ public function restore() return $result; } - /** - * Restore a soft-deleted model instance without raising any events. - * - * @return bool - */ - public function restoreQuietly() - { - return static::withoutEvents(fn () => $this->restore()); - } - /** * Determine if the model instance has been soft-deleted. * @@ -206,68 +104,49 @@ public function trashed() } /** - * Register a "softDeleted" model event callback with the dispatcher. + * Get a new query builder that includes soft deletes. * - * @param \Illuminate\Events\QueuedClosure|callable|class-string $callback - * @return void + * @return \Illuminate\Database\Eloquent\Builder|static */ - public static function softDeleted($callback) + public static function withTrashed() { - static::registerModelEvent('trashed', $callback); + return (new static)->newQueryWithoutScope(new SoftDeletingScope); } /** - * Register a "restoring" model event callback with the dispatcher. + * Get a new query builder that only includes soft deletes. * - * @param \Illuminate\Events\QueuedClosure|callable|class-string $callback - * @return void + * @return \Illuminate\Database\Eloquent\Builder|static */ - public static function restoring($callback) + public static function onlyTrashed() { - static::registerModelEvent('restoring', $callback); - } + $instance = new static; - /** - * Register a "restored" model event callback with the dispatcher. - * - * @param \Illuminate\Events\QueuedClosure|callable|class-string $callback - * @return void - */ - public static function restored($callback) - { - static::registerModelEvent('restored', $callback); - } + $column = $instance->getQualifiedDeletedAtColumn(); - /** - * Register a "forceDeleting" model event callback with the dispatcher. - * - * @param \Illuminate\Events\QueuedClosure|callable|class-string $callback - * @return void - */ - public static function forceDeleting($callback) - { - static::registerModelEvent('forceDeleting', $callback); + return $instance->newQueryWithoutScope(new SoftDeletingScope)->whereNotNull($column); } /** - * Register a "forceDeleted" model event callback with the dispatcher. + * Register a restoring model event with the dispatcher. * - * @param \Illuminate\Events\QueuedClosure|callable|class-string $callback + * @param \Closure|string $callback * @return void */ - public static function forceDeleted($callback) + public static function restoring($callback) { - static::registerModelEvent('forceDeleted', $callback); + static::registerModelEvent('restoring', $callback); } /** - * Determine if the model is currently force deleting. + * Register a restored model event with the dispatcher. * - * @return bool + * @param \Closure|string $callback + * @return void */ - public function isForceDeleting() + public static function restored($callback) { - return $this->forceDeleting; + static::registerModelEvent('restored', $callback); } /** @@ -277,16 +156,16 @@ public function isForceDeleting() */ public function getDeletedAtColumn() { - return defined(static::class.'::DELETED_AT') ? static::DELETED_AT : 'deleted_at'; + return defined('static::DELETED_AT') ? static::DELETED_AT : 'deleted_at'; } /** - * Get the fully-qualified "deleted at" column. + * Get the fully qualified "deleted at" column. * * @return string */ public function getQualifiedDeletedAtColumn() { - return $this->qualifyColumn($this->getDeletedAtColumn()); + return $this->getTable().'.'.$this->getDeletedAtColumn(); } } diff --git a/Eloquent/SoftDeletingScope.php b/Eloquent/SoftDeletingScope.php index d1ef0d22b9..dee43bb4d5 100644 --- a/Eloquent/SoftDeletingScope.php +++ b/Eloquent/SoftDeletingScope.php @@ -7,17 +7,15 @@ class SoftDeletingScope implements Scope /** * All of the extensions to be added to the builder. * - * @var string[] + * @var array */ - protected $extensions = ['Restore', 'RestoreOrCreate', 'CreateOrRestore', 'WithTrashed', 'WithoutTrashed', 'OnlyTrashed']; + protected $extensions = ['ForceDelete', 'Restore', 'WithTrashed', 'OnlyTrashed']; /** * Apply the scope to a given Eloquent query builder. * - * @template TModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Builder $builder - * @param TModel $model + * @param \Illuminate\Database\Eloquent\Builder $builder + * @param \Illuminate\Database\Eloquent\Model $model * @return void */ public function apply(Builder $builder, Model $model) @@ -28,7 +26,7 @@ public function apply(Builder $builder, Model $model) /** * Extend the query builder with the needed functions. * - * @param \Illuminate\Database\Eloquent\Builder<*> $builder + * @param \Illuminate\Database\Eloquent\Builder $builder * @return void */ public function extend(Builder $builder) @@ -49,107 +47,63 @@ public function extend(Builder $builder) /** * Get the "deleted at" column for the builder. * - * @param \Illuminate\Database\Eloquent\Builder<*> $builder + * @param \Illuminate\Database\Eloquent\Builder $builder * @return string */ protected function getDeletedAtColumn(Builder $builder) { - if (count((array) $builder->getQuery()->joins) > 0) { + if (count($builder->getQuery()->joins) > 0) { return $builder->getModel()->getQualifiedDeletedAtColumn(); + } else { + return $builder->getModel()->getDeletedAtColumn(); } - - return $builder->getModel()->getDeletedAtColumn(); } /** - * Add the restore extension to the builder. + * Add the force delete extension to the builder. * - * @param \Illuminate\Database\Eloquent\Builder<*> $builder + * @param \Illuminate\Database\Eloquent\Builder $builder * @return void */ - protected function addRestore(Builder $builder) + protected function addForceDelete(Builder $builder) { - $builder->macro('restore', function (Builder $builder) { - $builder->withTrashed(); - - return $builder->update([$builder->getModel()->getDeletedAtColumn() => null]); + $builder->macro('forceDelete', function (Builder $builder) { + return $builder->getQuery()->delete(); }); } /** - * Add the restore-or-create extension to the builder. - * - * @param \Illuminate\Database\Eloquent\Builder<*> $builder - * @return void - */ - protected function addRestoreOrCreate(Builder $builder) - { - $builder->macro('restoreOrCreate', function (Builder $builder, array $attributes = [], array $values = []) { - $builder->withTrashed(); - - return tap($builder->firstOrCreate($attributes, $values), function ($instance) { - $instance->restore(); - }); - }); - } - - /** - * Add the create-or-restore extension to the builder. + * Add the restore extension to the builder. * - * @param \Illuminate\Database\Eloquent\Builder<*> $builder + * @param \Illuminate\Database\Eloquent\Builder $builder * @return void */ - protected function addCreateOrRestore(Builder $builder) + protected function addRestore(Builder $builder) { - $builder->macro('createOrRestore', function (Builder $builder, array $attributes = [], array $values = []) { + $builder->macro('restore', function (Builder $builder) { $builder->withTrashed(); - return tap($builder->createOrFirst($attributes, $values), function ($instance) { - $instance->restore(); - }); + return $builder->update([$builder->getModel()->getDeletedAtColumn() => null]); }); } /** * Add the with-trashed extension to the builder. * - * @param \Illuminate\Database\Eloquent\Builder<*> $builder + * @param \Illuminate\Database\Eloquent\Builder $builder * @return void */ protected function addWithTrashed(Builder $builder) { - $builder->macro('withTrashed', function (Builder $builder, $withTrashed = true) { - if (! $withTrashed) { - return $builder->withoutTrashed(); - } - + $builder->macro('withTrashed', function (Builder $builder) { return $builder->withoutGlobalScope($this); }); } - /** - * Add the without-trashed extension to the builder. - * - * @param \Illuminate\Database\Eloquent\Builder<*> $builder - * @return void - */ - protected function addWithoutTrashed(Builder $builder) - { - $builder->macro('withoutTrashed', function (Builder $builder) { - $model = $builder->getModel(); - - $builder->withoutGlobalScope($this)->whereNull( - $model->getQualifiedDeletedAtColumn() - ); - - return $builder; - }); - } - /** * Add the only-trashed extension to the builder. * - * @param \Illuminate\Database\Eloquent\Builder<*> $builder + * @param \Illuminate\Database\Eloquent\Builder $builder * @return void */ protected function addOnlyTrashed(Builder $builder) diff --git a/Events/ConnectionEstablished.php b/Events/ConnectionEstablished.php deleted file mode 100644 index 22a45b834a..0000000000 --- a/Events/ConnectionEstablished.php +++ /dev/null @@ -1,8 +0,0 @@ -method = $method; - $this->migration = $migration; - } -} diff --git a/Events/MigrationSkipped.php b/Events/MigrationSkipped.php deleted file mode 100644 index f877020fcd..0000000000 --- a/Events/MigrationSkipped.php +++ /dev/null @@ -1,18 +0,0 @@ - $options The options provided when the migration method was invoked. - */ - public function __construct( - public $method, - public array $options = [], - ) { - } -} diff --git a/Events/MigrationsPruned.php b/Events/MigrationsPruned.php deleted file mode 100644 index 16e519e27c..0000000000 --- a/Events/MigrationsPruned.php +++ /dev/null @@ -1,42 +0,0 @@ -connection = $connection; - $this->connectionName = $connection->getName(); - $this->path = $path; - } -} diff --git a/Events/MigrationsStarted.php b/Events/MigrationsStarted.php deleted file mode 100644 index 5283b49916..0000000000 --- a/Events/MigrationsStarted.php +++ /dev/null @@ -1,8 +0,0 @@ - $models The class names of the models that were pruned. - */ - public function __construct( - public $models, - ) { - } -} diff --git a/Events/ModelPruningStarting.php b/Events/ModelPruningStarting.php deleted file mode 100644 index 581d3da8a4..0000000000 --- a/Events/ModelPruningStarting.php +++ /dev/null @@ -1,16 +0,0 @@ - $models The class names of the models that will be pruned. - */ - public function __construct( - public $models, - ) { - } -} diff --git a/Events/ModelsPruned.php b/Events/ModelsPruned.php deleted file mode 100644 index 2d9605e5fe..0000000000 --- a/Events/ModelsPruned.php +++ /dev/null @@ -1,18 +0,0 @@ -sql = $sql; $this->time = $time; $this->bindings = $bindings; $this->connection = $connection; $this->connectionName = $connection->getName(); - $this->readWriteType = $readWriteType; - } - - /** - * Get the raw SQL representation of the query with embedded bindings. - * - * @return string - */ - public function toRawSql() - { - return $this->connection - ->query() - ->getGrammar() - ->substituteBindingsIntoRawSql($this->sql, $this->connection->prepareBindings($this->bindings)); } } diff --git a/Events/SchemaDumped.php b/Events/SchemaDumped.php deleted file mode 100644 index 416462027c..0000000000 --- a/Events/SchemaDumped.php +++ /dev/null @@ -1,40 +0,0 @@ -connection = $connection; - $this->connectionName = $connection->getName(); - $this->path = $path; - } -} diff --git a/Events/SchemaLoaded.php b/Events/SchemaLoaded.php deleted file mode 100644 index d86ae53074..0000000000 --- a/Events/SchemaLoaded.php +++ /dev/null @@ -1,40 +0,0 @@ -connection = $connection; - $this->connectionName = $connection->getName(); - $this->path = $path; - } -} diff --git a/Events/StatementPrepared.php b/Events/StatementPrepared.php deleted file mode 100644 index 43f02a0e26..0000000000 --- a/Events/StatementPrepared.php +++ /dev/null @@ -1,18 +0,0 @@ -connection = $connection; - } + protected $tablePrefix = ''; /** * Wrap an array of values. * - * @param array<\Illuminate\Contracts\Database\Query\Expression|string> $values - * @return array + * @param array $values + * @return array */ public function wrapArray(array $values) { - return array_map($this->wrap(...), $values); + return array_map([$this, 'wrap'], $values); } /** * Wrap a table in keyword identifiers. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $table - * @param string|null $prefix + * @param \Illuminate\Database\Query\Expression|string $table * @return string */ - public function wrapTable($table, $prefix = null) + public function wrapTable($table) { if ($this->isExpression($table)) { return $this->getValue($table); } - $prefix ??= $this->connection->getTablePrefix(); - - // If the table being wrapped has an alias we'll need to separate the pieces - // so we can prefix the table and then wrap each of the segments on their - // own and then join these both back together using the "as" connector. - if (stripos($table, ' as ') !== false) { - return $this->wrapAliasedTable($table, $prefix); - } - - // If the table being wrapped has a custom schema name specified, we need to - // prefix the last segment as the table name then wrap each segment alone - // and eventually join them both back together using the dot connector. - if (str_contains($table, '.')) { - $table = substr_replace($table, '.'.$prefix, strrpos($table, '.'), 1); - - return (new Collection(explode('.', $table))) - ->map($this->wrapValue(...)) - ->implode('.'); - } - - return $this->wrapValue($prefix.$table); + return $this->wrap($this->tablePrefix.$table, true); } /** * Wrap a value in keyword identifiers. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $value + * @param \Illuminate\Database\Query\Expression|string $value + * @param bool $prefixAlias * @return string */ - public function wrap($value) + public function wrap($value, $prefixAlias = false) { if ($this->isExpression($value)) { return $this->getValue($value); } // If the value being wrapped has a column alias we will need to separate out - // the pieces so we can wrap each of the segments of the expression on its - // own, and then join these both back together using the "as" connector. - if (stripos($value, ' as ') !== false) { - return $this->wrapAliasedValue($value); - } - - // If the given value is a JSON selector we will wrap it differently than a - // traditional value. We will need to split this path and wrap each part - // wrapped, etc. Otherwise, we will simply wrap the value as a string. - if ($this->isJsonSelector($value)) { - return $this->wrapJsonSelector($value); - } + // the pieces so we can wrap each of the segments of the expression on it + // own, and then joins them both back together with the "as" connector. + if (strpos(strtolower($value), ' as ') !== false) { + $segments = explode(' ', $value); - return $this->wrapSegments(explode('.', $value)); - } + if ($prefixAlias) { + $segments[2] = $this->tablePrefix.$segments[2]; + } - /** - * Wrap a value that has an alias. - * - * @param string $value - * @return string - */ - protected function wrapAliasedValue($value) - { - $segments = preg_split('/\s+as\s+/i', $value); + return $this->wrap($segments[0]).' as '.$this->wrapValue($segments[2]); + } - return $this->wrap($segments[0]).' as '.$this->wrapValue($segments[1]); - } + $wrapped = []; - /** - * Wrap a table that has an alias. - * - * @param string $value - * @param string|null $prefix - * @return string - */ - protected function wrapAliasedTable($value, $prefix = null) - { - $segments = preg_split('/\s+as\s+/i', $value); - - $prefix ??= $this->connection->getTablePrefix(); + $segments = explode('.', $value); - return $this->wrapTable($segments[0], $prefix).' as '.$this->wrapValue($prefix.$segments[1]); - } + // If the value is not an aliased table expression, we'll just wrap it like + // normal, so if there is more than one segment, we will wrap the first + // segments as if it was a table and the rest as just regular values. + foreach ($segments as $key => $segment) { + if ($key == 0 && count($segments) > 1) { + $wrapped[] = $this->wrapTable($segment); + } else { + $wrapped[] = $this->wrapValue($segment); + } + } - /** - * Wrap the given value segments. - * - * @param list $segments - * @return string - */ - protected function wrapSegments($segments) - { - return (new Collection($segments))->map(function ($segment, $key) use ($segments) { - return $key == 0 && count($segments) > 1 - ? $this->wrapTable($segment) - : $this->wrapValue($segment); - })->implode('.'); + return implode('.', $wrapped); } /** @@ -156,63 +91,39 @@ protected function wrapSegments($segments) */ protected function wrapValue($value) { - if ($value !== '*') { - return '"'.str_replace('"', '""', $value).'"'; + if ($value === '*') { + return $value; } - return $value; - } - - /** - * Wrap the given JSON selector. - * - * @param string $value - * @return string - * - * @throws \RuntimeException - */ - protected function wrapJsonSelector($value) - { - throw new RuntimeException('This database engine does not support JSON operations.'); - } - - /** - * Determine if the given string is a JSON selector. - * - * @param string $value - * @return bool - */ - protected function isJsonSelector($value) - { - return str_contains($value, '->'); + return '"'.str_replace('"', '""', $value).'"'; } /** * Convert an array of column names into a delimited string. * - * @param array<\Illuminate\Contracts\Database\Query\Expression|string> $columns + * @param array $columns * @return string */ public function columnize(array $columns) { - return implode(', ', array_map($this->wrap(...), $columns)); + return implode(', ', array_map([$this, 'wrap'], $columns)); } /** * Create query parameter place-holders for an array. * - * @param array $values + * @param array $values * @return string */ public function parameterize(array $values) { - return implode(', ', array_map($this->parameter(...), $values)); + return implode(', ', array_map([$this, 'parameter'], $values)); } /** * Get the appropriate query parameter place-holder for a value. * - * @param mixed $value + * @param mixed $value * @return string */ public function parameter($value) @@ -221,30 +132,14 @@ public function parameter($value) } /** - * Quote the given string literal. + * Get the value of a raw expression. * - * @param string|array $value + * @param \Illuminate\Database\Query\Expression $expression * @return string */ - public function quoteString($value) - { - if (is_array($value)) { - return implode(', ', array_map([$this, __FUNCTION__], $value)); - } - - return "'$value'"; - } - - /** - * Escapes a value for safe SQL embedding. - * - * @param string|float|int|bool|null $value - * @param bool $binary - * @return string - */ - public function escape($value, $binary = false) + public function getValue($expression) { - return $this->connection->escape($value, $binary); + return $expression->getValue(); } /** @@ -258,21 +153,6 @@ public function isExpression($value) return $value instanceof Expression; } - /** - * Transforms expressions to their scalar types. - * - * @param \Illuminate\Contracts\Database\Query\Expression|string|int|float $expression - * @return string|int|float - */ - public function getValue($expression) - { - if ($this->isExpression($expression)) { - return $this->getValue($expression->getValue($this)); - } - - return $expression; - } - /** * Get the format for database stored dates. * @@ -286,26 +166,22 @@ public function getDateFormat() /** * Get the grammar's table prefix. * - * @deprecated Use DB::getTablePrefix() - * * @return string */ public function getTablePrefix() { - return $this->connection->getTablePrefix(); + return $this->tablePrefix; } /** * Set the grammar's table prefix. * - * @deprecated Use DB::setTablePrefix() - * * @param string $prefix * @return $this */ public function setTablePrefix($prefix) { - $this->connection->setTablePrefix($prefix); + $this->tablePrefix = $prefix; return $this; } diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 79810c848f..0000000000 --- a/LICENSE.md +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) Taylor Otwell - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/LazyLoadingViolationException.php b/LazyLoadingViolationException.php deleted file mode 100644 index f0a90f6c95..0000000000 --- a/LazyLoadingViolationException.php +++ /dev/null @@ -1,38 +0,0 @@ -model = $class; - $this->relation = $relation; - } -} diff --git a/LostConnectionDetector.php b/LostConnectionDetector.php deleted file mode 100644 index 486be8e20d..0000000000 --- a/LostConnectionDetector.php +++ /dev/null @@ -1,95 +0,0 @@ -getMessage(); - - return Str::contains($message, [ - 'server has gone away', - 'Server has gone away', - 'no connection to the server', - 'Lost connection', - 'is dead or not enabled', - 'Error while sending', - 'decryption failed or bad record mac', - 'server closed the connection unexpectedly', - 'SSL connection has been closed unexpectedly', - 'Error writing data to the connection', - 'Resource deadlock avoided', - 'Transaction() on null', - 'child connection forced to terminate due to client_idle_limit', - 'query_wait_timeout', - 'reset by peer', - 'Physical connection is not usable', - 'TCP Provider: Error code 0x68', - 'ORA-03114', - 'Packets out of order. Expected', - 'Adaptive Server connection failed', - 'Communication link failure', - 'connection is no longer usable', - 'Login timeout expired', - 'SQLSTATE[HY000] [2002] Connection refused', - 'running with the --read-only option so it cannot execute this statement', - 'The connection is broken and recovery is not possible. The connection is marked by the client driver as unrecoverable. No attempt was made to restore the connection.', - 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Try again', - 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Name or service not known', - 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo for', - 'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: EOF detected', - 'SSL error: unexpected eof', - 'SQLSTATE[HY000] [2002] Connection timed out', - 'SSL: Connection timed out', - 'SQLSTATE[HY000]: General error: 1105 The last transaction was aborted due to Seamless Scaling. Please retry.', - 'Temporary failure in name resolution', - 'SQLSTATE[08S01]: Communication link failure', - 'SQLSTATE[08006] [7] could not connect to server: Connection refused Is the server running on host', - 'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: No route to host', - 'The client was disconnected by the server because of inactivity. See wait_timeout and interactive_timeout for configuring this behavior.', - 'SQLSTATE[08006] [7] could not translate host name', - 'TCP Provider: Error code 0x274C', - 'SQLSTATE[HY000] [2002] No such file or directory', - 'SSL: Operation timed out', - 'Reason: Server is in script upgrade mode. Only administrator can connect at this time.', - 'Unknown $curl_error_code: 77', - 'SSL: Handshake timed out', - 'SSL error: sslv3 alert unexpected message', - 'unrecognized SSL error code:', - 'SQLSTATE[HY000] [1045] Access denied for user', - 'SQLSTATE[HY000] [2002] No connection could be made because the target machine actively refused it', - 'SQLSTATE[HY000] [2002] A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond', - 'SQLSTATE[HY000] [2002] Network is unreachable', - 'SQLSTATE[HY000] [2002] The requested address is not valid in its context', - 'SQLSTATE[HY000] [2002] A socket operation was attempted to an unreachable network', - 'SQLSTATE[HY000] [2002] Operation now in progress', - 'SQLSTATE[HY000] [2002] Operation in progress', - 'SQLSTATE[HY000]: General error: 3989', - 'went away', - 'No such file or directory', - 'server is shutting down', - 'failed to connect to', - 'Channel connection is closed', - 'Connection lost', - 'Broken pipe', - 'SQLSTATE[25006]: Read only sql transaction: 7', - 'vtgate connection error: no healthy endpoints', - 'primary is not serving, there may be a reparent operation in progress', - 'current keyspace is being resharded', - 'no healthy tablet available', - 'transaction pool connection limit exceeded', - 'SSL operation failed with code 5', - ]); - } -} diff --git a/LostConnectionException.php b/LostConnectionException.php deleted file mode 100644 index c8e57e368d..0000000000 --- a/LostConnectionException.php +++ /dev/null @@ -1,10 +0,0 @@ -schemaGrammar)) { - $this->useDefaultSchemaGrammar(); - } - - return new MariaDbBuilder($this); - } - - /** - * Get the default schema grammar instance. - * - * @return \Illuminate\Database\Schema\Grammars\MariaDbGrammar - */ - protected function getDefaultSchemaGrammar() - { - return new SchemaGrammar($this); - } - - /** - * Get the schema state for the connection. - * - * @param \Illuminate\Filesystem\Filesystem|null $files - * @param callable|null $processFactory - * @return \Illuminate\Database\Schema\MariaDbSchemaState - */ - public function getSchemaState(?Filesystem $files = null, ?callable $processFactory = null) - { - return new MariaDbSchemaState($this, $files, $processFactory); - } - - /** - * Get the default post processor instance. - * - * @return \Illuminate\Database\Query\Processors\MariaDbProcessor - */ - protected function getDefaultPostProcessor() - { - return new MariaDbProcessor; - } -} diff --git a/MigrationServiceProvider.php b/MigrationServiceProvider.php index 037106c735..bc0abebf10 100755 --- a/MigrationServiceProvider.php +++ b/MigrationServiceProvider.php @@ -2,38 +2,26 @@ namespace Illuminate\Database; -use Illuminate\Contracts\Events\Dispatcher; -use Illuminate\Contracts\Support\DeferrableProvider; -use Illuminate\Database\Console\Migrations\FreshCommand; +use Illuminate\Support\ServiceProvider; +use Illuminate\Database\Migrations\Migrator; +use Illuminate\Database\Migrations\MigrationCreator; +use Illuminate\Database\Console\Migrations\ResetCommand; +use Illuminate\Database\Console\Migrations\StatusCommand; use Illuminate\Database\Console\Migrations\InstallCommand; use Illuminate\Database\Console\Migrations\MigrateCommand; -use Illuminate\Database\Console\Migrations\MigrateMakeCommand; use Illuminate\Database\Console\Migrations\RefreshCommand; -use Illuminate\Database\Console\Migrations\ResetCommand; use Illuminate\Database\Console\Migrations\RollbackCommand; -use Illuminate\Database\Console\Migrations\StatusCommand; +use Illuminate\Database\Console\Migrations\MigrateMakeCommand; use Illuminate\Database\Migrations\DatabaseMigrationRepository; -use Illuminate\Database\Migrations\MigrationCreator; -use Illuminate\Database\Migrations\Migrator; -use Illuminate\Support\ServiceProvider; -class MigrationServiceProvider extends ServiceProvider implements DeferrableProvider +class MigrationServiceProvider extends ServiceProvider { /** - * The commands to be registered. + * Indicates if loading of the provider is deferred. * - * @var array + * @var bool */ - protected $commands = [ - 'Migrate' => MigrateCommand::class, - 'MigrateFresh' => FreshCommand::class, - 'MigrateInstall' => InstallCommand::class, - 'MigrateRefresh' => RefreshCommand::class, - 'MigrateReset' => ResetCommand::class, - 'MigrateRollback' => RollbackCommand::class, - 'MigrateStatus' => StatusCommand::class, - 'MigrateMake' => MigrateMakeCommand::class, - ]; + protected $defer = true; /** * Register the service provider. @@ -44,11 +32,14 @@ public function register() { $this->registerRepository(); + // Once we have registered the migrator instance we will go ahead and register + // all of the migration related commands that are used by the "Artisan" CLI + // so that they may be easily accessed for registering with the consoles. $this->registerMigrator(); $this->registerCreator(); - $this->registerCommands($this->commands); + $this->registerCommands(); } /** @@ -59,9 +50,7 @@ public function register() protected function registerRepository() { $this->app->singleton('migration.repository', function ($app) { - $migrations = $app['config']['database.migrations']; - - $table = is_array($migrations) ? ($migrations['table'] ?? null) : $migrations; + $table = $app['config']['database.migrations']; return new DatabaseMigrationRepository($app['db'], $table); }); @@ -80,10 +69,8 @@ protected function registerMigrator() $this->app->singleton('migrator', function ($app) { $repository = $app['migration.repository']; - return new Migrator($repository, $app['db'], $app['files'], $app['events']); + return new Migrator($repository, $app['db'], $app['files']); }); - - $this->app->bind(Migrator::class, fn ($app) => $app['migrator']); } /** @@ -94,123 +81,125 @@ protected function registerMigrator() protected function registerCreator() { $this->app->singleton('migration.creator', function ($app) { - return new MigrationCreator($app['files'], $app->basePath('stubs')); + return new MigrationCreator($app['files']); }); } /** - * Register the given commands. + * Register all of the migration commands. * - * @param array $commands * @return void */ - protected function registerCommands(array $commands) + protected function registerCommands() { - foreach (array_keys($commands) as $command) { - $this->{"register{$command}Command"}(); + $commands = ['Migrate', 'Rollback', 'Reset', 'Refresh', 'Install', 'Make', 'Status']; + + // We'll simply spin through the list of commands that are migration related + // and register each one of them with an application container. They will + // be resolved in the Artisan start file and registered on the console. + foreach ($commands as $command) { + $this->{'register'.$command.'Command'}(); } - $this->commands(array_values($commands)); + // Once the commands are registered in the application IoC container we will + // register them with the Artisan start event so that these are available + // when the Artisan application actually starts up and is getting used. + $this->commands( + 'command.migrate', 'command.migrate.make', + 'command.migrate.install', 'command.migrate.rollback', + 'command.migrate.reset', 'command.migrate.refresh', + 'command.migrate.status' + ); } /** - * Register the command. + * Register the "migrate" migration command. * * @return void */ protected function registerMigrateCommand() { - $this->app->singleton(MigrateCommand::class, function ($app) { - return new MigrateCommand($app['migrator'], $app[Dispatcher::class]); + $this->app->singleton('command.migrate', function ($app) { + return new MigrateCommand($app['migrator']); }); } /** - * Register the command. + * Register the "rollback" migration command. * * @return void */ - protected function registerMigrateFreshCommand() + protected function registerRollbackCommand() { - $this->app->singleton(FreshCommand::class, function ($app) { - return new FreshCommand($app['migrator']); + $this->app->singleton('command.migrate.rollback', function ($app) { + return new RollbackCommand($app['migrator']); }); } /** - * Register the command. + * Register the "reset" migration command. * * @return void */ - protected function registerMigrateInstallCommand() + protected function registerResetCommand() { - $this->app->singleton(InstallCommand::class, function ($app) { - return new InstallCommand($app['migration.repository']); + $this->app->singleton('command.migrate.reset', function ($app) { + return new ResetCommand($app['migrator']); }); } /** - * Register the command. + * Register the "refresh" migration command. * * @return void */ - protected function registerMigrateMakeCommand() + protected function registerRefreshCommand() { - $this->app->singleton(MigrateMakeCommand::class, function ($app) { - // Once we have the migration creator registered, we will create the command - // and inject the creator. The creator is responsible for the actual file - // creation of the migrations, and may be extended by these developers. - $creator = $app['migration.creator']; - - $composer = $app['composer']; - - return new MigrateMakeCommand($creator, $composer); + $this->app->singleton('command.migrate.refresh', function () { + return new RefreshCommand; }); } /** - * Register the command. + * Register the "make" migration command. * * @return void */ - protected function registerMigrateRefreshCommand() + protected function registerMakeCommand() { - $this->app->singleton(RefreshCommand::class); - } + $this->app->singleton('command.migrate.make', function ($app) { + // Once we have the migration creator registered, we will create the command + // and inject the creator. The creator is responsible for the actual file + // creation of the migrations, and may be extended by these developers. + $creator = $app['migration.creator']; - /** - * Register the command. - * - * @return void - */ - protected function registerMigrateResetCommand() - { - $this->app->singleton(ResetCommand::class, function ($app) { - return new ResetCommand($app['migrator']); + $composer = $app['composer']; + + return new MigrateMakeCommand($creator, $composer); }); } /** - * Register the command. + * Register the "status" migration command. * * @return void */ - protected function registerMigrateRollbackCommand() + protected function registerStatusCommand() { - $this->app->singleton(RollbackCommand::class, function ($app) { - return new RollbackCommand($app['migrator']); + $this->app->singleton('command.migrate.status', function ($app) { + return new StatusCommand($app['migrator']); }); } /** - * Register the command. + * Register the "install" migration command. * * @return void */ - protected function registerMigrateStatusCommand() + protected function registerInstallCommand() { - $this->app->singleton(StatusCommand::class, function ($app) { - return new StatusCommand($app['migrator']); + $this->app->singleton('command.migrate.install', function ($app) { + return new InstallCommand($app['migration.repository']); }); } @@ -221,8 +210,12 @@ protected function registerMigrateStatusCommand() */ public function provides() { - return array_merge([ - 'migrator', 'migration.repository', 'migration.creator', Migrator::class, - ], array_values($this->commands)); + return [ + 'migrator', 'migration.repository', 'command.migrate', + 'command.migrate.rollback', 'command.migrate.reset', + 'command.migrate.refresh', 'command.migrate.install', + 'command.migrate.status', 'migration.creator', + 'command.migrate.make', + ]; } } diff --git a/Migrations/DatabaseMigrationRepository.php b/Migrations/DatabaseMigrationRepository.php index 45c4389d27..0c5339a119 100755 --- a/Migrations/DatabaseMigrationRepository.php +++ b/Migrations/DatabaseMigrationRepository.php @@ -32,6 +32,7 @@ class DatabaseMigrationRepository implements MigrationRepositoryInterface * * @param \Illuminate\Database\ConnectionResolverInterface $resolver * @param string $table + * @return void */ public function __construct(Resolver $resolver, $table) { @@ -40,54 +41,22 @@ public function __construct(Resolver $resolver, $table) } /** - * Get the completed migrations. + * Get the ran migrations. * - * @return string[] + * @return array */ public function getRan() { return $this->table() - ->orderBy('batch', 'asc') - ->orderBy('migration', 'asc') - ->pluck('migration')->all(); - } - - /** - * Get the list of migrations. - * - * @param int $steps - * @return array{id: int, migration: string, batch: int}[] - */ - public function getMigrations($steps) - { - $query = $this->table()->where('batch', '>=', '1'); - - return $query->orderBy('batch', 'desc') - ->orderBy('migration', 'desc') - ->limit($steps) - ->get() - ->all(); - } - - /** - * Get the list of the migrations by batch number. - * - * @param int $batch - * @return array{id: int, migration: string, batch: int}[] - */ - public function getMigrationsByBatch($batch) - { - return $this->table() - ->where('batch', $batch) - ->orderBy('migration', 'desc') - ->get() - ->all(); + ->orderBy('batch', 'asc') + ->orderBy('migration', 'asc') + ->pluck('migration')->all(); } /** * Get the last migration batch. * - * @return array{id: int, migration: string, batch: int}[] + * @return array */ public function getLast() { @@ -96,24 +65,11 @@ public function getLast() return $query->orderBy('migration', 'desc')->get()->all(); } - /** - * Get the completed migrations with their batch numbers. - * - * @return array[] - */ - public function getMigrationBatches() - { - return $this->table() - ->orderBy('batch', 'asc') - ->orderBy('migration', 'asc') - ->pluck('batch', 'migration')->all(); - } - /** * Log that a migration was run. * * @param string $file - * @param int $batch + * @param int $batch * @return void */ public function log($file, $batch) @@ -126,7 +82,7 @@ public function log($file, $batch) /** * Remove a migration from the log. * - * @param object{id?: int, migration: string, batch?: int} $migration + * @param object $migration * @return void */ public function delete($migration) @@ -167,8 +123,8 @@ public function createRepository() // The migrations table is responsible for keeping track of which of the // migrations have actually run for the application. We'll create the // table to hold the migration file's path as well as the batch ID. - $table->increments('id'); $table->string('migration'); + $table->integer('batch'); }); } @@ -185,18 +141,6 @@ public function repositoryExists() return $schema->hasTable($this->table); } - /** - * Delete the migration repository data store. - * - * @return void - */ - public function deleteRepository() - { - $schema = $this->getConnection()->getSchemaBuilder(); - - $schema->drop($this->table); - } - /** * Get a query builder for the migration table. * @@ -204,7 +148,7 @@ public function deleteRepository() */ protected function table() { - return $this->getConnection()->table($this->table)->useWritePdo(); + return $this->getConnection()->table($this->table); } /** diff --git a/Migrations/Migration.php b/Migrations/Migration.php index 35c8d43be3..699154c9a6 100755 --- a/Migrations/Migration.php +++ b/Migrations/Migration.php @@ -7,34 +7,17 @@ abstract class Migration /** * The name of the database connection to use. * - * @var string|null + * @var string */ protected $connection; - /** - * Enables, if supported, wrapping the migration within a transaction. - * - * @var bool - */ - public $withinTransaction = true; - /** * Get the migration connection name. * - * @return string|null + * @return string */ public function getConnection() { return $this->connection; } - - /** - * Determine if this migration should run. - * - * @return bool - */ - public function shouldRun(): bool - { - return true; - } } diff --git a/Migrations/MigrationCreator.php b/Migrations/MigrationCreator.php index 7b9da51390..beacfe16cc 100755 --- a/Migrations/MigrationCreator.php +++ b/Migrations/MigrationCreator.php @@ -3,9 +3,8 @@ namespace Illuminate\Database\Migrations; use Closure; -use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Str; -use InvalidArgumentException; +use Illuminate\Filesystem\Filesystem; class MigrationCreator { @@ -16,17 +15,10 @@ class MigrationCreator */ protected $files; - /** - * The custom app stubs directory. - * - * @var string - */ - protected $customStubPath; - /** * The registered post create hooks. * - * @var (\Closure(string, string): void)[] + * @var array */ protected $postCreate = []; @@ -34,12 +26,11 @@ class MigrationCreator * Create a new migration creator instance. * * @param \Illuminate\Filesystem\Filesystem $files - * @param string $customStubPath + * @return void */ - public function __construct(Filesystem $files, $customStubPath) + public function __construct(Filesystem $files) { $this->files = $files; - $this->customStubPath = $customStubPath; } /** @@ -47,104 +38,66 @@ public function __construct(Filesystem $files, $customStubPath) * * @param string $name * @param string $path - * @param string|null $table - * @param bool $create + * @param string $table + * @param bool $create * @return string - * - * @throws \Exception */ public function create($name, $path, $table = null, $create = false) { - $this->ensureMigrationDoesntAlreadyExist($name, $path); + $path = $this->getPath($name, $path); // First we will get the stub file for the migration, which serves as a type // of template for the migration. Once we have those we will populate the // various place-holders, save the file, and run the post create event. $stub = $this->getStub($table, $create); - $path = $this->getPath($name, $path); - - $this->files->ensureDirectoryExists(dirname($path)); + $this->files->put($path, $this->populateStub($name, $stub, $table)); - $this->files->put( - $path, $this->populateStub($stub, $table) - ); - - // Next, we will fire any hooks that are supposed to fire after a migration is - // created. Once that is done we'll be ready to return the full path to the - // migration file so it can be used however it's needed by the developer. - $this->firePostCreateHooks($table, $path); + $this->firePostCreateHooks(); return $path; } - /** - * Ensure that a migration with the given name doesn't already exist. - * - * @param string $name - * @param string|null $migrationPath - * @return void - * - * @throws \InvalidArgumentException - */ - protected function ensureMigrationDoesntAlreadyExist($name, $migrationPath = null) - { - if (! empty($migrationPath)) { - $migrationFiles = $this->files->glob($migrationPath.'/*.php'); - - foreach ($migrationFiles as $migrationFile) { - $this->files->requireOnce($migrationFile); - } - } - - if (class_exists($className = $this->getClassName($name))) { - throw new InvalidArgumentException("A {$className} class already exists."); - } - } - /** * Get the migration stub file. * - * @param string|null $table - * @param bool $create + * @param string $table + * @param bool $create * @return string */ protected function getStub($table, $create) { if (is_null($table)) { - $stub = $this->files->exists($customPath = $this->customStubPath.'/migration.stub') - ? $customPath - : $this->stubPath().'/migration.stub'; - } elseif ($create) { - $stub = $this->files->exists($customPath = $this->customStubPath.'/migration.create.stub') - ? $customPath - : $this->stubPath().'/migration.create.stub'; - } else { - $stub = $this->files->exists($customPath = $this->customStubPath.'/migration.update.stub') - ? $customPath - : $this->stubPath().'/migration.update.stub'; + return $this->files->get($this->getStubPath().'/blank.stub'); } - return $this->files->get($stub); + // We also have stubs for creating new tables and modifying existing tables + // to save the developer some typing when they are creating a new tables + // or modifying existing tables. We'll grab the appropriate stub here. + else { + $stub = $create ? 'create.stub' : 'update.stub'; + + return $this->files->get($this->getStubPath()."/{$stub}"); + } } /** * Populate the place-holders in the migration stub. * + * @param string $name * @param string $stub - * @param string|null $table + * @param string $table * @return string */ - protected function populateStub($stub, $table) + protected function populateStub($name, $stub, $table) { + $stub = str_replace('DummyClass', $this->getClassName($name), $stub); + // Here we will replace the table place-holders with the table specified by // the developer, which is useful for quickly creating a tables creation // or update migration from the console instead of typing it manually. if (! is_null($table)) { - $stub = str_replace( - ['DummyTable', '{{ table }}', '{{table}}'], - $table, $stub - ); + $stub = str_replace('DummyTable', $table, $stub); } return $stub; @@ -154,43 +107,29 @@ protected function populateStub($stub, $table) * Get the class name of a migration name. * * @param string $name - * @return class-string<\Illuminate\Database\Migrations\Migration> + * @return string */ protected function getClassName($name) { return Str::studly($name); } - /** - * Get the full path to the migration. - * - * @param string $name - * @param string $path - * @return string - */ - protected function getPath($name, $path) - { - return $path.'/'.$this->getDatePrefix().'_'.$name.'.php'; - } - /** * Fire the registered post create hooks. * - * @param string|null $table - * @param string $path * @return void */ - protected function firePostCreateHooks($table, $path) + protected function firePostCreateHooks() { foreach ($this->postCreate as $callback) { - $callback($table, $path); + call_user_func($callback); } } /** * Register a post migration create hook. * - * @param (\Closure(string, string): void) $callback + * @param \Closure $callback * @return void */ public function afterCreate(Closure $callback) @@ -198,6 +137,18 @@ public function afterCreate(Closure $callback) $this->postCreate[] = $callback; } + /** + * Get the full path name to the migration. + * + * @param string $name + * @param string $path + * @return string + */ + protected function getPath($name, $path) + { + return $path.'/'.$this->getDatePrefix().'_'.$name.'.php'; + } + /** * Get the date prefix for the migration. * @@ -213,7 +164,7 @@ protected function getDatePrefix() * * @return string */ - public function stubPath() + public function getStubPath() { return __DIR__.'/stubs'; } diff --git a/Migrations/MigrationRepositoryInterface.php b/Migrations/MigrationRepositoryInterface.php index 49cc08e21a..5450a7af9c 100755 --- a/Migrations/MigrationRepositoryInterface.php +++ b/Migrations/MigrationRepositoryInterface.php @@ -5,47 +5,24 @@ interface MigrationRepositoryInterface { /** - * Get the completed migrations. + * Get the ran migrations for a given package. * - * @return string[] + * @return array */ public function getRan(); - /** - * Get the list of migrations. - * - * @param int $steps - * @return array{id: int, migration: string, batch: int}[] - */ - public function getMigrations($steps); - - /** - * Get the list of the migrations by batch. - * - * @param int $batch - * @return array{id: int, migration: string, batch: int}[] - */ - public function getMigrationsByBatch($batch); - /** * Get the last migration batch. * - * @return array{id: int, migration: string, batch: int}[] + * @return array */ public function getLast(); - /** - * Get the completed migrations with their batch numbers. - * - * @return array[] - */ - public function getMigrationBatches(); - /** * Log that a migration was run. * * @param string $file - * @param int $batch + * @param int $batch * @return void */ public function log($file, $batch); @@ -53,7 +30,7 @@ public function log($file, $batch); /** * Remove a migration from the log. * - * @param objectt{id?: int, migration: string, batch?: int} $migration + * @param object $migration * @return void */ public function delete($migration); @@ -79,13 +56,6 @@ public function createRepository(); */ public function repositoryExists(); - /** - * Delete the migration repository data store. - * - * @return void - */ - public function deleteRepository(); - /** * Set the information source to gather data. * diff --git a/Migrations/MigrationResult.php b/Migrations/MigrationResult.php deleted file mode 100644 index 649eb5b269..0000000000 --- a/Migrations/MigrationResult.php +++ /dev/null @@ -1,10 +0,0 @@ - - */ - protected static $requiredPathCache = []; - - /** - * The output interface implementation. - * - * @var \Symfony\Component\Console\Output\OutputInterface - */ - protected $output; - - /** - * The pending migrations to skip. - * - * @var list - */ - protected static $withoutMigrations = []; + protected $notes = []; /** * Create a new migrator instance. @@ -100,109 +50,65 @@ class Migrator * @param \Illuminate\Database\Migrations\MigrationRepositoryInterface $repository * @param \Illuminate\Database\ConnectionResolverInterface $resolver * @param \Illuminate\Filesystem\Filesystem $files - * @param \Illuminate\Contracts\Events\Dispatcher|null $dispatcher + * @return void */ - public function __construct( - MigrationRepositoryInterface $repository, - Resolver $resolver, - Filesystem $files, - ?Dispatcher $dispatcher = null, - ) { + public function __construct(MigrationRepositoryInterface $repository, + Resolver $resolver, + Filesystem $files) + { $this->files = $files; - $this->events = $dispatcher; $this->resolver = $resolver; $this->repository = $repository; } /** - * Run the pending migrations at a given path. + * Run the outstanding migrations at a given path. * - * @param string[]|string $paths - * @param array $options - * @return string[] + * @param string $path + * @param array $options + * @return void */ - public function run($paths = [], array $options = []) + public function run($path, array $options = []) { + $this->notes = []; + + $files = $this->getMigrationFiles($path); + // Once we grab all of the migration files for the path, we will compare them // against the migrations that have already been run for this package then // run each of the outstanding migrations against a database connection. - $files = $this->getMigrationFiles($paths); - - $this->requireFiles($migrations = $this->pendingMigrations( - $files, $this->repository->getRan() - )); + $ran = $this->repository->getRan(); - // Once we have all these migrations that are outstanding we are ready to run - // we will go ahead and run them "up". This will execute each migration as - // an operation against a database. Then we'll return this list of them. - $this->runPending($migrations, $options); + $migrations = array_diff($files, $ran); - return $migrations; - } - - /** - * Get the migration files that have not yet run. - * - * @param string[] $files - * @param string[] $ran - * @return string[] - */ - protected function pendingMigrations($files, $ran) - { - $migrationsToSkip = $this->migrationsToSkip(); - - return (new Collection($files)) - ->reject(fn ($file) => in_array($migrationName = $this->getMigrationName($file), $ran) || - in_array($migrationName, $migrationsToSkip) - ) - ->values() - ->all(); - } + $this->requireFiles($path, $migrations); - /** - * Get list of pending migrations to skip. - * - * @return list - */ - protected function migrationsToSkip() - { - return (new Collection(self::$withoutMigrations)) - ->map($this->getMigrationName(...)) - ->all(); + $this->runMigrationList($migrations, $options); } /** * Run an array of migrations. * - * @param string[] $migrations - * @param array $options + * @param array $migrations + * @param array $options * @return void */ - public function runPending(array $migrations, array $options = []) + public function runMigrationList($migrations, array $options = []) { // First we will just make sure that there are any migrations to run. If there // aren't, we will just make a note of it to the developer so they're aware // that all of the migrations have been run against this database system. - if (count($migrations) === 0) { - $this->fireMigrationEvent(new NoPendingMigrations('up')); - - $this->write(Info::class, 'Nothing to migrate'); + if (count($migrations) == 0) { + $this->note('Nothing to migrate.'); return; } - // Next, we will get the next batch number for the migrations so we can insert - // correct batch number in the database migrations repository when we store - // each migration's execution. We will also extract a few of the options. $batch = $this->repository->getNextBatchNumber(); - $pretend = $options['pretend'] ?? false; + $pretend = Arr::get($options, 'pretend', false); - $step = $options['step'] ?? false; - - $this->fireMigrationEvent(new MigrationsStarted('up', $options)); - - $this->write(Info::class, 'Running migrations.'); + $step = Arr::get($options, 'step', false); // Once we have the array of migrations, we will spin through them and run the // migrations "up" so the changes are made to the databases. We'll then log @@ -210,22 +116,21 @@ public function runPending(array $migrations, array $options = []) foreach ($migrations as $file) { $this->runUp($file, $batch, $pretend); + // If we are stepping through the migrations, then we will increment the + // batch value for each individual migration that is run. That way we + // can run "artisan migrate:rollback" and undo them one at a time. if ($step) { $batch++; } } - - $this->fireMigrationEvent(new MigrationsEnded('up', $options)); - - $this->output?->writeln(''); } /** * Run "up" a migration instance. * * @param string $file - * @param int $batch - * @param bool $pretend + * @param int $batch + * @param bool $pretend * @return void */ protected function runUp($file, $batch, $pretend) @@ -233,222 +138,150 @@ protected function runUp($file, $batch, $pretend) // First we will resolve a "real" instance of the migration class from this // migration file name. Once we have the instances we can run the actual // command such as "up" or "down", or we can just simulate the action. - $migration = $this->resolvePath($file); - - $name = $this->getMigrationName($file); + $migration = $this->resolve($file); if ($pretend) { return $this->pretendToRun($migration, 'up'); } - $shouldRunMigration = $migration instanceof Migration - ? $migration->shouldRun() - : true; - - if (! $shouldRunMigration) { - $this->fireMigrationEvent(new MigrationSkipped($name)); + $migration->up(); - $this->write(Task::class, $name, fn () => MigrationResult::Skipped->value); - } else { - $this->write(Task::class, $name, fn () => $this->runMigration($migration, 'up')); + // Once we have run a migrations class, we will log that it was run in this + // repository so that we don't try to run it next time we do a migration + // in the application. A migration repository keeps the migrate order. + $this->repository->log($file, $batch); - // Once we have run a migrations class, we will log that it was run in this - // repository so that we don't try to run it next time we do a migration - // in the application. A migration repository keeps the migrate order. - $this->repository->log($name, $batch); - } + $this->note("Migrated: $file"); } /** * Rollback the last migration operation. * - * @param string[]|string $paths - * @param array $options - * @return string[] + * @param bool $pretend + * @return int */ - public function rollback($paths = [], array $options = []) + public function rollback($pretend = false) { + $this->notes = []; + // We want to pull in the last batch of migrations that ran on the previous // migration operation. We'll then reverse those migrations and run each // of them "down" to reverse the last migration "operation" which ran. - $migrations = $this->getMigrationsForRollback($options); - - if (count($migrations) === 0) { - $this->fireMigrationEvent(new NoPendingMigrations('down')); + $migrations = $this->repository->getLast(); - $this->write(Info::class, 'Nothing to rollback.'); - - return []; - } - - return tap($this->rollbackMigrations($migrations, $paths, $options), function () { - $this->output?->writeln(''); - }); - } - - /** - * Get the migrations for a rollback operation. - * - * @param array $options - * @return array{id: int, migration: string, batch: int}[] - */ - protected function getMigrationsForRollback(array $options) - { - if (($steps = $options['step'] ?? 0) > 0) { - return $this->repository->getMigrations($steps); - } - - if (($batch = $options['batch'] ?? 0) > 0) { - return $this->repository->getMigrationsByBatch($batch); - } + $count = count($migrations); - return $this->repository->getLast(); - } - - /** - * Rollback the given migrations. - * - * @param array $migrations - * @param string[]|string $paths - * @param array $options - * @return string[] - */ - protected function rollbackMigrations(array $migrations, $paths, array $options) - { - $rolledBack = []; - - $this->requireFiles($files = $this->getMigrationFiles($paths)); - - $this->fireMigrationEvent(new MigrationsStarted('down', $options)); - - $this->write(Info::class, 'Rolling back migrations.'); - - // Next we will run through all of the migrations and call the "down" method - // which will reverse each migration in order. This getLast method on the - // repository already returns these migration's names in reverse order. - foreach ($migrations as $migration) { - $migration = (object) $migration; - - if (! $file = Arr::get($files, $migration->migration)) { - $this->write(TwoColumnDetail::class, $migration->migration, 'Migration not found'); - - continue; + if ($count === 0) { + $this->note('Nothing to rollback.'); + } else { + // We need to reverse these migrations so that they are "downed" in reverse + // to what they run on "up". It lets us backtrack through the migrations + // and properly reverse the entire database schema operation that ran. + foreach ($migrations as $migration) { + $this->runDown((object) $migration, $pretend); } - - $rolledBack[] = $file; - - $this->runDown( - $file, $migration, - $options['pretend'] ?? false - ); } - $this->fireMigrationEvent(new MigrationsEnded('down', $options)); - - return $rolledBack; + return $count; } /** * Rolls all of the currently applied migrations back. * - * @param string[]|string $paths * @param bool $pretend - * @return array + * @return int */ - public function reset($paths = [], $pretend = false) + public function reset($pretend = false) { - // Next, we will reverse the migration list so we can run them back in the - // correct order for resetting this database. This will allow us to get - // the database back into its "empty" state ready for the migrations. + $this->notes = []; + $migrations = array_reverse($this->repository->getRan()); - if (count($migrations) === 0) { - $this->write(Info::class, 'Nothing to rollback.'); + $count = count($migrations); - return []; + if ($count === 0) { + $this->note('Nothing to rollback.'); + } else { + foreach ($migrations as $migration) { + $this->runDown((object) ['migration' => $migration], $pretend); + } } - return tap($this->resetMigrations($migrations, Arr::wrap($paths), $pretend), function () { - $this->output?->writeln(''); - }); - } - - /** - * Reset the given migrations. - * - * @param string[] $migrations - * @param string[] $paths - * @param bool $pretend - * @return array - */ - protected function resetMigrations(array $migrations, array $paths, $pretend = false) - { - // Since the getRan method that retrieves the migration name just gives us the - // migration name, we will format the names into objects with the name as a - // property on the objects so that we can pass it to the rollback method. - $migrations = (new Collection($migrations))->map(fn ($m) => (object) ['migration' => $m])->all(); - - return $this->rollbackMigrations( - $migrations, $paths, compact('pretend') - ); + return $count; } /** * Run "down" a migration instance. * - * @param string $file * @param object $migration - * @param bool $pretend + * @param bool $pretend * @return void */ - protected function runDown($file, $migration, $pretend) + protected function runDown($migration, $pretend) { + $file = $migration->migration; + // First we will get the file name of the migration so we can resolve out an // instance of the migration. Once we get an instance we can either run a // pretend execution of the migration or we can run the real migration. - $instance = $this->resolvePath($file); - - $name = $this->getMigrationName($file); + $instance = $this->resolve($file); if ($pretend) { return $this->pretendToRun($instance, 'down'); } - $this->write(Task::class, $name, fn () => $this->runMigration($instance, 'down')); + $instance->down(); // Once we have successfully run the migration "down" we will remove it from // the migration repository so it will be considered to have not been run // by the application then will be able to fire by any later operation. $this->repository->delete($migration); + + $this->note("Rolled back: $file"); } /** - * Run a migration inside a transaction if the database supports it. + * Get all of the migration files in a given path. * - * @param object $migration - * @param string $method - * @return void + * @param string $path + * @return array */ - protected function runMigration($migration, $method) + public function getMigrationFiles($path) { - $connection = $this->resolveConnection( - $migration->getConnection() - ); + $files = $this->files->glob($path.'/*_*.php'); - $callback = function () use ($connection, $migration, $method) { - if (method_exists($migration, $method)) { - $this->fireMigrationEvent(new MigrationStarted($migration, $method)); + // Once we have the array of files in the directory we will just remove the + // extension and take the basename of the file which is all we need when + // finding the migrations that haven't been run against the databases. + if ($files === false) { + return []; + } - $this->runMethod($connection, $migration, $method); + $files = array_map(function ($file) { + return str_replace('.php', '', basename($file)); - $this->fireMigrationEvent(new MigrationEnded($migration, $method)); - } - }; + }, $files); + + // Once we have all of the formatted file names we will sort them and since + // they all start with a timestamp this should give us the migrations in + // the order they were actually created by the application developers. + sort($files); + + return $files; + } - $this->getSchemaGrammar($connection)->supportsSchemaTransactions() - && $migration->withinTransaction - ? $connection->transaction($callback) - : $callback(); + /** + * Require in all the migration files in a given path. + * + * @param string $path + * @param array $files + * @return void + */ + public function requireFiles($path, array $files) + { + foreach ($files as $file) { + $this->files->requireOnce($path.'/'.$file.'.php'); + } } /** @@ -460,20 +293,11 @@ protected function runMigration($migration, $method) */ protected function pretendToRun($migration, $method) { - $name = get_class($migration); + foreach ($this->getQueries($migration, $method) as $query) { + $name = get_class($migration); - $reflectionClass = new ReflectionClass($migration); - - if ($reflectionClass->isAnonymous()) { - $name = $this->getMigrationName($reflectionClass->getFileName()); + $this->note("{$name}: {$query['query']}"); } - - $this->write(TwoColumnDetail::class, $name); - - $this->write( - BulletList::class, - (new Collection($this->getQueries($migration, $method)))->map(fn ($query) => $query['query']) - ); } /** @@ -485,41 +309,18 @@ protected function pretendToRun($migration, $method) */ protected function getQueries($migration, $method) { + $connection = $migration->getConnection(); + // Now that we have the connections we can resolve it and pretend to run the // queries against the database returning the array of raw SQL statements // that would get fired against the database system for this migration. - $db = $this->resolveConnection( - $migration->getConnection() - ); + $db = $this->resolveConnection($connection); - return $db->pretend(function () use ($db, $migration, $method) { - if (method_exists($migration, $method)) { - $this->runMethod($db, $migration, $method); - } + return $db->pretend(function () use ($migration, $method) { + $migration->$method(); }); } - /** - * Run a migration method on the given connection. - * - * @param \Illuminate\Database\Connection $connection - * @param object $migration - * @param string $method - * @return void - */ - protected function runMethod($connection, $migration, $method) - { - $previousConnection = $this->resolver->getDefaultConnection(); - - try { - $this->resolver->setDefaultConnection($connection->getName()); - - $migration->{$method}(); - } finally { - $this->resolver->setDefaultConnection($previousConnection); - } - } - /** * Resolve a migration instance from a file. * @@ -528,150 +329,43 @@ protected function runMethod($connection, $migration, $method) */ public function resolve($file) { - $class = $this->getMigrationClass($file); - - return new $class; - } - - /** - * Resolve a migration instance from a migration path. - * - * @param string $path - * @return object - */ - protected function resolvePath(string $path) - { - $class = $this->getMigrationClass($this->getMigrationName($path)); - - if (class_exists($class) && realpath($path) == (new ReflectionClass($class))->getFileName()) { - return new $class; - } + $file = implode('_', array_slice(explode('_', $file), 4)); - $migration = static::$requiredPathCache[$path] ??= $this->files->getRequire($path); - - if (is_object($migration)) { - return method_exists($migration, '__construct') - ? $this->files->getRequire($path) - : clone $migration; - } + $class = Str::studly($file); return new $class; } /** - * Generate a migration class name based on the migration file name. - * - * @param string $migrationName - * @return string - */ - protected function getMigrationClass(string $migrationName): string - { - return Str::studly(implode('_', array_slice(explode('_', $migrationName), 4))); - } - - /** - * Get all of the migration files in a given path. - * - * @param string|array $paths - * @return array - */ - public function getMigrationFiles($paths) - { - return (new Collection($paths)) - ->flatMap(fn ($path) => str_ends_with($path, '.php') ? [$path] : $this->files->glob($path.'/*_*.php')) - ->filter() - ->values() - ->keyBy(fn ($file) => $this->getMigrationName($file)) - ->sortBy(fn ($file, $key) => $key) - ->all(); - } - - /** - * Require in all the migration files in a given path. - * - * @param string[] $files - * @return void - */ - public function requireFiles(array $files) - { - foreach ($files as $file) { - $this->files->requireOnce($file); - } - } - - /** - * Get the name of the migration. - * - * @param string $path - * @return string - */ - public function getMigrationName($path) - { - return str_replace('.php', '', basename($path)); - } - - /** - * Register a custom migration path. + * Raise a note event for the migrator. * - * @param string $path + * @param string $message * @return void */ - public function path($path) + protected function note($message) { - $this->paths = array_unique(array_merge($this->paths, [$path])); + $this->notes[] = $message; } /** - * Get all of the custom migration paths. + * Get the notes for the last operation. * - * @return string[] - */ - public function paths() - { - return $this->paths; - } - - /** - * Set the pending migrations to skip. - * - * @param list $migrations - * @return void + * @return array */ - public static function withoutMigrations(array $migrations) + public function getNotes() { - static::$withoutMigrations = $migrations; + return $this->notes; } /** - * Get the default connection name. + * Resolve the database connection instance. * - * @return string + * @param string $connection + * @return \Illuminate\Database\Connection */ - public function getConnection() + public function resolveConnection($connection) { - return $this->connection; - } - - /** - * Execute the given callback using the given connection as the default connection. - * - * @template TReturn - * - * @param string $name - * @param (callable(): TReturn) $callback - * @return mixed - */ - public function usingConnection($name, callable $callback) - { - $previousConnection = $this->resolver->getDefaultConnection(); - - $this->setConnection($name); - - try { - return $callback(); - } finally { - $this->setConnection($previousConnection); - } + return $this->resolver->connection($connection); } /** @@ -691,53 +385,6 @@ public function setConnection($name) $this->connection = $name; } - /** - * Resolve the database connection instance. - * - * @param string $connection - * @return \Illuminate\Database\Connection - */ - public function resolveConnection($connection) - { - if (static::$connectionResolverCallback) { - return call_user_func( - static::$connectionResolverCallback, - $this->resolver, - $connection ?: $this->connection - ); - } else { - return $this->resolver->connection($connection ?: $this->connection); - } - } - - /** - * Set a connection resolver callback. - * - * @param \Closure(\Illuminate\Database\ConnectionResolverInterface, ?string): \Illuminate\Database\Connection $callback - * @return void - */ - public static function resolveConnectionsUsing(Closure $callback) - { - static::$connectionResolverCallback = $callback; - } - - /** - * Get the schema grammar out of a migration connection. - * - * @param \Illuminate\Database\Connection $connection - * @return \Illuminate\Database\Schema\Grammars\Grammar - */ - protected function getSchemaGrammar($connection) - { - if (is_null($grammar = $connection->getSchemaGrammar())) { - $connection->useDefaultSchemaGrammar(); - - $grammar = $connection->getSchemaGrammar(); - } - - return $grammar; - } - /** * Get the migration repository instance. * @@ -758,26 +405,6 @@ public function repositoryExists() return $this->repository->repositoryExists(); } - /** - * Determine if any migrations have been run. - * - * @return bool - */ - public function hasRunAnyMigrations() - { - return $this->repositoryExists() && count($this->repository->getRan()) > 0; - } - - /** - * Delete the migration repository data store. - * - * @return void - */ - public function deleteRepository() - { - $this->repository->deleteRepository(); - } - /** * Get the file system instance. * @@ -787,48 +414,4 @@ public function getFilesystem() { return $this->files; } - - /** - * Set the output implementation that should be used by the console. - * - * @param \Symfony\Component\Console\Output\OutputInterface $output - * @return $this - */ - public function setOutput(OutputInterface $output) - { - $this->output = $output; - - return $this; - } - - /** - * Write to the console's output. - * - * @param string $component - * @param array|string ...$arguments - * @return void - */ - protected function write($component, ...$arguments) - { - if ($this->output && class_exists($component)) { - (new $component($this->output))->render(...$arguments); - } else { - foreach ($arguments as $argument) { - if (is_callable($argument)) { - $argument(); - } - } - } - } - - /** - * Fire the given event for the migration. - * - * @param \Illuminate\Contracts\Database\Events\MigrationEvent $event - * @return void - */ - public function fireMigrationEvent($event) - { - $this->events?->dispatch($event); - } } diff --git a/Migrations/stubs/migration.stub b/Migrations/stubs/blank.stub similarity index 62% rename from Migrations/stubs/migration.stub rename to Migrations/stubs/blank.stub index 88fa2f36b0..4ff5ee58d6 100755 --- a/Migrations/stubs/migration.stub +++ b/Migrations/stubs/blank.stub @@ -1,24 +1,27 @@ increments('id'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('DummyTable'); + } +} diff --git a/Migrations/stubs/migration.create.stub b/Migrations/stubs/migration.create.stub deleted file mode 100755 index 168c622236..0000000000 --- a/Migrations/stubs/migration.create.stub +++ /dev/null @@ -1,27 +0,0 @@ -id(); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('{{ table }}'); - } -}; diff --git a/Migrations/stubs/migration.update.stub b/Migrations/stubs/migration.update.stub deleted file mode 100755 index c9ff2c4b73..0000000000 --- a/Migrations/stubs/migration.update.stub +++ /dev/null @@ -1,28 +0,0 @@ -count = $count; - - parent::__construct("$count records were found.", $code, $previous); - } - - /** - * Get the number of records found. - * - * @return int - */ - public function getCount() - { - return $this->count; - } -} diff --git a/MySqlConnection.php b/MySqlConnection.php index ab541c9aa5..446426d024 100755 --- a/MySqlConnection.php +++ b/MySqlConnection.php @@ -2,116 +2,26 @@ namespace Illuminate\Database; -use Exception; -use Illuminate\Database\Query\Grammars\MySqlGrammar as QueryGrammar; +use Illuminate\Database\Schema\MySqlBuilder; use Illuminate\Database\Query\Processors\MySqlProcessor; +use Doctrine\DBAL\Driver\PDOMySql\Driver as DoctrineDriver; +use Illuminate\Database\Query\Grammars\MySqlGrammar as QueryGrammar; use Illuminate\Database\Schema\Grammars\MySqlGrammar as SchemaGrammar; -use Illuminate\Database\Schema\MySqlBuilder; -use Illuminate\Database\Schema\MySqlSchemaState; -use Illuminate\Filesystem\Filesystem; -use Illuminate\Support\Str; -use PDO; class MySqlConnection extends Connection { /** - * The last inserted ID generated by the server. - * - * @var string|int|null - */ - protected $lastInsertId; - - /** - * {@inheritdoc} - */ - public function getDriverTitle() - { - return $this->isMaria() ? 'MariaDB' : 'MySQL'; - } - - /** - * Run an insert statement against the database. - * - * @param string $query - * @param array $bindings - * @param string|null $sequence - * @return bool - */ - public function insert($query, $bindings = [], $sequence = null) - { - return $this->run($query, $bindings, function ($query, $bindings) use ($sequence) { - if ($this->pretending()) { - return true; - } - - $statement = $this->getPdo()->prepare($query); - - $this->bindValues($statement, $this->prepareBindings($bindings)); - - $this->recordsHaveBeenModified(); - - $result = $statement->execute(); - - $this->lastInsertId = $this->getPdo()->lastInsertId($sequence); - - return $result; - }); - } - - /** - * Escape a binary value for safe SQL embedding. - * - * @param string $value - * @return string - */ - protected function escapeBinary($value) - { - $hex = bin2hex($value); - - return "x'{$hex}'"; - } - - /** - * Determine if the given database exception was caused by a unique constraint violation. - * - * @param \Exception $exception - * @return bool - */ - protected function isUniqueConstraintError(Exception $exception) - { - return (bool) preg_match('#Integrity constraint violation: 1062#i', $exception->getMessage()); - } - - /** - * Get the connection's last insert ID. - * - * @return string|int|null - */ - public function getLastInsertId() - { - return $this->lastInsertId; - } - - /** - * Determine if the connected database is a MariaDB database. + * Get a schema builder instance for the connection. * - * @return bool + * @return \Illuminate\Database\Schema\MySqlBuilder */ - public function isMaria() + public function getSchemaBuilder() { - return str_contains($this->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION), 'MariaDB'); - } + if (is_null($this->schemaGrammar)) { + $this->useDefaultSchemaGrammar(); + } - /** - * Get the server version for the connection. - * - * @return string - */ - public function getServerVersion(): string - { - return str_contains($version = parent::getServerVersion(), 'MariaDB') - ? Str::between($version, '5.5.5-', '-MariaDB') - : $version; + return new MySqlBuilder($this); } /** @@ -121,21 +31,7 @@ public function getServerVersion(): string */ protected function getDefaultQueryGrammar() { - return new QueryGrammar($this); - } - - /** - * Get a schema builder instance for the connection. - * - * @return \Illuminate\Database\Schema\MySqlBuilder - */ - public function getSchemaBuilder() - { - if (is_null($this->schemaGrammar)) { - $this->useDefaultSchemaGrammar(); - } - - return new MySqlBuilder($this); + return $this->withTablePrefix(new QueryGrammar); } /** @@ -145,28 +41,26 @@ public function getSchemaBuilder() */ protected function getDefaultSchemaGrammar() { - return new SchemaGrammar($this); + return $this->withTablePrefix(new SchemaGrammar); } /** - * Get the schema state for the connection. + * Get the default post processor instance. * - * @param \Illuminate\Filesystem\Filesystem|null $files - * @param callable|null $processFactory - * @return \Illuminate\Database\Schema\MySqlSchemaState + * @return \Illuminate\Database\Query\Processors\MySqlProcessor */ - public function getSchemaState(?Filesystem $files = null, ?callable $processFactory = null) + protected function getDefaultPostProcessor() { - return new MySqlSchemaState($this, $files, $processFactory); + return new MySqlProcessor; } /** - * Get the default post processor instance. + * Get the Doctrine DBAL driver. * - * @return \Illuminate\Database\Query\Processors\MySqlProcessor + * @return \Doctrine\DBAL\Driver\PDOMySql\Driver */ - protected function getDefaultPostProcessor() + protected function getDoctrineDriver() { - return new MySqlProcessor; + return new DoctrineDriver; } } diff --git a/PostgresConnection.php b/PostgresConnection.php index f80b5dce5d..779888d0e5 100755 --- a/PostgresConnection.php +++ b/PostgresConnection.php @@ -2,57 +2,26 @@ namespace Illuminate\Database; -use Exception; -use Illuminate\Database\Query\Grammars\PostgresGrammar as QueryGrammar; +use Illuminate\Database\Schema\PostgresBuilder; +use Doctrine\DBAL\Driver\PDOPgSql\Driver as DoctrineDriver; use Illuminate\Database\Query\Processors\PostgresProcessor; +use Illuminate\Database\Query\Grammars\PostgresGrammar as QueryGrammar; use Illuminate\Database\Schema\Grammars\PostgresGrammar as SchemaGrammar; -use Illuminate\Database\Schema\PostgresBuilder; -use Illuminate\Database\Schema\PostgresSchemaState; -use Illuminate\Filesystem\Filesystem; class PostgresConnection extends Connection { /** - * {@inheritdoc} - */ - public function getDriverTitle() - { - return 'PostgreSQL'; - } - - /** - * Escape a binary value for safe SQL embedding. - * - * @param string $value - * @return string - */ - protected function escapeBinary($value) - { - $hex = bin2hex($value); - - return "'\x{$hex}'::bytea"; - } - - /** - * Escape a bool value for safe SQL embedding. + * Get a schema builder instance for the connection. * - * @param bool $value - * @return string + * @return \Illuminate\Database\Schema\PostgresBuilder */ - protected function escapeBool($value) + public function getSchemaBuilder() { - return $value ? 'true' : 'false'; - } + if (is_null($this->schemaGrammar)) { + $this->useDefaultSchemaGrammar(); + } - /** - * Determine if the given database exception was caused by a unique constraint violation. - * - * @param \Exception $exception - * @return bool - */ - protected function isUniqueConstraintError(Exception $exception) - { - return '23505' === $exception->getCode(); + return new PostgresBuilder($this); } /** @@ -62,21 +31,7 @@ protected function isUniqueConstraintError(Exception $exception) */ protected function getDefaultQueryGrammar() { - return new QueryGrammar($this); - } - - /** - * Get a schema builder instance for the connection. - * - * @return \Illuminate\Database\Schema\PostgresBuilder - */ - public function getSchemaBuilder() - { - if (is_null($this->schemaGrammar)) { - $this->useDefaultSchemaGrammar(); - } - - return new PostgresBuilder($this); + return $this->withTablePrefix(new QueryGrammar); } /** @@ -86,28 +41,26 @@ public function getSchemaBuilder() */ protected function getDefaultSchemaGrammar() { - return new SchemaGrammar($this); + return $this->withTablePrefix(new SchemaGrammar); } /** - * Get the schema state for the connection. + * Get the default post processor instance. * - * @param \Illuminate\Filesystem\Filesystem|null $files - * @param callable|null $processFactory - * @return \Illuminate\Database\Schema\PostgresSchemaState + * @return \Illuminate\Database\Query\Processors\PostgresProcessor */ - public function getSchemaState(?Filesystem $files = null, ?callable $processFactory = null) + protected function getDefaultPostProcessor() { - return new PostgresSchemaState($this, $files, $processFactory); + return new PostgresProcessor; } /** - * Get the default post processor instance. + * Get the Doctrine DBAL driver. * - * @return \Illuminate\Database\Query\Processors\PostgresProcessor + * @return \Doctrine\DBAL\Driver\PDOPgSql\Driver */ - protected function getDefaultPostProcessor() + protected function getDoctrineDriver() { - return new PostgresProcessor; + return new DoctrineDriver; } } diff --git a/Query/Builder.php b/Query/Builder.php index 442affeddf..8cba5bb640 100755 --- a/Query/Builder.php +++ b/Query/Builder.php @@ -2,136 +2,93 @@ namespace Illuminate\Database\Query; -use BackedEnum; use Closure; -use DatePeriod; -use DateTimeInterface; -use Illuminate\Contracts\Database\Query\Builder as BuilderContract; -use Illuminate\Contracts\Database\Query\ConditionExpression; -use Illuminate\Contracts\Database\Query\Expression as ExpressionContract; +use RuntimeException; +use BadMethodCallException; +use Illuminate\Support\Arr; +use Illuminate\Support\Str; +use InvalidArgumentException; +use Illuminate\Pagination\Paginator; +use Illuminate\Support\Traits\Macroable; use Illuminate\Contracts\Support\Arrayable; -use Illuminate\Database\Concerns\BuildsQueries; -use Illuminate\Database\Concerns\BuildsWhereDateClauses; -use Illuminate\Database\Concerns\ExplainsQueries; use Illuminate\Database\ConnectionInterface; -use Illuminate\Database\Eloquent\Builder as EloquentBuilder; -use Illuminate\Database\Eloquent\Relations\Relation; -use Illuminate\Database\PostgresConnection; use Illuminate\Database\Query\Grammars\Grammar; +use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Database\Query\Processors\Processor; -use Illuminate\Pagination\Paginator; -use Illuminate\Support\Arr; -use Illuminate\Support\Collection; -use Illuminate\Support\LazyCollection; -use Illuminate\Support\Str; -use Illuminate\Support\Traits\ForwardsCalls; -use Illuminate\Support\Traits\Macroable; -use InvalidArgumentException; -use LogicException; -use RuntimeException; -use UnitEnum; -use function Illuminate\Support\enum_value; - -class Builder implements BuilderContract +class Builder { - /** @use \Illuminate\Database\Concerns\BuildsQueries<\stdClass> */ - use BuildsWhereDateClauses, BuildsQueries, ExplainsQueries, ForwardsCalls, Macroable { + use Macroable { __call as macroCall; } /** * The database connection instance. * - * @var \Illuminate\Database\ConnectionInterface + * @var \Illuminate\Database\Connection */ - public $connection; + protected $connection; /** * The database query grammar instance. * * @var \Illuminate\Database\Query\Grammars\Grammar */ - public $grammar; + protected $grammar; /** * The database query post processor instance. * * @var \Illuminate\Database\Query\Processors\Processor */ - public $processor; + protected $processor; /** * The current query value bindings. * - * @var array{ - * select: list, - * from: list, - * join: list, - * where: list, - * groupBy: list, - * having: list, - * order: list, - * union: list, - * unionOrder: list, - * } - */ - public $bindings = [ + * @var array + */ + protected $bindings = [ 'select' => [], - 'from' => [], - 'join' => [], - 'where' => [], - 'groupBy' => [], + 'join' => [], + 'where' => [], 'having' => [], - 'order' => [], - 'union' => [], - 'unionOrder' => [], + 'order' => [], + 'union' => [], ]; /** * An aggregate function and column to be run. * - * @var array{ - * function: string, - * columns: array<\Illuminate\Contracts\Database\Query\Expression|string> - * }|null + * @var array */ public $aggregate; /** * The columns that should be returned. * - * @var array|null + * @var array */ public $columns; /** * Indicates if the query returns distinct results. * - * Occasionally contains the columns that should be distinct. - * - * @var bool|array + * @var bool */ public $distinct = false; /** * The table which the query is targeting. * - * @var \Illuminate\Database\Query\Expression|string + * @var string */ public $from; - /** - * The index hint for the query. - * - * @var \Illuminate\Database\Query\IndexHint|null - */ - public $indexHint; - /** * The table joins for the query. * - * @var array|null + * @var array */ public $joins; @@ -140,151 +97,125 @@ class Builder implements BuilderContract * * @var array */ - public $wheres = []; + public $wheres; /** * The groupings for the query. * - * @var array|null + * @var array */ public $groups; /** * The having constraints for the query. * - * @var array|null + * @var array */ public $havings; /** * The orderings for the query. * - * @var array|null + * @var array */ public $orders; /** * The maximum number of records to return. * - * @var int|null + * @var int */ public $limit; - /** - * The maximum number of records to return per group. - * - * @var array|null - */ - public $groupLimit; - /** * The number of records to skip. * - * @var int|null + * @var int */ public $offset; /** * The query union statements. * - * @var array|null + * @var array */ public $unions; /** * The maximum number of union records to return. * - * @var int|null + * @var int */ public $unionLimit; /** * The number of union records to skip. * - * @var int|null + * @var int */ public $unionOffset; /** * The orderings for the union query. * - * @var array|null + * @var array */ public $unionOrders; /** * Indicates whether row locking is being used. * - * @var string|bool|null + * @var string|bool */ public $lock; /** - * The query execution timeout in seconds. - * - * @var int|null - */ - public $timeout; - - /** - * The callbacks that should be invoked before the query is executed. + * The field backups currently in use. * * @var array */ - public $beforeQueryCallbacks = []; + protected $backups = []; /** - * The callbacks that should be invoked after retrieving data from the database. + * The binding backups currently in use. * * @var array */ - protected $afterQueryCallbacks = []; + protected $bindingBackups = []; /** * All of the available clause operators. * - * @var string[] + * @var array */ - public $operators = [ - '=', '<', '>', '<=', '>=', '<>', '!=', '<=>', - 'like', 'like binary', 'not like', 'ilike', - '&', '|', '^', '<<', '>>', '&~', 'is', 'is not', - 'rlike', 'not rlike', 'regexp', 'not regexp', + protected $operators = [ + '=', '<', '>', '<=', '>=', '<>', '!=', + 'like', 'like binary', 'not like', 'between', 'ilike', + '&', '|', '^', '<<', '>>', + 'rlike', 'regexp', 'not regexp', '~', '~*', '!~', '!~*', 'similar to', - 'not similar to', 'not ilike', '~~*', '!~~*', - ]; - - /** - * All of the available bitwise operators. - * - * @var string[] - */ - public $bitwiseOperators = [ - '&', '|', '^', '<<', '>>', '&~', + 'not similar to', ]; /** - * Whether to use write pdo for the select. + * Whether use write pdo for select. * * @var bool */ - public $useWritePdo = false; - - /** - * The custom arguments for the PDOStatement::fetchAll / fetch functions. - * - * @var array - */ - public array $fetchUsing = []; + protected $useWritePdo = false; /** * Create a new query builder instance. + * + * @param \Illuminate\Database\ConnectionInterface $connection + * @param \Illuminate\Database\Query\Grammars\Grammar $grammar + * @param \Illuminate\Database\Query\Processors\Processor $processor + * @return void */ - public function __construct( - ConnectionInterface $connection, - ?Grammar $grammar = null, - ?Processor $processor = null, - ) { + public function __construct(ConnectionInterface $connection, + Grammar $grammar = null, + Processor $processor = null) + { $this->connection = $connection; $this->grammar = $grammar ?: $connection->getQueryGrammar(); $this->processor = $processor ?: $connection->getPostProcessor(); @@ -293,64 +224,22 @@ public function __construct( /** * Set the columns to be selected. * - * @param mixed $columns + * @param array|mixed $columns * @return $this */ public function select($columns = ['*']) { - $this->columns = []; - $this->bindings['select'] = []; - - $columns = is_array($columns) ? $columns : func_get_args(); - - foreach ($columns as $as => $column) { - if (is_string($as) && $this->isQueryable($column)) { - $this->selectSub($column, $as); - } else { - $this->columns[] = $column; - } - } + $this->columns = is_array($columns) ? $columns : func_get_args(); return $this; } - /** - * Add a subselect expression to the query. - * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|string $query - * @param string $as - * @return $this - * - * @throws \InvalidArgumentException - */ - public function selectSub($query, $as) - { - [$query, $bindings] = $this->createSub($query); - - return $this->selectRaw( - '('.$query.') as '.$this->grammar->wrap($as), $bindings - ); - } - - /** - * Add a select expression to the query. - * - * @param \Illuminate\Contracts\Database\Query\Expression|string $expression - * @param string $as - * @return $this - */ - public function selectExpression($expression, $as) - { - return $this->selectRaw( - '('.$this->grammar->getValue($expression).') as '.$this->grammar->wrap($as) - ); - } - /** * Add a new "raw" select expression to the query. * * @param string $expression - * @return $this + * @param array $bindings + * @return \Illuminate\Database\Query\Builder|static */ public function selectRaw($expression, array $bindings = []) { @@ -364,162 +253,50 @@ public function selectRaw($expression, array $bindings = []) } /** - * Makes "from" fetch from a subquery. + * Add a subselect expression to the query. * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|string $query + * @param \Closure|\Illuminate\Database\Query\Builder|string $query * @param string $as - * @return $this + * @return \Illuminate\Database\Query\Builder|static * * @throws \InvalidArgumentException */ - public function fromSub($query, $as) - { - [$query, $bindings] = $this->createSub($query); - - return $this->fromRaw('('.$query.') as '.$this->grammar->wrapTable($as), $bindings); - } - - /** - * Add a raw "from" clause to the query. - * - * @param string $expression - * @param mixed $bindings - * @return $this - */ - public function fromRaw($expression, $bindings = []) - { - $this->from = new Expression($expression); - - $this->addBinding($bindings, 'from'); - - return $this; - } - - /** - * Creates a subquery and parse it. - * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|string $query - * @return array - */ - protected function createSub($query) + public function selectSub($query, $as) { - // If the given query is a Closure, we will execute it while passing in a new - // query instance to the Closure. This will give the developer a chance to - // format and work with the query before we cast it to a raw SQL string. if ($query instanceof Closure) { $callback = $query; - $callback($query = $this->forSubQuery()); + $callback($query = $this->newQuery()); } - return $this->parseSub($query); - } - - /** - * Parse the subquery into SQL and bindings. - * - * @param mixed $query - * @return array - * - * @throws \InvalidArgumentException - */ - protected function parseSub($query) - { - if ($query instanceof self || $query instanceof EloquentBuilder || $query instanceof Relation) { - $query = $this->prependDatabaseNameIfCrossDatabaseQuery($query); + if ($query instanceof self) { + $bindings = $query->getBindings(); - return [$query->toSql(), $query->getBindings()]; + $query = $query->toSql(); } elseif (is_string($query)) { - return [$query, []]; + $bindings = []; } else { - throw new InvalidArgumentException( - 'A subquery must be a query builder instance, a Closure, or a string.' - ); - } - } - - /** - * Prepend the database name if the given query is on another database. - * - * @param mixed $query - * @return mixed - */ - protected function prependDatabaseNameIfCrossDatabaseQuery($query) - { - if ($query->getConnection()->getDatabaseName() !== - $this->getConnection()->getDatabaseName()) { - $databaseName = $query->getConnection()->getDatabaseName(); - - if (! str_starts_with($query->from, $databaseName) && ! str_contains($query->from, '.')) { - $query->from($databaseName.'.'.$query->from); - } + throw new InvalidArgumentException; } - return $query; + return $this->selectRaw('('.$query.') as '.$this->grammar->wrap($as), $bindings); } /** * Add a new select column to the query. * - * @param mixed $column + * @param array|mixed $column * @return $this */ public function addSelect($column) { - $columns = is_array($column) ? $column : func_get_args(); - - foreach ($columns as $as => $column) { - if (is_string($as) && $this->isQueryable($column)) { - if (is_null($this->columns)) { - $this->select($this->from.'.*'); - } + $column = is_array($column) ? $column : func_get_args(); - $this->selectSub($column, $as); - } else { - if (is_array($this->columns) && in_array($column, $this->columns, true)) { - continue; - } - - $this->columns[] = $column; - } - } + $this->columns = array_merge((array) $this->columns, $column); return $this; } - /** - * Add a vector-similarity selection to the query. - * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param \Illuminate\Support\Collection|\Illuminate\Contracts\Support\Arrayable|array|string $vector - * @param string|null $as - * @return $this - */ - public function selectVectorDistance($column, $vector, $as = null) - { - $this->ensureConnectionSupportsVectors(); - - if (is_string($vector)) { - $vector = Str::of($vector)->toEmbeddings(cache: true); - } - - $this->addBinding( - json_encode( - $vector instanceof Arrayable - ? $vector->toArray() - : $vector, - flags: JSON_THROW_ON_ERROR - ), - 'select', - ); - - $as = $this->getGrammar()->wrap($as ?? $column.'_distance'); - - return $this->addSelect( - new Expression("({$this->getGrammar()->wrap($column)} <=> ?) as {$as}") - ); - } - /** * Force the query to only return distinct results. * @@ -527,13 +304,7 @@ public function selectVectorDistance($column, $vector, $as = null) */ public function distinct() { - $columns = func_get_args(); - - if (count($columns) > 0) { - $this->distinct = is_array($columns[0]) || is_bool($columns[0]) ? $columns[0] : $columns; - } else { - $this->distinct = true; - } + $this->distinct = true; return $this; } @@ -541,95 +312,53 @@ public function distinct() /** * Set the table which the query is targeting. * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|\Illuminate\Contracts\Database\Query\Expression|string $table - * @param string|null $as - * @return $this - */ - public function from($table, $as = null) - { - if ($this->isQueryable($table)) { - return $this->fromSub($table, $as); - } - - $this->from = $as ? "{$table} as {$as}" : $table; - - return $this; - } - - /** - * Add an index hint to suggest a query index. - * - * @param string $index - * @return $this - */ - public function useIndex($index) - { - $this->indexHint = new IndexHint('hint', $index); - - return $this; - } - - /** - * Add an index hint to force a query index. - * - * @param string $index - * @return $this - */ - public function forceIndex($index) - { - $this->indexHint = new IndexHint('force', $index); - - return $this; - } - - /** - * Add an index hint to ignore a query index. - * - * @param string $index + * @param string $table * @return $this */ - public function ignoreIndex($index) + public function from($table) { - $this->indexHint = new IndexHint('ignore', $index); + $this->from = $table; return $this; } /** - * Add a "join" clause to the query. + * Add a join clause to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $table - * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first - * @param string|null $operator - * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second + * @param string $table + * @param string $one + * @param string $operator + * @param string $two * @param string $type - * @param bool $where + * @param bool $where * @return $this */ - public function join($table, $first, $operator = null, $second = null, $type = 'inner', $where = false) + public function join($table, $one, $operator = null, $two = null, $type = 'inner', $where = false) { - $join = $this->newJoinClause($this, $type, $table); - // If the first "column" of the join is really a Closure instance the developer // is trying to build a join with a complex "on" clause containing more than // one condition, so we'll add the join and call a Closure with the query. - if ($first instanceof Closure) { - $first($join); + if ($one instanceof Closure) { + $join = new JoinClause($type, $table); + + call_user_func($one, $join); $this->joins[] = $join; - $this->addBinding($join->getBindings(), 'join'); + $this->addBinding($join->bindings, 'join'); } // If the column is simply a string, we can assume the join simply has a basic // "on" clause with a single condition. So we will just build the join with // this simple join clauses attached to it. There is not a join callback. else { - $method = $where ? 'where' : 'on'; + $join = new JoinClause($type, $table); - $this->joins[] = $join->$method($first, $operator, $second); + $this->joins[] = $join->on( + $one, $operator, $two, 'and', $where + ); - $this->addBinding($join->getBindings(), 'join'); + $this->addBinding($join->bindings, 'join'); } return $this; @@ -638,81 +367,26 @@ public function join($table, $first, $operator = null, $second = null, $type = ' /** * Add a "join where" clause to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $table - * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first + * @param string $table + * @param string $one * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|string $second - * @param string $type - * @return $this - */ - public function joinWhere($table, $first, $operator, $second, $type = 'inner') - { - return $this->join($table, $first, $operator, $second, $type, true); - } - - /** - * Add a "subquery join" clause to the query. - * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|string $query - * @param string $as - * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first - * @param string|null $operator - * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second + * @param string $two * @param string $type - * @param bool $where - * @return $this - * - * @throws \InvalidArgumentException - */ - public function joinSub($query, $as, $first, $operator = null, $second = null, $type = 'inner', $where = false) - { - [$query, $bindings] = $this->createSub($query); - - $expression = '('.$query.') as '.$this->grammar->wrapTable($as); - - $this->addBinding($bindings, 'join'); - - return $this->join(new Expression($expression), $first, $operator, $second, $type, $where); - } - - /** - * Add a "lateral join" clause to the query. - * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|string $query - * @return $this - */ - public function joinLateral($query, string $as, string $type = 'inner') - { - [$query, $bindings] = $this->createSub($query); - - $expression = '('.$query.') as '.$this->grammar->wrapTable($as); - - $this->addBinding($bindings, 'join'); - - $this->joins[] = $this->newJoinLateralClause($this, $type, new Expression($expression)); - - return $this; - } - - /** - * Add a lateral left join to the query. - * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|string $query - * @return $this + * @return \Illuminate\Database\Query\Builder|static */ - public function leftJoinLateral($query, string $as) + public function joinWhere($table, $one, $operator, $two, $type = 'inner') { - return $this->joinLateral($query, $as, 'left'); + return $this->join($table, $one, $operator, $two, $type, true); } /** * Add a left join to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $table - * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first - * @param string|null $operator - * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second - * @return $this + * @param string $table + * @param string $first + * @param string $operator + * @param string $second + * @return \Illuminate\Database\Query\Builder|static */ public function leftJoin($table, $first, $operator = null, $second = null) { @@ -722,40 +396,25 @@ public function leftJoin($table, $first, $operator = null, $second = null) /** * Add a "join where" clause to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $table - * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first + * @param string $table + * @param string $one * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second - * @return $this - */ - public function leftJoinWhere($table, $first, $operator, $second) - { - return $this->joinWhere($table, $first, $operator, $second, 'left'); - } - - /** - * Add a subquery left join to the query. - * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|string $query - * @param string $as - * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first - * @param string|null $operator - * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second - * @return $this + * @param string $two + * @return \Illuminate\Database\Query\Builder|static */ - public function leftJoinSub($query, $as, $first, $operator = null, $second = null) + public function leftJoinWhere($table, $one, $operator, $two) { - return $this->joinSub($query, $as, $first, $operator, $second, 'left'); + return $this->joinWhere($table, $one, $operator, $two, 'left'); } /** * Add a right join to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $table - * @param \Closure|string $first - * @param string|null $operator - * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second - * @return $this + * @param string $table + * @param string $first + * @param string $operator + * @param string $second + * @return \Illuminate\Database\Query\Builder|static */ public function rightJoin($table, $first, $operator = null, $second = null) { @@ -765,40 +424,25 @@ public function rightJoin($table, $first, $operator = null, $second = null) /** * Add a "right join where" clause to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $table - * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first + * @param string $table + * @param string $one * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|string $second - * @return $this - */ - public function rightJoinWhere($table, $first, $operator, $second) - { - return $this->joinWhere($table, $first, $operator, $second, 'right'); - } - - /** - * Add a subquery right join to the query. - * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|string $query - * @param string $as - * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first - * @param string|null $operator - * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second - * @return $this + * @param string $two + * @return \Illuminate\Database\Query\Builder|static */ - public function rightJoinSub($query, $as, $first, $operator = null, $second = null) + public function rightJoinWhere($table, $one, $operator, $two) { - return $this->joinSub($query, $as, $first, $operator, $second, 'right'); + return $this->joinWhere($table, $one, $operator, $two, 'right'); } /** * Add a "cross join" clause to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $table - * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string|null $first - * @param string|null $operator - * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second - * @return $this + * @param string $table + * @param string $first + * @param string $operator + * @param string $second + * @return \Illuminate\Database\Query\Builder|static */ public function crossJoin($table, $first = null, $operator = null, $second = null) { @@ -806,135 +450,42 @@ public function crossJoin($table, $first = null, $operator = null, $second = nul return $this->join($table, $first, $operator, $second, 'cross'); } - $this->joins[] = $this->newJoinClause($this, 'cross', $table); + $this->joins[] = new JoinClause('cross', $table); return $this; } /** - * Add a subquery cross join to the query. + * Apply the callback's query changes if the given "value" is true. * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|string $query - * @param string $as - * @return $this + * @param bool $value + * @param \Closure $callback + * @return \Illuminate\Database\Query\Builder */ - public function crossJoinSub($query, $as) + public function when($value, $callback) { - [$query, $bindings] = $this->createSub($query); - - $expression = '('.$query.') as '.$this->grammar->wrapTable($as); + $builder = $this; - $this->addBinding($bindings, 'join'); - - $this->joins[] = $this->newJoinClause($this, 'cross', new Expression($expression)); - - return $this; - } + if ($value) { + $builder = call_user_func($callback, $builder); + } - /** - * Add a straight join to the query. - * - * @param \Illuminate\Contracts\Database\Query\Expression|string $table - * @param \Closure|string $first - * @param string|null $operator - * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second - * @return $this - */ - public function straightJoin($table, $first, $operator = null, $second = null) - { - return $this->join($table, $first, $operator, $second, 'straight_join'); + return $builder; } /** - * Add a "straight join where" clause to the query. + * Add a basic where clause to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $table - * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first + * @param string|array|\Closure $column * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|string $second + * @param mixed $value + * @param string $boolean * @return $this + * + * @throws \InvalidArgumentException */ - public function straightJoinWhere($table, $first, $operator, $second) + public function where($column, $operator = null, $value = null, $boolean = 'and') { - return $this->joinWhere($table, $first, $operator, $second, 'straight_join'); - } - - /** - * Add a subquery straight join to the query. - * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|string $query - * @param string $as - * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first - * @param string|null $operator - * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second - * @return $this - */ - public function straightJoinSub($query, $as, $first, $operator = null, $second = null) - { - return $this->joinSub($query, $as, $first, $operator, $second, 'straight_join'); - } - - /** - * Get a new "join" clause. - * - * @param string $type - * @param \Illuminate\Contracts\Database\Query\Expression|string $table - * @return \Illuminate\Database\Query\JoinClause - */ - protected function newJoinClause(self $parentQuery, $type, $table) - { - return new JoinClause($parentQuery, $type, $table); - } - - /** - * Get a new "join lateral" clause. - * - * @param string $type - * @param \Illuminate\Contracts\Database\Query\Expression|string $table - * @return \Illuminate\Database\Query\JoinLateralClause - */ - protected function newJoinLateralClause(self $parentQuery, $type, $table) - { - return new JoinLateralClause($parentQuery, $type, $table); - } - - /** - * Merge an array of "where" clauses and bindings. - * - * @param array $wheres - * @param array $bindings - * @return $this - */ - public function mergeWheres($wheres, $bindings) - { - $this->wheres = array_merge($this->wheres, (array) $wheres); - - $this->bindings['where'] = array_values( - array_merge($this->bindings['where'], (array) $bindings) - ); - - return $this; - } - - /** - * Add a basic "where" clause to the query. - * - * @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @param string $boolean - * @return $this - */ - public function where($column, $operator = null, $value = null, $boolean = 'and') - { - if ($column instanceof ConditionExpression) { - $type = 'Expression'; - - $this->wheres[] = compact('type', 'column', 'boolean'); - - return $this; - } - // If the column is an array, we will assume it is an array of key-value pairs // and can add them each as a where clause. We will maintain the boolean we // received when the method was called and pass it into the nested where. @@ -945,38 +496,31 @@ public function where($column, $operator = null, $value = null, $boolean = 'and' // Here we will make some assumptions about the operator. If only 2 values are // passed to the method, we will assume that the operator is an equals sign // and keep going. Otherwise, we'll require the operator to be passed in. - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - - // If the column is actually a Closure instance, we will assume the developer - // wants to begin a nested where statement which is wrapped in parentheses. - // We will add that Closure to the query and return back out immediately. - if ($column instanceof Closure && is_null($operator)) { - return $this->whereNested($column, $boolean); + if (func_num_args() == 2) { + list($value, $operator) = [$operator, '=']; + } elseif ($this->invalidOperatorAndValue($operator, $value)) { + throw new InvalidArgumentException('Illegal operator and value combination.'); } - // If the column is a Closure instance and there is an operator value, we will - // assume the developer wants to run a subquery and then compare the result - // of that subquery with the given value that was provided to the method. - if ($this->isQueryable($column) && ! is_null($operator)) { - [$sub, $bindings] = $this->createSub($column); - - return $this->addBinding($bindings, 'where') - ->where(new Expression('('.$sub.')'), $operator, $value, $boolean); + // If the columns is actually a Closure instance, we will assume the developer + // wants to begin a nested where statement which is wrapped in parenthesis. + // We'll add that Closure to the query then return back out immediately. + if ($column instanceof Closure) { + return $this->whereNested($column, $boolean); } // If the given operator is not found in the list of valid operators we will // assume that the developer is just short-cutting the '=' operators and // we will set the operators to '=' and set the values appropriately. - if ($this->invalidOperator($operator)) { - [$value, $operator] = [$operator, '=']; + if (! in_array(strtolower($operator), $this->operators, true) && + ! in_array(strtolower($operator), $this->grammar->getOperators(), true)) { + list($value, $operator) = [$operator, '=']; } // If the value is a Closure, it means the developer is performing an entire // sub-select within the query and we will need to compile the sub-select // within the where clause to get the appropriate query record results. - if ($this->isQueryable($value)) { + if ($value instanceof Closure) { return $this->whereSub($column, $operator, $value, $boolean); } @@ -984,431 +528,342 @@ public function where($column, $operator = null, $value = null, $boolean = 'and' // where null clause to the query. So, we will allow a short-cut here to // that method for convenience so the developer doesn't have to check. if (is_null($value)) { - return $this->whereNull($column, $boolean, ! in_array($operator, ['=', '<=>'], true)); - } - - $type = 'Basic'; - - $columnString = ($column instanceof ExpressionContract) - ? $this->grammar->getValue($column) - : $column; - - // If the column is making a JSON reference we'll check to see if the value - // is a boolean. If it is, we'll add the raw boolean string as an actual - // value to the query to ensure this is properly handled by the query. - if (str_contains($columnString, '->') && is_bool($value)) { - $value = new Expression($value ? 'true' : 'false'); - - if (is_string($column)) { - $type = 'JsonBoolean'; - } - } - - if ($this->isBitwiseOperator($operator)) { - $type = 'Bitwise'; + return $this->whereNull($column, $boolean, $operator != '='); } // Now that we are working with just a simple query we can put the elements // in our array and add the query binding to our array of bindings that // will be bound to each SQL statements when it is finally executed. - $this->wheres[] = compact( - 'type', 'column', 'operator', 'value', 'boolean' - ); + $type = 'Basic'; + + $this->wheres[] = compact('type', 'column', 'operator', 'value', 'boolean'); - if (! $value instanceof ExpressionContract) { - $this->addBinding($this->flattenValue($value), 'where'); + if (! $value instanceof Expression) { + $this->addBinding($value, 'where'); } return $this; } /** - * Add an array of "where" clauses to the query. + * Add an array of where clauses to the query. * * @param array $column * @param string $boolean - * @param string $method * @return $this */ - protected function addArrayOfWheres($column, $boolean, $method = 'where') + protected function addArrayOfWheres($column, $boolean) { - return $this->whereNested(function ($query) use ($column, $method, $boolean) { + return $this->whereNested(function ($query) use ($column) { foreach ($column as $key => $value) { if (is_numeric($key) && is_array($value)) { - $query->{$method}(...array_values($value), boolean: $boolean); + call_user_func_array([$query, 'where'], $value); } else { - $query->{$method}($key, '=', $value, $boolean); + $query->where($key, '=', $value); } } }, $boolean); } /** - * Prepare the value and operator for a where clause. + * Add an "or where" clause to the query. * - * @param string $value + * @param string $column * @param string $operator - * @param bool $useDefault - * @return array - * - * @throws \InvalidArgumentException + * @param mixed $value + * @return \Illuminate\Database\Query\Builder|static */ - public function prepareValueAndOperator($value, $operator, $useDefault = false) + public function orWhere($column, $operator = null, $value = null) { - if ($useDefault) { - return [$operator, '=']; - } elseif ($this->invalidOperatorAndValue($operator, $value)) { - throw new InvalidArgumentException('Illegal operator and value combination.'); - } - - return [$value, $operator]; + return $this->where($column, $operator, $value, 'or'); } /** * Determine if the given operator and value combination is legal. * - * Prevents using Null values with invalid operators. - * * @param string $operator * @param mixed $value * @return bool */ protected function invalidOperatorAndValue($operator, $value) { - return is_null($value) && in_array($operator, $this->operators) && - ! in_array($operator, ['=', '<=>', '<>', '!=']); + $isOperator = in_array($operator, $this->operators); + + return is_null($value) && $isOperator && ! in_array($operator, ['=', '<>', '!=']); } /** - * Determine if the given operator is supported. + * Add a raw where clause to the query. * - * @param string $operator - * @return bool + * @param string $sql + * @param mixed $bindings + * @param string $boolean + * @return $this */ - protected function invalidOperator($operator) + public function whereRaw($sql, $bindings = [], $boolean = 'and') { - return ! is_string($operator) || (! in_array(strtolower($operator), $this->operators, true) && - ! in_array(strtolower($operator), $this->grammar->getOperators(), true)); + $type = 'raw'; + + $bindings = (array) $bindings; + + $this->wheres[] = compact('type', 'sql', 'boolean'); + + $this->addBinding($bindings, 'where'); + + return $this; } /** - * Determine if the operator is a bitwise operator. + * Add a raw or where clause to the query. * - * @param string $operator - * @return bool + * @param string $sql + * @param array $bindings + * @return \Illuminate\Database\Query\Builder|static */ - protected function isBitwiseOperator($operator) + public function orWhereRaw($sql, array $bindings = []) { - return in_array(strtolower($operator), $this->bitwiseOperators, true) || - in_array(strtolower($operator), $this->grammar->getBitwiseOperators(), true); + return $this->whereRaw($sql, $bindings, 'or'); } /** - * Add an "or where" clause to the query. + * Add a where between statement to the query. * - * @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value + * @param string $column + * @param array $values + * @param string $boolean + * @param bool $not * @return $this */ - public function orWhere($column, $operator = null, $value = null) + public function whereBetween($column, array $values, $boolean = 'and', $not = false) { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); + $type = 'between'; - return $this->where($column, $operator, $value, 'or'); + $this->wheres[] = compact('column', 'type', 'boolean', 'not'); + + $this->addBinding($values, 'where'); + + return $this; } /** - * Add a basic "where not" clause to the query. + * Add an or where between statement to the query. * - * @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @param string $boolean - * @return $this + * @param string $column + * @param array $values + * @return \Illuminate\Database\Query\Builder|static */ - public function whereNot($column, $operator = null, $value = null, $boolean = 'and') + public function orWhereBetween($column, array $values) { - if (is_array($column)) { - return $this->whereNested(function ($query) use ($column, $operator, $value, $boolean) { - $query->where($column, $operator, $value, $boolean); - }, $boolean.' not'); - } - - return $this->where($column, $operator, $value, $boolean.' not'); + return $this->whereBetween($column, $values, 'or'); } /** - * Add an "or where not" clause to the query. + * Add a where not between statement to the query. * - * @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @return $this + * @param string $column + * @param array $values + * @param string $boolean + * @return \Illuminate\Database\Query\Builder|static */ - public function orWhereNot($column, $operator = null, $value = null) + public function whereNotBetween($column, array $values, $boolean = 'and') { - return $this->whereNot($column, $operator, $value, 'or'); + return $this->whereBetween($column, $values, $boolean, true); } /** - * Add a "where" clause comparing two columns to the query. + * Add an or where not between statement to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string|array $first - * @param string|null $operator - * @param string|null $second - * @param string|null $boolean - * @return $this + * @param string $column + * @param array $values + * @return \Illuminate\Database\Query\Builder|static */ - public function whereColumn($first, $operator = null, $second = null, $boolean = 'and') + public function orWhereNotBetween($column, array $values) { - // If the column is an array, we will assume it is an array of key-value pairs - // and can add them each as a where clause. We will maintain the boolean we - // received when the method was called and pass it into the nested where. - if (is_array($first)) { - return $this->addArrayOfWheres($first, $boolean, 'whereColumn'); - } - - // If the given operator is not found in the list of valid operators we will - // assume that the developer is just short-cutting the '=' operators and - // we will set the operators to '=' and set the values appropriately. - if ($this->invalidOperator($operator)) { - [$second, $operator] = [$operator, '=']; - } - - // Finally, we will add this where clause into this array of clauses that we - // are building for the query. All of them will be compiled via a grammar - // once the query is about to be executed and run against the database. - $type = 'Column'; - - $this->wheres[] = compact( - 'type', 'first', 'operator', 'second', 'boolean' - ); - - return $this; + return $this->whereNotBetween($column, $values, 'or'); } /** - * Add an "or where" clause comparing two columns to the query. + * Add a nested where statement to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string|array $first - * @param string|null $operator - * @param string|null $second - * @return $this + * @param \Closure $callback + * @param string $boolean + * @return \Illuminate\Database\Query\Builder|static */ - public function orWhereColumn($first, $operator = null, $second = null) + public function whereNested(Closure $callback, $boolean = 'and') { - return $this->whereColumn($first, $operator, $second, 'or'); + $query = $this->forNestedWhere(); + + call_user_func($callback, $query); + + return $this->addNestedWhereQuery($query, $boolean); } /** - * Add a vector similarity clause to the query, filtering by minimum similarity and ordering by similarity. + * Create a new query instance for nested where condition. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param \Illuminate\Support\Collection|\Illuminate\Contracts\Support\Arrayable|array|string $vector - * @param float $minSimilarity A value between 0.0 and 1.0, where 1.0 is identical. - * @param bool $order - * @return $this + * @return \Illuminate\Database\Query\Builder */ - public function whereVectorSimilarTo($column, $vector, $minSimilarity = 0.6, $order = true) + public function forNestedWhere() { - if (is_string($vector)) { - $vector = Str::of($vector)->toEmbeddings(cache: true); - } - - $this->whereVectorDistanceLessThan($column, $vector, 1 - $minSimilarity); + $query = $this->newQuery(); - if ($order) { - $this->orderByVectorDistance($column, $vector); - } - - return $this; + return $query->from($this->from); } /** - * Add a vector distance "where" clause to the query. + * Add another query builder as a nested where to the query builder. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param \Illuminate\Support\Collection|\Illuminate\Contracts\Support\Arrayable|array|string $vector - * @param float $maxDistance + * @param \Illuminate\Database\Query\Builder|static $query * @param string $boolean * @return $this */ - public function whereVectorDistanceLessThan($column, $vector, $maxDistance, $boolean = 'and') + public function addNestedWhereQuery($query, $boolean = 'and') { - $this->ensureConnectionSupportsVectors(); + if (count($query->wheres)) { + $type = 'Nested'; + + $this->wheres[] = compact('type', 'query', 'boolean'); - if (is_string($vector)) { - $vector = Str::of($vector)->toEmbeddings(cache: true); + $this->addBinding($query->getBindings(), 'where'); } - return $this->whereRaw( - "({$this->getGrammar()->wrap($column)} <=> ?) <= ?", - [ - json_encode( - $vector instanceof Arrayable - ? $vector->toArray() - : $vector, - flags: JSON_THROW_ON_ERROR - ), - $maxDistance, - ], - $boolean - ); + return $this; } /** - * Add a vector distance "or where" clause to the query. + * Add a full sub-select to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param \Illuminate\Support\Collection|\Illuminate\Contracts\Support\Arrayable|array|string $vector - * @param float $maxDistance + * @param string $column + * @param string $operator + * @param \Closure $callback + * @param string $boolean * @return $this */ - public function orWhereVectorDistanceLessThan($column, $vector, $maxDistance) + protected function whereSub($column, $operator, Closure $callback, $boolean) { - return $this->whereVectorDistanceLessThan($column, $vector, $maxDistance, 'or'); - } + $type = 'Sub'; - /** - * Add a raw "where" clause to the query. - * - * @param \Illuminate\Contracts\Database\Query\Expression|string $sql - * @param mixed $bindings - * @param string $boolean - * @return $this - */ - public function whereRaw($sql, $bindings = [], $boolean = 'and') - { - $this->wheres[] = ['type' => 'raw', 'sql' => $sql, 'boolean' => $boolean]; + $query = $this->newQuery(); + + // Once we have the query instance we can simply execute it so it can add all + // of the sub-select's conditions to itself, and then we can cache it off + // in the array of where clauses for the "main" parent query instance. + call_user_func($callback, $query); - $this->addBinding((array) $bindings, 'where'); + $this->wheres[] = compact('type', 'column', 'operator', 'query', 'boolean'); + + $this->addBinding($query->getBindings(), 'where'); return $this; } /** - * Add a raw "or where" clause to the query. + * Add an exists clause to the query. * - * @param string $sql - * @param mixed $bindings + * @param \Closure $callback + * @param string $boolean + * @param bool $not * @return $this */ - public function orWhereRaw($sql, $bindings = []) + public function whereExists(Closure $callback, $boolean = 'and', $not = false) { - return $this->whereRaw($sql, $bindings, 'or'); + $query = $this->newQuery(); + + // Similar to the sub-select clause, we will create a new query instance so + // the developer may cleanly specify the entire exists query and we will + // compile the whole thing in the grammar and insert it into the SQL. + call_user_func($callback, $query); + + return $this->addWhereExistsQuery($query, $boolean, $not); } /** - * Add a "where like" clause to the query. + * Add an or exists clause to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $value - * @param bool $caseSensitive - * @param string $boolean - * @param bool $not - * @return $this + * @param \Closure $callback + * @param bool $not + * @return \Illuminate\Database\Query\Builder|static */ - public function whereLike($column, $value, $caseSensitive = false, $boolean = 'and', $not = false) + public function orWhereExists(Closure $callback, $not = false) { - $type = 'Like'; - - $this->wheres[] = compact('type', 'column', 'value', 'caseSensitive', 'boolean', 'not'); - - if (method_exists($this->grammar, 'prepareWhereLikeBinding')) { - $value = $this->grammar->prepareWhereLikeBinding($value, $caseSensitive); - } - - $this->addBinding($value); - - return $this; + return $this->whereExists($callback, 'or', $not); } /** - * Add an "or where like" clause to the query. + * Add a where not exists clause to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $value - * @param bool $caseSensitive - * @return $this + * @param \Closure $callback + * @param string $boolean + * @return \Illuminate\Database\Query\Builder|static */ - public function orWhereLike($column, $value, $caseSensitive = false) + public function whereNotExists(Closure $callback, $boolean = 'and') { - return $this->whereLike($column, $value, $caseSensitive, 'or', false); + return $this->whereExists($callback, $boolean, true); } /** - * Add a "where not like" clause to the query. + * Add a where not exists clause to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $value - * @param bool $caseSensitive - * @param string $boolean - * @return $this + * @param \Closure $callback + * @return \Illuminate\Database\Query\Builder|static */ - public function whereNotLike($column, $value, $caseSensitive = false, $boolean = 'and') + public function orWhereNotExists(Closure $callback) { - return $this->whereLike($column, $value, $caseSensitive, $boolean, true); + return $this->orWhereExists($callback, true); } /** - * Add an "or where not like" clause to the query. + * Add an exists clause to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $value - * @param bool $caseSensitive + * @param \Illuminate\Database\Query\Builder $query + * @param string $boolean + * @param bool $not * @return $this */ - public function orWhereNotLike($column, $value, $caseSensitive = false) + public function addWhereExistsQuery(Builder $query, $boolean = 'and', $not = false) { - return $this->whereNotLike($column, $value, $caseSensitive, 'or'); + $type = $not ? 'NotExists' : 'Exists'; + + $this->wheres[] = compact('type', 'operator', 'query', 'boolean'); + + $this->addBinding($query->getBindings(), 'where'); + + return $this; } /** * Add a "where in" clause to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param mixed $values + * @param string $column + * @param mixed $values * @param string $boolean - * @param bool $not + * @param bool $not * @return $this - * - * @throws \InvalidArgumentException */ public function whereIn($column, $values, $boolean = 'and', $not = false) { $type = $not ? 'NotIn' : 'In'; - // If the value is a query builder instance we will assume the developer wants to - // look for any values that exist within this given query. So, we will add the - // query accordingly so that this query is properly executed when it is run. - if ($this->isQueryable($values)) { - [$query, $bindings] = $this->createSub($values); - - $values = [new Expression($query)]; + if ($values instanceof static) { + return $this->whereInExistingQuery( + $column, $values, $boolean, $not + ); + } - $this->addBinding($bindings, 'where'); + // If the value of the where in clause is actually a Closure, we will assume that + // the developer is using a full sub-select for this "in" statement, and will + // execute those Closures, then we can re-construct the entire sub-selects. + if ($values instanceof Closure) { + return $this->whereInSub($column, $values, $boolean, $not); } - // Next, if the value is Arrayable we need to cast it to its raw array form so we - // have the underlying array value instead of an Arrayable object which is not - // able to be added as a binding, etc. We will then add to the wheres array. if ($values instanceof Arrayable) { $values = $values->toArray(); } $this->wheres[] = compact('type', 'column', 'values', 'boolean'); - if (count($values) !== count(Arr::flatten($values, 1))) { - throw new InvalidArgumentException('Nested arrays may not be passed to whereIn method.'); - } - - // Finally, we'll add a binding for each value unless that value is an expression - // in which case we will just skip over it since it will be the query as a raw - // string and not as a parameterized place-holder to be replaced by the PDO. - $this->addBinding($this->cleanBindings($values), 'where'); + $this->addBinding($values, 'where'); return $this; } @@ -1416,9 +871,9 @@ public function whereIn($column, $values, $boolean = 'and', $not = false) /** * Add an "or where in" clause to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param mixed $values - * @return $this + * @param string $column + * @param mixed $values + * @return \Illuminate\Database\Query\Builder|static */ public function orWhereIn($column, $values) { @@ -1428,10 +883,10 @@ public function orWhereIn($column, $values) /** * Add a "where not in" clause to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param mixed $values + * @param string $column + * @param mixed $values * @param string $boolean - * @return $this + * @return \Illuminate\Database\Query\Builder|static */ public function whereNotIn($column, $values, $boolean = 'and') { @@ -1441,9 +896,9 @@ public function whereNotIn($column, $values, $boolean = 'and') /** * Add an "or where not in" clause to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param mixed $values - * @return $this + * @param string $column + * @param mixed $values + * @return \Illuminate\Database\Query\Builder|static */ public function orWhereNotIn($column, $values) { @@ -1451,2401 +906,849 @@ public function orWhereNotIn($column, $values) } /** - * Add a "where in raw" clause for integer values to the query. + * Add a where in with a sub-select to the query. * - * @param string $column - * @param \Illuminate\Contracts\Support\Arrayable|array $values - * @param string $boolean - * @param bool $not + * @param string $column + * @param \Closure $callback + * @param string $boolean + * @param bool $not * @return $this */ - public function whereIntegerInRaw($column, $values, $boolean = 'and', $not = false) + protected function whereInSub($column, Closure $callback, $boolean, $not) { - $type = $not ? 'NotInRaw' : 'InRaw'; - - if ($values instanceof Arrayable) { - $values = $values->toArray(); - } + $type = $not ? 'NotInSub' : 'InSub'; - $values = Arr::flatten($values); + // To create the exists sub-select, we will actually create a query and call the + // provided callback with the query so the developer may set any of the query + // conditions they want for the in clause, then we'll put it in this array. + call_user_func($callback, $query = $this->newQuery()); - foreach ($values as &$value) { - $value = (int) ($value instanceof BackedEnum ? $value->value : $value); - } + $this->wheres[] = compact('type', 'column', 'query', 'boolean'); - $this->wheres[] = compact('type', 'column', 'values', 'boolean'); + $this->addBinding($query->getBindings(), 'where'); return $this; } /** - * Add an "or where in raw" clause for integer values to the query. + * Add a external sub-select to the query. * - * @param string $column - * @param \Illuminate\Contracts\Support\Arrayable|array $values + * @param string $column + * @param \Illuminate\Database\Query\Builder|static $query + * @param string $boolean + * @param bool $not * @return $this */ - public function orWhereIntegerInRaw($column, $values) + protected function whereInExistingQuery($column, $query, $boolean, $not) { - return $this->whereIntegerInRaw($column, $values, 'or'); + $type = $not ? 'NotInSub' : 'InSub'; + + $this->wheres[] = compact('type', 'column', 'query', 'boolean'); + + $this->addBinding($query->getBindings(), 'where'); + + return $this; } /** - * Add a "where not in raw" clause for integer values to the query. + * Add a "where null" clause to the query. * * @param string $column - * @param \Illuminate\Contracts\Support\Arrayable|array $values * @param string $boolean + * @param bool $not * @return $this */ - public function whereIntegerNotInRaw($column, $values, $boolean = 'and') + public function whereNull($column, $boolean = 'and', $not = false) { - return $this->whereIntegerInRaw($column, $values, $boolean, true); + $type = $not ? 'NotNull' : 'Null'; + + $this->wheres[] = compact('type', 'column', 'boolean'); + + return $this; } /** - * Add an "or where not in raw" clause for integer values to the query. + * Add an "or where null" clause to the query. * * @param string $column - * @param \Illuminate\Contracts\Support\Arrayable|array $values - * @return $this + * @return \Illuminate\Database\Query\Builder|static */ - public function orWhereIntegerNotInRaw($column, $values) + public function orWhereNull($column) { - return $this->whereIntegerNotInRaw($column, $values, 'or'); + return $this->whereNull($column, 'or'); } /** - * Add a "where null" clause to the query. + * Add a "where not null" clause to the query. * - * @param string|array|\Illuminate\Contracts\Database\Query\Expression $columns + * @param string $column * @param string $boolean - * @param bool $not - * @return $this + * @return \Illuminate\Database\Query\Builder|static */ - public function whereNull($columns, $boolean = 'and', $not = false) + public function whereNotNull($column, $boolean = 'and') { - $type = $not ? 'NotNull' : 'Null'; - - foreach (Arr::wrap($columns) as $column) { - $this->wheres[] = compact('type', 'column', 'boolean'); - } - - return $this; - } + return $this->whereNull($column, $boolean, true); + } /** - * Add an "or where null" clause to the query. + * Add an "or where not null" clause to the query. * - * @param string|array|\Illuminate\Contracts\Database\Query\Expression $column - * @return $this + * @param string $column + * @return \Illuminate\Database\Query\Builder|static */ - public function orWhereNull($column) + public function orWhereNotNull($column) { - return $this->whereNull($column, 'or'); + return $this->whereNotNull($column, 'or'); } /** - * Add a "where not null" clause to the query. + * Add a "where date" statement to the query. * - * @param string|array|\Illuminate\Contracts\Database\Query\Expression $columns - * @param string $boolean - * @return $this + * @param string $column + * @param string $operator + * @param int $value + * @param string $boolean + * @return \Illuminate\Database\Query\Builder|static */ - public function whereNotNull($columns, $boolean = 'and') + public function whereDate($column, $operator, $value, $boolean = 'and') { - return $this->whereNull($columns, $boolean, true); + return $this->addDateBasedWhere('Date', $column, $operator, $value, $boolean); } /** - * Add a "where between" statement to the query. + * Add an "or where date" statement to the query. * - * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|\Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $boolean - * @param bool $not - * @return $this + * @param string $column + * @param string $operator + * @param int $value + * @return \Illuminate\Database\Query\Builder|static */ - public function whereBetween($column, iterable $values, $boolean = 'and', $not = false) + public function orWhereDate($column, $operator, $value) { - $type = 'between'; - - if ($this->isQueryable($column)) { - [$sub, $bindings] = $this->createSub($column); - - return $this->addBinding($bindings, 'where') - ->whereBetween(new Expression('('.$sub.')'), $values, $boolean, $not); - } - - if ($values instanceof DatePeriod) { - $values = $this->resolveDatePeriodBounds($values); - } - - $this->wheres[] = compact('type', 'column', 'values', 'boolean', 'not'); - - $this->addBinding(array_slice($this->cleanBindings(Arr::flatten($values)), 0, 2), 'where'); - - return $this; + return $this->whereDate($column, $operator, $value, 'or'); } /** - * Add a "where between" statement using columns to the query. + * Add a "where day" statement to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $boolean - * @param bool $not - * @return $this + * @param string $column + * @param string $operator + * @param int $value + * @param string $boolean + * @return \Illuminate\Database\Query\Builder|static */ - public function whereBetweenColumns($column, array $values, $boolean = 'and', $not = false) + public function whereDay($column, $operator, $value, $boolean = 'and') { - $type = 'betweenColumns'; - - if ($this->isQueryable($column)) { - [$sub, $bindings] = $this->createSub($column); - - return $this->addBinding($bindings, 'where') - ->whereBetweenColumns(new Expression('('.$sub.')'), $values, $boolean, $not); - } - - $this->wheres[] = compact('type', 'column', 'values', 'boolean', 'not'); - - return $this; + return $this->addDateBasedWhere('Day', $column, $operator, $value, $boolean); } /** - * Add an "or where between" statement to the query. + * Add a "where month" statement to the query. * - * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|\Illuminate\Contracts\Database\Query\Expression|string $column - * @return $this + * @param string $column + * @param string $operator + * @param int $value + * @param string $boolean + * @return \Illuminate\Database\Query\Builder|static */ - public function orWhereBetween($column, iterable $values) + public function whereMonth($column, $operator, $value, $boolean = 'and') { - return $this->whereBetween($column, $values, 'or'); + return $this->addDateBasedWhere('Month', $column, $operator, $value, $boolean); } /** - * Add an "or where between" statement using columns to the query. + * Add a "where year" statement to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @return $this + * @param string $column + * @param string $operator + * @param int $value + * @param string $boolean + * @return \Illuminate\Database\Query\Builder|static */ - public function orWhereBetweenColumns($column, array $values) + public function whereYear($column, $operator, $value, $boolean = 'and') { - return $this->whereBetweenColumns($column, $values, 'or'); + return $this->addDateBasedWhere('Year', $column, $operator, $value, $boolean); } /** - * Add a "where not between" statement to the query. + * Add a date based (year, month, day) statement to the query. * - * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|\Illuminate\Contracts\Database\Query\Expression|string $column + * @param string $type + * @param string $column + * @param string $operator + * @param int $value * @param string $boolean * @return $this */ - public function whereNotBetween($column, iterable $values, $boolean = 'and') + protected function addDateBasedWhere($type, $column, $operator, $value, $boolean = 'and') { - return $this->whereBetween($column, $values, $boolean, true); + $this->wheres[] = compact('column', 'type', 'boolean', 'operator', 'value'); + + $this->addBinding($value, 'where'); + + return $this; } /** - * Add a "where not between" statement using columns to the query. + * Handles dynamic "where" clauses to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $boolean + * @param string $method + * @param string $parameters * @return $this */ - public function whereNotBetweenColumns($column, array $values, $boolean = 'and') + public function dynamicWhere($method, $parameters) { - return $this->whereBetweenColumns($column, $values, $boolean, true); + $finder = substr($method, 5); + + $segments = preg_split('/(And|Or)(?=[A-Z])/', $finder, -1, PREG_SPLIT_DELIM_CAPTURE); + + // The connector variable will determine which connector will be used for the + // query condition. We will change it as we come across new boolean values + // in the dynamic method strings, which could contain a number of these. + $connector = 'and'; + + $index = 0; + + foreach ($segments as $segment) { + // If the segment is not a boolean connector, we can assume it is a column's name + // and we will add it to the query as a new constraint as a where clause, then + // we can keep iterating through the dynamic method string's segments again. + if ($segment != 'And' && $segment != 'Or') { + $this->addDynamic($segment, $connector, $parameters, $index); + + $index++; + } + + // Otherwise, we will store the connector so we know how the next where clause we + // find in the query should be connected to the previous ones, meaning we will + // have the proper boolean connector to connect the next where clause found. + else { + $connector = $segment; + } + } + + return $this; } /** - * Add an "or where not between" statement to the query. + * Add a single dynamic where clause statement to the query. * - * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|\Illuminate\Contracts\Database\Query\Expression|string $column - * @return $this + * @param string $segment + * @param string $connector + * @param array $parameters + * @param int $index + * @return void */ - public function orWhereNotBetween($column, iterable $values) + protected function addDynamic($segment, $connector, $parameters, $index) { - return $this->whereNotBetween($column, $values, 'or'); + // Once we have parsed out the columns and formatted the boolean operators we + // are ready to add it to this query as a where clause just like any other + // clause on the query. Then we'll increment the parameter index values. + $bool = strtolower($connector); + + $this->where(Str::snake($segment), '=', $parameters[$index], $bool); } /** - * Add an "or where not between" statement using columns to the query. + * Add a "group by" clause to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column + * @param array ...$groups * @return $this */ - public function orWhereNotBetweenColumns($column, array $values) + public function groupBy(...$groups) { - return $this->whereNotBetweenColumns($column, $values, 'or'); + foreach ($groups as $group) { + $this->groups = array_merge((array) $this->groups, is_array($group) ? $group : [$group]); + } + + return $this; } /** - * Add a "where between columns" statement using a value to the query. + * Add a "having" clause to the query. * - * @param mixed $value - * @param array{\Illuminate\Contracts\Database\Query\Expression|string, \Illuminate\Contracts\Database\Query\Expression|string} $columns + * @param string $column + * @param string $operator + * @param string $value * @param string $boolean - * @param bool $not * @return $this */ - public function whereValueBetween($value, array $columns, $boolean = 'and', $not = false) + public function having($column, $operator = null, $value = null, $boolean = 'and') { - $type = 'valueBetween'; + $type = 'basic'; - $this->wheres[] = compact('type', 'value', 'columns', 'boolean', 'not'); + $this->havings[] = compact('type', 'column', 'operator', 'value', 'boolean'); - $this->addBinding($value, 'where'); + if (! $value instanceof Expression) { + $this->addBinding($value, 'having'); + } return $this; } /** - * Add an "or where between columns" statement using a value to the query. + * Add a "or having" clause to the query. * - * @param mixed $value - * @param array{\Illuminate\Contracts\Database\Query\Expression|string, \Illuminate\Contracts\Database\Query\Expression|string} $columns - * @return $this + * @param string $column + * @param string $operator + * @param string $value + * @return \Illuminate\Database\Query\Builder|static */ - public function orWhereValueBetween($value, array $columns) + public function orHaving($column, $operator = null, $value = null) { - return $this->whereValueBetween($value, $columns, 'or'); + return $this->having($column, $operator, $value, 'or'); } /** - * Add a "where not between columns" statement using a value to the query. + * Add a raw having clause to the query. * - * @param mixed $value - * @param array{\Illuminate\Contracts\Database\Query\Expression|string, \Illuminate\Contracts\Database\Query\Expression|string} $columns + * @param string $sql + * @param array $bindings * @param string $boolean * @return $this */ - public function whereValueNotBetween($value, array $columns, $boolean = 'and') + public function havingRaw($sql, array $bindings = [], $boolean = 'and') { - return $this->whereValueBetween($value, $columns, $boolean, true); - } + $type = 'raw'; - /** - * Add an "or where not between columns" statement using a value to the query. - * - * @param mixed $value - * @param array{\Illuminate\Contracts\Database\Query\Expression|string, \Illuminate\Contracts\Database\Query\Expression|string} $columns - * @return $this - */ - public function orWhereValueNotBetween($value, array $columns) - { - return $this->whereValueNotBetween($value, $columns, 'or'); + $this->havings[] = compact('type', 'sql', 'boolean'); + + $this->addBinding($bindings, 'having'); + + return $this; } /** - * Add an "or where not null" clause to the query. + * Add a raw or having clause to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @return $this + * @param string $sql + * @param array $bindings + * @return \Illuminate\Database\Query\Builder|static */ - public function orWhereNotNull($column) + public function orHavingRaw($sql, array $bindings = []) { - return $this->whereNotNull($column, 'or'); + return $this->havingRaw($sql, $bindings, 'or'); } /** - * Add a "where date" statement to the query. + * Add an "order by" clause to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param \DateTimeInterface|string|null $operator - * @param \DateTimeInterface|string|null $value - * @param string $boolean + * @param string $column + * @param string $direction * @return $this */ - public function whereDate($column, $operator, $value = null, $boolean = 'and') + public function orderBy($column, $direction = 'asc') { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - - // If the given operator is not found in the list of valid operators we will - // assume that the developer is just short-cutting the '=' operators and - // we will set the operators to '=' and set the values appropriately. - if ($this->invalidOperator($operator)) { - [$value, $operator] = [$operator, '=']; - } + $property = $this->unions ? 'unionOrders' : 'orders'; + $direction = strtolower($direction) == 'asc' ? 'asc' : 'desc'; - $value = $this->flattenValue($value); + $this->{$property}[] = compact('column', 'direction'); - if ($value instanceof DateTimeInterface) { - $value = $value->format('Y-m-d'); - } - - return $this->addDateBasedWhere('Date', $column, $operator, $value, $boolean); + return $this; } /** - * Add an "or where date" statement to the query. + * Add an "order by" clause for a timestamp to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param \DateTimeInterface|string|null $operator - * @param \DateTimeInterface|string|null $value - * @return $this + * @param string $column + * @return \Illuminate\Database\Query\Builder|static */ - public function orWhereDate($column, $operator, $value = null) + public function latest($column = 'created_at') { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - - return $this->whereDate($column, $operator, $value, 'or'); + return $this->orderBy($column, 'desc'); } /** - * Add a "where time" statement to the query. + * Add an "order by" clause for a timestamp to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param \DateTimeInterface|string|null $operator - * @param \DateTimeInterface|string|null $value - * @param string $boolean - * @return $this + * @param string $column + * @return \Illuminate\Database\Query\Builder|static */ - public function whereTime($column, $operator, $value = null, $boolean = 'and') + public function oldest($column = 'created_at') { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - - // If the given operator is not found in the list of valid operators we will - // assume that the developer is just short-cutting the '=' operators and - // we will set the operators to '=' and set the values appropriately. - if ($this->invalidOperator($operator)) { - [$value, $operator] = [$operator, '=']; - } - - $value = $this->flattenValue($value); - - if ($value instanceof DateTimeInterface) { - $value = $value->format('H:i:s'); - } - - return $this->addDateBasedWhere('Time', $column, $operator, $value, $boolean); + return $this->orderBy($column, 'asc'); } /** - * Add an "or where time" statement to the query. + * Add a raw "order by" clause to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param \DateTimeInterface|string|null $operator - * @param \DateTimeInterface|string|null $value + * @param string $sql + * @param array $bindings * @return $this */ - public function orWhereTime($column, $operator, $value = null) + public function orderByRaw($sql, $bindings = []) { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); + $property = $this->unions ? 'unionOrders' : 'orders'; + + $type = 'raw'; + + $this->{$property}[] = compact('type', 'sql'); - return $this->whereTime($column, $operator, $value, 'or'); + $this->addBinding($bindings, 'order'); + + return $this; } /** - * Add a "where day" statement to the query. + * Set the "offset" value of the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param \DateTimeInterface|string|int|null $operator - * @param \DateTimeInterface|string|int|null $value - * @param string $boolean + * @param int $value * @return $this */ - public function whereDay($column, $operator, $value = null, $boolean = 'and') + public function offset($value) { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - - // If the given operator is not found in the list of valid operators we will - // assume that the developer is just short-cutting the '=' operators and - // we will set the operators to '=' and set the values appropriately. - if ($this->invalidOperator($operator)) { - [$value, $operator] = [$operator, '=']; - } - - $value = $this->flattenValue($value); - - if ($value instanceof DateTimeInterface) { - $value = $value->format('d'); - } + $property = $this->unions ? 'unionOffset' : 'offset'; - if (! $value instanceof ExpressionContract) { - $value = sprintf('%02d', $value); - } + $this->$property = max(0, $value); - return $this->addDateBasedWhere('Day', $column, $operator, $value, $boolean); + return $this; } /** - * Add an "or where day" statement to the query. + * Alias to set the "offset" value of the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param \DateTimeInterface|string|int|null $operator - * @param \DateTimeInterface|string|int|null $value - * @return $this + * @param int $value + * @return \Illuminate\Database\Query\Builder|static */ - public function orWhereDay($column, $operator, $value = null) + public function skip($value) { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - - return $this->whereDay($column, $operator, $value, 'or'); + return $this->offset($value); } /** - * Add a "where month" statement to the query. + * Set the "limit" value of the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param \DateTimeInterface|string|int|null $operator - * @param \DateTimeInterface|string|int|null $value - * @param string $boolean + * @param int $value * @return $this */ - public function whereMonth($column, $operator, $value = null, $boolean = 'and') + public function limit($value) { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - - // If the given operator is not found in the list of valid operators we will - // assume that the developer is just short-cutting the '=' operators and - // we will set the operators to '=' and set the values appropriately. - if ($this->invalidOperator($operator)) { - [$value, $operator] = [$operator, '=']; - } - - $value = $this->flattenValue($value); - - if ($value instanceof DateTimeInterface) { - $value = $value->format('m'); - } + $property = $this->unions ? 'unionLimit' : 'limit'; - if (! $value instanceof ExpressionContract) { - $value = sprintf('%02d', $value); + if ($value >= 0) { + $this->$property = $value; } - return $this->addDateBasedWhere('Month', $column, $operator, $value, $boolean); + return $this; } /** - * Add an "or where month" statement to the query. + * Alias to set the "limit" value of the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param \DateTimeInterface|string|int|null $operator - * @param \DateTimeInterface|string|int|null $value - * @return $this + * @param int $value + * @return \Illuminate\Database\Query\Builder|static */ - public function orWhereMonth($column, $operator, $value = null) + public function take($value) { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - - return $this->whereMonth($column, $operator, $value, 'or'); + return $this->limit($value); } /** - * Add a "where year" statement to the query. + * Set the limit and offset for a given page. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param \DateTimeInterface|string|int|null $operator - * @param \DateTimeInterface|string|int|null $value - * @param string $boolean - * @return $this + * @param int $page + * @param int $perPage + * @return \Illuminate\Database\Query\Builder|static */ - public function whereYear($column, $operator, $value = null, $boolean = 'and') + public function forPage($page, $perPage = 15) { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); + return $this->skip(($page - 1) * $perPage)->take($perPage); + } - // If the given operator is not found in the list of valid operators we will - // assume that the developer is just short-cutting the '=' operators and - // we will set the operators to '=' and set the values appropriately. - if ($this->invalidOperator($operator)) { - [$value, $operator] = [$operator, '=']; + /** + * Constrain the query to the next "page" of results after a given ID. + * + * @param int $perPage + * @param int $lastId + * @param string $column + * @return \Illuminate\Database\Query\Builder|static + */ + public function forPageAfterId($perPage = 15, $lastId = 0, $column = 'id') + { + return $this->where($column, '>', $lastId) + ->orderBy($column, 'asc') + ->take($perPage); + } + + /** + * Add a union statement to the query. + * + * @param \Illuminate\Database\Query\Builder|\Closure $query + * @param bool $all + * @return \Illuminate\Database\Query\Builder|static + */ + public function union($query, $all = false) + { + if ($query instanceof Closure) { + call_user_func($query, $query = $this->newQuery()); } - $value = $this->flattenValue($value); + $this->unions[] = compact('query', 'all'); - if ($value instanceof DateTimeInterface) { - $value = $value->format('Y'); - } + $this->addBinding($query->getBindings(), 'union'); - return $this->addDateBasedWhere('Year', $column, $operator, $value, $boolean); + return $this; } /** - * Add an "or where year" statement to the query. + * Add a union all statement to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param \DateTimeInterface|string|int|null $operator - * @param \DateTimeInterface|string|int|null $value - * @return $this + * @param \Illuminate\Database\Query\Builder|\Closure $query + * @return \Illuminate\Database\Query\Builder|static */ - public function orWhereYear($column, $operator, $value = null) + public function unionAll($query) { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - - return $this->whereYear($column, $operator, $value, 'or'); + return $this->union($query, true); } /** - * Add a date based (year, month, day, time) statement to the query. + * Lock the selected rows in the table. * - * @param string $type - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $operator - * @param mixed $value - * @param string $boolean + * @param bool $value * @return $this */ - protected function addDateBasedWhere($type, $column, $operator, $value, $boolean = 'and') + public function lock($value = true) { - $this->wheres[] = compact('column', 'type', 'boolean', 'operator', 'value'); + $this->lock = $value; - if (! $value instanceof ExpressionContract) { - $this->addBinding($value, 'where'); + if ($this->lock) { + $this->useWritePdo(); } return $this; } /** - * Add a nested "where" statement to the query. + * Lock the selected rows in the table for updating. * - * @param string $boolean - * @return $this + * @return \Illuminate\Database\Query\Builder */ - public function whereNested(Closure $callback, $boolean = 'and') + public function lockForUpdate() { - $callback($query = $this->forNestedWhere()); - - return $this->addNestedWhereQuery($query, $boolean); + return $this->lock(true); } /** - * Create a new query instance for nested where condition. + * Share lock the selected rows in the table. * * @return \Illuminate\Database\Query\Builder */ - public function forNestedWhere() + public function sharedLock() { - return $this->newQuery()->from($this->from); + return $this->lock(false); } /** - * Add another query builder as a nested where to the query builder. + * Get the SQL representation of the query. * - * @param \Illuminate\Database\Query\Builder $query - * @param string $boolean - * @return $this + * @return string */ - public function addNestedWhereQuery($query, $boolean = 'and') + public function toSql() { - if (count($query->wheres)) { - $type = 'Nested'; - - $this->wheres[] = compact('type', 'query', 'boolean'); - - $this->addBinding($query->getRawBindings()['where'], 'where'); - } - - return $this; + return $this->grammar->compileSelect($this); } /** - * Add a full sub-select to the query. + * Execute a query for a single record by ID. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $operator - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*> $callback - * @param string $boolean - * @return $this + * @param int $id + * @param array $columns + * @return mixed|static */ - protected function whereSub($column, $operator, $callback, $boolean) + public function find($id, $columns = ['*']) { - $type = 'Sub'; - - if ($callback instanceof Closure) { - // Once we have the query instance we can simply execute it so it can add all - // of the sub-select's conditions to itself, and then we can cache it off - // in the array of where clauses for the "main" parent query instance. - $callback($query = $this->forSubQuery()); - } else { - $query = $callback instanceof EloquentBuilder ? $callback->toBase() : $callback; - } - - $this->wheres[] = compact( - 'type', 'column', 'operator', 'query', 'boolean' - ); - - $this->addBinding($query->getBindings(), 'where'); - - return $this; + return $this->where('id', '=', $id)->first($columns); } /** - * Add an "exists" clause to the query. + * Get a single column's value from the first result of a query. * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*> $callback - * @param string $boolean - * @param bool $not - * @return $this + * @param string $column + * @return mixed */ - public function whereExists($callback, $boolean = 'and', $not = false) + public function value($column) { - if ($callback instanceof Closure) { - $query = $this->forSubQuery(); - - // Similar to the sub-select clause, we will create a new query instance so - // the developer may cleanly specify the entire exists query and we will - // compile the whole thing in the grammar and insert it into the SQL. - $callback($query); - } else { - $query = $callback instanceof EloquentBuilder ? $callback->toBase() : $callback; - } + $result = (array) $this->first([$column]); - return $this->addWhereExistsQuery($query, $boolean, $not); + return count($result) > 0 ? reset($result) : null; } /** - * Add an "or where exists" clause to the query. + * Execute the query and get the first result. * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*> $callback - * @param bool $not - * @return $this + * @param array $columns + * @return \stdClass|array|null */ - public function orWhereExists($callback, $not = false) + public function first($columns = ['*']) { - return $this->whereExists($callback, 'or', $not); + return $this->take(1)->get($columns)->first(); } /** - * Add a "where not exists" clause to the query. + * Execute the query as a "select" statement. * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*> $callback - * @param string $boolean - * @return $this + * @param array $columns + * @return \Illuminate\Support\Collection */ - public function whereNotExists($callback, $boolean = 'and') + public function get($columns = ['*']) { - return $this->whereExists($callback, $boolean, true); + $original = $this->columns; + + if (is_null($original)) { + $this->columns = $columns; + } + + $results = $this->processor->processSelect($this, $this->runSelect()); + + $this->columns = $original; + + return collect($results); } /** - * Add an "or where not exists" clause to the query. + * Run the query as a "select" statement against the connection. * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*> $callback - * @return $this + * @return array */ - public function orWhereNotExists($callback) + protected function runSelect() { - return $this->orWhereExists($callback, true); + return $this->connection->select($this->toSql(), $this->getBindings(), ! $this->useWritePdo); } /** - * Add an "exists" clause to the query. + * Paginate the given query into a simple paginator. * - * @param string $boolean - * @param bool $not - * @return $this + * @param int $perPage + * @param array $columns + * @param string $pageName + * @param int|null $page + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator */ - public function addWhereExistsQuery(self $query, $boolean = 'and', $not = false) + public function paginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null) { - $type = $not ? 'NotExists' : 'Exists'; + $page = $page ?: Paginator::resolveCurrentPage($pageName); - $this->wheres[] = compact('type', 'query', 'boolean'); + $total = $this->getCountForPagination($columns); - $this->addBinding($query->getBindings(), 'where'); + $results = $this->forPage($page, $perPage)->get($columns); - return $this; + return new LengthAwarePaginator($results, $total, $perPage, $page, [ + 'path' => Paginator::resolveCurrentPath(), + 'pageName' => $pageName, + ]); } /** - * Adds a where condition using row values. + * Get a paginator only supporting simple next and previous links. * - * @param array $columns - * @param string $operator - * @param array $values - * @param string $boolean - * @return $this + * This is more efficient on larger data-sets, etc. * - * @throws \InvalidArgumentException + * @param int $perPage + * @param array $columns + * @param string $pageName + * @return \Illuminate\Contracts\Pagination\Paginator */ - public function whereRowValues($columns, $operator, $values, $boolean = 'and') + public function simplePaginate($perPage = 15, $columns = ['*'], $pageName = 'page') { - if (count($columns) !== count($values)) { - throw new InvalidArgumentException('The number of columns must match the number of values'); - } - - $type = 'RowValues'; + $page = Paginator::resolveCurrentPage($pageName); - $this->wheres[] = compact('type', 'columns', 'operator', 'values', 'boolean'); + $this->skip(($page - 1) * $perPage)->take($perPage + 1); - $this->addBinding($this->cleanBindings($values)); - - return $this; + return new Paginator($this->get($columns), $perPage, $page, [ + 'path' => Paginator::resolveCurrentPath(), + 'pageName' => $pageName, + ]); } /** - * Adds an or where condition using row values. + * Get the count of the total records for the paginator. * * @param array $columns - * @param string $operator - * @param array $values - * @return $this + * @return int */ - public function orWhereRowValues($columns, $operator, $values) + public function getCountForPagination($columns = ['*']) { - return $this->whereRowValues($columns, $operator, $values, 'or'); + $this->backupFieldsForCount(); + + $this->aggregate = ['function' => 'count', 'columns' => $this->clearSelectAliases($columns)]; + + $results = $this->get()->all(); + + $this->aggregate = null; + + $this->restoreFieldsForCount(); + + if (isset($this->groups)) { + return count($results); + } + + return isset($results[0]) ? (int) array_change_key_case((array) $results[0])['aggregate'] : 0; } /** - * Add a "where JSON contains" clause to the query. + * Backup some fields for the pagination count. * - * @param string $column - * @param mixed $value - * @param string $boolean - * @param bool $not - * @return $this + * @return void */ - public function whereJsonContains($column, $value, $boolean = 'and', $not = false) + protected function backupFieldsForCount() { - $type = 'JsonContains'; + foreach (['orders', 'limit', 'offset', 'columns'] as $field) { + $this->backups[$field] = $this->{$field}; - $this->wheres[] = compact('type', 'column', 'value', 'boolean', 'not'); - - if (! $value instanceof ExpressionContract) { - $this->addBinding($this->grammar->prepareBindingForJsonContains($value)); + $this->{$field} = null; } - return $this; + foreach (['order', 'select'] as $key) { + $this->bindingBackups[$key] = $this->bindings[$key]; + + $this->bindings[$key] = []; + } } /** - * Add an "or where JSON contains" clause to the query. + * Remove the column aliases since they will break count queries. * - * @param string $column - * @param mixed $value - * @return $this + * @param array $columns + * @return array */ - public function orWhereJsonContains($column, $value) + protected function clearSelectAliases(array $columns) { - return $this->whereJsonContains($column, $value, 'or'); + return array_map(function ($column) { + return is_string($column) && ($aliasPosition = strpos(strtolower($column), ' as ')) !== false + ? substr($column, 0, $aliasPosition) : $column; + }, $columns); } /** - * Add a "where JSON not contains" clause to the query. + * Restore some fields after the pagination count. * - * @param string $column - * @param mixed $value - * @param string $boolean - * @return $this + * @return void */ - public function whereJsonDoesntContain($column, $value, $boolean = 'and') + protected function restoreFieldsForCount() { - return $this->whereJsonContains($column, $value, $boolean, true); + foreach (['orders', 'limit', 'offset', 'columns'] as $field) { + $this->{$field} = $this->backups[$field]; + } + + foreach (['order', 'select'] as $key) { + $this->bindings[$key] = $this->bindingBackups[$key]; + } + + $this->backups = []; + $this->bindingBackups = []; } /** - * Add an "or where JSON not contains" clause to the query. + * Chunk the results of the query. * - * @param string $column - * @param mixed $value - * @return $this + * @param int $count + * @param callable $callback + * @return bool */ - public function orWhereJsonDoesntContain($column, $value) + public function chunk($count, callable $callback) { - return $this->whereJsonDoesntContain($column, $value, 'or'); + $results = $this->forPage($page = 1, $count)->get(); + + while (! $results->isEmpty()) { + // On each chunk result set, we will pass them to the callback and then let the + // developer take care of everything within the callback, which allows us to + // keep the memory low for spinning through large result sets for working. + if (call_user_func($callback, $results) === false) { + return false; + } + + $page++; + + $results = $this->forPage($page, $count)->get(); + } + + return true; } /** - * Add a "where JSON overlaps" clause to the query. + * Chunk the results of a query by comparing numeric IDs. * + * @param int $count + * @param callable $callback * @param string $column - * @param mixed $value - * @param string $boolean - * @param bool $not - * @return $this + * @return bool */ - public function whereJsonOverlaps($column, $value, $boolean = 'and', $not = false) + public function chunkById($count, callable $callback, $column = 'id') { - $type = 'JsonOverlaps'; + $lastId = null; + + $results = $this->forPageAfterId($count, 0, $column)->get(); + + while (! $results->isEmpty()) { + if (call_user_func($callback, $results) === false) { + return false; + } - $this->wheres[] = compact('type', 'column', 'value', 'boolean', 'not'); + $lastId = $results->last()->{$column}; - if (! $value instanceof ExpressionContract) { - $this->addBinding($this->grammar->prepareBindingForJsonContains($value)); + $results = $this->forPageAfterId($count, $lastId, $column)->get(); } - return $this; + return true; } /** - * Add an "or where JSON overlaps" clause to the query. + * Execute a callback over each item while chunking. * - * @param string $column - * @param mixed $value - * @return $this + * @param callable $callback + * @param int $count + * @return bool + * + * @throws \RuntimeException */ - public function orWhereJsonOverlaps($column, $value) + public function each(callable $callback, $count = 1000) { - return $this->whereJsonOverlaps($column, $value, 'or'); + if (is_null($this->orders) && is_null($this->unionOrders)) { + throw new RuntimeException('You must specify an orderBy clause when using the "each" function.'); + } + + return $this->chunk($count, function ($results) use ($callback) { + foreach ($results as $key => $value) { + if ($callback($value, $key) === false) { + return false; + } + } + }); } /** - * Add a "where JSON not overlap" clause to the query. + * Get an array with the values of a given column. * * @param string $column - * @param mixed $value - * @param string $boolean - * @return $this + * @param string|null $key + * @return \Illuminate\Support\Collection */ - public function whereJsonDoesntOverlap($column, $value, $boolean = 'and') + public function pluck($column, $key = null) { - return $this->whereJsonOverlaps($column, $value, $boolean, true); + $results = $this->get(is_null($key) ? [$column] : [$column, $key]); + + // If the columns are qualified with a table or have an alias, we cannot use + // those directly in the "pluck" operations since the results from the DB + // are only keyed by the column itself. We'll strip the table out here. + return $results->pluck( + $this->stripTableForPluck($column), + $this->stripTableForPluck($key) + ); } /** - * Add an "or where JSON not overlap" clause to the query. + * Strip off the table name or alias from a column identifier. * * @param string $column - * @param mixed $value - * @return $this + * @return string|null */ - public function orWhereJsonDoesntOverlap($column, $value) + protected function stripTableForPluck($column) { - return $this->whereJsonDoesntOverlap($column, $value, 'or'); + return is_null($column) ? $column : last(preg_split('~\.| ~', $column)); } /** - * Add a clause that determines if a JSON path exists to the query. - * - * @param string $column - * @param string $boolean - * @param bool $not - * @return $this - */ - public function whereJsonContainsKey($column, $boolean = 'and', $not = false) - { - $type = 'JsonContainsKey'; - - $this->wheres[] = compact('type', 'column', 'boolean', 'not'); - - return $this; - } - - /** - * Add an "or" clause that determines if a JSON path exists to the query. - * - * @param string $column - * @return $this - */ - public function orWhereJsonContainsKey($column) - { - return $this->whereJsonContainsKey($column, 'or'); - } - - /** - * Add a clause that determines if a JSON path does not exist to the query. - * - * @param string $column - * @param string $boolean - * @return $this - */ - public function whereJsonDoesntContainKey($column, $boolean = 'and') - { - return $this->whereJsonContainsKey($column, $boolean, true); - } - - /** - * Add an "or" clause that determines if a JSON path does not exist to the query. - * - * @param string $column - * @return $this - */ - public function orWhereJsonDoesntContainKey($column) - { - return $this->whereJsonDoesntContainKey($column, 'or'); - } - - /** - * Add a "where JSON length" clause to the query. - * - * @param string $column - * @param mixed $operator - * @param mixed $value - * @param string $boolean - * @return $this - */ - public function whereJsonLength($column, $operator, $value = null, $boolean = 'and') - { - $type = 'JsonLength'; - - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - - // If the given operator is not found in the list of valid operators we will - // assume that the developer is just short-cutting the '=' operators and - // we will set the operators to '=' and set the values appropriately. - if ($this->invalidOperator($operator)) { - [$value, $operator] = [$operator, '=']; - } - - $this->wheres[] = compact('type', 'column', 'operator', 'value', 'boolean'); - - if (! $value instanceof ExpressionContract) { - $this->addBinding((int) $this->flattenValue($value)); - } - - return $this; - } - - /** - * Add an "or where JSON length" clause to the query. - * - * @param string $column - * @param mixed $operator - * @param mixed $value - * @return $this - */ - public function orWhereJsonLength($column, $operator, $value = null) - { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - - return $this->whereJsonLength($column, $operator, $value, 'or'); - } - - /** - * Handles dynamic "where" clauses to the query. - * - * @param string $method - * @param array $parameters - * @return $this - */ - public function dynamicWhere($method, $parameters) - { - $finder = substr($method, 5); - - $segments = preg_split( - '/(And|Or)(?=[A-Z])/', $finder, -1, PREG_SPLIT_DELIM_CAPTURE - ); - - // The connector variable will determine which connector will be used for the - // query condition. We will change it as we come across new boolean values - // in the dynamic method strings, which could contain a number of these. - $connector = 'and'; - - $index = 0; - - foreach ($segments as $segment) { - // If the segment is not a boolean connector, we can assume it is a column's name - // and we will add it to the query as a new constraint as a where clause, then - // we can keep iterating through the dynamic method string's segments again. - if ($segment !== 'And' && $segment !== 'Or') { - $this->addDynamic($segment, $connector, $parameters, $index); - - $index++; - } - - // Otherwise, we will store the connector so we know how the next where clause we - // find in the query should be connected to the previous ones, meaning we will - // have the proper boolean connector to connect the next where clause found. - else { - $connector = $segment; - } - } - - return $this; - } - - /** - * Add a single dynamic "where" clause statement to the query. - * - * @param string $segment - * @param string $connector - * @param array $parameters - * @param int $index - * @return void - */ - protected function addDynamic($segment, $connector, $parameters, $index) - { - // Once we have parsed out the columns and formatted the boolean operators we - // are ready to add it to this query as a where clause just like any other - // clause on the query. Then we'll increment the parameter index values. - $bool = strtolower($connector); - - $this->where(Str::snake($segment), '=', $parameters[$index], $bool); - } - - /** - * Add a "where fulltext" clause to the query. - * - * @param string|string[] $columns - * @param string $value - * @param string $boolean - * @return $this - */ - public function whereFullText($columns, $value, array $options = [], $boolean = 'and') - { - $type = 'Fulltext'; - - $columns = (array) $columns; - - $this->wheres[] = compact('type', 'columns', 'value', 'options', 'boolean'); - - $this->addBinding($value); - - return $this; - } - - /** - * Add an "or where fulltext" clause to the query. - * - * @param string|string[] $columns - * @param string $value - * @return $this - */ - public function orWhereFullText($columns, $value, array $options = []) - { - return $this->whereFullText($columns, $value, $options, 'or'); - } - - /** - * Add a "where" clause to the query for multiple columns with "and" conditions between them. - * - * @param \Illuminate\Contracts\Database\Query\Expression[]|\Closure[]|string[] $columns - * @param mixed $operator - * @param mixed $value - * @param string $boolean - * @return $this - */ - public function whereAll($columns, $operator = null, $value = null, $boolean = 'and') - { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - - $this->whereNested(function ($query) use ($columns, $operator, $value) { - foreach ($columns as $column) { - $query->where($column, $operator, $value, 'and'); - } - }, $boolean); - - return $this; - } - - /** - * Add an "or where" clause to the query for multiple columns with "and" conditions between them. - * - * @param \Illuminate\Contracts\Database\Query\Expression[]|\Closure[]|string[] $columns - * @param mixed $operator - * @param mixed $value - * @return $this - */ - public function orWhereAll($columns, $operator = null, $value = null) - { - return $this->whereAll($columns, $operator, $value, 'or'); - } - - /** - * Add a "where" clause to the query for multiple columns with "or" conditions between them. - * - * @param \Illuminate\Contracts\Database\Query\Expression[]|\Closure[]|string[] $columns - * @param mixed $operator - * @param mixed $value - * @param string $boolean - * @return $this - */ - public function whereAny($columns, $operator = null, $value = null, $boolean = 'and') - { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - - $this->whereNested(function ($query) use ($columns, $operator, $value) { - foreach ($columns as $column) { - $query->where($column, $operator, $value, 'or'); - } - }, $boolean); - - return $this; - } - - /** - * Add an "or where" clause to the query for multiple columns with "or" conditions between them. - * - * @param \Illuminate\Contracts\Database\Query\Expression[]|\Closure[]|string[] $columns - * @param mixed $operator - * @param mixed $value - * @return $this - */ - public function orWhereAny($columns, $operator = null, $value = null) - { - return $this->whereAny($columns, $operator, $value, 'or'); - } - - /** - * Add a "where not" clause to the query for multiple columns where none of the conditions should be true. - * - * @param \Illuminate\Contracts\Database\Query\Expression[]|\Closure[]|string[] $columns - * @param mixed $operator - * @param mixed $value - * @param string $boolean - * @return $this - */ - public function whereNone($columns, $operator = null, $value = null, $boolean = 'and') - { - return $this->whereAny($columns, $operator, $value, $boolean.' not'); - } - - /** - * Add an "or where not" clause to the query for multiple columns where none of the conditions should be true. - * - * @param \Illuminate\Contracts\Database\Query\Expression[]|\Closure[]|string[] $columns - * @param mixed $operator - * @param mixed $value - * @return $this - */ - public function orWhereNone($columns, $operator = null, $value = null) - { - return $this->whereNone($columns, $operator, $value, 'or'); - } - - /** - * Add a "group by" clause to the query. - * - * @param array|\Illuminate\Contracts\Database\Query\Expression|string ...$groups - * @return $this - */ - public function groupBy(...$groups) - { - foreach ($groups as $group) { - $this->groups = array_merge( - (array) $this->groups, - Arr::wrap($group) - ); - } - - return $this; - } - - /** - * Add a raw "groupBy" clause to the query. - * - * @param string $sql - * @return $this - */ - public function groupByRaw($sql, array $bindings = []) - { - $this->groups[] = new Expression($sql); - - $this->addBinding($bindings, 'groupBy'); - - return $this; - } - - /** - * Add a "having" clause to the query. - * - * @param \Illuminate\Contracts\Database\Query\Expression|\Closure|string $column - * @param \DateTimeInterface|string|int|float|null $operator - * @param \Illuminate\Contracts\Database\Query\Expression|\DateTimeInterface|string|int|float|null $value - * @param string $boolean - * @return $this - */ - public function having($column, $operator = null, $value = null, $boolean = 'and') - { - $type = 'Basic'; - - if ($column instanceof ConditionExpression) { - $type = 'Expression'; - - $this->havings[] = compact('type', 'column', 'boolean'); - - return $this; - } - - // Here we will make some assumptions about the operator. If only 2 values are - // passed to the method, we will assume that the operator is an equals sign - // and keep going. Otherwise, we'll require the operator to be passed in. - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - - if ($column instanceof Closure && is_null($operator)) { - return $this->havingNested($column, $boolean); - } - - // If the given operator is not found in the list of valid operators we will - // assume that the developer is just short-cutting the '=' operators and - // we will set the operators to '=' and set the values appropriately. - if ($this->invalidOperator($operator)) { - [$value, $operator] = [$operator, '=']; - } - - if ($this->isBitwiseOperator($operator)) { - $type = 'Bitwise'; - } - - $this->havings[] = compact('type', 'column', 'operator', 'value', 'boolean'); - - if (! $value instanceof ExpressionContract) { - $this->addBinding($this->flattenValue($value), 'having'); - } - - return $this; - } - - /** - * Add an "or having" clause to the query. - * - * @param \Illuminate\Contracts\Database\Query\Expression|\Closure|string $column - * @param \DateTimeInterface|string|int|float|null $operator - * @param \Illuminate\Contracts\Database\Query\Expression|\DateTimeInterface|string|int|float|null $value - * @return $this - */ - public function orHaving($column, $operator = null, $value = null) - { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - - return $this->having($column, $operator, $value, 'or'); - } - - /** - * Add a nested "having" statement to the query. - * - * @param string $boolean - * @return $this - */ - public function havingNested(Closure $callback, $boolean = 'and') - { - $callback($query = $this->forNestedWhere()); - - return $this->addNestedHavingQuery($query, $boolean); - } - - /** - * Add another query builder as a nested having to the query builder. - * - * @param \Illuminate\Database\Query\Builder $query - * @param string $boolean - * @return $this - */ - public function addNestedHavingQuery($query, $boolean = 'and') - { - if (count($query->havings)) { - $type = 'Nested'; - - $this->havings[] = compact('type', 'query', 'boolean'); - - $this->addBinding($query->getRawBindings()['having'], 'having'); - } - - return $this; - } - - /** - * Add a "having null" clause to the query. - * - * @param array|string $columns - * @param string $boolean - * @param bool $not - * @return $this - */ - public function havingNull($columns, $boolean = 'and', $not = false) - { - $type = $not ? 'NotNull' : 'Null'; - - foreach (Arr::wrap($columns) as $column) { - $this->havings[] = compact('type', 'column', 'boolean'); - } - - return $this; - } - - /** - * Add an "or having null" clause to the query. - * - * @param string $column - * @return $this - */ - public function orHavingNull($column) - { - return $this->havingNull($column, 'or'); - } - - /** - * Add a "having not null" clause to the query. - * - * @param array|string $columns - * @param string $boolean - * @return $this - */ - public function havingNotNull($columns, $boolean = 'and') - { - return $this->havingNull($columns, $boolean, true); - } - - /** - * Add an "or having not null" clause to the query. - * - * @param string $column - * @return $this - */ - public function orHavingNotNull($column) - { - return $this->havingNotNull($column, 'or'); - } - - /** - * Add a "having between" clause to the query. - * - * @param string $column - * @param string $boolean - * @param bool $not - * @return $this - */ - public function havingBetween($column, iterable $values, $boolean = 'and', $not = false) - { - $type = 'between'; - - if ($values instanceof DatePeriod) { - $values = $this->resolveDatePeriodBounds($values); - } - - $this->havings[] = compact('type', 'column', 'values', 'boolean', 'not'); - - $this->addBinding(array_slice($this->cleanBindings(Arr::flatten($values)), 0, 2), 'having'); - - return $this; - } - - /** - * Add a "having not between" clause to the query. - * - * @param string $column - * @param iterable $values - * @param string $boolean - * @return $this - */ - public function havingNotBetween($column, iterable $values, $boolean = 'and') - { - return $this->havingBetween($column, $values, $boolean, true); - } - - /** - * Add an "or having between" clause to the query. - * - * @param string $column - * @param iterable $values - * @return $this - */ - public function orHavingBetween($column, iterable $values) - { - return $this->havingBetween($column, $values, 'or'); - } - - /** - * Add an "or having not between" clause to the query. - * - * @param string $column - * @param iterable $values - * @return $this - */ - public function orHavingNotBetween($column, iterable $values) - { - return $this->havingBetween($column, $values, 'or', true); - } - - /** - * Resolve the start and end dates from a DatePeriod. - * - * @param \DatePeriod $period - * @return array{\DateTimeInterface, \DateTimeInterface} - */ - protected function resolveDatePeriodBounds(DatePeriod $period) - { - [$start, $end] = [$period->getStartDate(), $period->getEndDate()]; - - if ($end === null) { - $end = clone $start; - - $recurrences = $period->getRecurrences(); - - for ($i = 0; $i < $recurrences; $i++) { - $end = $end->add($period->getDateInterval()); - } - } - - return [$start, $end]; - } - - /** - * Add a raw "having" clause to the query. - * - * @param string $sql - * @param string $boolean - * @return $this - */ - public function havingRaw($sql, array $bindings = [], $boolean = 'and') - { - $type = 'Raw'; - - $this->havings[] = compact('type', 'sql', 'boolean'); - - $this->addBinding($bindings, 'having'); - - return $this; - } - - /** - * Add a raw "or having" clause to the query. - * - * @param string $sql - * @return $this - */ - public function orHavingRaw($sql, array $bindings = []) - { - return $this->havingRaw($sql, $bindings, 'or'); - } - - /** - * Add an "order by" clause to the query. - * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|\Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $direction - * @return $this - * - * @throws \InvalidArgumentException - */ - public function orderBy($column, $direction = 'asc') - { - if ($this->isQueryable($column)) { - [$query, $bindings] = $this->createSub($column); - - $column = new Expression('('.$query.')'); - - $this->addBinding($bindings, $this->unions ? 'unionOrder' : 'order'); - } - - $direction = strtolower($direction); - - if (! in_array($direction, ['asc', 'desc'], true)) { - throw new InvalidArgumentException('Order direction must be "asc" or "desc".'); - } - - $this->{$this->unions ? 'unionOrders' : 'orders'}[] = [ - 'column' => $column, - 'direction' => $direction, - ]; - - return $this; - } - - /** - * Add a descending "order by" clause to the query. - * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|\Illuminate\Contracts\Database\Query\Expression|string $column - * @return $this - */ - public function orderByDesc($column) - { - return $this->orderBy($column, 'desc'); - } - - /** - * Add an "order by" clause for a timestamp to the query. - * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Contracts\Database\Query\Expression|string $column - * @return $this - */ - public function latest($column = 'created_at') - { - return $this->orderBy($column, 'desc'); - } - - /** - * Add an "order by" clause for a timestamp to the query. - * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Contracts\Database\Query\Expression|string $column - * @return $this - */ - public function oldest($column = 'created_at') - { - return $this->orderBy($column, 'asc'); - } - - /** - * Add a vector-distance "order by" clause to the query. - * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param \Illuminate\Support\Collection|\Illuminate\Contracts\Support\Arrayable|array $vector - * @return $this - */ - public function orderByVectorDistance($column, $vector) - { - $this->ensureConnectionSupportsVectors(); - - if (is_string($vector)) { - $vector = Str::of($vector)->toEmbeddings(cache: true); - } - - $this->addBinding( - json_encode( - $vector instanceof Arrayable - ? $vector->toArray() - : $vector, - flags: JSON_THROW_ON_ERROR - ), - $this->unions ? 'unionOrder' : 'order' - ); - - $this->{$this->unions ? 'unionOrders' : 'orders'}[] = [ - 'column' => new Expression("({$this->getGrammar()->wrap($column)} <=> ?)"), - 'direction' => 'asc', - ]; - - return $this; - } - - /** - * Put the query's results in random order. - * - * @param string|int $seed - * @return $this - */ - public function inRandomOrder($seed = '') - { - return $this->orderByRaw($this->grammar->compileRandom($seed)); - } - - /** - * Add a raw "order by" clause to the query. - * - * @param string $sql - * @param array $bindings - * @return $this - */ - public function orderByRaw($sql, $bindings = []) - { - $type = 'Raw'; - - $this->{$this->unions ? 'unionOrders' : 'orders'}[] = compact('type', 'sql'); - - $this->addBinding($bindings, $this->unions ? 'unionOrder' : 'order'); - - return $this; - } - - /** - * Alias to set the "offset" value of the query. - * - * @param int $value - * @return $this - */ - public function skip($value) - { - return $this->offset($value); - } - - /** - * Set the "offset" value of the query. - * - * @param int $value - * @return $this - */ - public function offset($value) - { - $property = $this->unions ? 'unionOffset' : 'offset'; - - $this->$property = max(0, (int) $value); - - return $this; - } - - /** - * 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) - { - $property = $this->unions ? 'unionLimit' : 'limit'; - - if ($value >= 0) { - $this->$property = ! is_null($value) ? (int) $value : null; - } - - return $this; - } - - /** - * Add a "group limit" clause to the query. - * - * @param int $value - * @param string $column - * @return $this - */ - public function groupLimit($value, $column) - { - if ($value >= 0) { - $this->groupLimit = compact('value', 'column'); - } - - return $this; - } - - /** - * Set the limit and offset for a given page. - * - * @param int $page - * @param int $perPage - * @return $this - */ - public function forPage($page, $perPage = 15) - { - return $this->offset(($page - 1) * $perPage)->limit($perPage); - } - - /** - * Constrain the query to the previous "page" of results before a given ID. - * - * @param int $perPage - * @param int|null $lastId - * @param string $column - * @return $this - */ - public function forPageBeforeId($perPage = 15, $lastId = 0, $column = 'id') - { - $this->orders = $this->removeExistingOrdersFor($column); - - if (is_null($lastId)) { - $this->whereNotNull($column); - } else { - $this->where($column, '<', $lastId); - } - - return $this->orderBy($column, 'desc') - ->limit($perPage); - } - - /** - * Constrain the query to the next "page" of results after a given ID. - * - * @param int $perPage - * @param int|null $lastId - * @param string $column - * @return $this - */ - public function forPageAfterId($perPage = 15, $lastId = 0, $column = 'id') - { - $this->orders = $this->removeExistingOrdersFor($column); - - if (is_null($lastId)) { - $this->whereNotNull($column); - } else { - $this->where($column, '>', $lastId); - } - - return $this->orderBy($column, 'asc') - ->limit($perPage); - } - - /** - * Remove all existing orders and optionally add a new order. - * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Contracts\Database\Query\Expression|string|null $column - * @param string $direction - * @return $this - */ - public function reorder($column = null, $direction = 'asc') - { - $this->orders = null; - $this->unionOrders = null; - $this->bindings['order'] = []; - $this->bindings['unionOrder'] = []; - - if ($column) { - return $this->orderBy($column, $direction); - } - - return $this; - } - - /** - * Add descending "reorder" clause to the query. - * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Contracts\Database\Query\Expression|string|null $column - * @return $this - */ - public function reorderDesc($column) - { - return $this->reorder($column, 'desc'); - } - - /** - * Get an array with all orders with a given column removed. - * - * @param string $column - * @return array - */ - protected function removeExistingOrdersFor($column) - { - return (new Collection($this->orders)) - ->reject(fn ($order) => isset($order['column']) && $order['column'] === $column) - ->values() - ->all(); - } - - /** - * Add a "union" statement to the query. - * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*> $query - * @param bool $all - * @return $this - */ - public function union($query, $all = false) - { - if ($query instanceof Closure) { - $query($query = $this->newQuery()); - } - - $this->unions[] = compact('query', 'all'); - - $this->addBinding($query->getBindings(), 'union'); - - return $this; - } - - /** - * Add a "union all" statement to the query. - * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*> $query - * @return $this - */ - public function unionAll($query) - { - return $this->union($query, true); - } - - /** - * Lock the selected rows in the table. - * - * @param string|bool $value - * @return $this - */ - public function lock($value = true) - { - $this->lock = $value; - - if (! is_null($this->lock)) { - $this->useWritePdo(); - } - - return $this; - } - - /** - * Lock the selected rows in the table for updating. - * - * @return $this - */ - public function lockForUpdate() - { - return $this->lock(true); - } - - /** - * Share lock the selected rows in the table. - * - * @return $this - */ - public function sharedLock() - { - return $this->lock(false); - } - - /** - * Set a query execution timeout in seconds. - * - * @param int|null $seconds - * @return $this - * - * @throws InvalidArgumentException - */ - public function timeout(?int $seconds): static - { - if ($seconds !== null && $seconds <= 0) { - throw new InvalidArgumentException('Timeout must be greater than zero.'); - } - - $this->timeout = $seconds; - - return $this; - } - - /** - * Register a closure to be invoked before the query is executed. - * - * @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 = []; - } - - /** - * Register a closure to be invoked after the query is executed. - * - * @return $this - */ - public function afterQuery(Closure $callback) - { - $this->afterQueryCallbacks[] = $callback; - - return $this; - } - - /** - * Invoke the "after query" modification callbacks. - * - * @param mixed $result - * @return mixed - */ - public function applyAfterQueryCallbacks($result) - { - foreach ($this->afterQueryCallbacks as $afterQueryCallback) { - $result = $afterQueryCallback($result) ?: $result; - } - - return $result; - } - - /** - * Get the SQL representation of the query. - * - * @return string - */ - public function toSql() - { - $this->applyBeforeQueryCallbacks(); - - return $this->grammar->compileSelect($this); - } - - /** - * Get the raw SQL representation of the query with embedded bindings. - * - * @return string - */ - public function toRawSql() - { - return $this->grammar->substituteBindingsIntoRawSql( - $this->toSql(), $this->connection->prepareBindings($this->getBindings()) - ); - } - - /** - * Execute a query for a single record by ID. - * - * @param int|string $id - * @param string|\Illuminate\Contracts\Database\Query\Expression|array $columns - * @return \stdClass|null - */ - public function find($id, $columns = ['*']) - { - return $this->where('id', '=', $id)->first($columns); - } - - /** - * Execute a query for a single record by ID or call a callback. - * - * @template TValue - * - * @param mixed $id - * @param (\Closure(): TValue)|string|\Illuminate\Contracts\Database\Query\Expression|array $columns - * @param (\Closure(): TValue)|null $callback - * @return \stdClass|TValue - */ - public function findOr($id, $columns = ['*'], ?Closure $callback = null) - { - if ($columns instanceof Closure) { - $callback = $columns; - - $columns = ['*']; - } - - if (! is_null($data = $this->find($id, $columns))) { - return $data; - } - - return $callback(); - } - - /** - * Get a single column's value from the first result of a query. - * - * @param string $column - * @return mixed - */ - public function value($column) - { - $result = (array) $this->first([$column]); - - return count($result) > 0 ? array_first($result) : null; - } - - /** - * Get a single expression value from the first result of a query. - * - * @return mixed - */ - public function rawValue(string $expression, array $bindings = []) - { - $result = (array) $this->selectRaw($expression, $bindings)->first(); - - return count($result) > 0 ? array_first($result) : null; - } - - /** - * Get a single column's value from the first result of a query if it's the sole matching record. - * - * @param string $column - * @return mixed - * - * @throws \Illuminate\Database\RecordsNotFoundException - * @throws \Illuminate\Database\MultipleRecordsFoundException - */ - public function soleValue($column) - { - $result = (array) $this->sole([$column]); - - return array_first($result); - } - - /** - * Execute the query as a "select" statement. - * - * @param string|\Illuminate\Contracts\Database\Query\Expression|array $columns - * @return \Illuminate\Support\Collection - */ - public function get($columns = ['*']) - { - $original = $this->columns; - - $this->columns ??= Arr::wrap($columns); - - $items = new Collection($this->processor->processSelect($this, $this->runSelect())); - - $this->columns = $original; - - return $this->applyAfterQueryCallbacks( - isset($this->groupLimit) ? $this->withoutGroupLimitKeys($items) : $items - ); - } - - /** - * Run the query as a "select" statement against the connection. - * - * @return array - */ - protected function runSelect() - { - return $this->connection->select( - $this->toSql(), $this->getBindings(), ! $this->useWritePdo, $this->fetchUsing - ); - } - - /** - * Remove the group limit keys from the results in the collection. - * - * @param \Illuminate\Support\Collection $items - * @return \Illuminate\Support\Collection - */ - protected function withoutGroupLimitKeys($items) - { - $keysToRemove = ['laravel_row']; - - if (is_string($this->groupLimit['column'])) { - $column = last(explode('.', $this->groupLimit['column'])); - - $keysToRemove[] = '@laravel_group := '.$this->grammar->wrap($column); - $keysToRemove[] = '@laravel_group := '.$this->grammar->wrap('pivot_'.$column); - } - - $items->each(function ($item) use ($keysToRemove) { - foreach ($keysToRemove as $key) { - unset($item->$key); - } - }); - - return $items; - } - - /** - * Paginate the given query into a simple paginator. - * - * @param int|\Closure $perPage - * @param string|\Illuminate\Contracts\Database\Query\Expression|array $columns - * @param string $pageName - * @param int|null $page - * @param \Closure|int|null $total - * @return \Illuminate\Pagination\LengthAwarePaginator - */ - public function paginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null, $total = null) - { - $page = $page ?: Paginator::resolveCurrentPage($pageName); - - $total = value($total) ?? $this->getCountForPagination(); - - $perPage = value($perPage, $total); - - $results = $total ? $this->forPage($page, $perPage)->get($columns) : new Collection; - - return $this->paginator($results, $total, $perPage, $page, [ - 'path' => Paginator::resolveCurrentPath(), - 'pageName' => $pageName, - ]); - } - - /** - * Get a paginator only supporting simple next and previous links. - * - * This is more efficient on larger data-sets, etc. - * - * @param int $perPage - * @param string|\Illuminate\Contracts\Database\Query\Expression|array $columns - * @param string $pageName - * @param int|null $page - * @return \Illuminate\Contracts\Pagination\Paginator - */ - public function simplePaginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null) - { - $page = $page ?: Paginator::resolveCurrentPage($pageName); - - $this->offset(($page - 1) * $perPage)->limit($perPage + 1); - - return $this->simplePaginator($this->get($columns), $perPage, $page, [ - 'path' => Paginator::resolveCurrentPath(), - 'pageName' => $pageName, - ]); - } - - /** - * Get a paginator only supporting simple next and previous links. - * - * This is more efficient on larger data-sets, etc. - * - * @param int|null $perPage - * @param string|\Illuminate\Contracts\Database\Query\Expression|array $columns - * @param string $cursorName - * @param \Illuminate\Pagination\Cursor|string|null $cursor - * @return \Illuminate\Contracts\Pagination\CursorPaginator - */ - public function cursorPaginate($perPage = 15, $columns = ['*'], $cursorName = 'cursor', $cursor = null) - { - return $this->paginateUsingCursor($perPage, $columns, $cursorName, $cursor); - } - - /** - * Ensure the proper order by required for cursor pagination. - * - * @param bool $shouldReverse - * @return \Illuminate\Support\Collection - */ - protected function ensureOrderForCursorPagination($shouldReverse = false) - { - if (empty($this->orders) && empty($this->unionOrders)) { - $this->enforceOrderBy(); - } - - $reverseDirection = function ($order) { - if (! isset($order['direction'])) { - return $order; - } - - $order['direction'] = $order['direction'] === 'asc' ? 'desc' : 'asc'; - - return $order; - }; - - if ($shouldReverse) { - $this->orders = (new Collection($this->orders))->map($reverseDirection)->toArray(); - $this->unionOrders = (new Collection($this->unionOrders))->map($reverseDirection)->toArray(); - } - - $orders = ! empty($this->unionOrders) ? $this->unionOrders : $this->orders; - - return (new Collection($orders)) - ->filter(fn ($order) => Arr::has($order, 'direction')) - ->values(); - } - - /** - * Get the count of the total records for the paginator. - * - * @param array $columns - * @return int<0, max> - */ - public function getCountForPagination($columns = ['*']) - { - $results = $this->runPaginationCountQuery($columns); - - // Once we have run the pagination count query, we will get the resulting count and - // take into account what type of query it was. When there is a group by we will - // just return the count of the entire results set since that will be correct. - if (! isset($results[0])) { - return 0; - } elseif (is_object($results[0])) { - return (int) $results[0]->aggregate; - } - - return (int) array_change_key_case((array) $results[0])['aggregate']; - } - - /** - * Run a pagination count query. - * - * @param array $columns - * @return array - */ - protected function runPaginationCountQuery($columns = ['*']) - { - if ($this->groups || $this->havings) { - $clone = $this->cloneForPaginationCount(); - - if (is_null($clone->columns) && ! empty($this->joins)) { - $clone->select($this->from.'.*'); - } - - return $this->newQuery() - ->from(new Expression('('.$clone->toSql().') as '.$this->grammar->wrap('aggregate_table'))) - ->mergeBindings($clone) - ->setAggregate('count', $this->withoutSelectAliases($columns)) - ->get()->all(); - } - - $without = $this->unions ? ['unionOrders', 'unionLimit', 'unionOffset'] : ['columns', 'orders', 'limit', 'offset']; - - return $this->cloneWithout($without) - ->cloneWithoutBindings($this->unions ? ['unionOrder'] : ['select', 'order']) - ->setAggregate('count', $this->withoutSelectAliases($columns)) - ->get()->all(); - } - - /** - * Clone the existing query instance for usage in a pagination subquery. - * - * @return self - */ - protected function cloneForPaginationCount() - { - return $this->cloneWithout(['orders', 'limit', 'offset']) - ->cloneWithoutBindings(['order']); - } - - /** - * Remove the column aliases since they will break count queries. - * - * @param array $columns - * @return array - */ - protected function withoutSelectAliases(array $columns) - { - return array_map(function ($column) { - return is_string($column) && ($aliasPosition = stripos($column, ' as ')) !== false - ? substr($column, 0, $aliasPosition) - : $column; - }, $columns); - } - - /** - * Get a lazy collection for the given query. - * - * @return \Illuminate\Support\LazyCollection - */ - public function cursor() - { - if (is_null($this->columns)) { - $this->columns = ['*']; - } - - return (new LazyCollection(function () { - yield from $this->connection->cursor( - $this->toSql(), $this->getBindings(), ! $this->useWritePdo, $this->fetchUsing - ); - }))->map(function ($item) { - return $this->applyAfterQueryCallbacks(new Collection([$item]))->first(); - })->reject(fn ($item) => is_null($item)); - } - - /** - * Throw an exception if the query doesn't have an orderBy clause. - * - * @return void - * - * @throws \RuntimeException - */ - protected function enforceOrderBy() - { - if (empty($this->orders) && empty($this->unionOrders)) { - throw new RuntimeException('You must specify an orderBy clause when using this function.'); - } - } - - /** - * Get a collection instance containing the values of a given column. - * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string|null $key - * @return \Illuminate\Support\Collection - */ - public function pluck($column, $key = null) - { - $original = $this->columns; - - // First, we will need to select the results of the query accounting for the - // given columns / key. Once we have the results, we will be able to take - // the results and get the exact data that was requested for the query. - $this->columns ??= is_null($key) || $key === $column - ? [$column] - : [$column, $key]; - - $queryResult = $this->processor->processSelect($this, $this->runSelect()); - - $this->columns = $original; - - if (empty($queryResult)) { - return new Collection; - } - - // If the columns are qualified with a table or have an alias, we cannot use - // those directly in the "pluck" operations since the results from the DB - // are only keyed by the column itself. We'll strip the table out here. - $column = $this->stripTableForPluck($column); - - $key = $this->stripTableForPluck($key); - - return $this->applyAfterQueryCallbacks( - is_array($queryResult[0]) - ? $this->pluckFromArrayColumn($queryResult, $column, $key) - : $this->pluckFromObjectColumn($queryResult, $column, $key) - ); - } - - /** - * Strip off the table name or alias from a column identifier. - * - * @param string $column - * @return string|null - */ - protected function stripTableForPluck($column) - { - if (is_null($column)) { - return $column; - } - - $columnString = $column instanceof ExpressionContract - ? $this->grammar->getValue($column) - : $column; - - $separator = str_contains(strtolower($columnString), ' as ') ? ' as ' : '\.'; - - return last(preg_split('~'.$separator.'~i', $columnString)); - } - - /** - * Retrieve column values from rows represented as objects. - * - * @param array $queryResult - * @param string $column - * @param string $key - * @return \Illuminate\Support\Collection - */ - protected function pluckFromObjectColumn($queryResult, $column, $key) - { - $results = []; - - if (is_null($key)) { - foreach ($queryResult as $row) { - $results[] = $row->$column; - } - } else { - foreach ($queryResult as $row) { - $results[$row->$key] = $row->$column; - } - } - - return new Collection($results); - } - - /** - * Retrieve column values from rows represented as arrays. - * - * @param array $queryResult - * @param string $column - * @param string $key - * @return \Illuminate\Support\Collection - */ - protected function pluckFromArrayColumn($queryResult, $column, $key) - { - $results = []; - - if (is_null($key)) { - foreach ($queryResult as $row) { - $results[] = $row[$column]; - } - } else { - foreach ($queryResult as $row) { - $results[$row[$key]] = $row[$column]; - } - } - - return new Collection($results); - } - - /** - * Concatenate values of a given column as a string. + * Concatenate values of a given column as a string. * * @param string $column * @param string $glue @@ -3863,15 +1766,10 @@ public function implode($column, $glue = '') */ public function exists() { - $this->applyBeforeQueryCallbacks(); + $sql = $this->grammar->compileExists($this); - $results = $this->connection->select( - $this->grammar->compileExists($this), $this->getBindings(), ! $this->useWritePdo - ); + $results = $this->connection->select($sql, $this->getBindings(), ! $this->useWritePdo); - // If the results have rows, we will get the row and see if the exists column is a - // boolean true. If there are no results for this query we will return false as - // there are no rows for this query at all, and we can return that info here. if (isset($results[0])) { $results = (array) $results[0]; @@ -3881,52 +1779,26 @@ public function exists() return false; } - /** - * Determine if no rows exist for the current query. - * - * @return bool - */ - public function doesntExist() - { - return ! $this->exists(); - } - - /** - * Execute the given callback if no rows exist for the current query. - * - * @return mixed - */ - public function existsOr(Closure $callback) - { - return $this->exists() ? true : $callback(); - } - - /** - * Execute the given callback if rows exist for the current query. - * - * @return mixed - */ - public function doesntExistOr(Closure $callback) - { - return $this->doesntExist() ? true : $callback(); - } - /** * Retrieve the "count" result of the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $columns - * @return int<0, max> + * @param string $columns + * @return int */ public function count($columns = '*') { - return (int) $this->aggregate(__FUNCTION__, Arr::wrap($columns)); + if (! is_array($columns)) { + $columns = [$columns]; + } + + return (int) $this->aggregate(__FUNCTION__, $columns); } /** * Retrieve the minimum value of a given column. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @return mixed + * @param string $column + * @return float|int */ public function min($column) { @@ -3936,8 +1808,8 @@ public function min($column) /** * Retrieve the maximum value of a given column. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @return mixed + * @param string $column + * @return float|int */ public function max($column) { @@ -3947,8 +1819,8 @@ public function max($column) /** * Retrieve the sum of the values of a given column. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @return mixed + * @param string $column + * @return float|int */ public function sum($column) { @@ -3960,498 +1832,221 @@ public function sum($column) /** * Retrieve the average of the values of a given column. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @return mixed - */ - public function avg($column) - { - return $this->aggregate(__FUNCTION__, [$column]); - } - - /** - * Alias for the "avg" method. - * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @return mixed - */ - public function average($column) - { - return $this->avg($column); - } - - /** - * Execute an aggregate function on the database. - * - * @param string $function - * @param array $columns - * @return mixed - */ - public function aggregate($function, $columns = ['*']) - { - $results = $this->cloneWithout($this->unions || $this->havings ? [] : ['columns']) - ->cloneWithoutBindings($this->unions || $this->havings ? [] : ['select']) - ->setAggregate($function, $columns) - ->get($columns); - - if (! $results->isEmpty()) { - return array_change_key_case((array) $results[0])['aggregate']; - } - } - - /** - * Execute a numeric aggregate function on the database. - * - * @param string $function - * @param array $columns + * @param string $column * @return float|int */ - public function numericAggregate($function, $columns = ['*']) - { - $result = $this->aggregate($function, $columns); - - // If there is no result, we can obviously just return 0 here. Next, we will check - // if the result is an integer or float. If it is already one of these two data - // types we can just return the result as-is, otherwise we will convert this. - if (! $result) { - return 0; - } - - if (is_int($result) || is_float($result)) { - return $result; - } - - // If the result doesn't contain a decimal place, we will assume it is an int then - // cast it to one. When it does we will cast it to a float since it needs to be - // cast to the expected data type for the developers out of pure convenience. - return ! str_contains((string) $result, '.') - ? (int) $result - : (float) $result; - } - - /** - * Set the aggregate property without running the query. - * - * @param string $function - * @param array<\Illuminate\Contracts\Database\Query\Expression|string> $columns - * @return $this - */ - protected function setAggregate($function, $columns) - { - $this->aggregate = compact('function', 'columns'); - - if (empty($this->groups)) { - $this->orders = null; - - $this->bindings['order'] = []; - } - - return $this; - } - - /** - * Insert new records into the database. - * - * @return bool - */ - public function insert(array $values) - { - // Since every insert gets treated like a batch insert, we will make sure the - // bindings are structured in a way that is convenient when building these - // inserts statements by verifying these elements are actually an array. - if (empty($values)) { - return true; - } - - if (! is_array(array_first($values))) { - $values = [$values]; - } - - // Here, we will sort the insert keys for every record so that each insert is - // in the same order for the record. We need to make sure this is the case - // so there are not any errors or problems when inserting these records. - else { - foreach ($values as $key => $value) { - ksort($value); - - $values[$key] = $value; - } - } - - $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. - return $this->connection->insert( - $this->grammar->compileInsert($this, $values), - $this->cleanBindings(Arr::flatten($values, 1)) - ); - } - - /** - * Insert new records into the database while ignoring errors. - * - * @return int<0, max> - */ - public function insertOrIgnore(array $values) - { - if (empty($values)) { - return 0; - } - - if (! is_array(array_first($values))) { - $values = [$values]; - } else { - foreach ($values as $key => $value) { - ksort($value); - - $values[$key] = $value; - } - } - - $this->applyBeforeQueryCallbacks(); - - return $this->connection->affectingStatement( - $this->grammar->compileInsertOrIgnore($this, $values), - $this->cleanBindings(Arr::flatten($values, 1)) - ); - } - - /** - * Insert new records into the database while ignoring specific conflicts and returning specified columns. - * - * @param non-empty-string|non-empty-array $uniqueBy - * @param non-empty-array $returning - * @return \Illuminate\Support\Collection - */ - public function insertOrIgnoreReturning(array $values, array|string $uniqueBy, array $returning = ['*']) - { - if (empty($values)) { - return new Collection; - } - - if ($uniqueBy === [] || $uniqueBy === '') { - throw new InvalidArgumentException('The unique columns must not be empty.'); - } - - if ($returning === []) { - throw new InvalidArgumentException('The returning columns must not be empty.'); - } - - if (! is_array(array_first($values))) { - $values = [$values]; - } else { - foreach ($values as $key => $value) { - ksort($value); - - $values[$key] = $value; - } - } - - $this->applyBeforeQueryCallbacks(); - - $sql = $this->grammar->compileInsertOrIgnoreReturning($this, $values, (array) $uniqueBy, $returning); - - $this->connection->recordsHaveBeenModified(); - - return new Collection( - $this->connection->selectFromWriteConnection($sql, $this->cleanBindings(Arr::flatten($values, 1))) - ); - } - - /** - * Insert a new record and get the value of the primary key. - * - * @param string|null $sequence - * @return int - */ - public function insertGetId(array $values, $sequence = null) - { - $this->applyBeforeQueryCallbacks(); - - $sql = $this->grammar->compileInsertGetId($this, $values, $sequence); - - $values = $this->cleanBindings($values); - - return $this->processor->processInsertGetId($this, $sql, $values, $sequence); - } - - /** - * Insert new records into the table using a subquery. - * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|string $query - * @return int - */ - public function insertUsing(array $columns, $query) - { - $this->applyBeforeQueryCallbacks(); - - [$sql, $bindings] = $this->createSub($query); - - return $this->connection->affectingStatement( - $this->grammar->compileInsertUsing($this, $columns, $sql), - $this->cleanBindings($bindings) - ); - } - - /** - * Insert new records into the table using a subquery while ignoring errors. - * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|string $query - * @return int - */ - public function insertOrIgnoreUsing(array $columns, $query) - { - $this->applyBeforeQueryCallbacks(); - - [$sql, $bindings] = $this->createSub($query); - - return $this->connection->affectingStatement( - $this->grammar->compileInsertOrIgnoreUsing($this, $columns, $sql), - $this->cleanBindings($bindings) - ); + public function avg($column) + { + return $this->aggregate(__FUNCTION__, [$column]); } /** - * Update records in the database. + * Alias for the "avg" method. * - * @return int<0, max> + * @param string $column + * @return float|int */ - public function update(array $values) + public function average($column) { - $this->applyBeforeQueryCallbacks(); - - $values = (new Collection($values))->map(function ($value) { - if (! $value instanceof self && ! $value instanceof EloquentBuilder && ! $value instanceof Relation) { - return ['value' => $value, 'bindings' => match (true) { - $value instanceof Collection => $value->all(), - $value instanceof UnitEnum => enum_value($value), - default => $value, - }]; - } - - [$query, $bindings] = $this->parseSub($value); - - return ['value' => new Expression("({$query})"), 'bindings' => fn () => $bindings]; - }); - - $sql = $this->grammar->compileUpdate($this, $values->map(fn ($value) => $value['value'])->all()); - - return $this->connection->update($sql, $this->cleanBindings( - $this->grammar->prepareBindingsForUpdate($this->bindings, $values->map(fn ($value) => $value['bindings'])->all()) - )); + return $this->avg($column); } /** - * Update records in a PostgreSQL database using the update from syntax. - * - * @return int + * Execute an aggregate function on the database. * - * @throws \LogicException + * @param string $function + * @param array $columns + * @return float|int */ - public function updateFrom(array $values) + public function aggregate($function, $columns = ['*']) { - if (! method_exists($this->grammar, 'compileUpdateFrom')) { - throw new LogicException('This database engine does not support the updateFrom method.'); - } + $this->aggregate = compact('function', 'columns'); - $this->applyBeforeQueryCallbacks(); + $previousColumns = $this->columns; - $sql = $this->grammar->compileUpdateFrom($this, $values); + // We will also back up the select bindings since the select clause will be + // removed when performing the aggregate function. Once the query is run + // we will add the bindings back onto this query so they can get used. + $previousSelectBindings = $this->bindings['select']; - return $this->connection->update($sql, $this->cleanBindings( - $this->grammar->prepareBindingsForUpdateFrom($this->bindings, $values) - )); - } + $this->bindings['select'] = []; - /** - * Insert or update a record matching the attributes, and fill it with values. - * - * @return bool - */ - public function updateOrInsert(array $attributes, array|callable $values = []) - { - $exists = $this->where($attributes)->exists(); + $results = $this->get($columns); - if ($values instanceof Closure) { - $values = $values($exists); - } + // Once we have executed the query, we will reset the aggregate property so + // that more select queries can be executed against the database without + // the aggregate value getting in the way when the grammar builds it. + $this->aggregate = null; - if (! $exists) { - return $this->insert(array_merge($attributes, $values)); - } + $this->columns = $previousColumns; - if (empty($values)) { - return true; - } + $this->bindings['select'] = $previousSelectBindings; + + if (! $results->isEmpty()) { + $result = array_change_key_case((array) $results[0]); - return (bool) $this->limit(1)->update($values); + return $result['aggregate']; + } } /** - * Insert new records or update the existing ones. + * Insert a new record into the database. * - * @param non-empty-string|non-empty-array $uniqueBy - * @return int + * @param array $values + * @return bool */ - public function upsert(array $values, array|string $uniqueBy, ?array $update = null) + public function insert(array $values) { - if ($uniqueBy === [] || $uniqueBy === '') { - throw new InvalidArgumentException('The unique columns must not be empty.'); - } - if (empty($values)) { - return 0; - } elseif ($update === []) { - return (int) $this->insert($values); + return true; } - if (! is_array(array_first($values))) { + // Since every insert gets treated like a batch insert, we will make sure the + // bindings are structured in a way that is convenient for building these + // inserts statements by verifying the elements are actually an array. + if (! is_array(reset($values))) { $values = [$values]; - } else { + } + + // Since every insert gets treated like a batch insert, we will make sure the + // bindings are structured in a way that is convenient for building these + // inserts statements by verifying the elements are actually an array. + else { foreach ($values as $key => $value) { ksort($value); - $values[$key] = $value; } } - if (is_null($update)) { - $update = array_keys(array_first($values)); + // We'll treat every insert like a batch insert so we can easily insert each + // of the records into the database consistently. This will make it much + // easier on the grammars to just handle one type of record insertion. + $bindings = []; + + foreach ($values as $record) { + foreach ($record as $value) { + $bindings[] = $value; + } } - $this->applyBeforeQueryCallbacks(); + $sql = $this->grammar->compileInsert($this, $values); - $bindings = $this->cleanBindings(array_merge( - Arr::flatten($values, 1), - (new Collection($update)) - ->reject(fn ($value, $key) => is_int($key)) - ->all() - )); + // Once we have compiled the insert statement's SQL we can execute it on the + // connection and return a result as a boolean success indicator as that + // is the same type of result returned by the raw connection instance. + $bindings = $this->cleanBindings($bindings); - return $this->connection->affectingStatement( - $this->grammar->compileUpsert($this, $values, (array) $uniqueBy, $update), - $bindings - ); + return $this->connection->insert($sql, $bindings); } /** - * Increment a column's value by a given amount. - * - * @param string $column - * @param float|int $amount - * @return int<0, max> + * Insert a new record and get the value of the primary key. * - * @throws \InvalidArgumentException + * @param array $values + * @param string $sequence + * @return int */ - public function increment($column, $amount = 1, array $extra = []) + public function insertGetId(array $values, $sequence = null) { - if (! is_numeric($amount)) { - throw new InvalidArgumentException('Non-numeric value passed to increment method.'); - } + $sql = $this->grammar->compileInsertGetId($this, $values, $sequence); - return $this->incrementEach([$column => $amount], $extra); + $values = $this->cleanBindings($values); + + return $this->processor->processInsertGetId($this, $sql, $values, $sequence); } /** - * Increment the given column's values by the given amounts. - * - * @param array $columns - * @param array $extra - * @return int<0, max> + * Update a record in the database. * - * @throws \InvalidArgumentException + * @param array $values + * @return int */ - public function incrementEach(array $columns, array $extra = []) + public function update(array $values) { - foreach ($columns as $column => $amount) { - if (! is_numeric($amount)) { - throw new InvalidArgumentException("Non-numeric value passed as increment amount for column: '$column'."); - } elseif (! is_string($column)) { - throw new InvalidArgumentException('Non-associative array passed to incrementEach method.'); - } + $bindings = array_values(array_merge($values, $this->getBindings())); - $columns[$column] = $this->raw("{$this->grammar->wrap($column)} + $amount"); - } + $sql = $this->grammar->compileUpdate($this, $values); - return $this->update(array_merge($columns, $extra)); + return $this->connection->update($sql, $this->cleanBindings($bindings)); } /** - * Decrement a column's value by a given amount. - * - * @param string $column - * @param float|int $amount - * @return int<0, max> + * Insert or update a record matching the attributes, and fill it with values. * - * @throws \InvalidArgumentException + * @param array $attributes + * @param array $values + * @return bool */ - public function decrement($column, $amount = 1, array $extra = []) + public function updateOrInsert(array $attributes, array $values = []) { - if (! is_numeric($amount)) { - throw new InvalidArgumentException('Non-numeric value passed to decrement method.'); + if (! $this->where($attributes)->exists()) { + return $this->insert(array_merge($attributes, $values)); } - return $this->decrementEach([$column => $amount], $extra); + return (bool) $this->where($attributes)->take(1)->update($values); } /** - * Decrement the given column's values by the given amounts. + * Increment a column's value by a given amount. * - * @param array $columns - * @param array $extra - * @return int<0, max> + * @param string $column + * @param int $amount + * @param array $extra + * @return int + */ + public function increment($column, $amount = 1, array $extra = []) + { + $wrapped = $this->grammar->wrap($column); + + $columns = array_merge([$column => $this->raw("$wrapped + $amount")], $extra); + + return $this->update($columns); + } + + /** + * Decrement a column's value by a given amount. * - * @throws \InvalidArgumentException + * @param string $column + * @param int $amount + * @param array $extra + * @return int */ - public function decrementEach(array $columns, array $extra = []) + public function decrement($column, $amount = 1, array $extra = []) { - foreach ($columns as $column => $amount) { - if (! is_numeric($amount)) { - throw new InvalidArgumentException("Non-numeric value passed as decrement amount for column: '$column'."); - } elseif (! is_string($column)) { - throw new InvalidArgumentException('Non-associative array passed to decrementEach method.'); - } + $wrapped = $this->grammar->wrap($column); - $columns[$column] = $this->raw("{$this->grammar->wrap($column)} - $amount"); - } + $columns = array_merge([$column => $this->raw("$wrapped - $amount")], $extra); - return $this->update(array_merge($columns, $extra)); + return $this->update($columns); } /** - * Delete records from the database. + * Delete a record from the database. * * @param mixed $id * @return int */ public function delete($id = null) { - // If an ID is passed to the method, we will set the where clause to check the - // ID to let developers to simply and quickly remove a single row from this - // database without manually specifying the "where" clauses on the query. + // If an ID is passed to the method, we will set the where clause to check + // the ID to allow developers to simply and quickly remove a single row + // from their database without manually specifying the where clauses. if (! is_null($id)) { - $this->where($this->from.'.id', '=', $id); + $this->where('id', '=', $id); } - $this->applyBeforeQueryCallbacks(); + $sql = $this->grammar->compileDelete($this); - return $this->connection->delete( - $this->grammar->compileDelete($this), $this->cleanBindings( - $this->grammar->prepareBindingsForDelete($this->bindings) - ) - ); + return $this->connection->delete($sql, $this->getBindings()); } /** - * Run a "truncate" statement on the table. + * Run a truncate statement on the table. * * @return void */ public function truncate() { - $this->applyBeforeQueryCallbacks(); - foreach ($this->grammar->compileTruncate($this) as $sql => $bindings) { $this->connection->statement($sql, $bindings); } @@ -4468,78 +2063,47 @@ public function newQuery() } /** - * Create a new query instance for a sub-query. + * Merge an array of where clauses and bindings. * - * @return \Illuminate\Database\Query\Builder + * @param array $wheres + * @param array $bindings + * @return void */ - protected function forSubQuery() + public function mergeWheres($wheres, $bindings) { - return $this->newQuery(); + $this->wheres = array_merge((array) $this->wheres, (array) $wheres); + + $this->bindings['where'] = array_values(array_merge($this->bindings['where'], (array) $bindings)); } /** - * Get all of the query builder's columns in a text-only array with all expressions evaluated. + * Remove all of the expressions from a list of bindings. * - * @return list + * @param array $bindings + * @return array */ - public function getColumns() + protected function cleanBindings(array $bindings) { - return ! is_null($this->columns) - ? array_map(fn ($column) => $this->grammar->getValue($column), $this->columns) - : []; + return array_values(array_filter($bindings, function ($binding) { + return ! $binding instanceof Expression; + })); } /** * Create a raw database expression. * * @param mixed $value - * @return \Illuminate\Contracts\Database\Query\Expression + * @return \Illuminate\Database\Query\Expression */ public function raw($value) { return $this->connection->raw($value); } - /** - * Get the query builder instances that are used in the union of the query. - * - * @return \Illuminate\Support\Collection - */ - protected function getUnionBuilders() - { - return isset($this->unions) - ? (new Collection($this->unions))->pluck('query') - : new Collection; - } - - /** - * Get the "limit" value for the query or null if it's not set. - * - * @return mixed - */ - public function getLimit() - { - $value = $this->unions ? $this->unionLimit : $this->limit; - - return ! is_null($value) ? (int) $value : null; - } - - /** - * Get the "offset" value for the query or null if it's not set. - * - * @return mixed - */ - public function getOffset() - { - $value = $this->unions ? $this->unionOffset : $this->offset; - - return ! is_null($value) ? (int) $value : null; - } - /** * Get the current query value bindings in a flattened array. * - * @return list + * @return array */ public function getBindings() { @@ -4549,17 +2113,7 @@ public function getBindings() /** * Get the raw array of bindings. * - * @return array{ - * select: list, - * from: list, - * join: list, - * where: list, - * groupBy: list, - * having: list, - * order: list, - * union: list, - * unionOrder: list, - * } + * @return array */ public function getRawBindings() { @@ -4569,8 +2123,8 @@ public function getRawBindings() /** * Set the bindings on the query builder. * - * @param list $bindings - * @param "select"|"from"|"join"|"where"|"groupBy"|"having"|"order"|"union"|"unionOrder" $type + * @param array $bindings + * @param string $type * @return $this * * @throws \InvalidArgumentException @@ -4589,8 +2143,8 @@ public function setBindings(array $bindings, $type = 'where') /** * Add a binding to the query. * - * @param mixed $value - * @param "select"|"from"|"join"|"where"|"groupBy"|"having"|"order"|"union"|"unionOrder" $type + * @param mixed $value + * @param string $type * @return $this * * @throws \InvalidArgumentException @@ -4602,79 +2156,27 @@ public function addBinding($value, $type = 'where') } if (is_array($value)) { - $this->bindings[$type] = array_values(array_map( - $this->castBinding(...), - array_merge($this->bindings[$type], $value), - )); + $this->bindings[$type] = array_values(array_merge($this->bindings[$type], $value)); } else { - $this->bindings[$type][] = $this->castBinding($value); + $this->bindings[$type][] = $value; } return $this; } - /** - * Cast the given binding value. - * - * @param mixed $value - * @return mixed - */ - public function castBinding($value) - { - return enum_value($value); - } - /** * Merge an array of bindings into our bindings. * - * @param self $query + * @param \Illuminate\Database\Query\Builder $query * @return $this */ - public function mergeBindings(self $query) + public function mergeBindings(Builder $query) { $this->bindings = array_merge_recursive($this->bindings, $query->bindings); return $this; } - /** - * Remove all of the expressions from a list of bindings. - * - * @param array $bindings - * @return list - */ - public function cleanBindings(array $bindings) - { - return (new Collection($bindings)) - ->reject(function ($binding) { - return $binding instanceof ExpressionContract; - }) - ->map($this->castBinding(...)) - ->values() - ->all(); - } - - /** - * Get a scalar type value from an unknown type of input. - * - * @param mixed $value - * @return mixed - */ - protected function flattenValue($value) - { - return is_array($value) ? head(Arr::flatten($value)) : $value; - } - - /** - * Get the default key name of the table. - * - * @return string - */ - protected function defaultKeyName() - { - return 'id'; - } - /** * Get the database connection instance. * @@ -4685,20 +2187,6 @@ public function getConnection() return $this->connection; } - /** - * Ensure the database connection supports vector queries. - * - * @return void - * - * @throws \RuntimeException - */ - protected function ensureConnectionSupportsVectors() - { - if (! $this->connection instanceof PostgresConnection) { - throw new RuntimeException('Vector distance queries are only supported by Postgres.'); - } - } - /** * Get the database query processor instance. * @@ -4720,7 +2208,7 @@ public function getGrammar() } /** - * Use the "write" PDO connection when executing the query. + * Use the write pdo for query. * * @return $this */ @@ -4731,125 +2219,11 @@ public function useWritePdo() return $this; } - /** - * Specify arguments for the PDOStatement::fetchAll / fetch functions. - * - * @param mixed ...$fetchUsing - * @return $this - */ - public function fetchUsing(...$fetchUsing) - { - $this->fetchUsing = $fetchUsing; - - return $this; - } - - /** - * Determine if the value is a query builder instance or a Closure. - * - * @param mixed $value - * @return bool - */ - protected function isQueryable($value) - { - return $value instanceof self || - $value instanceof EloquentBuilder || - $value instanceof Relation || - $value instanceof Closure; - } - - /** - * Clone the query. - * - * @return static - */ - public function clone() - { - return clone $this; - } - - /** - * Clone the query without the given properties. - * - * @return static - */ - public function cloneWithout(array $properties) - { - return tap($this->clone(), function ($clone) use ($properties) { - foreach ($properties as $property) { - $clone->{$property} = null; - } - }); - } - - /** - * Clone the query without the given bindings. - * - * @return static - */ - public function cloneWithoutBindings(array $except) - { - return tap($this->clone(), function ($clone) use ($except) { - foreach ($except as $type) { - $clone->bindings[$type] = []; - } - }); - } - - /** - * Dump the current SQL and bindings. - * - * @param mixed ...$args - * @return $this - */ - public function dump(...$args) - { - dump( - $this->toSql(), - $this->getBindings(), - ...$args, - ); - - return $this; - } - - /** - * Dump the raw current SQL with embedded bindings. - * - * @return $this - */ - public function dumpRawSql() - { - dump($this->toRawSql()); - - return $this; - } - - /** - * Die and dump the current SQL and bindings. - * - * @return never - */ - public function dd() - { - dd($this->toSql(), $this->getBindings()); - } - - /** - * Die and dump the current SQL with embedded bindings. - * - * @return never - */ - public function ddRawSql() - { - dd($this->toRawSql()); - } - /** * Handle dynamic method calls into the method. * * @param string $method - * @param array $parameters + * @param array $parameters * @return mixed * * @throws \BadMethodCallException @@ -4860,10 +2234,12 @@ public function __call($method, $parameters) return $this->macroCall($method, $parameters); } - if (str_starts_with($method, 'where')) { + if (Str::startsWith($method, 'where')) { return $this->dynamicWhere($method, $parameters); } - static::throwBadMethodCallException($method); + $className = static::class; + + throw new BadMethodCallException("Call to undefined method {$className}::{$method}()"); } } diff --git a/Query/Expression.php b/Query/Expression.php index 839dea833a..de69029980 100755 --- a/Query/Expression.php +++ b/Query/Expression.php @@ -2,32 +2,43 @@ namespace Illuminate\Database\Query; -use Illuminate\Contracts\Database\Query\Expression as ExpressionContract; -use Illuminate\Database\Grammar; - -/** - * @template TValue of string|int|float - */ -class Expression implements ExpressionContract +class Expression { + /** + * The value of the expression. + * + * @var mixed + */ + protected $value; + /** * Create a new raw query expression. * - * @param TValue $value + * @param mixed $value + * @return void */ - public function __construct( - protected $value, - ) { + public function __construct($value) + { + $this->value = $value; } /** * Get the value of the expression. * - * @param \Illuminate\Database\Grammar $grammar - * @return TValue + * @return mixed */ - public function getValue(Grammar $grammar) + public function getValue() { return $this->value; } + + /** + * Get the value of the expression. + * + * @return string + */ + public function __toString() + { + return (string) $this->getValue(); + } } diff --git a/Query/Grammars/Grammar.php b/Query/Grammars/Grammar.php index aa0dd52344..112dd28a63 100755 --- a/Query/Grammars/Grammar.php +++ b/Query/Grammars/Grammar.php @@ -2,20 +2,11 @@ namespace Illuminate\Database\Query\Grammars; -use Illuminate\Contracts\Database\Query\Expression; -use Illuminate\Database\Concerns\CompilesJsonPaths; -use Illuminate\Database\Grammar as BaseGrammar; use Illuminate\Database\Query\Builder; -use Illuminate\Database\Query\JoinClause; -use Illuminate\Database\Query\JoinLateralClause; -use Illuminate\Support\Arr; -use Illuminate\Support\Collection; -use RuntimeException; +use Illuminate\Database\Grammar as BaseGrammar; class Grammar extends BaseGrammar { - use CompilesJsonPaths; - /** * The grammar specific operators. * @@ -23,23 +14,15 @@ class Grammar extends BaseGrammar */ protected $operators = []; - /** - * The grammar specific bitwise operators. - * - * @var array - */ - protected $bitwiseOperators = []; - /** * The components that make up a select clause. * - * @var string[] + * @var array */ protected $selectComponents = [ 'aggregate', 'columns', 'from', - 'indexHint', 'joins', 'wheres', 'groups', @@ -47,6 +30,7 @@ class Grammar extends BaseGrammar 'orders', 'limit', 'offset', + 'unions', 'lock', ]; @@ -58,40 +42,13 @@ class Grammar extends BaseGrammar */ public function compileSelect(Builder $query) { - if (($query->unions || $query->havings) && $query->aggregate) { - return $this->compileUnionAggregate($query); - } - - // If a "group limit" is in place, we will need to compile the SQL to use a - // different syntax. This primarily supports limits on eager loads using - // Eloquent. We'll also set the columns if they have not been defined. - if (isset($query->groupLimit)) { - if (is_null($query->columns)) { - $query->columns = ['*']; - } - - return $this->compileGroupLimit($query); - } - - // If the query does not have any columns set, we'll set the columns to the - // * character to just get all of the columns from the database. Then we - // can build the query and concatenate all the pieces together as one. $original = $query->columns; if (is_null($query->columns)) { $query->columns = ['*']; } - // To compile the query, we'll spin through each component of the query and - // see if that component exists. If it does we'll just call the compiler - // function for the component which is responsible for making the SQL. - $sql = trim($this->concatenate( - $this->compileComponents($query)) - ); - - if ($query->unions) { - $sql = $this->wrapUnion($sql).' '.$this->compileUnions($query); - } + $sql = trim($this->concatenate($this->compileComponents($query))); $query->columns = $original; @@ -109,7 +66,10 @@ protected function compileComponents(Builder $query) $sql = []; foreach ($this->selectComponents as $component) { - if (isset($query->$component)) { + // To compile the query, we'll spin through each component of the query and + // see if that component exists. If it does we'll just call the compiler + // function for the component which is responsible for making the SQL. + if (! is_null($query->$component)) { $method = 'compile'.ucfirst($component); $sql[$component] = $this->$method($query, $query->$component); @@ -123,7 +83,7 @@ protected function compileComponents(Builder $query) * Compile an aggregated select clause. * * @param \Illuminate\Database\Query\Builder $query - * @param array{function: string, columns: array<\Illuminate\Contracts\Database\Query\Expression|string>} $aggregate + * @param array $aggregate * @return string */ protected function compileAggregate(Builder $query, $aggregate) @@ -133,9 +93,7 @@ protected function compileAggregate(Builder $query, $aggregate) // If the query has a "distinct" constraint and we're not asking for all columns // we need to prepend "distinct" onto the column name so that the query takes // it into account when it performs the aggregating operations on the data. - if (is_array($query->distinct)) { - $column = 'distinct '.$this->columnize($query->distinct); - } elseif ($query->distinct && $column !== '*') { + if ($query->distinct && $column !== '*') { $column = 'distinct '.$column; } @@ -158,11 +116,7 @@ protected function compileColumns(Builder $query, $columns) return; } - if ($query->distinct) { - $select = 'select distinct '; - } else { - $select = 'select '; - } + $select = $query->distinct ? 'select distinct ' : 'select '; return $select.$this->columnize($columns); } @@ -188,47 +142,93 @@ protected function compileFrom(Builder $query, $table) */ protected function compileJoins(Builder $query, $joins) { - return (new Collection($joins))->map(function ($join) use ($query) { + $sql = []; + + foreach ($joins as $join) { $table = $this->wrapTable($join->table); - $nestedJoins = is_null($join->joins) ? '' : ' '.$this->compileJoins($query, $join->joins); + $type = $join->type; + + // Cross joins generate a cartesian product between this first table and a joined + // table. In case the user didn't specify any "on" clauses on the join we will + // append this SQL and jump right back into the next iteration of this loop. + if ($type === 'cross' && ! $join->clauses) { + $sql[] = "cross join $table"; + + continue; + } - $tableAndNestedJoins = is_null($join->joins) ? $table : '('.$table.$nestedJoins.')'; + // First we need to build all of the "on" clauses for the join. There may be many + // of these clauses so we will need to iterate through each one and build them + // separately, then we'll join them up into a single string when we're done. + $clauses = []; - if ($join instanceof JoinLateralClause) { - return $this->compileJoinLateral($join, $tableAndNestedJoins); + foreach ($join->clauses as $clause) { + $clauses[] = $this->compileJoinConstraint($clause); } - $joinWord = ($join->type === 'straight_join' && $this->supportsStraightJoins()) ? '' : ' join'; + // Once we have constructed the clauses, we'll need to take the boolean connector + // off of the first clause as it obviously will not be required on that clause + // because it leads the rest of the clauses, thus not requiring any boolean. + $clauses[0] = $this->removeLeadingBoolean($clauses[0]); - return trim("{$join->type}{$joinWord} {$tableAndNestedJoins} {$this->compileWheres($join)}"); - })->implode(' '); + $clauses = implode(' ', $clauses); + + // Once we have everything ready to go, we will just concatenate all the parts to + // build the final join statement SQL for the query and we can then return the + // final clause back to the callers as a single, stringified join statement. + $sql[] = "$type join $table on $clauses"; + } + + return implode(' ', $sql); } /** - * Compile a "lateral join" clause. + * Create a join clause constraint segment. * - * @param \Illuminate\Database\Query\JoinLateralClause $join - * @param string $expression + * @param array $clause * @return string - * - * @throws \RuntimeException */ - public function compileJoinLateral(JoinLateralClause $join, string $expression): string + protected function compileJoinConstraint(array $clause) { - throw new RuntimeException('This database engine does not support lateral joins.'); + if ($clause['nested']) { + return $this->compileNestedJoinConstraint($clause); + } + + $first = $this->wrap($clause['first']); + + if ($clause['where']) { + if ($clause['operator'] === 'in' || $clause['operator'] === 'not in') { + $second = '('.implode(', ', array_fill(0, $clause['second'], '?')).')'; + } else { + $second = '?'; + } + } else { + $second = $this->wrap($clause['second']); + } + + return "{$clause['boolean']} $first {$clause['operator']} $second"; } /** - * Determine if the grammar supports straight joins. - * - * @return bool + * Create a nested join clause constraint segment. * - * @throws \RuntimeException + * @param array $clause + * @return string */ - protected function supportsStraightJoins() + protected function compileNestedJoinConstraint(array $clause) { - throw new RuntimeException('This database engine does not support straight joins.'); + $clauses = []; + + foreach ($clause['join']->clauses as $nestedClause) { + $clauses[] = $this->compileJoinConstraint($nestedClause); + } + + $clauses[0] = $this->removeLeadingBoolean($clauses[0]); + + $clauses = implode(' ', $clauses); + + return "{$clause['boolean']} ({$clauses})"; } /** @@ -237,110 +237,113 @@ protected function supportsStraightJoins() * @param \Illuminate\Database\Query\Builder $query * @return string */ - public function compileWheres(Builder $query) + protected function compileWheres(Builder $query) { - // Each type of where clause has its own compiler function, which is responsible - // for actually creating the where clauses SQL. This helps keep the code nice - // and maintainable since each clause has a very small method that it uses. + $sql = []; + if (is_null($query->wheres)) { return ''; } + // Each type of where clauses has its own compiler function which is responsible + // for actually creating the where clauses SQL. This helps keep the code nice + // and maintainable since each clause has a very small method that it uses. + foreach ($query->wheres as $where) { + $method = "where{$where['type']}"; + + $sql[] = $where['boolean'].' '.$this->$method($query, $where); + } + // If we actually have some where clauses, we will strip off the first boolean // operator, which is added by the query builders for convenience so we can // avoid checking for the first clauses in each of the compilers methods. - if (count($sql = $this->compileWheresToArray($query)) > 0) { - return $this->concatenateWhereClauses($query, $sql); + if (count($sql) > 0) { + $sql = implode(' ', $sql); + + return 'where '.$this->removeLeadingBoolean($sql); } return ''; } /** - * Get an array of all the where clauses for the query. + * Compile a nested where clause. * * @param \Illuminate\Database\Query\Builder $query - * @return array + * @param array $where + * @return string */ - protected function compileWheresToArray($query) + protected function whereNested(Builder $query, $where) { - return (new Collection($query->wheres)) - ->map(fn ($where) => $where['boolean'].' '.$this->{"where{$where['type']}"}($query, $where)) - ->all(); + $nested = $where['query']; + + return '('.substr($this->compileWheres($nested), 6).')'; } /** - * Format the where clause statements into one string. + * Compile a where condition with a sub-select. * - * @param \Illuminate\Database\Query\Builder $query - * @param array $sql + * @param \Illuminate\Database\Query\Builder $query + * @param array $where * @return string */ - protected function concatenateWhereClauses($query, $sql) + protected function whereSub(Builder $query, $where) { - $conjunction = $query instanceof JoinClause ? 'on' : 'where'; + $select = $this->compileSelect($where['query']); - return $conjunction.' '.$this->removeLeadingBoolean(implode(' ', $sql)); + return $this->wrap($where['column']).' '.$where['operator']." ($select)"; } /** - * Compile a raw where clause. + * Compile a basic where clause. * * @param \Illuminate\Database\Query\Builder $query * @param array $where * @return string */ - protected function whereRaw(Builder $query, $where) + protected function whereBasic(Builder $query, $where) { - return $where['sql'] instanceof Expression ? $where['sql']->getValue($this) : $where['sql']; + $value = $this->parameter($where['value']); + + return $this->wrap($where['column']).' '.$where['operator'].' '.$value; } /** - * Compile a basic where clause. + * Compile a "between" where clause. * * @param \Illuminate\Database\Query\Builder $query * @param array $where * @return string */ - protected function whereBasic(Builder $query, $where) + protected function whereBetween(Builder $query, $where) { - $value = $this->parameter($where['value']); - - $operator = str_replace('?', '??', $where['operator']); + $between = $where['not'] ? 'not between' : 'between'; - return $this->wrap($where['column']).' '.$operator.' '.$value; + return $this->wrap($where['column']).' '.$between.' ? and ?'; } /** - * Compile a bitwise operator where clause. + * Compile a where exists clause. * * @param \Illuminate\Database\Query\Builder $query * @param array $where * @return string */ - protected function whereBitwise(Builder $query, $where) + protected function whereExists(Builder $query, $where) { - return $this->whereBasic($query, $where); + return 'exists ('.$this->compileSelect($where['query']).')'; } /** - * Compile a "where like" clause. + * Compile a where exists clause. * * @param \Illuminate\Database\Query\Builder $query * @param array $where * @return string - * - * @throws \RuntimeException */ - protected function whereLike(Builder $query, $where) + protected function whereNotExists(Builder $query, $where) { - if ($where['caseSensitive']) { - throw new RuntimeException('This database engine does not support case sensitive like operations.'); - } - - $where['operator'] = $where['not'] ? 'not like' : 'like'; - - return $this->whereBasic($query, $where); + return 'not exists ('.$this->compileSelect($where['query']).')'; } /** @@ -352,11 +355,13 @@ protected function whereLike(Builder $query, $where) */ protected function whereIn(Builder $query, $where) { - if (! empty($where['values'])) { - return $this->wrap($where['column']).' in ('.$this->parameterize($where['values']).')'; + if (empty($where['values'])) { + return '0 = 1'; } - return '0 = 1'; + $values = $this->parameterize($where['values']); + + return $this->wrap($where['column']).' in ('.$values.')'; } /** @@ -368,47 +373,41 @@ protected function whereIn(Builder $query, $where) */ protected function whereNotIn(Builder $query, $where) { - if (! empty($where['values'])) { - return $this->wrap($where['column']).' not in ('.$this->parameterize($where['values']).')'; + if (empty($where['values'])) { + return '1 = 1'; } - return '1 = 1'; + $values = $this->parameterize($where['values']); + + return $this->wrap($where['column']).' not in ('.$values.')'; } /** - * Compile a "where not in raw" clause. - * - * For safety, whereIntegerInRaw ensures this method is only used with integer values. + * Compile a where in sub-select clause. * * @param \Illuminate\Database\Query\Builder $query * @param array $where * @return string */ - protected function whereNotInRaw(Builder $query, $where) + protected function whereInSub(Builder $query, $where) { - if (! empty($where['values'])) { - return $this->wrap($where['column']).' not in ('.implode(', ', $where['values']).')'; - } + $select = $this->compileSelect($where['query']); - return '1 = 1'; + return $this->wrap($where['column']).' in ('.$select.')'; } /** - * Compile a "where in raw" clause. - * - * For safety, whereIntegerInRaw ensures this method is only used with integer values. + * Compile a where not in sub-select clause. * * @param \Illuminate\Database\Query\Builder $query * @param array $where * @return string */ - protected function whereInRaw(Builder $query, $where) + protected function whereNotInSub(Builder $query, $where) { - if (! empty($where['values'])) { - return $this->wrap($where['column']).' in ('.implode(', ', $where['values']).')'; - } + $select = $this->compileSelect($where['query']); - return '0 = 1'; + return $this->wrap($where['column']).' not in ('.$select.')'; } /** @@ -435,60 +434,6 @@ protected function whereNotNull(Builder $query, $where) return $this->wrap($where['column']).' is not null'; } - /** - * Compile a "between" where clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereBetween(Builder $query, $where) - { - $between = $where['not'] ? 'not between' : 'between'; - - $min = $this->parameter(is_array($where['values']) ? array_first($where['values']) : $where['values'][0]); - - $max = $this->parameter(is_array($where['values']) ? array_last($where['values']) : $where['values'][1]); - - return $this->wrap($where['column']).' '.$between.' '.$min.' and '.$max; - } - - /** - * Compile a "between" where clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereBetweenColumns(Builder $query, $where) - { - $between = $where['not'] ? 'not between' : 'between'; - - $min = $this->wrap(is_array($where['values']) ? array_first($where['values']) : $where['values'][0]); - - $max = $this->wrap(is_array($where['values']) ? array_last($where['values']) : $where['values'][1]); - - return $this->wrap($where['column']).' '.$between.' '.$min.' and '.$max; - } - - /** - * Compile a "value between" where clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereValueBetween(Builder $query, $where) - { - $between = $where['not'] ? 'not between' : 'between'; - - $min = $this->wrap(is_array($where['columns']) ? array_first($where['columns']) : $where['columns'][0]); - - $max = $this->wrap(is_array($where['columns']) ? array_last($where['columns']) : $where['columns'][1]); - - return $this->parameter($where['value']).' '.$between.' '.$min.' and '.$max; - } - /** * Compile a "where date" clause. * @@ -501,18 +446,6 @@ protected function whereDate(Builder $query, $where) return $this->dateBasedWhere('date', $query, $where); } - /** - * Compile a "where time" clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereTime(Builder $query, $where) - { - return $this->dateBasedWhere('time', $query, $where); - } - /** * Compile a "where day" clause. * @@ -565,701 +498,213 @@ protected function dateBasedWhere($type, Builder $query, $where) } /** - * Compile a where clause comparing two columns. + * Compile a raw where clause. * * @param \Illuminate\Database\Query\Builder $query * @param array $where * @return string */ - protected function whereColumn(Builder $query, $where) + protected function whereRaw(Builder $query, $where) { - return $this->wrap($where['first']).' '.$where['operator'].' '.$this->wrap($where['second']); + return $where['sql']; } /** - * Compile a nested where clause. + * Compile the "group by" portions of the query. * * @param \Illuminate\Database\Query\Builder $query - * @param array $where + * @param array $groups * @return string */ - protected function whereNested(Builder $query, $where) + protected function compileGroups(Builder $query, $groups) { - // Here we will calculate what portion of the string we need to remove. If this - // is a join clause query, we need to remove the "on" portion of the SQL and - // if it is a normal query we need to take the leading "where" of queries. - $offset = $where['query'] instanceof JoinClause ? 3 : 6; - - return '('.substr($this->compileWheres($where['query']), $offset).')'; + return 'group by '.$this->columnize($groups); } /** - * Compile a where condition with a sub-select. + * Compile the "having" portions of the query. * * @param \Illuminate\Database\Query\Builder $query - * @param array $where + * @param array $havings * @return string */ - protected function whereSub(Builder $query, $where) + protected function compileHavings(Builder $query, $havings) { - $select = $this->compileSelect($where['query']); + $sql = implode(' ', array_map([$this, 'compileHaving'], $havings)); - return $this->wrap($where['column']).' '.$where['operator']." ($select)"; + return 'having '.$this->removeLeadingBoolean($sql); } /** - * Compile a where exists clause. + * Compile a single having clause. * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where + * @param array $having * @return string */ - protected function whereExists(Builder $query, $where) + protected function compileHaving(array $having) { - return 'exists ('.$this->compileSelect($where['query']).')'; - } + // If the having clause is "raw", we can just return the clause straight away + // without doing any more processing on it. Otherwise, we will compile the + // clause into SQL based on the components that make it up from builder. + if ($having['type'] === 'raw') { + return $having['boolean'].' '.$having['sql']; + } - /** - * Compile a where exists clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereNotExists(Builder $query, $where) - { - return 'not exists ('.$this->compileSelect($where['query']).')'; + return $this->compileBasicHaving($having); } /** - * Compile a where row values condition. + * Compile a basic having clause. * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where + * @param array $having * @return string */ - protected function whereRowValues(Builder $query, $where) + protected function compileBasicHaving($having) { - $columns = $this->columnize($where['columns']); + $column = $this->wrap($having['column']); - $values = $this->parameterize($where['values']); + $parameter = $this->parameter($having['value']); - return '('.$columns.') '.$where['operator'].' ('.$values.')'; + return $having['boolean'].' '.$column.' '.$having['operator'].' '.$parameter; } /** - * Compile a "where JSON boolean" clause. + * Compile the "order by" portions of the query. * * @param \Illuminate\Database\Query\Builder $query - * @param array $where + * @param array $orders * @return string */ - protected function whereJsonBoolean(Builder $query, $where) + protected function compileOrders(Builder $query, $orders) { - $column = $this->wrapJsonBooleanSelector($where['column']); - - $value = $this->wrapJsonBooleanValue( - $this->parameter($where['value']) - ); + return 'order by '.implode(', ', array_map(function ($order) { + if (isset($order['sql'])) { + return $order['sql']; + } - return $column.' '.$where['operator'].' '.$value; + return $this->wrap($order['column']).' '.$order['direction']; + }, $orders)); } /** - * Compile a "where JSON contains" clause. + * Compile the "limit" portions of the query. * * @param \Illuminate\Database\Query\Builder $query - * @param array $where + * @param int $limit * @return string */ - protected function whereJsonContains(Builder $query, $where) + protected function compileLimit(Builder $query, $limit) { - $not = $where['not'] ? 'not ' : ''; - - return $not.$this->compileJsonContains( - $where['column'], - $this->parameter($where['value']) - ); + return 'limit '.(int) $limit; } /** - * Compile a "JSON contains" statement into SQL. + * Compile the "offset" portions of the query. * - * @param string $column - * @param string $value + * @param \Illuminate\Database\Query\Builder $query + * @param int $offset * @return string - * - * @throws \RuntimeException */ - protected function compileJsonContains($column, $value) + protected function compileOffset(Builder $query, $offset) { - throw new RuntimeException('This database engine does not support JSON contains operations.'); + return 'offset '.(int) $offset; } /** - * Compile a "where JSON overlaps" clause. + * Compile the "union" queries attached to the main query. * * @param \Illuminate\Database\Query\Builder $query - * @param array $where * @return string */ - protected function whereJsonOverlaps(Builder $query, $where) + protected function compileUnions(Builder $query) { - $not = $where['not'] ? 'not ' : ''; + $sql = ''; + + foreach ($query->unions as $union) { + $sql .= $this->compileUnion($union); + } + + if (isset($query->unionOrders)) { + $sql .= ' '.$this->compileOrders($query, $query->unionOrders); + } + + if (isset($query->unionLimit)) { + $sql .= ' '.$this->compileLimit($query, $query->unionLimit); + } + + if (isset($query->unionOffset)) { + $sql .= ' '.$this->compileOffset($query, $query->unionOffset); + } - return $not.$this->compileJsonOverlaps( - $where['column'], - $this->parameter($where['value']) - ); + return ltrim($sql); } /** - * Compile a "JSON overlaps" statement into SQL. + * Compile a single union statement. * - * @param string $column - * @param string $value + * @param array $union * @return string - * - * @throws \RuntimeException */ - protected function compileJsonOverlaps($column, $value) + protected function compileUnion(array $union) { - throw new RuntimeException('This database engine does not support JSON overlaps operations.'); + $joiner = $union['all'] ? ' union all ' : ' union '; + + return $joiner.$union['query']->toSql(); } /** - * Prepare the binding for a "JSON contains" statement. + * Compile an exists statement into SQL. * - * @param mixed $binding + * @param \Illuminate\Database\Query\Builder $query * @return string */ - public function prepareBindingForJsonContains($binding) + public function compileExists(Builder $query) { - return json_encode($binding, JSON_UNESCAPED_UNICODE); + $select = $this->compileSelect($query); + + return "select exists($select) as {$this->wrap('exists')}"; } /** - * Compile a "where JSON contains key" clause. + * Compile an insert statement into SQL. * * @param \Illuminate\Database\Query\Builder $query - * @param array $where + * @param array $values * @return string */ - protected function whereJsonContainsKey(Builder $query, $where) + public function compileInsert(Builder $query, array $values) { - $not = $where['not'] ? 'not ' : ''; + // Essentially we will force every insert to be treated as a batch insert which + // simply makes creating the SQL easier for us since we can utilize the same + // basic routine regardless of an amount of records given to us to insert. + $table = $this->wrapTable($query->from); - return $not.$this->compileJsonContainsKey( - $where['column'] - ); - } + if (! is_array(reset($values))) { + $values = [$values]; + } - /** - * Compile a "JSON contains key" statement into SQL. - * - * @param string $column - * @return string - * - * @throws \RuntimeException - */ - protected function compileJsonContainsKey($column) - { - throw new RuntimeException('This database engine does not support JSON contains key operations.'); - } - - /** - * Compile a "where JSON length" clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereJsonLength(Builder $query, $where) - { - return $this->compileJsonLength( - $where['column'], - $where['operator'], - $this->parameter($where['value']) - ); - } - - /** - * Compile a "JSON length" statement into SQL. - * - * @param string $column - * @param string $operator - * @param string $value - * @return string - * - * @throws \RuntimeException - */ - protected function compileJsonLength($column, $operator, $value) - { - throw new RuntimeException('This database engine does not support JSON length operations.'); - } - - /** - * Compile a "JSON value cast" statement into SQL. - * - * @param string $value - * @return string - */ - public function compileJsonValueCast($value) - { - return $value; - } - - /** - * Compile a "where fulltext" clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - * - * @throws \RuntimeException - */ - public function whereFullText(Builder $query, $where) - { - throw new RuntimeException('This database engine does not support fulltext search operations.'); - } - - /** - * Compile a clause based on an expression. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - public function whereExpression(Builder $query, $where) - { - return $where['column']->getValue($this); - } - - /** - * Compile the "group by" portions of the query. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $groups - * @return string - */ - protected function compileGroups(Builder $query, $groups) - { - return 'group by '.$this->columnize($groups); - } - - /** - * Compile the "having" portions of the query. - * - * @param \Illuminate\Database\Query\Builder $query - * @return string - */ - protected function compileHavings(Builder $query) - { - return 'having '.$this->removeLeadingBoolean((new Collection($query->havings))->map(function ($having) { - return $having['boolean'].' '.$this->compileHaving($having); - })->implode(' ')); - } - - /** - * Compile a single having clause. - * - * @param array $having - * @return string - */ - protected function compileHaving(array $having) - { - // If the having clause is "raw", we can just return the clause straight away - // without doing any more processing on it. Otherwise, we will compile the - // clause into SQL based on the components that make it up from builder. - return match ($having['type']) { - 'Raw' => $having['sql'], - 'between' => $this->compileHavingBetween($having), - 'Null' => $this->compileHavingNull($having), - 'NotNull' => $this->compileHavingNotNull($having), - 'bit' => $this->compileHavingBit($having), - 'Expression' => $this->compileHavingExpression($having), - 'Nested' => $this->compileNestedHavings($having), - default => $this->compileBasicHaving($having), - }; - } - - /** - * Compile a basic having clause. - * - * @param array $having - * @return string - */ - protected function compileBasicHaving($having) - { - $column = $this->wrap($having['column']); - - $parameter = $this->parameter($having['value']); - - return $column.' '.$having['operator'].' '.$parameter; - } - - /** - * Compile a "between" having clause. - * - * @param array $having - * @return string - */ - protected function compileHavingBetween($having) - { - $between = $having['not'] ? 'not between' : 'between'; - - $column = $this->wrap($having['column']); - - $min = $this->parameter(head($having['values'])); - - $max = $this->parameter(last($having['values'])); - - return $column.' '.$between.' '.$min.' and '.$max; - } - - /** - * Compile a having null clause. - * - * @param array $having - * @return string - */ - protected function compileHavingNull($having) - { - $column = $this->wrap($having['column']); - - return $column.' is null'; - } - - /** - * Compile a having not null clause. - * - * @param array $having - * @return string - */ - protected function compileHavingNotNull($having) - { - $column = $this->wrap($having['column']); - - return $column.' is not null'; - } - - /** - * Compile a having clause involving a bit operator. - * - * @param array $having - * @return string - */ - protected function compileHavingBit($having) - { - $column = $this->wrap($having['column']); - - $parameter = $this->parameter($having['value']); - - return '('.$column.' '.$having['operator'].' '.$parameter.') != 0'; - } - - /** - * Compile a having clause involving an expression. - * - * @param array $having - * @return string - */ - protected function compileHavingExpression($having) - { - return $having['column']->getValue($this); - } - - /** - * Compile a nested having clause. - * - * @param array $having - * @return string - */ - protected function compileNestedHavings($having) - { - return '('.substr($this->compileHavings($having['query']), 7).')'; - } - - /** - * Compile the "order by" portions of the query. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $orders - * @return string - */ - protected function compileOrders(Builder $query, $orders) - { - if (! empty($orders)) { - return 'order by '.implode(', ', $this->compileOrdersToArray($query, $orders)); - } - - return ''; - } - - /** - * Compile the query orders to an array. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $orders - * @return array - */ - protected function compileOrdersToArray(Builder $query, $orders) - { - return array_map(function ($order) use ($query) { - if (isset($order['sql']) && $order['sql'] instanceof Expression) { - return $order['sql']->getValue($query->getGrammar()); - } - - return $order['sql'] ?? $this->wrap($order['column']).' '.$order['direction']; - }, $orders); - } - - /** - * Compile the random statement into SQL. - * - * @param string|int $seed - * @return string - */ - public function compileRandom($seed) - { - return 'RANDOM()'; - } - - /** - * Compile the "limit" portions of the query. - * - * @param \Illuminate\Database\Query\Builder $query - * @param int $limit - * @return string - */ - protected function compileLimit(Builder $query, $limit) - { - return 'limit '.(int) $limit; - } - - /** - * Compile a group limit clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @return string - */ - protected function compileGroupLimit(Builder $query) - { - $selectBindings = array_merge($query->getRawBindings()['select'], $query->getRawBindings()['order']); - - $query->setBindings($selectBindings, 'select'); - $query->setBindings([], 'order'); - - $limit = (int) $query->groupLimit['value']; - $offset = $query->offset; - - if (isset($offset)) { - $offset = (int) $offset; - $limit += $offset; - - $query->offset = null; - } - - $components = $this->compileComponents($query); - - $components['columns'] .= $this->compileRowNumber( - $query->groupLimit['column'], - $components['orders'] ?? '' - ); - - unset($components['orders']); - - $table = $this->wrap('laravel_table'); - $row = $this->wrap('laravel_row'); - - $sql = $this->concatenate($components); - - $sql = 'select * from ('.$sql.') as '.$table.' where '.$row.' <= '.$limit; - - if (isset($offset)) { - $sql .= ' and '.$row.' > '.$offset; - } - - return $sql.' order by '.$row; - } - - /** - * Compile a row number clause. - * - * @param string $partition - * @param string $orders - * @return string - */ - protected function compileRowNumber($partition, $orders) - { - $over = trim('partition by '.$this->wrap($partition).' '.$orders); - - return ', row_number() over ('.$over.') as '.$this->wrap('laravel_row'); - } - - /** - * Compile the "offset" portions of the query. - * - * @param \Illuminate\Database\Query\Builder $query - * @param int $offset - * @return string - */ - protected function compileOffset(Builder $query, $offset) - { - return 'offset '.(int) $offset; - } - - /** - * Compile the "union" queries attached to the main query. - * - * @param \Illuminate\Database\Query\Builder $query - * @return string - */ - protected function compileUnions(Builder $query) - { - $sql = ''; - - foreach ($query->unions as $union) { - $sql .= $this->compileUnion($union); - } - - if (! empty($query->unionOrders)) { - $sql .= ' '.$this->compileOrders($query, $query->unionOrders); - } - - if (isset($query->unionLimit)) { - $sql .= ' '.$this->compileLimit($query, $query->unionLimit); - } - - if (isset($query->unionOffset)) { - $sql .= ' '.$this->compileOffset($query, $query->unionOffset); - } - - return ltrim($sql); - } - - /** - * Compile a single union statement. - * - * @param array $union - * @return string - */ - protected function compileUnion(array $union) - { - $conjunction = $union['all'] ? ' union all ' : ' union '; - - return $conjunction.$this->wrapUnion($union['query']->toSql()); - } - - /** - * Wrap a union subquery in parentheses. - * - * @param string $sql - * @return string - */ - protected function wrapUnion($sql) - { - return '('.$sql.')'; - } - - /** - * Compile a union aggregate query into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @return string - */ - protected function compileUnionAggregate(Builder $query) - { - $sql = $this->compileAggregate($query, $query->aggregate); - - $query->aggregate = null; - - return $sql.' from ('.$this->compileSelect($query).') as '.$this->wrapTable('temp_table'); - } - - /** - * Compile an exists statement into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @return string - */ - public function compileExists(Builder $query) - { - $select = $this->compileSelect($query); - - return "select exists({$select}) as {$this->wrap('exists')}"; - } - - /** - * Compile an insert statement into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @return string - */ - public function compileInsert(Builder $query, array $values) - { - // Essentially we will force every insert to be treated as a batch insert which - // simply makes creating the SQL easier for us since we can utilize the same - // basic routine regardless of an amount of records given to us to insert. - $table = $this->wrapTable($query->from); - - if (empty($values)) { - return "insert into {$table} default values"; - } - - if (! is_array(array_first($values))) { - $values = [$values]; - } - - $columns = $this->columnize(array_keys(array_first($values))); + $columns = $this->columnize(array_keys(reset($values))); // We need to build a list of parameter place-holders of values that are bound - // to the query. Each insert should have the exact same number of parameter + // to the query. Each insert should have the exact same amount of parameter // bindings so we will loop through the record and parameterize them all. - $parameters = (new Collection($values)) - ->map(fn ($record) => '('.$this->parameterize($record).')') - ->implode(', '); + $parameters = []; - return "insert into $table ($columns) values $parameters"; - } + foreach ($values as $record) { + $parameters[] = '('.$this->parameterize($record).')'; + } - /** - * Compile an insert ignore statement into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @return string - * - * @throws \RuntimeException - */ - public function compileInsertOrIgnore(Builder $query, array $values) - { - throw new RuntimeException('This database engine does not support inserting while ignoring errors.'); - } + $parameters = implode(', ', $parameters); - /** - * Compile an insert or ignore statement with a returning clause into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @param array $uniqueBy - * @param array $returning - * @return string - * - * @throws \RuntimeException - */ - public function compileInsertOrIgnoreReturning(Builder $query, array $values, array $uniqueBy, array $returning) - { - throw new RuntimeException('This database engine does not support insert or ignore with returning.'); + return "insert into $table ($columns) values $parameters"; } /** * Compile an insert and get ID statement into SQL. * * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @param string|null $sequence + * @param array $values + * @param string $sequence * @return string */ public function compileInsertGetId(Builder $query, $values, $sequence) @@ -1267,40 +712,6 @@ public function compileInsertGetId(Builder $query, $values, $sequence) return $this->compileInsert($query, $values); } - /** - * Compile an insert statement using a subquery into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $columns - * @param string $sql - * @return string - */ - public function compileInsertUsing(Builder $query, array $columns, string $sql) - { - $table = $this->wrapTable($query->from); - - if (empty($columns) || $columns === ['*']) { - return "insert into {$table} $sql"; - } - - return "insert into {$table} ({$this->columnize($columns)}) $sql"; - } - - /** - * Compile an insert ignore statement using a subquery into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $columns - * @param string $sql - * @return string - * - * @throws \RuntimeException - */ - public function compileInsertOrIgnoreUsing(Builder $query, array $columns, string $sql) - { - throw new RuntimeException('This database engine does not support inserting while ignoring errors.'); - } - /** * Compile an update statement into SQL. * @@ -1308,97 +719,36 @@ public function compileInsertOrIgnoreUsing(Builder $query, array $columns, strin * @param array $values * @return string */ - public function compileUpdate(Builder $query, array $values) + public function compileUpdate(Builder $query, $values) { $table = $this->wrapTable($query->from); - $columns = $this->compileUpdateColumns($query, $values); - - $where = $this->compileWheres($query); - - return trim( - isset($query->joins) - ? $this->compileUpdateWithJoins($query, $table, $columns, $where) - : $this->compileUpdateWithoutJoins($query, $table, $columns, $where) - ); - } - - /** - * Compile the columns for an update statement. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @return string - */ - protected function compileUpdateColumns(Builder $query, array $values) - { - return (new Collection($values)) - ->map(fn ($value, $key) => $this->wrap($key).' = '.$this->parameter($value)) - ->implode(', '); - } + // Each one of the columns in the update statements needs to be wrapped in the + // keyword identifiers, also a place-holder needs to be created for each of + // the values in the list of bindings so we can make the sets statements. + $columns = []; - /** - * Compile an update statement without joins into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param string $table - * @param string $columns - * @param string $where - * @return string - */ - protected function compileUpdateWithoutJoins(Builder $query, $table, $columns, $where) - { - return "update {$table} set {$columns} {$where}"; - } - - /** - * Compile an update statement with joins into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param string $table - * @param string $columns - * @param string $where - * @return string - */ - protected function compileUpdateWithJoins(Builder $query, $table, $columns, $where) - { - $joins = $this->compileJoins($query, $query->joins); + foreach ($values as $key => $value) { + $columns[] = $this->wrap($key).' = '.$this->parameter($value); + } - return "update {$table} {$joins} set {$columns} {$where}"; - } + $columns = implode(', ', $columns); - /** - * Compile an "upsert" statement into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @param array $uniqueBy - * @param array $update - * @return string - * - * @throws \RuntimeException - */ - public function compileUpsert(Builder $query, array $values, array $uniqueBy, array $update) - { - throw new RuntimeException('This database engine does not support upserts.'); - } - - /** - * Prepare the bindings for an update statement. - * - * @param array $bindings - * @param array $values - * @return array - */ - public function prepareBindingsForUpdate(array $bindings, array $values) - { - $cleanBindings = Arr::except($bindings, ['select', 'join']); + // If the query has any "join" clauses, we will setup the joins on the builder + // and compile them so we can attach them to this update, as update queries + // can get join statements to attach to other tables when they're needed. + if (isset($query->joins)) { + $joins = ' '.$this->compileJoins($query, $query->joins); + } else { + $joins = ''; + } - $values = Arr::flatten(array_map(fn ($value) => value($value), $values)); + // Of course, update queries may also be constrained by where clauses so we'll + // need to compile the where clauses and attach it to the query so only the + // intended records are updated by the SQL statements we generate to run. + $where = $this->compileWheres($query); - return array_values( - array_merge($bindings['join'], $values, Arr::flatten($cleanBindings)) - ); + return trim("update {$table}{$joins} set $columns $where"); } /** @@ -1411,56 +761,9 @@ public function compileDelete(Builder $query) { $table = $this->wrapTable($query->from); - $where = $this->compileWheres($query); + $where = is_array($query->wheres) ? $this->compileWheres($query) : ''; - return trim( - isset($query->joins) - ? $this->compileDeleteWithJoins($query, $table, $where) - : $this->compileDeleteWithoutJoins($query, $table, $where) - ); - } - - /** - * Compile a delete statement without joins into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param string $table - * @param string $where - * @return string - */ - protected function compileDeleteWithoutJoins(Builder $query, $table, $where) - { - return "delete from {$table} {$where}"; - } - - /** - * Compile a delete statement with joins into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param string $table - * @param string $where - * @return string - */ - protected function compileDeleteWithJoins(Builder $query, $table, $where) - { - $alias = last(explode(' as ', $table)); - - $joins = $this->compileJoins($query, $query->joins); - - return "delete {$alias} from {$table} {$joins} {$where}"; - } - - /** - * Prepare the bindings for a delete statement. - * - * @param array $bindings - * @return array - */ - public function prepareBindingsForDelete(array $bindings) - { - return Arr::flatten( - Arr::except($bindings, 'select') - ); + return trim("delete from $table ".$where); } /** @@ -1471,7 +774,7 @@ public function prepareBindingsForDelete(array $bindings) */ public function compileTruncate(Builder $query) { - return ['truncate table '.$this->wrapTable($query->from) => []]; + return ['truncate '.$this->wrapTable($query->from) => []]; } /** @@ -1486,16 +789,6 @@ protected function compileLock(Builder $query, $value) return is_string($value) ? $value : ''; } - /** - * Compile a query to get the number of open connections for a database. - * - * @return string|null - */ - public function compileThreadCount() - { - return null; - } - /** * Determine if the grammar supports savepoints. * @@ -1528,32 +821,10 @@ public function compileSavepointRollBack($name) return 'ROLLBACK TO SAVEPOINT '.$name; } - /** - * Wrap the given JSON selector for boolean values. - * - * @param string $value - * @return string - */ - protected function wrapJsonBooleanSelector($value) - { - return $this->wrapJsonSelector($value); - } - - /** - * Wrap the given JSON boolean value. - * - * @param string $value - * @return string - */ - protected function wrapJsonBooleanValue($value) - { - return $value; - } - /** * Concatenate an array of segments, removing empties. * - * @param array $segments + * @param array $segments * @return string */ protected function concatenate($segments) @@ -1575,45 +846,7 @@ protected function removeLeadingBoolean($value) } /** - * Substitute the given bindings into the given raw SQL query. - * - * @param string $sql - * @param array $bindings - * @return string - */ - public function substituteBindingsIntoRawSql($sql, $bindings) - { - $bindings = array_map(fn ($value) => $this->escape($value, is_resource($value) || gettype($value) === 'resource (closed)'), $bindings); - - $query = ''; - - $isStringLiteral = false; - - for ($i = 0; $i < strlen($sql); $i++) { - $char = $sql[$i]; - $nextChar = $sql[$i + 1] ?? null; - - // Single quotes can be escaped as '' according to the SQL standard while - // MySQL uses \'. Postgres has operators like ?| that must get encoded - // in PHP like ??|. We should skip over the escaped characters here. - if (in_array($char.$nextChar, ["\'", "''", '??'])) { - $query .= $char.$nextChar; - $i += 1; - } elseif ($char === "'") { // Starting / leaving string literal... - $query .= $char; - $isStringLiteral = ! $isStringLiteral; - } elseif ($char === '?' && ! $isStringLiteral) { // Substitutable binding... - $query .= array_shift($bindings) ?? '?'; - } else { // Normal character... - $query .= $char; - } - } - - return $query; - } - - /** - * Get the grammar specific operators. + * Get the gramar specific operators. * * @return array */ @@ -1621,14 +854,4 @@ public function getOperators() { return $this->operators; } - - /** - * Get the grammar specific bitwise operators. - * - * @return array - */ - public function getBitwiseOperators() - { - return $this->bitwiseOperators; - } } diff --git a/Query/Grammars/MariaDbGrammar.php b/Query/Grammars/MariaDbGrammar.php deleted file mode 100755 index 9ffc39d74b..0000000000 --- a/Query/Grammars/MariaDbGrammar.php +++ /dev/null @@ -1,69 +0,0 @@ -wrapJsonFieldAndPath($value); - - return 'json_value('.$field.$path.')'; - } -} diff --git a/Query/Grammars/MySqlGrammar.php b/Query/Grammars/MySqlGrammar.php index 013d30e4ae..99c8829de4 100755 --- a/Query/Grammars/MySqlGrammar.php +++ b/Query/Grammars/MySqlGrammar.php @@ -2,20 +2,29 @@ namespace Illuminate\Database\Query\Grammars; -use Illuminate\Database\Query\Builder; -use Illuminate\Database\Query\JoinLateralClause; -use Illuminate\Support\Collection; use Illuminate\Support\Str; -use InvalidArgumentException; +use Illuminate\Database\Query\Builder; class MySqlGrammar extends Grammar { /** - * The grammar specific operators. + * The components that make up a select clause. * - * @var string[] + * @var array */ - protected $operators = ['sounds like']; + protected $selectComponents = [ + 'aggregate', + 'columns', + 'from', + 'joins', + 'wheres', + 'groups', + 'havings', + 'orders', + 'limit', + 'offset', + 'lock', + ]; /** * Compile a select query into SQL. @@ -27,315 +36,24 @@ public function compileSelect(Builder $query) { $sql = parent::compileSelect($query); - if ($query->timeout === null) { - return $sql; + if ($query->unions) { + $sql = '('.$sql.') '.$this->compileUnions($query); } - $milliseconds = $query->timeout * 1000; - - return preg_replace( - '/^select\b/i', - 'select /*+ MAX_EXECUTION_TIME('.$milliseconds.') */', - $sql, - 1 - ); - } - - /** - * Compile a "where like" clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereLike(Builder $query, $where) - { - $where['operator'] = $where['not'] ? 'not ' : ''; - - $where['operator'] .= $where['caseSensitive'] ? 'like binary' : 'like'; - - return $this->whereBasic($query, $where); - } - - /** - * Add a "where null" clause to the query. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereNull(Builder $query, $where) - { - $columnValue = (string) $this->getValue($where['column']); - - if ($this->isJsonSelector($columnValue)) { - [$field, $path] = $this->wrapJsonFieldAndPath($columnValue); - - return '(json_extract('.$field.$path.') is null OR json_type(json_extract('.$field.$path.')) = \'NULL\')'; - } - - return parent::whereNull($query, $where); - } - - /** - * Add a "where not null" clause to the query. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereNotNull(Builder $query, $where) - { - $columnValue = (string) $this->getValue($where['column']); - - if ($this->isJsonSelector($columnValue)) { - [$field, $path] = $this->wrapJsonFieldAndPath($columnValue); - - return '(json_extract('.$field.$path.') is not null AND json_type(json_extract('.$field.$path.')) != \'NULL\')'; - } - - return parent::whereNotNull($query, $where); - } - - /** - * Compile a "where fulltext" clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - public function whereFullText(Builder $query, $where) - { - $columns = $this->columnize($where['columns']); - - $value = $this->parameter($where['value']); - - $mode = ($where['options']['mode'] ?? []) === 'boolean' - ? ' in boolean mode' - : ' in natural language mode'; - - $expanded = ($where['options']['expanded'] ?? []) && ($where['options']['mode'] ?? []) !== 'boolean' - ? ' with query expansion' - : ''; - - return "match ({$columns}) against (".$value."{$mode}{$expanded})"; - } - - /** - * Compile the index hints for the query. - * - * @param \Illuminate\Database\Query\Builder $query - * @param \Illuminate\Database\Query\IndexHint $indexHint - * @return string - * - * @throws \InvalidArgumentException - */ - protected function compileIndexHint(Builder $query, $indexHint) - { - $index = $indexHint->index; - - $indexes = array_map('trim', explode(',', $index)); - - foreach ($indexes as $i) { - if (! preg_match('/^[a-zA-Z0-9_$]+$/', $i)) { - throw new InvalidArgumentException('Index name contains invalid characters.'); - } - } - - return match ($indexHint->type) { - 'hint' => "use index ({$index})", - 'force' => "force index ({$index})", - default => "ignore index ({$index})", - }; - } - - /** - * Compile a group limit clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @return string - */ - protected function compileGroupLimit(Builder $query) - { - return $this->useLegacyGroupLimit($query) - ? $this->compileLegacyGroupLimit($query) - : parent::compileGroupLimit($query); - } - - /** - * Determine whether to use a legacy group limit clause for MySQL < 8.0. - * - * @param \Illuminate\Database\Query\Builder $query - * @return bool - */ - public function useLegacyGroupLimit(Builder $query) - { - $version = $query->getConnection()->getServerVersion(); - - return ! $query->getConnection()->isMaria() && version_compare($version, '8.0.11', '<'); - } - - /** - * Compile a group limit clause for MySQL < 8.0. - * - * Derived from https://softonsofa.com/tweaking-eloquent-relations-how-to-get-n-related-models-per-parent/. - * - * @param \Illuminate\Database\Query\Builder $query - * @return string - */ - protected function compileLegacyGroupLimit(Builder $query) - { - $limit = (int) $query->groupLimit['value']; - $offset = $query->offset; - - if (isset($offset)) { - $offset = (int) $offset; - $limit += $offset; - - $query->offset = null; - } - - $column = last(explode('.', $query->groupLimit['column'])); - $column = $this->wrap($column); - - $partition = ', @laravel_row := if(@laravel_group = '.$column.', @laravel_row + 1, 1) as `laravel_row`'; - $partition .= ', @laravel_group := '.$column; - - $orders = (array) $query->orders; - - array_unshift($orders, [ - 'column' => $query->groupLimit['column'], - 'direction' => 'asc', - ]); - - $query->orders = $orders; - - $components = $this->compileComponents($query); - - $sql = $this->concatenate($components); - - $from = '(select @laravel_row := 0, @laravel_group := 0) as `laravel_vars`, ('.$sql.') as `laravel_table`'; - - $sql = 'select `laravel_table`.*'.$partition.' from '.$from.' having `laravel_row` <= '.$limit; - - if (isset($offset)) { - $sql .= ' and `laravel_row` > '.$offset; - } - - return $sql.' order by `laravel_row`'; - } - - /** - * Compile an insert ignore statement into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @return string - */ - public function compileInsertOrIgnore(Builder $query, array $values) - { - return Str::replaceFirst('insert', 'insert ignore', $this->compileInsert($query, $values)); - } - - /** - * Compile an insert ignore statement using a subquery into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $columns - * @param string $sql - * @return string - */ - public function compileInsertOrIgnoreUsing(Builder $query, array $columns, string $sql) - { - return Str::replaceFirst('insert', 'insert ignore', $this->compileInsertUsing($query, $columns, $sql)); - } - - /** - * Compile a "JSON contains" statement into SQL. - * - * @param string $column - * @param string $value - * @return string - */ - protected function compileJsonContains($column, $value) - { - [$field, $path] = $this->wrapJsonFieldAndPath($column); - - return 'json_contains('.$field.', '.$value.$path.')'; - } - - /** - * Compile a "JSON overlaps" statement into SQL. - * - * @param string $column - * @param string $value - * @return string - */ - protected function compileJsonOverlaps($column, $value) - { - [$field, $path] = $this->wrapJsonFieldAndPath($column); - - return 'json_overlaps('.$field.', '.$value.$path.')'; - } - - /** - * Compile a "JSON contains key" statement into SQL. - * - * @param string $column - * @return string - */ - protected function compileJsonContainsKey($column) - { - [$field, $path] = $this->wrapJsonFieldAndPath($column); - - return 'ifnull(json_contains_path('.$field.', \'one\''.$path.'), 0)'; - } - - /** - * Compile a "JSON length" statement into SQL. - * - * @param string $column - * @param string $operator - * @param string $value - * @return string - */ - protected function compileJsonLength($column, $operator, $value) - { - [$field, $path] = $this->wrapJsonFieldAndPath($column); - - return 'json_length('.$field.$path.') '.$operator.' '.$value; + return $sql; } /** - * Compile a "JSON value cast" statement into SQL. + * Compile a single union statement. * - * @param string $value + * @param array $union * @return string */ - public function compileJsonValueCast($value) + protected function compileUnion(array $union) { - return 'cast('.$value.' as json)'; - } + $joiner = $union['all'] ? ' union all ' : ' union '; - /** - * Compile the random statement into SQL. - * - * @param string|int $seed - * @return string - * - * @throws \InvalidArgumentException - */ - public function compileRandom($seed) - { - if ($seed === '' || $seed === null) { - return 'RAND()'; - } - - if (! is_numeric($seed)) { - throw new InvalidArgumentException('The seed value must be numeric.'); - } - - return 'RAND('.(int) $seed.')'; + return $joiner.'('.$union['query']->toSql().')'; } /** @@ -347,137 +65,25 @@ public function compileRandom($seed) */ protected function compileLock(Builder $query, $value) { - if (! is_string($value)) { - return $value ? 'for update' : 'lock in share mode'; - } - - return $value; - } - - /** - * Compile an insert statement into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @return string - */ - public function compileInsert(Builder $query, array $values) - { - if (empty($values)) { - $values = [[]]; + if (is_string($value)) { + return $value; } - return parent::compileInsert($query, $values); - } - - /** - * Compile the columns for an update statement. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @return string - */ - protected function compileUpdateColumns(Builder $query, array $values) - { - return (new Collection($values))->map(function ($value, $key) { - if ($this->isJsonSelector($key)) { - return $this->compileJsonUpdateColumn($key, $value); - } - - return $this->wrap($key).' = '.$this->parameter($value); - })->implode(', '); + return $value ? 'for update' : 'lock in share mode'; } /** - * Compile an "upsert" statement into SQL. + * Compile an update statement into SQL. * * @param \Illuminate\Database\Query\Builder $query * @param array $values - * @param array $uniqueBy - * @param array $update - * @return string - */ - public function compileUpsert(Builder $query, array $values, array $uniqueBy, array $update) - { - $useUpsertAlias = $query->connection->getConfig('use_upsert_alias'); - - $sql = $this->compileInsert($query, $values); - - if ($useUpsertAlias) { - $sql .= ' as laravel_upsert_alias'; - } - - $sql .= ' on duplicate key update '; - - $columns = (new Collection($update))->map(function ($value, $key) use ($useUpsertAlias) { - if (! is_numeric($key)) { - return $this->wrap($key).' = '.$this->parameter($value); - } - - return $useUpsertAlias - ? $this->wrap($value).' = '.$this->wrap('laravel_upsert_alias').'.'.$this->wrap($value) - : $this->wrap($value).' = values('.$this->wrap($value).')'; - })->implode(', '); - - return $sql.$columns; - } - - /** - * Compile a "lateral join" clause. - * - * @param \Illuminate\Database\Query\JoinLateralClause $join - * @param string $expression - * @return string - */ - public function compileJoinLateral(JoinLateralClause $join, string $expression): string - { - return trim("{$join->type} join lateral {$expression} on true"); - } - - /** - * {@inheritdoc} - */ - protected function supportsStraightJoins() - { - return true; - } - - /** - * Prepare a JSON column being updated using the JSON_SET function. - * - * @param string $key - * @param mixed $value - * @return string - */ - protected function compileJsonUpdateColumn($key, $value) - { - if (is_bool($value)) { - $value = $value ? 'true' : 'false'; - } elseif (is_array($value)) { - $value = 'cast(? as json)'; - } else { - $value = $this->parameter($value); - } - - [$field, $path] = $this->wrapJsonFieldAndPath($key); - - return "{$field} = json_set({$field}{$path}, {$value})"; - } - - /** - * Compile an update statement without joins into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param string $table - * @param string $columns - * @param string $where * @return string */ - protected function compileUpdateWithoutJoins(Builder $query, $table, $columns, $where) + public function compileUpdate(Builder $query, $values) { - $sql = parent::compileUpdateWithoutJoins($query, $table, $columns, $where); + $sql = parent::compileUpdate($query, $values); - if (! empty($query->orders)) { + if (isset($query->orders)) { $sql .= ' '.$this->compileOrders($query, $query->orders); } @@ -485,69 +91,30 @@ protected function compileUpdateWithoutJoins(Builder $query, $table, $columns, $ $sql .= ' '.$this->compileLimit($query, $query->limit); } - return $sql; + return rtrim($sql); } /** - * Prepare the bindings for an update statement. - * - * Booleans, integers, and doubles are inserted into JSON updates as raw values. - * - * @param array $bindings - * @param array $values - * @return array - */ - #[\Override] - public function prepareBindingsForUpdate(array $bindings, array $values) - { - $values = (new Collection($values)) - ->reject(fn ($value, $column) => $this->isJsonSelector($column) && is_bool($value)) - ->map(fn ($value) => is_array($value) ? json_encode($value) : $value) - ->all(); - - return parent::prepareBindingsForUpdate($bindings, $values); - } - - /** - * Compile a delete statement without joins into SQL. + * Compile a delete statement into SQL. * * @param \Illuminate\Database\Query\Builder $query - * @param string $table - * @param string $where * @return string */ - protected function compileDeleteWithoutJoins(Builder $query, $table, $where) + public function compileDelete(Builder $query) { - $sql = parent::compileDeleteWithoutJoins($query, $table, $where); - - if (! empty($query->orders)) { - $sql .= ' '.$this->compileOrders($query, $query->orders); - } + $table = $this->wrapTable($query->from); - if (isset($query->limit)) { - $sql .= ' '.$this->compileLimit($query, $query->limit); - } + $where = is_array($query->wheres) ? $this->compileWheres($query) : ''; - return $sql; - } + if (isset($query->joins)) { + $joins = ' '.$this->compileJoins($query, $query->joins); - /** - * Compile a delete statement with joins into SQL. - * - * Adds ORDER BY and LIMIT if present, for platforms that allow them (e.g., PlanetScale). - * - * Standard MySQL does not support ORDER BY or LIMIT with joined deletes and will throw a syntax error. - * - * @param \Illuminate\Database\Query\Builder $query - * @param string $table - * @param string $where - * @return string - */ - protected function compileDeleteWithJoins(Builder $query, $table, $where) - { - $sql = parent::compileDeleteWithJoins($query, $table, $where); + $sql = trim("delete $table from {$table}{$joins} $where"); + } else { + $sql = trim("delete from $table $where"); + } - if (! empty($query->orders)) { + if (isset($query->orders)) { $sql .= ' '.$this->compileOrders($query, $query->orders); } @@ -558,16 +125,6 @@ protected function compileDeleteWithJoins(Builder $query, $table, $where) return $sql; } - /** - * Compile a query to get the number of open connections for a database. - * - * @return string - */ - public function compileThreadCount() - { - return 'select variable_value as `Value` from performance_schema.session_status where variable_name = \'threads_connected\''; - } - /** * Wrap a single string in keyword identifiers. * @@ -576,7 +133,15 @@ public function compileThreadCount() */ protected function wrapValue($value) { - return $value === '*' ? $value : '`'.str_replace('`', '``', $value).'`'; + if ($value === '*') { + return $value; + } + + if (Str::contains($value, '->')) { + return $this->wrapJsonSelector($value); + } + + return '`'.str_replace('`', '``', $value).'`'; } /** @@ -587,21 +152,10 @@ protected function wrapValue($value) */ protected function wrapJsonSelector($value) { - [$field, $path] = $this->wrapJsonFieldAndPath($value); + $path = explode('->', $value); - return 'json_unquote(json_extract('.$field.$path.'))'; - } - - /** - * Wrap the given JSON selector for boolean values. - * - * @param string $value - * @return string - */ - protected function wrapJsonBooleanSelector($value) - { - [$field, $path] = $this->wrapJsonFieldAndPath($value); + $field = $this->wrapValue(array_shift($path)); - return 'json_extract('.$field.$path.')'; + return $field.'->'.'"$.'.implode('.', $path).'"'; } } diff --git a/Query/Grammars/PostgresGrammar.php b/Query/Grammars/PostgresGrammar.php index a21443523c..d1b69d4cd8 100755 --- a/Query/Grammars/PostgresGrammar.php +++ b/Query/Grammars/PostgresGrammar.php @@ -2,139 +2,87 @@ namespace Illuminate\Database\Query\Grammars; -use Illuminate\Database\Query\Builder; -use Illuminate\Database\Query\JoinLateralClause; -use Illuminate\Support\Arr; -use Illuminate\Support\Collection; use Illuminate\Support\Str; +use Illuminate\Database\Query\Builder; class PostgresGrammar extends Grammar { /** * All of the available clause operators. * - * @var string[] + * @var array */ protected $operators = [ '=', '<', '>', '<=', '>=', '<>', '!=', - 'like', 'not like', 'between', 'ilike', 'not ilike', - '~', '&', '|', '#', '<<', '>>', '<<=', '>>=', - '&&', '@>', '<@', '?', '?|', '?&', '||', '-', '@?', '@@', '#-', - 'is distinct from', 'is not distinct from', - ]; - - /** - * The Postgres grammar specific custom operators. - * - * @var array - */ - protected static $customOperators = []; - - /** - * The grammar specific bitwise operators. - * - * @var array - */ - protected $bitwiseOperators = [ - '~', '&', '|', '#', '<<', '>>', '<<=', '>>=', + 'like', 'not like', 'between', 'ilike', + '&', '|', '#', '<<', '>>', + '@>', '<@', '?', '?|', '?&', '||', '-', '-', '#-', ]; /** - * Indicates if the cascade option should be used when truncating. - * - * @var bool - */ - protected static $cascadeTruncate = true; - - /** - * Compile a basic where clause. + * Compile the lock into SQL. * * @param \Illuminate\Database\Query\Builder $query - * @param array $where + * @param bool|string $value * @return string */ - protected function whereBasic(Builder $query, $where) + protected function compileLock(Builder $query, $value) { - if (str_contains(strtolower($where['operator']), 'like')) { - return sprintf( - '%s::text %s %s', - $this->wrap($where['column']), - $where['operator'], - $this->parameter($where['value']) - ); + if (is_string($value)) { + return $value; } - return parent::whereBasic($query, $where); + return $value ? 'for update' : 'for share'; } /** - * Compile a bitwise operator where clause. + * Compile a "where date" clause. * * @param \Illuminate\Database\Query\Builder $query * @param array $where * @return string */ - protected function whereBitwise(Builder $query, $where) + protected function whereDate(Builder $query, $where) { $value = $this->parameter($where['value']); - $operator = str_replace('?', '??', $where['operator']); - - return '('.$this->wrap($where['column']).' '.$operator.' '.$value.')::bool'; + return $this->wrap($where['column']).'::date '.$where['operator'].' '.$value; } /** - * Compile a "where like" clause. + * Compile a "where day" clause. * * @param \Illuminate\Database\Query\Builder $query * @param array $where * @return string */ - protected function whereLike(Builder $query, $where) + protected function whereDay(Builder $query, $where) { - $where['operator'] = $where['not'] ? 'not ' : ''; - - $where['operator'] .= $where['caseSensitive'] ? 'like' : 'ilike'; - - return $this->whereBasic($query, $where); + return $this->dateBasedWhere('day', $query, $where); } /** - * Compile a "where date" clause. + * Compile a "where month" clause. * * @param \Illuminate\Database\Query\Builder $query * @param array $where * @return string */ - protected function whereDate(Builder $query, $where) + protected function whereMonth(Builder $query, $where) { - $column = $this->wrap($where['column']); - $value = $this->parameter($where['value']); - - if ($this->isJsonSelector($where['column'])) { - $column = '('.$column.')'; - } - - return $column.'::date '.$where['operator'].' '.$value; + return $this->dateBasedWhere('month', $query, $where); } /** - * Compile a "where time" clause. + * Compile a "where year" clause. * * @param \Illuminate\Database\Query\Builder $query * @param array $where * @return string */ - protected function whereTime(Builder $query, $where) + protected function whereYear(Builder $query, $where) { - $column = $this->wrap($where['column']); - $value = $this->parameter($where['value']); - - if ($this->isJsonSelector($where['column'])) { - $column = '('.$column.')'; - } - - return $column.'::time '.$where['operator'].' '.$value; + return $this->dateBasedWhere('year', $query, $where); } /** @@ -153,392 +101,72 @@ protected function dateBasedWhere($type, Builder $query, $where) } /** - * Compile a "where fulltext" clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - public function whereFullText(Builder $query, $where) - { - $language = $where['options']['language'] ?? 'english'; - - if (! in_array($language, $this->validFullTextLanguages())) { - $language = 'english'; - } - - $isVector = $where['options']['vector'] ?? false; - - $columns = (new Collection($where['columns'])) - ->map(fn ($column) => $isVector - ? $this->wrap($column) - : "to_tsvector('{$language}', {$this->wrap($column)})") - ->implode(' || '); - - $mode = 'plainto_tsquery'; - - if (($where['options']['mode'] ?? []) === 'phrase') { - $mode = 'phraseto_tsquery'; - } - - if (($where['options']['mode'] ?? []) === 'websearch') { - $mode = 'websearch_to_tsquery'; - } - - if (($where['options']['mode'] ?? []) === 'raw') { - $mode = 'to_tsquery'; - } - - return "({$columns}) @@ {$mode}('{$language}', {$this->parameter($where['value'])})"; - } - - /** - * Get an array of valid full text languages. - * - * @return array - */ - protected function validFullTextLanguages() - { - return [ - 'simple', - 'arabic', - 'danish', - 'dutch', - 'english', - 'finnish', - 'french', - 'german', - 'hungarian', - 'indonesian', - 'irish', - 'italian', - 'lithuanian', - 'nepali', - 'norwegian', - 'portuguese', - 'romanian', - 'russian', - 'spanish', - 'swedish', - 'tamil', - 'turkish', - ]; - } - - /** - * Compile the "select *" portion of the query. + * Compile an update statement into SQL. * * @param \Illuminate\Database\Query\Builder $query - * @param array $columns - * @return string|null - */ - protected function compileColumns(Builder $query, $columns) - { - // If the query is actually performing an aggregating select, we will let that - // compiler handle the building of the select clauses, as it will need some - // more syntax that is best handled by that function to keep things neat. - if (! is_null($query->aggregate)) { - return; - } - - if (is_array($query->distinct)) { - $select = 'select distinct on ('.$this->columnize($query->distinct).') '; - } elseif ($query->distinct) { - $select = 'select distinct '; - } else { - $select = 'select '; - } - - return $select.$this->columnize($columns); - } - - /** - * Compile a "JSON contains" statement into SQL. - * - * @param string $column - * @param string $value - * @return string - */ - protected function compileJsonContains($column, $value) - { - $column = str_replace('->>', '->', $this->wrap($column)); - - return '('.$column.')::jsonb @> '.$value; - } - - /** - * Compile a "JSON contains key" statement into SQL. - * - * @param string $column + * @param array $values * @return string */ - protected function compileJsonContainsKey($column) + public function compileUpdate(Builder $query, $values) { - $segments = explode('->', $column); - - $lastSegment = array_pop($segments); - - if (filter_var($lastSegment, FILTER_VALIDATE_INT) !== false) { - $i = $lastSegment; - } elseif (preg_match('/\[(-?[0-9]+)\]$/', $lastSegment, $matches)) { - $segments[] = Str::beforeLast($lastSegment, $matches[0]); - - $i = $matches[1]; - } + $table = $this->wrapTable($query->from); - $column = str_replace('->>', '->', $this->wrap(implode('->', $segments))); + // Each one of the columns in the update statements needs to be wrapped in the + // keyword identifiers, also a place-holder needs to be created for each of + // the values in the list of bindings so we can make the sets statements. + $columns = $this->compileUpdateColumns($values); - if (isset($i)) { - return vsprintf('case when %s then %s else false end', [ - 'jsonb_typeof(('.$column.")::jsonb) = 'array'", - 'jsonb_array_length(('.$column.')::jsonb) >= '.($i < 0 ? abs($i) : $i + 1), - ]); - } + $from = $this->compileUpdateFrom($query); - $key = "'".str_replace("'", "''", $lastSegment)."'"; + $where = $this->compileUpdateWheres($query); - return 'coalesce(('.$column.')::jsonb ?? '.$key.', false)'; + return trim("update {$table} set {$columns}{$from} $where"); } /** - * Compile a "JSON length" statement into SQL. + * Compile the columns for the update statement. * - * @param string $column - * @param string $operator - * @param string $value + * @param array $values * @return string */ - protected function compileJsonLength($column, $operator, $value) + protected function compileUpdateColumns($values) { - $column = str_replace('->>', '->', $this->wrap($column)); + $columns = []; - return 'jsonb_array_length(('.$column.')::jsonb) '.$operator.' '.$value; - } - - /** - * Compile a single having clause. - * - * @param array $having - * @return string - */ - protected function compileHaving(array $having) - { - if ($having['type'] === 'Bitwise') { - return $this->compileHavingBitwise($having); + // When gathering the columns for an update statement, we'll wrap each of the + // columns and convert it to a parameter value. Then we will concatenate a + // list of the columns that can be added into this update query clauses. + foreach ($values as $key => $value) { + $columns[] = $this->wrap($key).' = '.$this->parameter($value); } - return parent::compileHaving($having); + return implode(', ', $columns); } /** - * Compile a having clause involving a bitwise operator. - * - * @param array $having - * @return string - */ - protected function compileHavingBitwise($having) - { - $column = $this->wrap($having['column']); - - $parameter = $this->parameter($having['value']); - - return '('.$column.' '.$having['operator'].' '.$parameter.')::bool'; - } - - /** - * Compile the lock into SQL. + * Compile the "from" clause for an update with a join. * * @param \Illuminate\Database\Query\Builder $query - * @param bool|string $value - * @return string + * @return string|null */ - protected function compileLock(Builder $query, $value) + protected function compileUpdateFrom(Builder $query) { - if (! is_string($value)) { - return $value ? 'for update' : 'for share'; + if (! isset($query->joins)) { + return ''; } - return $value; - } - - /** - * Compile an insert ignore statement into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @return string - */ - public function compileInsertOrIgnore(Builder $query, array $values) - { - return $this->compileInsert($query, $values).' on conflict do nothing'; - } - - /** - * Compile an insert or ignore statement with a returning clause into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @param array $uniqueBy - * @param array $returning - * @return string - */ - public function compileInsertOrIgnoreReturning(Builder $query, array $values, array $uniqueBy, array $returning) - { - return $this->compileInsert($query, $values) - .' on conflict ('.$this->columnize($uniqueBy).') do nothing' - .' returning '.$this->columnize($returning); - } - - /** - * Compile an insert ignore statement using a subquery into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $columns - * @param string $sql - * @return string - */ - public function compileInsertOrIgnoreUsing(Builder $query, array $columns, string $sql) - { - return $this->compileInsertUsing($query, $columns, $sql).' on conflict do nothing'; - } + $froms = []; - /** - * Compile an insert and get ID statement into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @param string|null $sequence - * @return string - */ - public function compileInsertGetId(Builder $query, $values, $sequence) - { - return $this->compileInsert($query, $values).' returning '.$this->wrap($sequence ?: 'id'); - } - - /** - * Compile an update statement into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @return string - */ - public function compileUpdate(Builder $query, array $values) - { - if (isset($query->joins) || isset($query->limit)) { - return $this->compileUpdateWithJoinsOrLimit($query, $values); + // When using Postgres, updates with joins list the joined tables in the from + // clause, which is different than other systems like MySQL. Here, we will + // compile out the tables that are joined and add them to a from clause. + foreach ($query->joins as $join) { + $froms[] = $this->wrapTable($join->table); } - return parent::compileUpdate($query, $values); - } - - /** - * Compile the columns for an update statement. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @return string - */ - protected function compileUpdateColumns(Builder $query, array $values) - { - return (new Collection($values))->map(function ($value, $key) { - $column = last(explode('.', $key)); - - if ($this->isJsonSelector($key)) { - return $this->compileJsonUpdateColumn($column, $value); - } - - return $this->wrap($column).' = '.$this->parameter($value); - })->implode(', '); - } - - /** - * Compile an "upsert" statement into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @param array $uniqueBy - * @param array $update - * @return string - */ - public function compileUpsert(Builder $query, array $values, array $uniqueBy, array $update) - { - $sql = $this->compileInsert($query, $values); - - $sql .= ' on conflict ('.$this->columnize($uniqueBy).') do update set '; - - $columns = (new Collection($update))->map(function ($value, $key) { - return is_numeric($key) - ? $this->wrap($value).' = '.$this->wrapValue('excluded').'.'.$this->wrap($value) - : $this->wrap($key).' = '.$this->parameter($value); - })->implode(', '); - - return $sql.$columns; - } - - /** - * Compile a "lateral join" clause. - * - * @param \Illuminate\Database\Query\JoinLateralClause $join - * @param string $expression - * @return string - */ - public function compileJoinLateral(JoinLateralClause $join, string $expression): string - { - return trim("{$join->type} join lateral {$expression} on true"); - } - - /** - * Prepares a JSON column being updated using the JSONB_SET function. - * - * @param string $key - * @param mixed $value - * @return string - */ - protected function compileJsonUpdateColumn($key, $value) - { - $segments = explode('->', $key); - - $field = $this->wrap(array_shift($segments)); - - $path = "'{".implode(',', $this->wrapJsonPathAttributes($segments, '"'))."}'"; - - return "{$field} = jsonb_set({$field}::jsonb, {$path}, {$this->parameter($value)})"; - } - - /** - * Compile an update from statement into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @return string - */ - public function compileUpdateFrom(Builder $query, $values) - { - $table = $this->wrapTable($query->from); - - // Each one of the columns in the update statements needs to be wrapped in the - // keyword identifiers, also a place-holder needs to be created for each of - // the values in the list of bindings so we can make the sets statements. - $columns = $this->compileUpdateColumns($query, $values); - - $from = ''; - - if (isset($query->joins)) { - // When using Postgres, updates with joins list the joined tables in the from - // clause, which is different than other systems like MySQL. Here, we will - // compile out the tables that are joined and add them to a from clause. - $froms = (new Collection($query->joins)) - ->map(fn ($join) => $this->wrapTable($join->table)) - ->all(); - - if (count($froms) > 0) { - $from = ' from '.implode(', ', $froms); - } + if (count($froms) > 0) { + return ' from '.implode(', ', $froms); } - - $where = $this->compileUpdateWheres($query); - - return trim("update {$table} set {$columns}{$from} {$where}"); } /** @@ -549,26 +177,26 @@ public function compileUpdateFrom(Builder $query, $values) */ protected function compileUpdateWheres(Builder $query) { - $baseWheres = $this->compileWheres($query); + $baseWhere = $this->compileWheres($query); if (! isset($query->joins)) { - return $baseWheres; + return $baseWhere; } // Once we compile the join constraints, we will either use them as the where // clause or append them to the existing base where clauses. If we need to // strip the leading boolean we will do so when using as the only where. - $joinWheres = $this->compileUpdateJoinWheres($query); + $joinWhere = $this->compileUpdateJoinWheres($query); - if (trim($baseWheres) == '') { - return 'where '.$this->removeLeadingBoolean($joinWheres); + if (trim($baseWhere) == '') { + return 'where '.$this->removeLeadingBoolean($joinWhere); } - return $baseWheres.' '.$joinWheres; + return $baseWhere.' '.$joinWhere; } /** - * Compile the "join" clause where clauses for an update. + * Compile the "join" clauses for an update. * * @param \Illuminate\Database\Query\Builder $query * @return string @@ -581,10 +209,8 @@ protected function compileUpdateJoinWheres(Builder $query) // all out then implode them. This should give us "where" like syntax after // everything has been built and then we will join it to the real wheres. foreach ($query->joins as $join) { - foreach ($join->wheres as $where) { - $method = "where{$where['type']}"; - - $joinWheres[] = $where['boolean'].' '.$this->$method($query, $where); + foreach ($join->clauses as $clause) { + $joinWheres[] = $this->compileJoinConstraint($clause); } } @@ -592,104 +218,20 @@ protected function compileUpdateJoinWheres(Builder $query) } /** - * Prepare the bindings for an update statement. - * - * @param array $bindings - * @param array $values - * @return array - */ - public function prepareBindingsForUpdateFrom(array $bindings, array $values) - { - $values = (new Collection($values)) - ->map(function ($value, $column) { - return is_array($value) || ($this->isJsonSelector($column) && ! $this->isExpression($value)) - ? json_encode($value) - : $value; - }) - ->all(); - - $bindingsWithoutWhere = Arr::except($bindings, ['select', 'where']); - - return array_values( - array_merge($values, $bindings['where'], Arr::flatten($bindingsWithoutWhere)) - ); - } - - /** - * Compile an update statement with joins or limit into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @return string - */ - protected function compileUpdateWithJoinsOrLimit(Builder $query, array $values) - { - $table = $this->wrapTable($query->from); - - $columns = $this->compileUpdateColumns($query, $values); - - $alias = last(preg_split('/\s+as\s+/i', $query->from)); - - $selectSql = $this->compileSelect($query->select($alias.'.ctid')); - - return "update {$table} set {$columns} where {$this->wrap('ctid')} in ({$selectSql})"; - } - - /** - * Prepare the bindings for an update statement. - * - * @param array $bindings - * @param array $values - * @return array - */ - #[\Override] - public function prepareBindingsForUpdate(array $bindings, array $values) - { - $values = (new Collection($values))->map(function ($value, $column) { - return is_array($value) || ($this->isJsonSelector($column) && ! $this->isExpression($value)) - ? json_encode($value) - : $value; - })->all(); - - $cleanBindings = Arr::except($bindings, 'select'); - - $values = Arr::flatten(array_map(fn ($value) => value($value), $values)); - - return array_values( - array_merge($values, Arr::flatten($cleanBindings)) - ); - } - - /** - * Compile a delete statement into SQL. + * Compile an insert and get ID statement into SQL. * * @param \Illuminate\Database\Query\Builder $query + * @param array $values + * @param string $sequence * @return string */ - public function compileDelete(Builder $query) + public function compileInsertGetId(Builder $query, $values, $sequence) { - if (isset($query->joins) || isset($query->limit)) { - return $this->compileDeleteWithJoinsOrLimit($query); + if (is_null($sequence)) { + $sequence = 'id'; } - return parent::compileDelete($query); - } - - /** - * Compile a delete statement with joins or limit into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @return string - */ - protected function compileDeleteWithJoinsOrLimit(Builder $query) - { - $table = $this->wrapTable($query->from); - - $alias = last(preg_split('/\s+as\s+/i', $query->from)); - - $selectSql = $this->compileSelect($query->select($alias.'.ctid')); - - return "delete from {$table} where {$this->wrap('ctid')} in ({$selectSql})"; + return $this->compileInsert($query, $values).' returning '.$this->wrap($sequence); } /** @@ -700,17 +242,26 @@ protected function compileDeleteWithJoinsOrLimit(Builder $query) */ public function compileTruncate(Builder $query) { - return ['truncate '.$this->wrapTable($query->from).' restart identity'.(static::$cascadeTruncate ? ' cascade' : '') => []]; + return ['truncate '.$this->wrapTable($query->from).' restart identity' => []]; } /** - * Compile a query to get the number of open connections for a database. + * Wrap a single string in keyword identifiers. * + * @param string $value * @return string */ - public function compileThreadCount() + protected function wrapValue($value) { - return 'select count(*) as "Value" from pg_stat_activity'; + if ($value === '*') { + return $value; + } + + if (Str::contains($value, '->')) { + return $this->wrapJsonSelector($value); + } + + return '"'.str_replace('"', '""', $value).'"'; } /** @@ -723,7 +274,7 @@ protected function wrapJsonSelector($value) { $path = explode('->', $value); - $field = $this->wrapSegments(explode('.', array_shift($path))); + $field = $this->wrapValue(array_shift($path)); $wrappedPath = $this->wrapJsonPathAttributes($path); @@ -737,137 +288,15 @@ protected function wrapJsonSelector($value) } /** - * Wrap the given JSON selector for boolean values. - * - * @param string $value - * @return string - */ - protected function wrapJsonBooleanSelector($value) - { - $selector = str_replace( - '->>', '->', - $this->wrapJsonSelector($value) - ); - - return '('.$selector.')::jsonb'; - } - - /** - * Wrap the given JSON boolean value. - * - * @param string $value - * @return string - */ - protected function wrapJsonBooleanValue($value) - { - return "'".$value."'::jsonb"; - } - - /** - * Wrap the attributes of the given JSON path. + * Wrap the attributes of the give JSON path. * * @param array $path * @return array */ protected function wrapJsonPathAttributes($path) { - $quote = func_num_args() === 2 ? func_get_arg(1) : "'"; - - return (new Collection($path)) - ->map(fn ($attribute) => $this->parseJsonPathArrayKeys($attribute)) - ->collapse() - ->map(function ($attribute) use ($quote) { - return filter_var($attribute, FILTER_VALIDATE_INT) !== false - ? $attribute - : $quote.$attribute.$quote; - }) - ->all(); - } - - /** - * Parse the given JSON path attribute for array keys. - * - * @param string $attribute - * @return array - */ - protected function parseJsonPathArrayKeys($attribute) - { - if (preg_match('/(\[[^\]]+\])+$/', $attribute, $parts)) { - $key = Str::beforeLast($attribute, $parts[0]); - - preg_match_all('/\[([^\]]+)\]/', $parts[0], $keys); - - return (new Collection([$key])) - ->merge($keys[1]) - ->diff('') - ->values() - ->all(); - } - - return [$attribute]; - } - - /** - * Substitute the given bindings into the given raw SQL query. - * - * @param string $sql - * @param array $bindings - * @return string - */ - public function substituteBindingsIntoRawSql($sql, $bindings) - { - $query = parent::substituteBindingsIntoRawSql($sql, $bindings); - - foreach ($this->operators as $operator) { - if (! str_contains($operator, '?')) { - continue; - } - - $query = str_replace(str_replace('?', '??', $operator), $operator, $query); - } - - return $query; - } - - /** - * Get the Postgres grammar specific operators. - * - * @return array - */ - public function getOperators() - { - return array_values(array_unique(array_merge(parent::getOperators(), static::$customOperators))); - } - - /** - * Set any Postgres grammar specific custom operators. - * - * @param array $operators - * @return void - */ - public static function customOperators(array $operators) - { - static::$customOperators = array_values( - array_merge(static::$customOperators, array_filter(array_filter($operators, 'is_string'))) - ); - } - - /** - * Enable or disable the "cascade" option when compiling the truncate statement. - * - * @param bool $value - * @return void - */ - public static function cascadeOnTruncate(bool $value = true) - { - static::$cascadeTruncate = $value; - } - - /** - * @deprecated use cascadeOnTruncate - */ - public static function cascadeOnTrucate(bool $value = true) - { - self::cascadeOnTruncate($value); + return array_map(function ($attribute) { + return "'$attribute'"; + }, $path); } } diff --git a/Query/Grammars/SQLiteGrammar.php b/Query/Grammars/SQLiteGrammar.php index 28401b9e97..dd1f9c00a9 100755 --- a/Query/Grammars/SQLiteGrammar.php +++ b/Query/Grammars/SQLiteGrammar.php @@ -3,97 +3,74 @@ namespace Illuminate\Database\Query\Grammars; use Illuminate\Database\Query\Builder; -use Illuminate\Support\Arr; -use Illuminate\Support\Collection; -use Illuminate\Support\Str; -use InvalidArgumentException; class SQLiteGrammar extends Grammar { /** * All of the available clause operators. * - * @var string[] + * @var array */ protected $operators = [ '=', '<', '>', '<=', '>=', '<>', '!=', - 'like', 'not like', 'ilike', + 'like', 'not like', 'between', 'ilike', '&', '|', '<<', '>>', ]; /** - * Compile the lock into SQL. + * Compile an insert statement into SQL. * * @param \Illuminate\Database\Query\Builder $query - * @param bool|string $value + * @param array $values * @return string */ - protected function compileLock(Builder $query, $value) + public function compileInsert(Builder $query, array $values) { - return ''; - } + // Essentially we will force every insert to be treated as a batch insert which + // simply makes creating the SQL easier for us since we can utilize the same + // basic routine regardless of an amount of records given to us to insert. + $table = $this->wrapTable($query->from); - /** - * Wrap a union subquery in parentheses. - * - * @param string $sql - * @return string - */ - protected function wrapUnion($sql) - { - return 'select * from ('.$sql.')'; - } + if (! is_array(reset($values))) { + $values = [$values]; + } - /** - * Compile a basic where clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereBasic(Builder $query, $where) - { - if ($where['operator'] === '<=>') { - $column = $this->wrap($where['column']); - $value = $this->parameter($where['value']); + // If there is only one record being inserted, we will just use the usual query + // grammar insert builder because no special syntax is needed for the single + // row inserts in SQLite. However, if there are multiples, we'll continue. + if (count($values) == 1) { + return parent::compileInsert($query, reset($values)); + } + + $names = $this->columnize(array_keys(reset($values))); + + $columns = []; - return "{$column} IS {$value}"; + // SQLite requires us to build the multi-row insert as a listing of select with + // unions joining them together. So we'll build out this list of columns and + // then join them all together with select unions to complete the queries. + foreach (array_keys(reset($values)) as $column) { + $columns[] = '? as '.$this->wrap($column); } - return parent::whereBasic($query, $where); + $columns = array_fill(0, count($values), implode(', ', $columns)); + + return "insert into $table ($names) select ".implode(' union all select ', $columns); } /** - * Compile a "where like" clause. + * Compile a truncate table statement into SQL. * * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string + * @return array */ - protected function whereLike(Builder $query, $where) + public function compileTruncate(Builder $query) { - if ($where['caseSensitive'] == false) { - return parent::whereLike($query, $where); - } - $where['operator'] = $where['not'] ? 'not glob' : 'glob'; + $sql = ['delete from sqlite_sequence where name = ?' => [$query->from]]; - return $this->whereBasic($query, $where); - } + $sql['delete from '.$this->wrapTable($query->from)] = []; - /** - * Convert a LIKE pattern to a GLOB pattern using simple string replacement. - * - * @param string $value - * @param bool $caseSensitive - * @return string - */ - public function prepareWhereLikeBinding($value, $caseSensitive) - { - return $caseSensitive === false ? $value : str_replace( - ['*', '?', '%', '_'], - ['[*]', '[?]', '*', '?'], - $value - ); + return $sql; } /** @@ -144,18 +121,6 @@ protected function whereYear(Builder $query, $where) return $this->dateBasedWhere('%Y', $query, $where); } - /** - * Compile a "where time" clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereTime(Builder $query, $where) - { - return $this->dateBasedWhere('%H:%M:%S', $query, $where); - } - /** * Compile a date based where clause. * @@ -166,350 +131,10 @@ protected function whereTime(Builder $query, $where) */ protected function dateBasedWhere($type, Builder $query, $where) { - $value = $this->parameter($where['value']); - - return "strftime('{$type}', {$this->wrap($where['column'])}) {$where['operator']} cast({$value} as text)"; - } - - /** - * Compile the index hints for the query. - * - * @param \Illuminate\Database\Query\Builder $query - * @param \Illuminate\Database\Query\IndexHint $indexHint - * @return string - * - * @throws \InvalidArgumentException - */ - protected function compileIndexHint(Builder $query, $indexHint) - { - if ($indexHint->type !== 'force') { - return ''; - } - - $index = $indexHint->index; - - if (! preg_match('/^[a-zA-Z0-9_$]+$/', $index)) { - throw new InvalidArgumentException('Index name contains invalid characters.'); - } - - return "indexed by {$index}"; - } - - /** - * Compile a "JSON length" statement into SQL. - * - * @param string $column - * @param string $operator - * @param string $value - * @return string - */ - protected function compileJsonLength($column, $operator, $value) - { - [$field, $path] = $this->wrapJsonFieldAndPath($column); - - return 'json_array_length('.$field.$path.') '.$operator.' '.$value; - } - - /** - * Compile a "JSON contains" statement into SQL. - * - * @param string $column - * @param mixed $value - * @return string - */ - protected function compileJsonContains($column, $value) - { - [$field, $path] = $this->wrapJsonFieldAndPath($column); - - return 'exists (select 1 from json_each('.$field.$path.') where '.$this->wrap('json_each.value').' is '.$value.')'; - } - - /** - * Prepare the binding for a "JSON contains" statement. - * - * @param mixed $binding - * @return mixed - */ - public function prepareBindingForJsonContains($binding) - { - return $binding; - } - - /** - * Compile a "JSON contains key" statement into SQL. - * - * @param string $column - * @return string - */ - protected function compileJsonContainsKey($column) - { - [$field, $path] = $this->wrapJsonFieldAndPath($column); - - return 'json_type('.$field.$path.') is not null'; - } - - /** - * Compile a group limit clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @return string - */ - protected function compileGroupLimit(Builder $query) - { - $version = $query->getConnection()->getServerVersion(); - - if (version_compare($version, '3.25.0', '>=')) { - return parent::compileGroupLimit($query); - } - - $query->groupLimit = null; - - return $this->compileSelect($query); - } - - /** - * Compile an update statement into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @return string - */ - public function compileUpdate(Builder $query, array $values) - { - if (isset($query->joins) || isset($query->limit)) { - return $this->compileUpdateWithJoinsOrLimit($query, $values); - } - - return parent::compileUpdate($query, $values); - } - - /** - * Compile an insert ignore statement into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @return string - */ - public function compileInsertOrIgnore(Builder $query, array $values) - { - return Str::replaceFirst('insert', 'insert or ignore', $this->compileInsert($query, $values)); - } - - /** - * Compile an insert or ignore statement with a returning clause into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @param array $uniqueBy - * @param array $returning - * @return string - */ - public function compileInsertOrIgnoreReturning(Builder $query, array $values, array $uniqueBy, array $returning) - { - return $this->compileInsert($query, $values) - .' on conflict ('.$this->columnize($uniqueBy).') do nothing' - .' returning '.$this->columnize($returning); - } - - /** - * Compile an insert ignore statement using a subquery into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $columns - * @param string $sql - * @return string - */ - public function compileInsertOrIgnoreUsing(Builder $query, array $columns, string $sql) - { - return Str::replaceFirst('insert', 'insert or ignore', $this->compileInsertUsing($query, $columns, $sql)); - } - - /** - * Compile the columns for an update statement. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @return string - */ - protected function compileUpdateColumns(Builder $query, array $values) - { - $jsonGroups = $this->groupJsonColumnsForUpdate($values); - - return (new Collection($values)) - ->reject(fn ($value, $key) => $this->isJsonSelector($key)) - ->merge($jsonGroups) - ->map(function ($value, $key) use ($jsonGroups) { - $column = last(explode('.', $key)); - - $value = isset($jsonGroups[$key]) ? $this->compileJsonPatch($column, $value) : $this->parameter($value); - - return $this->wrap($column).' = '.$value; - }) - ->implode(', '); - } - - /** - * Compile an "upsert" statement into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @param array $uniqueBy - * @param array $update - * @return string - */ - public function compileUpsert(Builder $query, array $values, array $uniqueBy, array $update) - { - $sql = $this->compileInsert($query, $values); - - $sql .= ' on conflict ('.$this->columnize($uniqueBy).') do update set '; - - $columns = (new Collection($update))->map(function ($value, $key) { - return is_numeric($key) - ? $this->wrap($value).' = '.$this->wrapValue('excluded').'.'.$this->wrap($value) - : $this->wrap($key).' = '.$this->parameter($value); - })->implode(', '); - - return $sql.$columns; - } - - /** - * Group the nested JSON columns. - * - * @param array $values - * @return array - */ - protected function groupJsonColumnsForUpdate(array $values) - { - $groups = []; - - foreach ($values as $key => $value) { - if ($this->isJsonSelector($key)) { - Arr::set($groups, str_replace('->', '.', Str::after($key, '.')), $value); - } - } - - return $groups; - } - - /** - * Compile a "JSON" patch statement into SQL. - * - * @param string $column - * @param mixed $value - * @return string - */ - protected function compileJsonPatch($column, $value) - { - return "json_patch(ifnull({$this->wrap($column)}, json('{}')), json({$this->parameter($value)}))"; - } - - /** - * Compile an update statement with joins or limit into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @return string - */ - protected function compileUpdateWithJoinsOrLimit(Builder $query, array $values) - { - $table = $this->wrapTable($query->from); - - $columns = $this->compileUpdateColumns($query, $values); - - $alias = last(preg_split('/\s+as\s+/i', $query->from)); - - $selectSql = $this->compileSelect($query->select($alias.'.rowid')); + $value = str_pad($where['value'], 2, '0', STR_PAD_LEFT); - return "update {$table} set {$columns} where {$this->wrap('rowid')} in ({$selectSql})"; - } - - /** - * Prepare the bindings for an update statement. - * - * @param array $bindings - * @param array $values - * @return array - */ - #[\Override] - public function prepareBindingsForUpdate(array $bindings, array $values) - { - $groups = $this->groupJsonColumnsForUpdate($values); - - $values = (new Collection($values)) - ->reject(fn ($value, $key) => $this->isJsonSelector($key)) - ->merge($groups) - ->map(fn ($value) => is_array($value) ? json_encode($value) : $value) - ->all(); - - $cleanBindings = Arr::except($bindings, 'select'); - - $values = Arr::flatten(array_map(fn ($value) => value($value), $values)); - - return array_values( - array_merge($values, Arr::flatten($cleanBindings)) - ); - } - - /** - * Compile a delete statement into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @return string - */ - public function compileDelete(Builder $query) - { - if (isset($query->joins) || isset($query->limit)) { - return $this->compileDeleteWithJoinsOrLimit($query); - } - - return parent::compileDelete($query); - } - - /** - * Compile a delete statement with joins or limit into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @return string - */ - protected function compileDeleteWithJoinsOrLimit(Builder $query) - { - $table = $this->wrapTable($query->from); - - $alias = last(preg_split('/\s+as\s+/i', $query->from)); - - $selectSql = $this->compileSelect($query->select($alias.'.rowid')); - - return "delete from {$table} where {$this->wrap('rowid')} in ({$selectSql})"; - } - - /** - * Compile a truncate table statement into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @return array - */ - public function compileTruncate(Builder $query) - { - [$schema, $table] = $query->getConnection()->getSchemaBuilder()->parseSchemaAndTable($query->from); - - $schema = $schema ? $this->wrapValue($schema).'.' : ''; - - return [ - 'delete from '.$schema.'sqlite_sequence where name = ?' => [$query->getConnection()->getTablePrefix().$table], - 'delete from '.$this->wrapTable($query->from) => [], - ]; - } - - /** - * Wrap the given JSON selector. - * - * @param string $value - * @return string - */ - protected function wrapJsonSelector($value) - { - [$field, $path] = $this->wrapJsonFieldAndPath($value); + $value = $this->parameter($value); - return 'json_extract('.$field.$path.')'; + return 'strftime(\''.$type.'\', '.$this->wrap($where['column']).') '.$where['operator'].' '.$value; } } diff --git a/Query/Grammars/SqlServerGrammar.php b/Query/Grammars/SqlServerGrammar.php index f22f528e35..f9163c43ca 100755 --- a/Query/Grammars/SqlServerGrammar.php +++ b/Query/Grammars/SqlServerGrammar.php @@ -3,45 +3,20 @@ namespace Illuminate\Database\Query\Grammars; use Illuminate\Database\Query\Builder; -use Illuminate\Database\Query\JoinLateralClause; -use Illuminate\Support\Arr; -use Illuminate\Support\Collection; -use Illuminate\Support\Str; -use InvalidArgumentException; class SqlServerGrammar extends Grammar { /** * All of the available clause operators. * - * @var string[] + * @var array */ protected $operators = [ '=', '<', '>', '<=', '>=', '!<', '!>', '<>', '!=', - 'like', 'not like', 'ilike', + 'like', 'not like', 'between', 'ilike', '&', '&=', '|', '|=', '^', '^=', ]; - /** - * The components that make up a select clause. - * - * @var string[] - */ - protected $selectComponents = [ - 'aggregate', - 'columns', - 'from', - 'indexHint', - 'joins', - 'wheres', - 'groups', - 'havings', - 'orders', - 'offset', - 'limit', - 'lock', - ]; - /** * Compile a select query into SQL. * @@ -50,12 +25,26 @@ class SqlServerGrammar extends Grammar */ public function compileSelect(Builder $query) { - // An order by clause is required for SQL Server offset to function... - if ($query->offset && empty($query->orders)) { - $query->orders[] = ['sql' => '(SELECT 0)']; + $original = $query->columns; + + if (is_null($query->columns)) { + $query->columns = ['*']; + } + + $components = $this->compileComponents($query); + + // If an offset is present on the query, we will need to wrap the query in + // a big "ANSI" offset syntax block. This is very nasty compared to the + // other database systems but is necessary for implementing features. + if ($query->offset > 0) { + return $this->compileAnsiOffset($query, $components); } - return parent::compileSelect($query); + $sql = $this->concatenate($components); + + $query->columns = $original; + + return $sql; } /** @@ -76,8 +65,8 @@ protected function compileColumns(Builder $query, $columns) // If there is a limit on the query, but not an offset, we will add the top // clause to the query, which serves as a "limit" type clause within the // SQL Server system similar to the limit keywords available in MySQL. - if (is_numeric($query->limit) && $query->limit > 0 && $query->offset <= 0) { - $select .= 'top '.((int) $query->limit).' '; + if ($query->limit > 0 && $query->offset <= 0) { + $select .= 'top '.$query->limit.' '; } return $select.$this->columnize($columns); @@ -106,205 +95,83 @@ protected function compileFrom(Builder $query, $table) } /** - * Compile the index hints for the query. + * Create a full ANSI offset clause for the query. * * @param \Illuminate\Database\Query\Builder $query - * @param \Illuminate\Database\Query\IndexHint $indexHint + * @param array $components * @return string - * - * @throws \InvalidArgumentException */ - protected function compileIndexHint(Builder $query, $indexHint) + protected function compileAnsiOffset(Builder $query, $components) { - if ($indexHint->type !== 'force') { - return ''; + // An ORDER BY clause is required to make this offset query work, so if one does + // not exist we'll just create a dummy clause to trick the database and so it + // does not complain about the queries for not having an "order by" clause. + if (! isset($components['orders'])) { + $components['orders'] = 'order by (select 0)'; } - $index = $indexHint->index; + // We need to add the row number to the query so we can compare it to the offset + // and limit values given for the statements. So we will add an expression to + // the "select" that will give back the row numbers on each of the records. + $orderings = $components['orders']; - if (! preg_match('/^[a-zA-Z0-9_$]+$/', $index)) { - throw new InvalidArgumentException('Index name contains invalid characters.'); - } + $components['columns'] .= $this->compileOver($orderings); - return "with (index([{$index}]))"; - } + unset($components['orders']); - /** - * {@inheritdoc} - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereBitwise(Builder $query, $where) - { - $value = $this->parameter($where['value']); + // Next we need to calculate the constraints that should be placed on the query + // to get the right offset and limit from our query but if there is no limit + // set we will just handle the offset only since that is all that matters. + $constraint = $this->compileRowConstraint($query); - $operator = str_replace('?', '??', $where['operator']); + $sql = $this->concatenate($components); - return '('.$this->wrap($where['column']).' '.$operator.' '.$value.') != 0'; + // We are now ready to build the final SQL query so we'll create a common table + // expression from the query and get the records with row numbers within our + // given limit and offset value that we just put on as a query constraint. + return $this->compileTableExpression($sql, $constraint); } /** - * Compile a "where date" clause. + * Compile the over statement for a table expression. * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where + * @param string $orderings * @return string */ - protected function whereDate(Builder $query, $where) + protected function compileOver($orderings) { - $value = $this->parameter($where['value']); - - return 'cast('.$this->wrap($where['column']).' as date) '.$where['operator'].' '.$value; + return ", row_number() over ({$orderings}) as row_num"; } /** - * Compile a "where time" clause. + * Compile the limit / offset row constraint for a query. * * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereTime(Builder $query, $where) - { - $value = $this->parameter($where['value']); - - return 'cast('.$this->wrap($where['column']).' as time) '.$where['operator'].' '.$value; - } - - /** - * Compile a "JSON contains" statement into SQL. - * - * @param string $column - * @param string $value * @return string */ - protected function compileJsonContains($column, $value) + protected function compileRowConstraint($query) { - [$field, $path] = $this->wrapJsonFieldAndPath($column); + $start = $query->offset + 1; - return $value.' in (select [value] from openjson('.$field.$path.'))'; - } - - /** - * Prepare the binding for a "JSON contains" statement. - * - * @param mixed $binding - * @return string - */ - public function prepareBindingForJsonContains($binding) - { - return is_bool($binding) ? json_encode($binding) : $binding; - } - - /** - * Compile a "JSON contains key" statement into SQL. - * - * @param string $column - * @return string - */ - protected function compileJsonContainsKey($column) - { - $segments = explode('->', $column); + if ($query->limit > 0) { + $finish = $query->offset + $query->limit; - $lastSegment = array_pop($segments); - - if (preg_match('/\[([0-9]+)\]$/', $lastSegment, $matches)) { - $segments[] = Str::beforeLast($lastSegment, $matches[0]); - - $key = $matches[1]; - } else { - $key = "'".str_replace("'", "''", $lastSegment)."'"; - } - - [$field, $path] = $this->wrapJsonFieldAndPath(implode('->', $segments)); - - return $key.' in (select [key] from openjson('.$field.$path.'))'; - } - - /** - * Compile a "JSON length" statement into SQL. - * - * @param string $column - * @param string $operator - * @param string $value - * @return string - */ - protected function compileJsonLength($column, $operator, $value) - { - [$field, $path] = $this->wrapJsonFieldAndPath($column); - - return '(select count(*) from openjson('.$field.$path.')) '.$operator.' '.$value; - } - - /** - * Compile a "JSON value cast" statement into SQL. - * - * @param string $value - * @return string - */ - public function compileJsonValueCast($value) - { - return 'json_query('.$value.')'; - } - - /** - * Compile a single having clause. - * - * @param array $having - * @return string - */ - protected function compileHaving(array $having) - { - if ($having['type'] === 'Bitwise') { - return $this->compileHavingBitwise($having); + return "between {$start} and {$finish}"; } - return parent::compileHaving($having); - } - - /** - * Compile a having clause involving a bitwise operator. - * - * @param array $having - * @return string - */ - protected function compileHavingBitwise($having) - { - $column = $this->wrap($having['column']); - - $parameter = $this->parameter($having['value']); - - return '('.$column.' '.$having['operator'].' '.$parameter.') != 0'; + return ">= {$start}"; } /** - * Compile a delete statement without joins into SQL. + * Compile a common table expression for a query. * - * @param \Illuminate\Database\Query\Builder $query - * @param string $table - * @param string $where - * @return string - */ - protected function compileDeleteWithoutJoins(Builder $query, $table, $where) - { - $sql = parent::compileDeleteWithoutJoins($query, $table, $where); - - return ! is_null($query->limit) && $query->limit > 0 && $query->offset <= 0 - ? Str::replaceFirst('delete', 'delete top ('.$query->limit.')', $sql) - : $sql; - } - - /** - * Compile the random statement into SQL. - * - * @param string|int $seed + * @param string $sql + * @param string $constraint * @return string */ - public function compileRandom($seed) + protected function compileTableExpression($sql, $constraint) { - return 'NEWID()'; + return "select * from ({$sql}) as temp_table where row_num {$constraint}"; } /** @@ -316,31 +183,9 @@ public function compileRandom($seed) */ protected function compileLimit(Builder $query, $limit) { - $limit = (int) $limit; - - if ($limit && $query->offset > 0) { - return "fetch next {$limit} rows only"; - } - return ''; } - /** - * Compile a row number clause. - * - * @param string $partition - * @param string $orders - * @return string - */ - protected function compileRowNumber($partition, $orders) - { - if (empty($orders)) { - $orders = 'order by (select 0)'; - } - - return parent::compileRowNumber($partition, $orders); - } - /** * Compile the "offset" portions of the query. * @@ -350,42 +195,24 @@ protected function compileRowNumber($partition, $orders) */ protected function compileOffset(Builder $query, $offset) { - $offset = (int) $offset; - - if ($offset) { - return "offset {$offset} rows"; - } - return ''; } /** - * Compile the lock into SQL. + * Compile a truncate table statement into SQL. * * @param \Illuminate\Database\Query\Builder $query - * @param bool|string $value - * @return string - */ - protected function compileLock(Builder $query, $value) - { - return ''; - } - - /** - * Wrap a union subquery in parentheses. - * - * @param string $sql - * @return string + * @return array */ - protected function wrapUnion($sql) + public function compileTruncate(Builder $query) { - return 'select * from ('.$sql.') as '.$this->wrapTable('temp_table'); + return ['truncate table '.$this->wrapTable($query->from) => []]; } /** * Compile an exists statement into SQL. * - * @param \Illuminate\Database\Query\Builder $query + * @param \Illuminate\Database\Query\Builder $query * @return string */ public function compileExists(Builder $query) @@ -398,128 +225,27 @@ public function compileExists(Builder $query) } /** - * Compile an update statement with joins into SQL. - * - * @param \Illuminate\Database\Query\Builder $query - * @param string $table - * @param string $columns - * @param string $where - * @return string - */ - protected function compileUpdateWithJoins(Builder $query, $table, $columns, $where) - { - $alias = last(explode(' as ', $table)); - - $joins = $this->compileJoins($query, $query->joins); - - return "update {$alias} set {$columns} from {$table} {$joins} {$where}"; - } - - /** - * Compile an "upsert" statement into SQL. + * Compile a "where date" clause. * * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @param array $uniqueBy - * @param array $update - * @return string - */ - public function compileUpsert(Builder $query, array $values, array $uniqueBy, array $update) - { - $columns = $this->columnize(array_keys(array_first($values))); - - $sql = 'merge '.$this->wrapTable($query->from).' '; - - $parameters = (new Collection($values)) - ->map(fn ($record) => '('.$this->parameterize($record).')') - ->implode(', '); - - $sql .= 'using (values '.$parameters.') '.$this->wrapTable('laravel_source').' ('.$columns.') '; - - $on = (new Collection($uniqueBy)) - ->map(fn ($column) => $this->wrap('laravel_source.'.$column).' = '.$this->wrap($query->from.'.'.$column)) - ->implode(' and '); - - $sql .= 'on '.$on.' '; - - if ($update) { - $update = (new Collection($update))->map(function ($value, $key) { - return is_numeric($key) - ? $this->wrap($value).' = '.$this->wrap('laravel_source.'.$value) - : $this->wrap($key).' = '.$this->parameter($value); - })->implode(', '); - - $sql .= 'when matched then update set '.$update.' '; - } - - $sql .= 'when not matched then insert ('.$columns.') values ('.$columns.');'; - - return $sql; - } - - /** - * Prepare the bindings for an update statement. - * - * @param array $bindings - * @param array $values - * @return array - */ - #[\Override] - public function prepareBindingsForUpdate(array $bindings, array $values) - { - $cleanBindings = Arr::except($bindings, 'select'); - - $values = Arr::flatten(array_map(fn ($value) => value($value), $values)); - - return array_values( - array_merge($values, Arr::flatten($cleanBindings)) - ); - } - - /** - * Compile a "lateral join" clause. - * - * @param \Illuminate\Database\Query\JoinLateralClause $join - * @param string $expression - * @return string - */ - public function compileJoinLateral(JoinLateralClause $join, string $expression): string - { - $type = $join->type == 'left' ? 'outer' : 'cross'; - - return trim("{$type} apply {$expression}"); - } - - /** - * Compile the SQL statement to define a savepoint. - * - * @param string $name + * @param array $where * @return string */ - public function compileSavepoint($name) + protected function whereDate(Builder $query, $where) { - return 'SAVE TRANSACTION '.$name; - } + $value = $this->parameter($where['value']); - /** - * Compile the SQL statement to execute a savepoint rollback. - * - * @param string $name - * @return string - */ - public function compileSavepointRollBack($name) - { - return 'ROLLBACK TRANSACTION '.$name; + return 'cast('.$this->wrap($where['column']).' as date) '.$where['operator'].' '.$value; } /** - * Compile a query to get the number of open connections for a database. + * Determine if the grammar supports savepoints. * - * @return string + * @return bool */ - public function compileThreadCount() + public function supportsSavepoints() { - return 'select count(*) Value from sys.dm_exec_sessions where status = N\'running\''; + return false; } /** @@ -529,7 +255,7 @@ public function compileThreadCount() */ public function getDateFormat() { - return 'Y-m-d H:i:s.v'; + return 'Y-m-d H:i:s.000'; } /** @@ -540,47 +266,71 @@ public function getDateFormat() */ protected function wrapValue($value) { - return $value === '*' ? $value : '['.str_replace(']', ']]', $value).']'; + if ($value === '*') { + return $value; + } + + return '['.str_replace(']', ']]', $value).']'; } /** - * Wrap the given JSON selector. + * Compile an update statement into SQL. * - * @param string $value + * @param \Illuminate\Database\Query\Builder $query + * @param array $values * @return string */ - protected function wrapJsonSelector($value) + public function compileUpdate(Builder $query, $values) { - [$field, $path] = $this->wrapJsonFieldAndPath($value); + $table = $alias = $this->wrapTable($query->from); - return 'json_value('.$field.$path.')'; - } + if (strpos(strtolower($table), '] as [') !== false) { + $segments = explode('] as [', $table); - /** - * Wrap the given JSON boolean value. - * - * @param string $value - * @return string - */ - protected function wrapJsonBooleanValue($value) - { - return "'".$value."'"; + $alias = '['.$segments[1]; + } + + // Each one of the columns in the update statements needs to be wrapped in the + // keyword identifiers, also a place-holder needs to be created for each of + // the values in the list of bindings so we can make the sets statements. + $columns = []; + + foreach ($values as $key => $value) { + $columns[] = $this->wrap($key).' = '.$this->parameter($value); + } + + $columns = implode(', ', $columns); + + // If the query has any "join" clauses, we will setup the joins on the builder + // and compile them so we can attach them to this update, as update queries + // can get join statements to attach to other tables when they're needed. + if (isset($query->joins)) { + $joins = ' '.$this->compileJoins($query, $query->joins); + } else { + $joins = ''; + } + + // Of course, update queries may also be constrained by where clauses so we'll + // need to compile the where clauses and attach it to the query so only the + // intended records are updated by the SQL statements we generate to run. + $where = $this->compileWheres($query); + + if (! empty($joins)) { + return trim("update {$alias} set {$columns} from {$table}{$joins} {$where}"); + } + + return trim("update {$table}{$joins} set $columns $where"); } /** * Wrap a table in keyword identifiers. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $table - * @param string|null $prefix + * @param \Illuminate\Database\Query\Expression|string $table * @return string */ - public function wrapTable($table, $prefix = null) + public function wrapTable($table) { - if (! $this->isExpression($table)) { - return $this->wrapTableValuedFunction(parent::wrapTable($table, $prefix)); - } - - return $this->getValue($table); + return $this->wrapTableValuedFunction(parent::wrapTable($table)); } /** diff --git a/Query/IndexHint.php b/Query/IndexHint.php deleted file mode 100755 index 5659daa548..0000000000 --- a/Query/IndexHint.php +++ /dev/null @@ -1,32 +0,0 @@ -type = $type; - $this->index = $index; - } -} diff --git a/Query/JoinClause.php b/Query/JoinClause.php index d5733f3550..29ce03652e 100755 --- a/Query/JoinClause.php +++ b/Query/JoinClause.php @@ -3,8 +3,9 @@ namespace Illuminate\Database\Query; use Closure; +use InvalidArgumentException; -class JoinClause extends Builder +class JoinClause { /** * The type of join being performed. @@ -16,57 +17,35 @@ class JoinClause extends Builder /** * The table the join clause is joining to. * - * @var \Illuminate\Contracts\Database\Query\Expression|string + * @var string */ public $table; /** - * The connection of the parent query builder. - * - * @var \Illuminate\Database\ConnectionInterface - */ - protected $parentConnection; - - /** - * The grammar of the parent query builder. + * The "on" clauses for the join. * - * @var \Illuminate\Database\Query\Grammars\Grammar + * @var array */ - protected $parentGrammar; + public $clauses = []; /** - * The processor of the parent query builder. + * The "on" bindings for the join. * - * @var \Illuminate\Database\Query\Processors\Processor + * @var array */ - protected $parentProcessor; - - /** - * The class name of the parent query builder. - * - * @var string - */ - protected $parentClass; + public $bindings = []; /** * Create a new join clause instance. * - * @param \Illuminate\Database\Query\Builder $parentQuery * @param string $type * @param string $table + * @return void */ - public function __construct(Builder $parentQuery, $type, $table) + public function __construct($type, $table) { $this->type = $type; $this->table = $table; - $this->parentClass = get_class($parentQuery); - $this->parentGrammar = $parentQuery->getGrammar(); - $this->parentProcessor = $parentQuery->getProcessor(); - $this->parentConnection = $parentQuery->getConnection(); - - parent::__construct( - $this->parentConnection, $this->parentGrammar, $this->parentProcessor - ); } /** @@ -79,31 +58,48 @@ public function __construct(Builder $parentQuery, $type, $table) * * will produce the following SQL: * - * on `contacts`.`user_id` = `users`.`id` and `contacts`.`info_id` = `info`.`id` + * on `contacts`.`user_id` = `users`.`id` and `contacts`.`info_id` = `info`.`id` * - * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first + * @param \Closure|string $first * @param string|null $operator - * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second + * @param string|null $second * @param string $boolean + * @param bool $where * @return $this * * @throws \InvalidArgumentException */ - public function on($first, $operator = null, $second = null, $boolean = 'and') + public function on($first, $operator = null, $second = null, $boolean = 'and', $where = false) { if ($first instanceof Closure) { - return $this->whereNested($first, $boolean); + return $this->nest($first, $boolean); + } + + if (func_num_args() < 3) { + throw new InvalidArgumentException('Not enough arguments for the on clause.'); + } + + if ($where) { + $this->bindings[] = $second; } - return $this->whereColumn($first, $operator, $second, $boolean); + if ($where && ($operator === 'in' || $operator === 'not in') && is_array($second)) { + $second = count($second); + } + + $nested = false; + + $this->clauses[] = compact('first', 'operator', 'second', 'boolean', 'where', 'nested'); + + return $this; } /** * Add an "or on" clause to the join. * - * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first + * @param \Closure|string $first * @param string|null $operator - * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second + * @param string|null $second * @return \Illuminate\Database\Query\JoinClause */ public function orOn($first, $operator = null, $second = null) @@ -112,34 +108,146 @@ public function orOn($first, $operator = null, $second = null) } /** - * Get a new instance of the join clause builder. + * Add an "on where" clause to the join. * + * @param \Closure|string $first + * @param string|null $operator + * @param string|null $second + * @param string $boolean * @return \Illuminate\Database\Query\JoinClause */ - public function newQuery() + public function where($first, $operator = null, $second = null, $boolean = 'and') { - return new static($this->newParentQuery(), $this->type, $this->table); + return $this->on($first, $operator, $second, $boolean, true); } /** - * Create a new query instance for sub-query. + * Add an "or on where" clause to the join. * - * @return \Illuminate\Database\Query\Builder + * @param \Closure|string $first + * @param string|null $operator + * @param string|null $second + * @return \Illuminate\Database\Query\JoinClause */ - protected function forSubQuery() + public function orWhere($first, $operator = null, $second = null) { - return $this->newParentQuery()->newQuery(); + return $this->on($first, $operator, $second, 'or', true); } /** - * Create a new parent query instance. + * Add an "on where is null" clause to the join. * - * @return \Illuminate\Database\Query\Builder + * @param string $column + * @param string $boolean + * @return \Illuminate\Database\Query\JoinClause */ - protected function newParentQuery() + public function whereNull($column, $boolean = 'and') { - $class = $this->parentClass; + return $this->on($column, 'is', new Expression('null'), $boolean, false); + } + + /** + * Add an "or on where is null" clause to the join. + * + * @param string $column + * @return \Illuminate\Database\Query\JoinClause + */ + public function orWhereNull($column) + { + return $this->whereNull($column, 'or'); + } + + /** + * Add an "on where is not null" clause to the join. + * + * @param string $column + * @param string $boolean + * @return \Illuminate\Database\Query\JoinClause + */ + public function whereNotNull($column, $boolean = 'and') + { + return $this->on($column, 'is', new Expression('not null'), $boolean, false); + } + + /** + * Add an "or on where is not null" clause to the join. + * + * @param string $column + * @return \Illuminate\Database\Query\JoinClause + */ + public function orWhereNotNull($column) + { + return $this->whereNotNull($column, 'or'); + } + + /** + * Add an "on where in (...)" clause to the join. + * + * @param string $column + * @param array $values + * @return \Illuminate\Database\Query\JoinClause + */ + public function whereIn($column, array $values) + { + return $this->on($column, 'in', $values, 'and', true); + } + + /** + * Add an "on where not in (...)" clause to the join. + * + * @param string $column + * @param array $values + * @return \Illuminate\Database\Query\JoinClause + */ + public function whereNotIn($column, array $values) + { + return $this->on($column, 'not in', $values, 'and', true); + } + + /** + * Add an "or on where in (...)" clause to the join. + * + * @param string $column + * @param array $values + * @return \Illuminate\Database\Query\JoinClause + */ + public function orWhereIn($column, array $values) + { + return $this->on($column, 'in', $values, 'or', true); + } + + /** + * Add an "or on where not in (...)" clause to the join. + * + * @param string $column + * @param array $values + * @return \Illuminate\Database\Query\JoinClause + */ + public function orWhereNotIn($column, array $values) + { + return $this->on($column, 'not in', $values, 'or', true); + } + + /** + * Add a nested where statement to the query. + * + * @param \Closure $callback + * @param string $boolean + * @return \Illuminate\Database\Query\JoinClause + */ + public function nest(Closure $callback, $boolean = 'and') + { + $join = new static($this->type, $this->table); + + $callback($join); + + if (count($join->clauses)) { + $nested = true; + + $this->clauses[] = compact('nested', 'join', 'boolean'); + $this->bindings = array_merge($this->bindings, $join->bindings); + } - return new $class($this->parentConnection, $this->parentGrammar, $this->parentProcessor); + return $this; } } diff --git a/Query/JoinLateralClause.php b/Query/JoinLateralClause.php deleted file mode 100644 index 1be31d2962..0000000000 --- a/Query/JoinLateralClause.php +++ /dev/null @@ -1,8 +0,0 @@ -column_name; - }, $results); - } - - /** - * Process an "insert get ID" query. - * - * @param \Illuminate\Database\Query\Builder $query - * @param string $sql - * @param array $values - * @param string|null $sequence - * @return int - */ - public function processInsertGetId(Builder $query, $sql, $values, $sequence = null) - { - $query->getConnection()->insert($sql, $values, $sequence); - - $id = $query->getConnection()->getLastInsertId(); - - return is_numeric($id) ? (int) $id : $id; - } - - /** @inheritDoc */ - public function processColumns($results) - { - return array_map(function ($result) { - $result = (object) $result; - - return [ - 'name' => $result->name, - 'type_name' => $result->type_name, - 'type' => $result->type, - 'collation' => $result->collation, - 'nullable' => $result->nullable === 'YES', - 'default' => $result->default, - 'auto_increment' => $result->extra === 'auto_increment', - 'comment' => $result->comment ?: null, - 'generation' => $result->expression ? [ - 'type' => match ($result->extra) { - 'STORED GENERATED' => 'stored', - 'VIRTUAL GENERATED' => 'virtual', - default => null, - }, - 'expression' => $result->expression, - ] : null, - ]; - }, $results); - } - - /** @inheritDoc */ - public function processIndexes($results) - { - return array_map(function ($result) { - $result = (object) $result; + $mapping = function ($r) { + $r = (object) $r; - return [ - 'name' => $name = strtolower($result->name), - 'columns' => $result->columns ? explode(',', $result->columns) : [], - 'type' => strtolower($result->type), - 'unique' => (bool) $result->unique, - 'primary' => $name === 'primary', - ]; - }, $results); - } - - /** @inheritDoc */ - public function processForeignKeys($results) - { - return array_map(function ($result) { - $result = (object) $result; + return $r->column_name; + }; - return [ - 'name' => $result->name, - 'columns' => explode(',', $result->columns), - 'foreign_schema' => $result->foreign_schema, - 'foreign_table' => $result->foreign_table, - 'foreign_columns' => explode(',', $result->foreign_columns), - 'on_update' => strtolower($result->on_update), - 'on_delete' => strtolower($result->on_delete), - ]; - }, $results); + return array_map($mapping, $results); } } diff --git a/Query/Processors/PostgresProcessor.php b/Query/Processors/PostgresProcessor.php index 2818f91d8c..ab350cb058 100755 --- a/Query/Processors/PostgresProcessor.php +++ b/Query/Processors/PostgresProcessor.php @@ -11,143 +11,37 @@ class PostgresProcessor extends Processor * * @param \Illuminate\Database\Query\Builder $query * @param string $sql - * @param array $values - * @param string|null $sequence + * @param array $values + * @param string $sequence * @return int */ public function processInsertGetId(Builder $query, $sql, $values, $sequence = null) { - $connection = $query->getConnection(); - - $connection->recordsHaveBeenModified(); - - $result = $connection->selectFromWriteConnection($sql, $values)[0]; + $results = $query->getConnection()->selectFromWriteConnection($sql, $values); $sequence = $sequence ?: 'id'; - $id = is_object($result) ? $result->{$sequence} : $result[$sequence]; - - return is_numeric($id) ? (int) $id : $id; - } - - /** @inheritDoc */ - public function processTypes($results) - { - return array_map(function ($result) { - $result = (object) $result; - - return [ - 'name' => $result->name, - 'schema' => $result->schema, - 'schema_qualified_name' => $result->schema.'.'.$result->name, - 'implicit' => (bool) $result->implicit, - 'type' => match (strtolower($result->type)) { - 'b' => 'base', - 'c' => 'composite', - 'd' => 'domain', - 'e' => 'enum', - 'p' => 'pseudo', - 'r' => 'range', - 'm' => 'multirange', - default => null, - }, - 'category' => match (strtolower($result->category)) { - 'a' => 'array', - 'b' => 'boolean', - 'c' => 'composite', - 'd' => 'date_time', - 'e' => 'enum', - 'g' => 'geometric', - 'i' => 'network_address', - 'n' => 'numeric', - 'p' => 'pseudo', - 'r' => 'range', - 's' => 'string', - 't' => 'timespan', - 'u' => 'user_defined', - 'v' => 'bit_string', - 'x' => 'unknown', - 'z' => 'internal_use', - default => null, - }, - ]; - }, $results); - } - - /** @inheritDoc */ - public function processColumns($results) - { - return array_map(function ($result) { - $result = (object) $result; + $result = (array) $results[0]; - $autoincrement = $result->default !== null && str_starts_with($result->default, 'nextval('); + $id = $result[$sequence]; - return [ - 'name' => $result->name, - 'type_name' => $result->type_name, - 'type' => $result->type, - 'collation' => $result->collation, - 'nullable' => (bool) $result->nullable, - 'default' => $result->generated ? null : $result->default, - 'auto_increment' => $autoincrement, - 'comment' => $result->comment, - 'generation' => $result->generated ? [ - 'type' => match ($result->generated) { - 's' => 'stored', - 'v' => 'virtual', - default => null, - }, - 'expression' => $result->default, - ] : null, - ]; - }, $results); + return is_numeric($id) ? (int) $id : $id; } - /** @inheritDoc */ - public function processIndexes($results) + /** + * Process the results of a column listing query. + * + * @param array $results + * @return array + */ + public function processColumnListing($results) { - return array_map(function ($result) { - $result = (object) $result; - - return [ - 'name' => strtolower($result->name), - 'columns' => $result->columns ? explode(',', $result->columns) : [], - 'type' => strtolower($result->type), - 'unique' => (bool) $result->unique, - 'primary' => (bool) $result->primary, - ]; - }, $results); - } + $mapping = function ($r) { + $r = (object) $r; - /** @inheritDoc */ - public function processForeignKeys($results) - { - return array_map(function ($result) { - $result = (object) $result; + return $r->column_name; + }; - return [ - 'name' => $result->name, - 'columns' => explode(',', $result->columns), - 'foreign_schema' => $result->foreign_schema, - 'foreign_table' => $result->foreign_table, - 'foreign_columns' => explode(',', $result->foreign_columns), - 'on_update' => match (strtolower($result->on_update)) { - 'a' => 'no action', - 'r' => 'restrict', - 'c' => 'cascade', - 'n' => 'set null', - 'd' => 'set default', - default => null, - }, - 'on_delete' => match (strtolower($result->on_delete)) { - 'a' => 'no action', - 'r' => 'restrict', - 'c' => 'cascade', - 'n' => 'set null', - 'd' => 'set default', - default => null, - }, - ]; - }, $results); + return array_map($mapping, $results); } } diff --git a/Query/Processors/Processor.php b/Query/Processors/Processor.php index 46f692e49a..f78429fbaf 100755 --- a/Query/Processors/Processor.php +++ b/Query/Processors/Processor.php @@ -23,8 +23,8 @@ public function processSelect(Builder $query, $results) * * @param \Illuminate\Database\Query\Builder $query * @param string $sql - * @param array $values - * @param string|null $sequence + * @param array $values + * @param string $sequence * @return int */ public function processInsertGetId(Builder $query, $sql, $values, $sequence = null) @@ -37,107 +37,12 @@ public function processInsertGetId(Builder $query, $sql, $values, $sequence = nu } /** - * Process the results of a schemas query. + * Process the results of a column listing query. * - * @param list> $results - * @return list - */ - public function processSchemas($results) - { - return array_map(function ($result) { - $result = (object) $result; - - return [ - 'name' => $result->name, - 'path' => $result->path ?? null, // SQLite Only... - 'default' => (bool) $result->default, - ]; - }, $results); - } - - /** - * Process the results of a tables query. - * - * @param list> $results - * @return list - */ - public function processTables($results) - { - return array_map(function ($result) { - $result = (object) $result; - - return [ - 'name' => $result->name, - 'schema' => $result->schema ?? null, - 'schema_qualified_name' => isset($result->schema) ? $result->schema.'.'.$result->name : $result->name, - 'size' => isset($result->size) ? (int) $result->size : null, - 'comment' => $result->comment ?? null, // MySQL and PostgreSQL - 'collation' => $result->collation ?? null, // MySQL only - 'engine' => $result->engine ?? null, // MySQL only - ]; - }, $results); - } - - /** - * Process the results of a views query. - * - * @param list> $results - * @return list - */ - public function processViews($results) - { - return array_map(function ($result) { - $result = (object) $result; - - return [ - 'name' => $result->name, - 'schema' => $result->schema ?? null, - 'schema_qualified_name' => isset($result->schema) ? $result->schema.'.'.$result->name : $result->name, - 'definition' => $result->definition, - ]; - }, $results); - } - - /** - * Process the results of a types query. - * - * @param list> $results - * @return list - */ - public function processTypes($results) - { - return $results; - } - - /** - * Process the results of a columns query. - * - * @param list> $results - * @return list - */ - public function processColumns($results) - { - return $results; - } - - /** - * Process the results of an indexes query. - * - * @param list> $results - * @return list, type: string, unique: bool, primary: bool}> - */ - public function processIndexes($results) - { - return $results; - } - - /** - * Process the results of a foreign keys query. - * - * @param list> $results - * @return list, foreign_schema: string, foreign_table: string, foreign_columns: list, on_update: string, on_delete: string}> + * @param array $results + * @return array */ - public function processForeignKeys($results) + public function processColumnListing($results) { return $results; } diff --git a/Query/Processors/SQLiteProcessor.php b/Query/Processors/SQLiteProcessor.php index ed4916a7a5..e4a895041f 100644 --- a/Query/Processors/SQLiteProcessor.php +++ b/Query/Processors/SQLiteProcessor.php @@ -4,96 +4,20 @@ class SQLiteProcessor extends Processor { - /** @inheritDoc */ - public function processColumns($results, $sql = '') + /** + * Process the results of a column listing query. + * + * @param array $results + * @return array + */ + public function processColumnListing($results) { - $hasPrimaryKey = array_sum(array_column($results, 'primary')) === 1; + $mapping = function ($r) { + $r = (object) $r; - return array_map(function ($result) use ($hasPrimaryKey, $sql) { - $result = (object) $result; + return $r->name; + }; - $type = strtolower($result->type); - - $safeName = preg_quote($result->name, '/'); - - $collation = preg_match( - '/\b'.$safeName.'\b[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:default|check|as)\s*(?:\(.*?\))?[^,]*)*collate\s+["\'`]?(\w+)/i', - $sql, - $matches - ) === 1 ? strtolower($matches[1]) : null; - - $isGenerated = in_array($result->extra, [2, 3]); - - $expression = $isGenerated && preg_match( - '/\b'.$safeName.'\b[^,]+\s+as\s+\(((?:[^()]+|\((?:[^()]+|\([^()]*\))*\))*)\)/i', - $sql, - $matches - ) === 1 ? $matches[1] : null; - - return [ - 'name' => $result->name, - 'type_name' => strtok($type, '(') ?: '', - 'type' => $type, - 'collation' => $collation, - 'nullable' => (bool) $result->nullable, - 'default' => $result->default, - 'auto_increment' => $hasPrimaryKey && $result->primary && $type === 'integer', - 'comment' => null, - 'generation' => $isGenerated ? [ - 'type' => match ((int) $result->extra) { - 3 => 'stored', - 2 => 'virtual', - default => null, - }, - 'expression' => $expression, - ] : null, - ]; - }, $results); - } - - /** @inheritDoc */ - public function processIndexes($results) - { - $primaryCount = 0; - - $indexes = array_map(function ($result) use (&$primaryCount) { - $result = (object) $result; - - if ($isPrimary = (bool) $result->primary) { - $primaryCount += 1; - } - - return [ - 'name' => strtolower($result->name), - 'columns' => $result->columns ? explode(',', $result->columns) : [], - 'type' => null, - 'unique' => (bool) $result->unique, - 'primary' => $isPrimary, - ]; - }, $results); - - if ($primaryCount > 1) { - $indexes = array_filter($indexes, fn ($index) => $index['name'] !== 'primary'); - } - - return $indexes; - } - - /** @inheritDoc */ - public function processForeignKeys($results) - { - return array_map(function ($result) { - $result = (object) $result; - - return [ - 'name' => null, - 'columns' => explode(',', $result->columns), - 'foreign_schema' => $result->foreign_schema, - 'foreign_table' => $result->foreign_table, - 'foreign_columns' => explode(',', $result->foreign_columns), - 'on_update' => strtolower($result->on_update), - 'on_delete' => strtolower($result->on_delete), - ]; - }, $results); + return array_map($mapping, $results); } } diff --git a/Query/Processors/SqlServerProcessor.php b/Query/Processors/SqlServerProcessor.php index 8d000c4579..e4764ae192 100755 --- a/Query/Processors/SqlServerProcessor.php +++ b/Query/Processors/SqlServerProcessor.php @@ -13,8 +13,8 @@ class SqlServerProcessor extends Processor * * @param \Illuminate\Database\Query\Builder $query * @param string $sql - * @param array $values - * @param string|null $sequence + * @param array $values + * @param string $sequence * @return int */ public function processInsertGetId(Builder $query, $sql, $values, $sequence = null) @@ -37,85 +37,33 @@ public function processInsertGetId(Builder $query, $sql, $values, $sequence = nu * * @param \Illuminate\Database\Connection $connection * @return int - * * @throws \Exception */ protected function processInsertGetIdForOdbc(Connection $connection) { - $result = $connection->selectFromWriteConnection( - 'SELECT CAST(COALESCE(SCOPE_IDENTITY(), @@IDENTITY) AS int) AS insertid' - ); + $result = $connection->select('SELECT CAST(COALESCE(SCOPE_IDENTITY(), @@IDENTITY) AS int) AS insertid'); if (! $result) { throw new Exception('Unable to retrieve lastInsertID for ODBC.'); } - $row = $result[0]; - - return is_object($row) ? $row->insertid : $row['insertid']; - } - - /** @inheritDoc */ - public function processColumns($results) - { - return array_map(function ($result) { - $result = (object) $result; - - $type = match ($typeName = $result->type_name) { - 'binary', 'varbinary', 'char', 'varchar', 'nchar', 'nvarchar' => $result->length == -1 ? $typeName.'(max)' : $typeName."($result->length)", - 'decimal', 'numeric' => $typeName."($result->precision,$result->places)", - 'float', 'datetime2', 'datetimeoffset', 'time' => $typeName."($result->precision)", - default => $typeName, - }; - - return [ - 'name' => $result->name, - 'type_name' => $result->type_name, - 'type' => $type, - 'collation' => $result->collation, - 'nullable' => (bool) $result->nullable, - 'default' => $result->default, - 'auto_increment' => (bool) $result->autoincrement, - 'comment' => $result->comment, - 'generation' => $result->expression ? [ - 'type' => $result->persisted ? 'stored' : 'virtual', - 'expression' => $result->expression, - ] : null, - ]; - }, $results); + return $result[0]->insertid; } - /** @inheritDoc */ - public function processIndexes($results) + /** + * Process the results of a column listing query. + * + * @param array $results + * @return array + */ + public function processColumnListing($results) { - return array_map(function ($result) { - $result = (object) $result; + $mapping = function ($r) { + $r = (object) $r; - return [ - 'name' => strtolower($result->name), - 'columns' => $result->columns ? explode(',', $result->columns) : [], - 'type' => strtolower($result->type), - 'unique' => (bool) $result->unique, - 'primary' => (bool) $result->primary, - ]; - }, $results); - } - - /** @inheritDoc */ - public function processForeignKeys($results) - { - return array_map(function ($result) { - $result = (object) $result; + return $r->name; + }; - return [ - 'name' => $result->name, - 'columns' => explode(',', $result->columns), - 'foreign_schema' => $result->foreign_schema, - 'foreign_table' => $result->foreign_table, - 'foreign_columns' => explode(',', $result->foreign_columns), - 'on_update' => strtolower(str_replace('_', ' ', $result->on_update)), - 'on_delete' => strtolower(str_replace('_', ' ', $result->on_delete)), - ]; - }, $results); + return array_map($mapping, $results); } } diff --git a/QueryException.php b/QueryException.php index b9251d2fe0..d4fb085817 100644 --- a/QueryException.php +++ b/QueryException.php @@ -2,20 +2,11 @@ namespace Illuminate\Database; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Str; use PDOException; -use Throwable; +use Illuminate\Support\Str; class QueryException extends PDOException { - /** - * The database connection name. - * - * @var string - */ - public $connectionName; - /** * The SQL for the query. * @@ -30,41 +21,23 @@ class QueryException extends PDOException */ protected $bindings; - /** - * The PDO read / write type for the executed query. - * - * @var null|'read'|'write' - */ - public $readWriteType; - - /** - * The connection details for the query (host, port, database, etc.). - * - * @var array - */ - protected $connectionDetails = []; - /** * Create a new query exception instance. * - * @param string $connectionName * @param string $sql * @param array $bindings - * @param \Throwable $previous - * @param null|'read'|'write' $readWriteType - * @param array $connectionDetails + * @param \Exception $previous + * @return void */ - public function __construct($connectionName, $sql, array $bindings, Throwable $previous, array $connectionDetails = [], $readWriteType = null) + public function __construct($sql, array $bindings, $previous) { parent::__construct('', 0, $previous); - $this->connectionName = $connectionName; $this->sql = $sql; $this->bindings = $bindings; - $this->connectionDetails = $connectionDetails; - $this->readWriteType = $readWriteType; + $this->previous = $previous; $this->code = $previous->getCode(); - $this->message = $this->formatMessage($connectionName, $sql, $bindings, $previous); + $this->message = $this->formatMessage($sql, $bindings, $previous); if ($previous instanceof PDOException) { $this->errorInfo = $previous->errorInfo; @@ -74,58 +47,14 @@ public function __construct($connectionName, $sql, array $bindings, Throwable $p /** * Format the SQL error message. * - * @param string $connectionName * @param string $sql * @param array $bindings - * @param \Throwable $previous + * @param \Exception $previous * @return string */ - protected function formatMessage($connectionName, $sql, $bindings, Throwable $previous) + protected function formatMessage($sql, $bindings, $previous) { - $details = $this->formatConnectionDetails(); - - return $previous->getMessage().' (Connection: '.$connectionName.$details.', SQL: '.Str::replaceArray('?', $bindings, $sql).')'; - } - - /** - * Format the connection details for the error message. - * - * @return string - */ - protected function formatConnectionDetails() - { - if (empty($this->connectionDetails)) { - return ''; - } - - $driver = $this->connectionDetails['driver'] ?? ''; - - $segments = []; - - if ($driver !== 'sqlite') { - if (! empty($this->connectionDetails['unix_socket'])) { - $segments[] = 'Socket: '.$this->connectionDetails['unix_socket']; - } else { - $host = $this->connectionDetails['host'] ?? ''; - - $segments[] = 'Host: '.(is_array($host) ? implode(', ', $host) : $host); - $segments[] = 'Port: '.($this->connectionDetails['port'] ?? ''); - } - } - - $segments[] = 'Database: '.($this->connectionDetails['database'] ?? ''); - - return ', '.implode(', ', $segments); - } - - /** - * Get the connection name for the query. - * - * @return string - */ - public function getConnectionName() - { - return $this->connectionName; + return $previous->getMessage().' (SQL: '.Str::replaceArray('?', $bindings, $sql).')'; } /** @@ -138,16 +67,6 @@ public function getSql() return $this->sql; } - /** - * Get the raw SQL representation of the query with embedded bindings. - */ - public function getRawSql(): string - { - return DB::connection($this->getConnectionName()) - ->getQueryGrammar() - ->substituteBindingsIntoRawSql($this->getSql(), $this->getBindings()); - } - /** * Get the bindings for the query. * @@ -157,14 +76,4 @@ public function getBindings() { return $this->bindings; } - - /** - * Get information about the connection such as host, port, database, etc. - * - * @return array - */ - public function getConnectionDetails() - { - return $this->connectionDetails; - } } diff --git a/README.md b/README.md index 9019936025..1675a9325d 100755 --- a/README.md +++ b/README.md @@ -12,14 +12,14 @@ use Illuminate\Database\Capsule\Manager as Capsule; $capsule = new Capsule; $capsule->addConnection([ - 'driver' => 'mysql', - 'host' => 'localhost', - 'database' => 'database', - 'username' => 'root', - 'password' => 'password', - 'charset' => 'utf8', + 'driver' => 'mysql', + 'host' => 'localhost', + 'database' => 'database', + 'username' => 'root', + 'password' => 'password', + 'charset' => 'utf8', 'collation' => 'utf8_unicode_ci', - 'prefix' => '', + 'prefix' => '', ]); // Set the event dispatcher used by Eloquent models... (optional) @@ -45,13 +45,14 @@ $users = Capsule::table('users')->where('votes', '>', 100)->get(); ``` Other core methods may be accessed directly from the Capsule in the same manner as from the DB facade: ```PHP -$results = Capsule::select('select * from users where id = ?', [1]); +$results = Capsule::select('select * from users where id = ?', array(1)); ``` **Using The Schema Builder** ```PHP -Capsule::schema()->create('users', function ($table) { +Capsule::schema()->create('users', function($table) +{ $table->increments('id'); $table->string('email')->unique(); $table->timestamps(); @@ -66,4 +67,4 @@ class User extends Illuminate\Database\Eloquent\Model {} $users = User::where('votes', '>', 1)->get(); ``` -For further documentation on using the various database facilities this library provides, consult the [Laravel framework documentation](https://laravel.com/docs). +For further documentation on using the various database facilities this library provides, consult the [Laravel framework documentation](http://laravel.com/docs). diff --git a/RecordNotFoundException.php b/RecordNotFoundException.php deleted file mode 100644 index 3a717feeed..0000000000 --- a/RecordNotFoundException.php +++ /dev/null @@ -1,10 +0,0 @@ -=')) { - $mode = $this->getConfig('transaction_mode') ?? 'DEFERRED'; - - $this->getPdo()->exec("BEGIN {$mode} TRANSACTION"); - - return; - } - - $this->getPdo()->beginTransaction(); - } - - /** - * Escape a binary value for safe SQL embedding. - * - * @param string $value - * @return string - */ - protected function escapeBinary($value) - { - $hex = bin2hex($value); - - return "x'{$hex}'"; - } - - /** - * Determine if the given database exception was caused by a unique constraint violation. - * - * @param \Exception $exception - * @return bool - */ - protected function isUniqueConstraintError(Exception $exception) - { - return (bool) preg_match('#(column(s)? .* (is|are) not unique|UNIQUE constraint failed: .*)#i', $exception->getMessage()); - } - /** * Get the default query grammar instance. * @@ -69,21 +16,7 @@ protected function isUniqueConstraintError(Exception $exception) */ protected function getDefaultQueryGrammar() { - return new QueryGrammar($this); - } - - /** - * Get a schema builder instance for the connection. - * - * @return \Illuminate\Database\Schema\SQLiteBuilder - */ - public function getSchemaBuilder() - { - if (is_null($this->schemaGrammar)) { - $this->useDefaultSchemaGrammar(); - } - - return new SQLiteBuilder($this); + return $this->withTablePrefix(new QueryGrammar); } /** @@ -93,29 +26,26 @@ public function getSchemaBuilder() */ protected function getDefaultSchemaGrammar() { - return new SchemaGrammar($this); + return $this->withTablePrefix(new SchemaGrammar); } /** - * Get the schema state for the connection. - * - * @param \Illuminate\Filesystem\Filesystem|null $files - * @param callable|null $processFactory + * Get the default post processor instance. * - * @throws \RuntimeException + * @return \Illuminate\Database\Query\Processors\SQLiteProcessor */ - public function getSchemaState(?Filesystem $files = null, ?callable $processFactory = null) + protected function getDefaultPostProcessor() { - return new SqliteSchemaState($this, $files, $processFactory); + return new SQLiteProcessor; } /** - * Get the default post processor instance. + * Get the Doctrine DBAL driver. * - * @return \Illuminate\Database\Query\Processors\SQLiteProcessor + * @return \Doctrine\DBAL\Driver\PDOSqlite\Driver */ - protected function getDefaultPostProcessor() + protected function getDoctrineDriver() { - return new SQLiteProcessor; + return new DoctrineDriver; } } diff --git a/SQLiteDatabaseDoesNotExistException.php b/SQLiteDatabaseDoesNotExistException.php deleted file mode 100644 index 8ea87cf39e..0000000000 --- a/SQLiteDatabaseDoesNotExistException.php +++ /dev/null @@ -1,27 +0,0 @@ -path = $path; - } -} diff --git a/Schema/Blueprint.php b/Schema/Blueprint.php index 8c5c1cd7dc..e7ff6b2749 100755 --- a/Schema/Blueprint.php +++ b/Schema/Blueprint.php @@ -3,32 +3,12 @@ namespace Illuminate\Database\Schema; use Closure; +use Illuminate\Support\Fluent; use Illuminate\Database\Connection; -use Illuminate\Database\Eloquent\Concerns\HasUlids; -use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Grammars\Grammar; -use Illuminate\Database\Schema\Grammars\MySqlGrammar; -use Illuminate\Database\Schema\Grammars\SQLiteGrammar; -use Illuminate\Support\Collection; -use Illuminate\Support\Fluent; -use Illuminate\Support\Traits\Macroable; - -use function Illuminate\Support\enum_value; class Blueprint { - use Macroable; - - /** - * The database connection instance. - */ - protected Connection $connection; - - /** - * The schema grammar instance. - */ - protected Grammar $grammar; - /** * The table the blueprint describes. * @@ -39,14 +19,14 @@ class Blueprint /** * The columns that should be added to the table. * - * @var \Illuminate\Database\Schema\ColumnDefinition[] + * @var array */ protected $columns = []; /** * The commands that should be run for the table. * - * @var \Illuminate\Support\Fluent[] + * @var array */ protected $commands = []; @@ -59,15 +39,11 @@ class Blueprint /** * The default character set that should be used for the table. - * - * @var string */ public $charset; /** * The collation that should be used for the table. - * - * @var string */ public $collation; @@ -78,31 +54,15 @@ class Blueprint */ public $temporary = false; - /** - * The column to add new columns after. - * - * @var string - */ - public $after; - - /** - * The blueprint state instance. - * - * @var \Illuminate\Database\Schema\BlueprintState|null - */ - protected $state; - /** * Create a new schema blueprint. * - * @param \Illuminate\Database\Connection $connection * @param string $table - * @param (\Closure(self): void)|null $callback + * @param \Closure|null $callback + * @return void */ - public function __construct(Connection $connection, $table, ?Closure $callback = null) + public function __construct($table, Closure $callback = null) { - $this->connection = $connection; - $this->grammar = $connection->getSchemaGrammar(); $this->table = $table; if (! is_null($callback)) { @@ -113,21 +73,25 @@ public function __construct(Connection $connection, $table, ?Closure $callback = /** * Execute the blueprint against the database. * + * @param \Illuminate\Database\Connection $connection + * @param \Illuminate\Database\Schema\Grammars\Grammar $grammar * @return void */ - public function build() + public function build(Connection $connection, Grammar $grammar) { - foreach ($this->toSql() as $statement) { - $this->connection->statement($statement); + foreach ($this->toSql($connection, $grammar) as $statement) { + $connection->statement($statement); } } /** * Get the raw SQL statements for the blueprint. * + * @param \Illuminate\Database\Connection $connection + * @param \Illuminate\Database\Schema\Grammars\Grammar $grammar * @return array */ - public function toSql() + public function toSql(Connection $connection, Grammar $grammar) { $this->addImpliedCommands(); @@ -136,21 +100,11 @@ public function toSql() // Each type of command has a corresponding compiler function on the schema // grammar which is used to build the necessary SQL statements to build // the blueprint element, so we'll just call that compilers function. - $this->ensureCommandsAreValid(); - foreach ($this->commands as $command) { - if ($command->shouldBeSkipped) { - continue; - } - $method = 'compile'.ucfirst($command->name); - if (method_exists($this->grammar, $method) || $this->grammar::hasMacro($method)) { - if ($this->hasState()) { - $this->state->update($command); - } - - if (! is_null($sql = $this->grammar->$method($this, $command))) { + if (method_exists($grammar, $method)) { + if (! is_null($sql = $grammar->$method($this, $command, $connection))) { $statements = array_merge($statements, (array) $sql); } } @@ -160,51 +114,21 @@ public function toSql() } /** - * Ensure the commands on the blueprint are valid for the connection type. - * - * @return void - * - * @throws \BadMethodCallException - */ - protected function ensureCommandsAreValid() - { - // - } - - /** - * Get all of the commands matching the given names. - * - * @deprecated Will be removed in a future Laravel version. - * - * @param array $names - * @return \Illuminate\Support\Collection - */ - protected function commandsNamed(array $names) - { - return (new Collection($this->commands)) - ->filter(fn ($command) => in_array($command->name, $names)); - } - - /** - * Add the commands that are implied by the blueprint's state. + * Add the commands that are implied by the blueprint. * * @return void */ protected function addImpliedCommands() { - $this->addFluentIndexes(); - $this->addFluentCommands(); - - if (! $this->creating()) { - $this->commands = array_map( - fn ($command) => $command instanceof ColumnDefinition - ? $this->createCommand($command->change ? 'change' : 'add', ['column' => $command]) - : $command, - $this->commands - ); + if (count($this->getAddedColumns()) > 0 && ! $this->creating()) { + array_unshift($this->commands, $this->createCommand('add')); + } - $this->addAlterCommands(); + if (count($this->getChangedColumns()) > 0 && ! $this->creating()) { + array_unshift($this->commands, $this->createCommand('change')); } + + $this->addFluentIndexes(); } /** @@ -215,118 +139,42 @@ protected function addImpliedCommands() protected function addFluentIndexes() { foreach ($this->columns as $column) { - foreach (['primary', 'unique', 'index', 'fulltext', 'fullText', 'spatialIndex', 'vectorIndex'] as $index) { - // If the column is supposed to be changed to an auto increment column and - // the specified index is primary, there is no need to add a command on - // MySQL, as it will be handled during the column definition instead. - if ($index === 'primary' && $column->autoIncrement && $column->change && $this->grammar instanceof MySqlGrammar) { - continue 2; - } - - // If the index has been specified on the given column, but is simply equal - // to "true" (boolean), no name has been specified for this index so the - // index method can be called without a name and it will generate one. - if ($column->{$index} === true) { - $indexMethod = $index === 'index' && $column->type === 'vector' - ? 'vectorIndex' - : $index; - - $this->{$indexMethod}($column->name); - $column->{$index} = null; + foreach (['primary', 'unique', 'index'] as $index) { + // If the index has been specified on the given column, but is simply + // equal to "true" (boolean), no name has been specified for this + // index, so we will simply call the index methods without one. + if ($column->$index === true) { + $this->$index($column->name); continue 2; } - // If the index has been specified on the given column, but it equals false - // and the column is supposed to be changed, we will call the drop index - // method with an array of column to drop it by its conventional name. - elseif ($column->{$index} === false && $column->change) { - $this->{'drop'.ucfirst($index)}([$column->name]); - $column->{$index} = null; + // If the index has been specified on the column and it is something + // other than boolean true, we will assume a name was provided on + // the index specification, and pass in the name to the method. + elseif (isset($column->$index)) { + $this->$index($column->name, $column->$index); continue 2; } - - // If the index has been specified on the given column, and it has a string - // value, we'll go ahead and call the index method and pass the name for - // the index since the developer specified the explicit name for this. - elseif (isset($column->{$index})) { - $indexMethod = $index === 'index' && $column->type === 'vector' - ? 'vectorIndex' - : $index; - - $this->{$indexMethod}($column->name, $column->{$index}); - $column->{$index} = null; - - continue 2; - } - } - } - } - - /** - * Add the fluent commands specified on any columns. - * - * @return void - */ - public function addFluentCommands() - { - foreach ($this->columns as $column) { - foreach ($this->grammar->getFluentCommands() as $commandName) { - $this->addCommand($commandName, compact('column')); } } } /** - * Add the alter commands if whenever needed. + * Determine if the blueprint has a create command. * - * @return void + * @return bool */ - public function addAlterCommands() + protected function creating() { - if (! $this->grammar instanceof SQLiteGrammar) { - return; - } - - $alterCommands = $this->grammar->getAlterCommands(); - - [$commands, $lastCommandWasAlter, $hasAlterCommand] = [ - [], false, false, - ]; - foreach ($this->commands as $command) { - if (in_array($command->name, $alterCommands)) { - $hasAlterCommand = true; - $lastCommandWasAlter = true; - } elseif ($lastCommandWasAlter) { - $commands[] = $this->createCommand('alter'); - $lastCommandWasAlter = false; + if ($command->name == 'create') { + return true; } - - $commands[] = $command; - } - - if ($lastCommandWasAlter) { - $commands[] = $this->createCommand('alter'); - } - - if ($hasAlterCommand) { - $this->state = new BlueprintState($this, $this->connection); } - $this->commands = $commands; - } - - /** - * Determine if the blueprint has a create command. - * - * @return bool - */ - public function creating() - { - return (new Collection($this->commands)) - ->contains(fn ($command) => ! $command instanceof ColumnDefinition && $command->name === 'create'); + return false; } /** @@ -339,49 +187,6 @@ public function create() return $this->addCommand('create'); } - /** - * Specify the storage engine that should be used for the table. - * - * @param string $engine - * @return void - */ - public function engine($engine) - { - $this->engine = $engine; - } - - /** - * Specify that the InnoDB storage engine should be used for the table (MySQL only). - * - * @return void - */ - public function innoDb() - { - $this->engine('InnoDB'); - } - - /** - * Specify the character set that should be used for the table. - * - * @param string $charset - * @return void - */ - public function charset($charset) - { - $this->charset = $charset; - } - - /** - * Specify the collation that should be used for the table. - * - * @param string $collation - * @return void - */ - public function collation($collation) - { - $this->collation = $collation; - } - /** * Indicate that the table needs to be temporary. * @@ -415,12 +220,12 @@ public function dropIfExists() /** * Indicate that the given columns should be dropped. * - * @param mixed $columns + * @param array|mixed $columns * @return \Illuminate\Support\Fluent */ public function dropColumn($columns) { - $columns = is_array($columns) ? $columns : func_get_args(); + $columns = is_array($columns) ? $columns : (array) func_get_args(); return $this->addCommand('dropColumn', compact('columns')); } @@ -440,7 +245,7 @@ public function renameColumn($from, $to) /** * Indicate that the given primary key should be dropped. * - * @param string|array|null $index + * @param string|array $index * @return \Illuminate\Support\Fluent */ public function dropPrimary($index = null) @@ -470,28 +275,6 @@ public function dropIndex($index) return $this->dropIndexCommand('dropIndex', 'index', $index); } - /** - * Indicate that the given fulltext index should be dropped. - * - * @param string|array $index - * @return \Illuminate\Support\Fluent - */ - public function dropFullText($index) - { - return $this->dropIndexCommand('dropFullText', 'fulltext', $index); - } - - /** - * Indicate that the given spatial index should be dropped. - * - * @param string|array $index - * @return \Illuminate\Support\Fluent - */ - public function dropSpatialIndex($index) - { - return $this->dropIndexCommand('dropSpatialIndex', 'spatialIndex', $index); - } - /** * Indicate that the given foreign key should be dropped. * @@ -503,63 +286,6 @@ public function dropForeign($index) return $this->dropIndexCommand('dropForeign', 'foreign', $index); } - /** - * Indicate that the given column and foreign key should be dropped. - * - * @param string $column - * @return \Illuminate\Support\Fluent - */ - public function dropConstrainedForeignId($column) - { - $this->dropForeign([$column]); - - return $this->dropColumn($column); - } - - /** - * Indicate that the given foreign key should be dropped. - * - * @param \Illuminate\Database\Eloquent\Model|string $model - * @param string|null $column - * @return \Illuminate\Support\Fluent - */ - public function dropForeignIdFor($model, $column = null) - { - if (is_string($model)) { - $model = new $model; - } - - return $this->dropColumn($column ?: $model->getForeignKey()); - } - - /** - * Indicate that the given foreign key should be dropped. - * - * @param \Illuminate\Database\Eloquent\Model|string $model - * @param string|null $column - * @return \Illuminate\Support\Fluent - */ - public function dropConstrainedForeignIdFor($model, $column = null) - { - if (is_string($model)) { - $model = new $model; - } - - return $this->dropConstrainedForeignId($column ?: $model->getForeignKey()); - } - - /** - * Indicate that the given indexes should be renamed. - * - * @param string $from - * @param string $to - * @return \Illuminate\Support\Fluent - */ - public function renameIndex($from, $to) - { - return $this->addCommand('renameIndex', compact('from', 'to')); - } - /** * Indicate that the timestamp columns should be dropped. * @@ -583,23 +309,11 @@ public function dropTimestampsTz() /** * Indicate that the soft delete column should be dropped. * - * @param string $column - * @return void - */ - public function dropSoftDeletes($column = 'deleted_at') - { - $this->dropColumn($column); - } - - /** - * Indicate that the soft delete column should be dropped. - * - * @param string $column * @return void */ - public function dropSoftDeletesTz($column = 'deleted_at') + public function dropSoftDeletes() { - $this->dropSoftDeletes($column); + $this->dropColumn('deleted_at'); } /** @@ -612,20 +326,6 @@ public function dropRememberToken() $this->dropColumn('remember_token'); } - /** - * Indicate that the polymorphic columns should be dropped. - * - * @param string $name - * @param string|null $indexName - * @return void - */ - public function dropMorphs($name, $indexName = null) - { - $this->dropIndex($indexName ?: $this->createIndexName('index', ["{$name}_type", "{$name}_id"])); - - $this->dropColumn("{$name}_type", "{$name}_id"); - } - /** * Rename the table to a given name. * @@ -641,9 +341,9 @@ public function rename($to) * Specify the primary key(s) for the table. * * @param string|array $columns - * @param string|null $name + * @param string $name * @param string|null $algorithm - * @return \Illuminate\Database\Schema\IndexDefinition + * @return \Illuminate\Support\Fluent */ public function primary($columns, $name = null, $algorithm = null) { @@ -654,9 +354,9 @@ public function primary($columns, $name = null, $algorithm = null) * Specify a unique index for the table. * * @param string|array $columns - * @param string|null $name + * @param string $name * @param string|null $algorithm - * @return \Illuminate\Database\Schema\IndexDefinition + * @return \Illuminate\Support\Fluent */ public function unique($columns, $name = null, $algorithm = null) { @@ -667,9 +367,9 @@ public function unique($columns, $name = null, $algorithm = null) * Specify an index for the table. * * @param string|array $columns - * @param string|null $name + * @param string $name * @param string|null $algorithm - * @return \Illuminate\Database\Schema\IndexDefinition + * @return \Illuminate\Support\Fluent */ public function index($columns, $name = null, $algorithm = null) { @@ -677,244 +377,138 @@ public function index($columns, $name = null, $algorithm = null) } /** - * Specify a fulltext index for the table. + * Specify a foreign key for the table. * * @param string|array $columns - * @param string|null $name - * @param string|null $algorithm - * @return \Illuminate\Database\Schema\IndexDefinition + * @param string $name + * @return \Illuminate\Support\Fluent */ - public function fullText($columns, $name = null, $algorithm = null) + public function foreign($columns, $name = null) { - return $this->indexCommand('fulltext', $columns, $name, $algorithm); + return $this->indexCommand('foreign', $columns, $name); } /** - * Specify a spatial index for the table. + * Create a new auto-incrementing integer (4-byte) column on the table. * - * @param string|array $columns - * @param string|null $name - * @param string|null $operatorClass - * @return \Illuminate\Database\Schema\IndexDefinition + * @param string $column + * @return \Illuminate\Support\Fluent */ - public function spatialIndex($columns, $name = null, $operatorClass = null) + public function increments($column) { - return $this->indexCommand('spatialIndex', $columns, $name, null, $operatorClass); + return $this->unsignedInteger($column, true); } /** - * Specify a vector index for the table. + * Create a new auto-incrementing small integer (2-byte) column on the table. * * @param string $column - * @param string|null $name - * @return \Illuminate\Database\Schema\IndexDefinition + * @return \Illuminate\Support\Fluent */ - public function vectorIndex($column, $name = null) + public function smallIncrements($column) { - return $this->indexCommand('vectorIndex', $column, $name, 'hnsw', 'vector_cosine_ops'); + return $this->unsignedSmallInteger($column, true); } /** - * Specify a raw index for the table. + * Create a new auto-incrementing medium integer (3-byte) column on the table. * - * @param string $expression - * @param string $name - * @return \Illuminate\Database\Schema\IndexDefinition + * @param string $column + * @return \Illuminate\Support\Fluent */ - public function rawIndex($expression, $name) + public function mediumIncrements($column) { - return $this->index([new Expression($expression)], $name); + return $this->unsignedMediumInteger($column, true); } /** - * Specify a foreign key for the table. + * Create a new auto-incrementing big integer (8-byte) column on the table. * - * @param string|array $columns - * @param string|null $name - * @return \Illuminate\Database\Schema\ForeignKeyDefinition + * @param string $column + * @return \Illuminate\Support\Fluent */ - public function foreign($columns, $name = null) + public function bigIncrements($column) { - $command = new ForeignKeyDefinition( - $this->indexCommand('foreign', $columns, $name)->getAttributes() - ); - - $this->commands[count($this->commands) - 1] = $command; - - return $command; + return $this->unsignedBigInteger($column, true); } /** - * Create a new auto-incrementing big integer column on the table (8-byte, 0 to 18,446,744,073,709,551,615). + * Create a new char column on the table. * * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @param int $length + * @return \Illuminate\Support\Fluent */ - public function id($column = 'id') + public function char($column, $length = 255) { - return $this->bigIncrements($column); + return $this->addColumn('char', $column, compact('length')); } /** - * Create a new auto-incrementing integer column on the table (4-byte, 0 to 4,294,967,295). + * Create a new string column on the table. * * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @param int $length + * @return \Illuminate\Support\Fluent */ - public function increments($column) + public function string($column, $length = 255) { - return $this->unsignedInteger($column, true); + return $this->addColumn('string', $column, compact('length')); } /** - * Create a new auto-incrementing integer column on the table (4-byte, 0 to 4,294,967,295). + * Create a new text column on the table. * * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ - public function integerIncrements($column) + public function text($column) { - return $this->unsignedInteger($column, true); + return $this->addColumn('text', $column); } /** - * Create a new auto-incrementing tiny integer column on the table (1-byte, 0 to 255). + * Create a new medium text column on the table. * * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ - public function tinyIncrements($column) + public function mediumText($column) { - return $this->unsignedTinyInteger($column, true); + return $this->addColumn('mediumText', $column); } /** - * Create a new auto-incrementing small integer column on the table (2-byte, 0 to 65,535). + * Create a new long text column on the table. * * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ - public function smallIncrements($column) + public function longText($column) { - return $this->unsignedSmallInteger($column, true); + return $this->addColumn('longText', $column); } /** - * Create a new auto-incrementing medium integer column on the table (3-byte, 0 to 16,777,215). + * Create a new integer (4-byte) column on the table. * * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @param bool $autoIncrement + * @param bool $unsigned + * @return \Illuminate\Support\Fluent */ - public function mediumIncrements($column) + public function integer($column, $autoIncrement = false, $unsigned = false) { - return $this->unsignedMediumInteger($column, true); - } - - /** - * Create a new auto-incrementing big integer column on the table (8-byte, 0 to 18,446,744,073,709,551,615). - * - * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function bigIncrements($column) - { - return $this->unsignedBigInteger($column, true); - } - - /** - * Create a new char column on the table. - * - * @param string $column - * @param int|null $length - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function char($column, $length = null) - { - $length = ! is_null($length) ? $length : Builder::$defaultStringLength; - - return $this->addColumn('char', $column, compact('length')); - } - - /** - * Create a new string column on the table. - * - * @param string $column - * @param int|null $length - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function string($column, $length = null) - { - $length = $length ?: Builder::$defaultStringLength; - - return $this->addColumn('string', $column, compact('length')); - } - - /** - * Create a new tiny text column on the table (up to 255 characters). - * - * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function tinyText($column) - { - return $this->addColumn('tinyText', $column); - } - - /** - * Create a new text column on the table (up to 65,535 characters / ~64 KB). - * - * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function text($column) - { - return $this->addColumn('text', $column); - } - - /** - * Create a new medium text column on the table (up to 16,777,215 characters / ~16 MB). - * - * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function mediumText($column) - { - return $this->addColumn('mediumText', $column); - } - - /** - * Create a new long text column on the table (up to 4,294,967,295 characters / ~4 GB). - * - * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function longText($column) - { - return $this->addColumn('longText', $column); - } - - /** - * Create a new integer (4-byte) column on the table. - * Range: -2,147,483,648 to 2,147,483,647 (signed) or 0 to 4,294,967,295 (unsigned). - * - * @param string $column - * @param bool $autoIncrement - * @param bool $unsigned - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function integer($column, $autoIncrement = false, $unsigned = false) - { - return $this->addColumn('integer', $column, compact('autoIncrement', 'unsigned')); + return $this->addColumn('integer', $column, compact('autoIncrement', 'unsigned')); } /** * Create a new tiny integer (1-byte) column on the table. - * Range: -128 to 127 (signed) or 0 to 255 (unsigned). * * @param string $column * @param bool $autoIncrement * @param bool $unsigned - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ public function tinyInteger($column, $autoIncrement = false, $unsigned = false) { @@ -923,12 +517,11 @@ public function tinyInteger($column, $autoIncrement = false, $unsigned = false) /** * Create a new small integer (2-byte) column on the table. - * Range: -32,768 to 32,767 (signed) or 0 to 65,535 (unsigned). * * @param string $column * @param bool $autoIncrement * @param bool $unsigned - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ public function smallInteger($column, $autoIncrement = false, $unsigned = false) { @@ -937,12 +530,11 @@ public function smallInteger($column, $autoIncrement = false, $unsigned = false) /** * Create a new medium integer (3-byte) column on the table. - * Range: -8,388,608 to 8,388,607 (signed) or 0 to 16,777,215 (unsigned). * * @param string $column * @param bool $autoIncrement * @param bool $unsigned - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ public function mediumInteger($column, $autoIncrement = false, $unsigned = false) { @@ -951,12 +543,11 @@ public function mediumInteger($column, $autoIncrement = false, $unsigned = false /** * Create a new big integer (8-byte) column on the table. - * Range: -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 (signed) or 0 to 18,446,744,073,709,551,615 (unsigned). * * @param string $column * @param bool $autoIncrement * @param bool $unsigned - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ public function bigInteger($column, $autoIncrement = false, $unsigned = false) { @@ -964,23 +555,11 @@ public function bigInteger($column, $autoIncrement = false, $unsigned = false) } /** - * Create a new unsigned integer column on the table (4-byte, 0 to 4,294,967,295). + * Create a new unsigned tiny integer (1-byte) column on the table. * * @param string $column * @param bool $autoIncrement - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function unsignedInteger($column, $autoIncrement = false) - { - return $this->integer($column, $autoIncrement, true); - } - - /** - * Create a new unsigned tiny integer column on the table (1-byte, 0 to 255). - * - * @param string $column - * @param bool $autoIncrement - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ public function unsignedTinyInteger($column, $autoIncrement = false) { @@ -988,11 +567,11 @@ public function unsignedTinyInteger($column, $autoIncrement = false) } /** - * Create a new unsigned small integer column on the table (2-byte, 0 to 65,535). + * Create a new unsigned small integer (2-byte) column on the table. * * @param string $column * @param bool $autoIncrement - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ public function unsignedSmallInteger($column, $autoIncrement = false) { @@ -1000,11 +579,11 @@ public function unsignedSmallInteger($column, $autoIncrement = false) } /** - * Create a new unsigned medium integer column on the table (3-byte, 0 to 16,777,215). + * Create a new unsigned medium integer (3-byte) column on the table. * * @param string $column * @param bool $autoIncrement - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ public function unsignedMediumInteger($column, $autoIncrement = false) { @@ -1012,97 +591,62 @@ public function unsignedMediumInteger($column, $autoIncrement = false) } /** - * Create a new unsigned big integer column on the table (8-byte, 0 to 18,446,744,073,709,551,615). + * Create a new unsigned integer (4-byte) column on the table. * * @param string $column * @param bool $autoIncrement - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ - public function unsignedBigInteger($column, $autoIncrement = false) + public function unsignedInteger($column, $autoIncrement = false) { - return $this->bigInteger($column, $autoIncrement, true); + return $this->integer($column, $autoIncrement, true); } /** - * Create a new unsigned big integer column on the table (8-byte, 0 to 18,446,744,073,709,551,615). + * Create a new unsigned big integer (8-byte) column on the table. * * @param string $column - * @return \Illuminate\Database\Schema\ForeignIdColumnDefinition - */ - public function foreignId($column) - { - return $this->addColumnDefinition(new ForeignIdColumnDefinition($this, [ - 'type' => 'bigInteger', - 'name' => $column, - 'autoIncrement' => false, - 'unsigned' => true, - ])); - } - - /** - * Create a foreign ID column for the given model. - * - * @param \Illuminate\Database\Eloquent\Model|string $model - * @param string|null $column - * @return \Illuminate\Database\Schema\ForeignIdColumnDefinition + * @param bool $autoIncrement + * @return \Illuminate\Support\Fluent */ - public function foreignIdFor($model, $column = null) + public function unsignedBigInteger($column, $autoIncrement = false) { - if (is_string($model)) { - $model = new $model; - } - - $column = $column ?: $model->getForeignKey(); - - if ($model->getKeyType() === 'int') { - return $this->foreignId($column) - ->table($model->getTable()) - ->referencesModelColumn($model->getKeyName()); - } - - $modelTraits = class_uses_recursive($model); - - if (in_array(HasUlids::class, $modelTraits, true)) { - return $this->foreignUlid($column, 26) - ->table($model->getTable()) - ->referencesModelColumn($model->getKeyName()); - } - - return $this->foreignUuid($column) - ->table($model->getTable()) - ->referencesModelColumn($model->getKeyName()); + return $this->bigInteger($column, $autoIncrement, true); } /** * Create a new float column on the table. * * @param string $column - * @param int $precision - * @return \Illuminate\Database\Schema\ColumnDefinition + * @param int $total + * @param int $places + * @return \Illuminate\Support\Fluent */ - public function float($column, $precision = 53) + public function float($column, $total = 8, $places = 2) { - return $this->addColumn('float', $column, compact('precision')); + return $this->addColumn('float', $column, compact('total', 'places')); } /** * Create a new double column on the table. * - * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @param string $column + * @param int|null $total + * @param int|null $places + * @return \Illuminate\Support\Fluent */ - public function double($column) + public function double($column, $total = null, $places = null) { - return $this->addColumn('double', $column); + return $this->addColumn('double', $column, compact('total', 'places')); } /** * Create a new decimal column on the table. * * @param string $column - * @param int $total - * @param int $places - * @return \Illuminate\Database\Schema\ColumnDefinition + * @param int $total + * @param int $places + * @return \Illuminate\Support\Fluent */ public function decimal($column, $total = 8, $places = 2) { @@ -1113,7 +657,7 @@ public function decimal($column, $total = 8, $places = 2) * Create a new boolean column on the table. * * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ public function boolean($column) { @@ -1124,33 +668,19 @@ public function boolean($column) * Create a new enum column on the table. * * @param string $column - * @param array $allowed - * @return \Illuminate\Database\Schema\ColumnDefinition + * @param array $allowed + * @return \Illuminate\Support\Fluent */ public function enum($column, array $allowed) { - $allowed = array_map(fn ($value) => enum_value($value), $allowed); - return $this->addColumn('enum', $column, compact('allowed')); } - /** - * Create a new set column on the table. - * - * @param string $column - * @param array $allowed - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function set($column, array $allowed) - { - return $this->addColumn('set', $column, compact('allowed')); - } - /** * Create a new json column on the table. * * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ public function json($column) { @@ -1161,7 +691,7 @@ public function json($column) * Create a new jsonb column on the table. * * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ public function jsonb($column) { @@ -1172,7 +702,7 @@ public function jsonb($column) * Create a new date column on the table. * * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ public function date($column) { @@ -1183,98 +713,66 @@ public function date($column) * Create a new date-time column on the table. * * @param string $column - * @param int|null $precision - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ - public function dateTime($column, $precision = null) + public function dateTime($column) { - $precision ??= $this->defaultTimePrecision(); - - return $this->addColumn('dateTime', $column, compact('precision')); + return $this->addColumn('dateTime', $column); } /** * Create a new date-time column (with time zone) on the table. * * @param string $column - * @param int|null $precision - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ - public function dateTimeTz($column, $precision = null) + public function dateTimeTz($column) { - $precision ??= $this->defaultTimePrecision(); - - return $this->addColumn('dateTimeTz', $column, compact('precision')); + return $this->addColumn('dateTimeTz', $column); } /** * Create a new time column on the table. * * @param string $column - * @param int|null $precision - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ - public function time($column, $precision = null) + public function time($column) { - $precision ??= $this->defaultTimePrecision(); - - return $this->addColumn('time', $column, compact('precision')); + return $this->addColumn('time', $column); } /** * Create a new time column (with time zone) on the table. * * @param string $column - * @param int|null $precision - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ - public function timeTz($column, $precision = null) + public function timeTz($column) { - $precision ??= $this->defaultTimePrecision(); - - return $this->addColumn('timeTz', $column, compact('precision')); + return $this->addColumn('timeTz', $column); } /** * Create a new timestamp column on the table. * * @param string $column - * @param int|null $precision - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ - public function timestamp($column, $precision = null) + public function timestamp($column) { - $precision ??= $this->defaultTimePrecision(); - - return $this->addColumn('timestamp', $column, compact('precision')); + return $this->addColumn('timestamp', $column); } /** * Create a new timestamp (with time zone) column on the table. * * @param string $column - * @param int|null $precision - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function timestampTz($column, $precision = null) - { - $precision ??= $this->defaultTimePrecision(); - - return $this->addColumn('timestampTz', $column, compact('precision')); - } - - /** - * Add nullable creation and update timestamps to the table. - * - * @param int|null $precision - * @return \Illuminate\Support\Collection + * @return \Illuminate\Support\Fluent */ - public function timestamps($precision = null) + public function timestampTz($column) { - return new Collection([ - $this->timestamp('created_at', $precision)->nullable(), - $this->timestamp('updated_at', $precision)->nullable(), - ]); + return $this->addColumn('timestampTz', $column); } /** @@ -1282,175 +780,76 @@ public function timestamps($precision = null) * * Alias for self::timestamps(). * - * @param int|null $precision - * @return \Illuminate\Support\Collection - */ - public function nullableTimestamps($precision = null) - { - return $this->timestamps($precision); - } - - /** - * Add nullable creation and update timestampTz columns to the table. - * - * @param int|null $precision - * @return \Illuminate\Support\Collection - */ - public function timestampsTz($precision = null) - { - return new Collection([ - $this->timestampTz('created_at', $precision)->nullable(), - $this->timestampTz('updated_at', $precision)->nullable(), - ]); - } - - /** - * Add nullable creation and update timestampTz columns to the table. - * - * Alias for self::timestampsTz(). - * - * @param int|null $precision - * @return \Illuminate\Support\Collection + * @return void */ - public function nullableTimestampsTz($precision = null) + public function nullableTimestamps() { - return $this->timestampsTz($precision); + $this->timestamps(); } /** - * Add creation and update datetime columns to the table. + * Add nullable creation and update timestamps to the table. * - * @param int|null $precision - * @return \Illuminate\Support\Collection + * @return void */ - public function datetimes($precision = null) + public function timestamps() { - return new Collection([ - $this->datetime('created_at', $precision)->nullable(), - $this->datetime('updated_at', $precision)->nullable(), - ]); - } + $this->timestamp('created_at')->nullable(); - /** - * Add a "deleted at" timestamp for the table. - * - * @param string $column - * @param int|null $precision - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function softDeletes($column = 'deleted_at', $precision = null) - { - return $this->timestamp($column, $precision)->nullable(); + $this->timestamp('updated_at')->nullable(); } /** - * Add a "deleted at" timestampTz for the table. + * Add creation and update timestampTz columns to the table. * - * @param string $column - * @param int|null $precision - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return void */ - public function softDeletesTz($column = 'deleted_at', $precision = null) + public function timestampsTz() { - return $this->timestampTz($column, $precision)->nullable(); - } + $this->timestampTz('created_at')->nullable(); - /** - * Add a "deleted at" datetime column to the table. - * - * @param string $column - * @param int|null $precision - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function softDeletesDatetime($column = 'deleted_at', $precision = null) - { - return $this->dateTime($column, $precision)->nullable(); + $this->timestampTz('updated_at')->nullable(); } /** - * Create a new year column on the table. + * Add a "deleted at" timestamp for the table. * - * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ - public function year($column) + public function softDeletes() { - return $this->addColumn('year', $column); + return $this->timestamp('deleted_at')->nullable(); } /** * Create a new binary column on the table. * * @param string $column - * @param int|null $length - * @param bool $fixed - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ - public function binary($column, $length = null, $fixed = false) + public function binary($column) { - return $this->addColumn('binary', $column, compact('length', 'fixed')); + return $this->addColumn('binary', $column); } /** - * Create a new UUID column on the table. + * Create a new uuid column on the table. * * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ - public function uuid($column = 'uuid') + public function uuid($column) { return $this->addColumn('uuid', $column); } - /** - * Create a new UUID column on the table with a foreign key constraint. - * - * @param string $column - * @return \Illuminate\Database\Schema\ForeignIdColumnDefinition - */ - public function foreignUuid($column) - { - return $this->addColumnDefinition(new ForeignIdColumnDefinition($this, [ - 'type' => 'uuid', - 'name' => $column, - ])); - } - - /** - * Create a new ULID column on the table. - * - * @param string $column - * @param int|null $length - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function ulid($column = 'ulid', $length = 26) - { - return $this->char($column, $length); - } - - /** - * Create a new ULID column on the table with a foreign key constraint. - * - * @param string $column - * @param int|null $length - * @return \Illuminate\Database\Schema\ForeignIdColumnDefinition - */ - public function foreignUlid($column, $length = 26) - { - return $this->addColumnDefinition(new ForeignIdColumnDefinition($this, [ - 'type' => 'char', - 'name' => $column, - 'length' => $length, - ])); - } - /** * Create a new IP address column on the table. * * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ - public function ipAddress($column = 'ip_address') + public function ipAddress($column) { return $this->addColumn('ipAddress', $column); } @@ -1459,280 +858,39 @@ public function ipAddress($column = 'ip_address') * Create a new MAC address column on the table. * * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ - public function macAddress($column = 'mac_address') + public function macAddress($column) { return $this->addColumn('macAddress', $column); } - /** - * Create a new geometry column on the table. - * - * @param string $column - * @param string|null $subtype - * @param int $srid - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function geometry($column, $subtype = null, $srid = 0) - { - return $this->addColumn('geometry', $column, compact('subtype', 'srid')); - } - - /** - * Create a new geography column on the table. - * - * @param string $column - * @param string|null $subtype - * @param int $srid - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function geography($column, $subtype = null, $srid = 4326) - { - return $this->addColumn('geography', $column, compact('subtype', 'srid')); - } - - /** - * Create a new generated, computed column on the table. - * - * @param string $column - * @param string $expression - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function computed($column, $expression) - { - return $this->addColumn('computed', $column, compact('expression')); - } - - /** - * Create a new vector column on the table. - * - * @param string $column - * @param int|null $dimensions - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function vector($column, $dimensions = null) - { - $options = $dimensions ? compact('dimensions') : []; - - return $this->addColumn('vector', $column, $options); - } - /** * Add the proper columns for a polymorphic table. * * @param string $name * @param string|null $indexName - * @param string|null $after - * @return void - */ - public function morphs($name, $indexName = null, $after = null) - { - if (Builder::$defaultMorphKeyType === 'uuid') { - $this->uuidMorphs($name, $indexName, $after); - } elseif (Builder::$defaultMorphKeyType === 'ulid') { - $this->ulidMorphs($name, $indexName, $after); - } else { - $this->numericMorphs($name, $indexName, $after); - } - } - - /** - * Add nullable columns for a polymorphic table. - * - * @param string $name - * @param string|null $indexName - * @param string|null $after - * @return void - */ - public function nullableMorphs($name, $indexName = null, $after = null) - { - if (Builder::$defaultMorphKeyType === 'uuid') { - $this->nullableUuidMorphs($name, $indexName, $after); - } elseif (Builder::$defaultMorphKeyType === 'ulid') { - $this->nullableUlidMorphs($name, $indexName, $after); - } else { - $this->nullableNumericMorphs($name, $indexName, $after); - } - } - - /** - * Add the proper columns for a polymorphic table using numeric IDs (incremental). - * - * @param string $name - * @param string|null $indexName - * @param string|null $after - * @return void - */ - public function numericMorphs($name, $indexName = null, $after = null) - { - $this->string("{$name}_type") - ->after($after); - - $this->unsignedBigInteger("{$name}_id") - ->after(! is_null($after) ? "{$name}_type" : null); - - $this->index(["{$name}_type", "{$name}_id"], $indexName); - } - - /** - * Add nullable columns for a polymorphic table using numeric IDs (incremental). - * - * @param string $name - * @param string|null $indexName - * @param string|null $after - * @return void - */ - public function nullableNumericMorphs($name, $indexName = null, $after = null) - { - $this->string("{$name}_type") - ->nullable() - ->after($after); - - $this->unsignedBigInteger("{$name}_id") - ->nullable() - ->after(! is_null($after) ? "{$name}_type" : null); - - $this->index(["{$name}_type", "{$name}_id"], $indexName); - } - - /** - * Add the proper columns for a polymorphic table using UUIDs. - * - * @param string $name - * @param string|null $indexName - * @param string|null $after - * @return void - */ - public function uuidMorphs($name, $indexName = null, $after = null) - { - $this->string("{$name}_type") - ->after($after); - - $this->uuid("{$name}_id") - ->after(! is_null($after) ? "{$name}_type" : null); - - $this->index(["{$name}_type", "{$name}_id"], $indexName); - } - - /** - * Add nullable columns for a polymorphic table using UUIDs. - * - * @param string $name - * @param string|null $indexName - * @param string|null $after - * @return void - */ - public function nullableUuidMorphs($name, $indexName = null, $after = null) - { - $this->string("{$name}_type") - ->nullable() - ->after($after); - - $this->uuid("{$name}_id") - ->nullable() - ->after(! is_null($after) ? "{$name}_type" : null); - - $this->index(["{$name}_type", "{$name}_id"], $indexName); - } - - /** - * Add the proper columns for a polymorphic table using ULIDs. - * - * @param string $name - * @param string|null $indexName - * @param string|null $after - * @return void - */ - public function ulidMorphs($name, $indexName = null, $after = null) - { - $this->string("{$name}_type") - ->after($after); - - $this->ulid("{$name}_id") - ->after(! is_null($after) ? "{$name}_type" : null); - - $this->index(["{$name}_type", "{$name}_id"], $indexName); - } - - /** - * Add nullable columns for a polymorphic table using ULIDs. - * - * @param string $name - * @param string|null $indexName - * @param string|null $after * @return void */ - public function nullableUlidMorphs($name, $indexName = null, $after = null) + public function morphs($name, $indexName = null) { - $this->string("{$name}_type") - ->nullable() - ->after($after); + $this->unsignedInteger("{$name}_id"); - $this->ulid("{$name}_id") - ->nullable() - ->after(! is_null($after) ? "{$name}_type" : null); + $this->string("{$name}_type"); - $this->index(["{$name}_type", "{$name}_id"], $indexName); + $this->index(["{$name}_id", "{$name}_type"], $indexName); } /** - * Add the `remember_token` column to the table. + * Adds the `remember_token` column to the table. * - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ public function rememberToken() { return $this->string('remember_token', 100)->nullable(); } - /** - * Create a new custom column on the table. - * - * @param string $column - * @param string $definition - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function rawColumn($column, $definition) - { - return $this->addColumn('raw', $column, compact('definition')); - } - - /** - * Add a comment to the table. - * - * @param string $comment - * @return \Illuminate\Support\Fluent - */ - public function comment($comment) - { - return $this->addCommand('tableComment', compact('comment')); - } - - /** - * Create a new index command on the blueprint. - * - * @param string $type - * @param string|array $columns - * @param string $index - * @param string|null $algorithm - * @param string|null $operatorClass - * @return \Illuminate\Support\Fluent - */ - protected function indexCommand($type, $columns, $index, $algorithm = null, $operatorClass = null) - { - $columns = (array) $columns; - - // If no name was specified for this index, we will create one using a basic - // convention of the table name, followed by the columns, followed by an - // index type, such as primary or index, which makes the index unique. - $index = $index ?: $this->createIndexName($type, $columns); - - return $this->addCommand( - $type, compact('index', 'columns', 'algorithm', 'operatorClass') - ); - } - /** * Create a new drop index command on the blueprint. * @@ -1749,86 +907,66 @@ protected function dropIndexCommand($command, $type, $index) // to drop an index merely by specifying the columns involved without the // conventional name, so we will build the index name from the columns. if (is_array($index)) { - $index = $this->createIndexName($type, $columns = $index); + $columns = $index; + + $index = $this->createIndexName($type, $columns); } return $this->indexCommand($command, $columns, $index); } /** - * Create a default index name for the table. + * Add a new index command to the blueprint. * - * @param string $type - * @param array $columns - * @return string + * @param string $type + * @param string|array $columns + * @param string $index + * @param string|null $algorithm + * @return \Illuminate\Support\Fluent */ - protected function createIndexName($type, array $columns) + protected function indexCommand($type, $columns, $index, $algorithm = null) { - $table = $this->table; + $columns = (array) $columns; - if ($this->connection->getConfig('prefix_indexes')) { - $table = str_contains($this->table, '.') - ? substr_replace($this->table, '.'.$this->connection->getTablePrefix(), strrpos($this->table, '.'), 1) - : $this->connection->getTablePrefix().$this->table; + // If no name was specified for this index, we will create one using a basic + // convention of the table name, followed by the columns, followed by an + // index type, such as primary or index, which makes the index unique. + if (is_null($index)) { + $index = $this->createIndexName($type, $columns); } - $index = strtolower($table.'_'.implode('_', $columns).'_'.$type); - - return str_replace(['-', '.'], '_', $index); + return $this->addCommand($type, compact('index', 'columns', 'algorithm')); } /** - * Add a new column to the blueprint. + * Create a default index name for the table. * * @param string $type - * @param string $name - * @param array $parameters - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function addColumn($type, $name, array $parameters = []) - { - return $this->addColumnDefinition(new ColumnDefinition( - array_merge(compact('type', 'name'), $parameters) - )); - } - - /** - * Add a new column definition to the blueprint. - * - * @param \Illuminate\Database\Schema\ColumnDefinition $definition - * @return \Illuminate\Database\Schema\ColumnDefinition + * @param array $columns + * @return string */ - protected function addColumnDefinition($definition) + protected function createIndexName($type, array $columns) { - $this->columns[] = $definition; - - if (! $this->creating()) { - $this->commands[] = $definition; - } + $index = strtolower($this->table.'_'.implode('_', $columns).'_'.$type); - if ($this->after) { - $definition->after($this->after); - - $this->after = $definition->name; - } - - return $definition; + return str_replace(['-', '.'], '_', $index); } /** - * Add the columns from the callback after the given column. + * Add a new column to the blueprint. * - * @param string $column - * @param (\Closure(self): void) $callback - * @return void + * @param string $type + * @param string $name + * @param array $parameters + * @return \Illuminate\Support\Fluent */ - public function after($column, Closure $callback) + public function addColumn($type, $name, array $parameters = []) { - $this->after = $column; + $attributes = array_merge(compact('type', 'name'), $parameters); - $callback($this); + $this->columns[] = $column = new Fluent($attributes); - $this->after = null; + return $column; } /** @@ -1840,11 +978,7 @@ public function after($column, Closure $callback) public function removeColumn($name) { $this->columns = array_values(array_filter($this->columns, function ($c) use ($name) { - return $c['name'] != $name; - })); - - $this->commands = array_values(array_filter($this->commands, function ($c) use ($name) { - return ! $c instanceof ColumnDefinition || $c['name'] != $name; + return $c['attributes']['name'] != $name; })); return $this; @@ -1868,7 +1002,7 @@ protected function addCommand($name, array $parameters = []) * Create a new Fluent command. * * @param string $name - * @param array $parameters + * @param array $parameters * @return \Illuminate\Support\Fluent */ protected function createCommand($name, array $parameters = []) @@ -1886,22 +1020,10 @@ public function getTable() return $this->table; } - /** - * Get the table prefix. - * - * @deprecated Use DB::getTablePrefix() - * - * @return string - */ - public function getPrefix() - { - return $this->connection->getTablePrefix(); - } - /** * Get the columns on the blueprint. * - * @return \Illuminate\Database\Schema\ColumnDefinition[] + * @return array */ public function getColumns() { @@ -1911,37 +1033,17 @@ public function getColumns() /** * Get the commands on the blueprint. * - * @return \Illuminate\Support\Fluent[] + * @return array */ public function getCommands() { return $this->commands; } - /** - * Determine if the blueprint has state. - * - * @return bool - */ - private function hasState(): bool - { - return ! is_null($this->state); - } - - /** - * Get the state of the blueprint. - * - * @return \Illuminate\Database\Schema\BlueprintState - */ - public function getState() - { - return $this->state; - } - /** * Get the columns on the blueprint that should be added. * - * @return \Illuminate\Database\Schema\ColumnDefinition[] + * @return array */ public function getAddedColumns() { @@ -1953,9 +1055,7 @@ public function getAddedColumns() /** * Get the columns on the blueprint that should be changed. * - * @deprecated Will be removed in a future Laravel version. - * - * @return \Illuminate\Database\Schema\ColumnDefinition[] + * @return array */ public function getChangedColumns() { @@ -1963,12 +1063,4 @@ public function getChangedColumns() return (bool) $column->change; }); } - - /** - * Get the default time precision. - */ - protected function defaultTimePrecision(): ?int - { - return $this->connection->getSchemaBuilder()::$defaultTimePrecision; - } } diff --git a/Schema/BlueprintState.php b/Schema/BlueprintState.php deleted file mode 100644 index a4ad1149d4..0000000000 --- a/Schema/BlueprintState.php +++ /dev/null @@ -1,247 +0,0 @@ -blueprint = $blueprint; - $this->connection = $connection; - - $schema = $connection->getSchemaBuilder(); - $table = $blueprint->getTable(); - - $this->columns = (new Collection($schema->getColumns($table)))->map(fn ($column) => new ColumnDefinition([ - 'name' => $column['name'], - 'type' => $column['type_name'], - 'full_type_definition' => $column['type'], - 'nullable' => $column['nullable'], - 'default' => is_null($column['default']) ? null : new Expression(Str::wrap($column['default'], '(', ')')), - 'autoIncrement' => $column['auto_increment'], - 'collation' => $column['collation'], - 'comment' => $column['comment'], - 'virtualAs' => ! is_null($column['generation']) && $column['generation']['type'] === 'virtual' - ? $column['generation']['expression'] - : null, - 'storedAs' => ! is_null($column['generation']) && $column['generation']['type'] === 'stored' - ? $column['generation']['expression'] - : null, - ]))->all(); - - [$primary, $indexes] = (new Collection($schema->getIndexes($table)))->map(fn ($index) => new IndexDefinition([ - 'name' => match (true) { - $index['primary'] => 'primary', - $index['unique'] => 'unique', - default => 'index', - }, - 'index' => $index['name'], - 'columns' => $index['columns'], - ]))->partition(fn ($index) => $index->name === 'primary'); - - $this->indexes = $indexes->all(); - $this->primaryKey = $primary->first(); - - $this->foreignKeys = (new Collection($schema->getForeignKeys($table)))->map(fn ($foreignKey) => new ForeignKeyDefinition([ - 'columns' => $foreignKey['columns'], - 'on' => new Expression($foreignKey['foreign_table']), - 'references' => $foreignKey['foreign_columns'], - 'onUpdate' => $foreignKey['on_update'], - 'onDelete' => $foreignKey['on_delete'], - ]))->all(); - } - - /** - * Get the primary key. - * - * @return \Illuminate\Database\Schema\IndexDefinition|null - */ - public function getPrimaryKey() - { - return $this->primaryKey; - } - - /** - * Get the columns. - * - * @return \Illuminate\Database\Schema\ColumnDefinition[] - */ - public function getColumns() - { - return $this->columns; - } - - /** - * Get the indexes. - * - * @return \Illuminate\Database\Schema\IndexDefinition[] - */ - public function getIndexes() - { - return $this->indexes; - } - - /** - * Get the foreign keys. - * - * @return \Illuminate\Database\Schema\ForeignKeyDefinition[] - */ - public function getForeignKeys() - { - return $this->foreignKeys; - } - - /* - * Update the blueprint's state. - * - * @param \Illuminate\Support\Fluent $command - * @return void - */ - public function update(Fluent $command) - { - switch ($command->name) { - case 'alter': - // Already handled... - break; - - case 'add': - $this->columns[] = $command->column; - break; - - case 'change': - foreach ($this->columns as &$column) { - if ($column->name === $command->column->name) { - $column = $command->column; - break; - } - } - - break; - - case 'renameColumn': - foreach ($this->columns as $column) { - if ($column->name === $command->from) { - $column->name = $command->to; - break; - } - } - - if ($this->primaryKey) { - $this->primaryKey->columns = str_replace($command->from, $command->to, $this->primaryKey->columns); - } - - foreach ($this->indexes as $index) { - $index->columns = str_replace($command->from, $command->to, $index->columns); - } - - foreach ($this->foreignKeys as $foreignKey) { - $foreignKey->columns = str_replace($command->from, $command->to, $foreignKey->columns); - } - - break; - - case 'dropColumn': - $this->columns = array_values( - array_filter($this->columns, fn ($column) => ! in_array($column->name, $command->columns)) - ); - - break; - - case 'primary': - $this->primaryKey = $command; - break; - - case 'unique': - case 'index': - $this->indexes[] = $command; - break; - - case 'renameIndex': - foreach ($this->indexes as $index) { - if ($index->index === $command->from) { - $index->index = $command->to; - break; - } - } - - break; - - case 'foreign': - $this->foreignKeys[] = $command; - break; - - case 'dropPrimary': - $this->primaryKey = null; - break; - - case 'dropIndex': - case 'dropUnique': - $this->indexes = array_values( - array_filter($this->indexes, fn ($index) => $index->index !== $command->index) - ); - - break; - - case 'dropForeign': - $this->foreignKeys = array_values( - array_filter($this->foreignKeys, fn ($fk) => $fk->columns !== $command->columns) - ); - - break; - } - } -} diff --git a/Schema/Builder.php b/Schema/Builder.php index 180e647743..74341dd7f1 100755 --- a/Schema/Builder.php +++ b/Schema/Builder.php @@ -3,18 +3,10 @@ namespace Illuminate\Database\Schema; use Closure; -use Illuminate\Container\Container; use Illuminate\Database\Connection; -use Illuminate\Database\PostgresConnection; -use Illuminate\Support\Traits\Macroable; -use InvalidArgumentException; -use LogicException; -use RuntimeException; class Builder { - use Macroable; - /** * The database connection instance. * @@ -32,33 +24,15 @@ class Builder /** * The Blueprint resolver callback. * - * @var \Closure(\Illuminate\Database\Connection, string, \Closure|null): \Illuminate\Database\Schema\Blueprint + * @var \Closure */ protected $resolver; - /** - * The default string length for migrations. - * - * @var non-negative-int|null - */ - public static $defaultStringLength = 255; - - /** - * The default time precision for migrations. - */ - public static ?int $defaultTimePrecision = 0; - - /** - * The default relationship morph key type. - * - * @var 'int'|'uuid'|'ulid' - */ - public static $defaultMorphKeyType = 'int'; - /** * Create a new database Schema manager. * * @param \Illuminate\Database\Connection $connection + * @return void */ public function __construct(Connection $connection) { @@ -66,100 +40,6 @@ public function __construct(Connection $connection) $this->grammar = $connection->getSchemaGrammar(); } - /** - * Set the default string length for migrations. - * - * @param non-negative-int $length - * @return void - */ - public static function defaultStringLength($length) - { - static::$defaultStringLength = $length; - } - - /** - * Set the default time precision for migrations. - */ - public static function defaultTimePrecision(?int $precision): void - { - static::$defaultTimePrecision = $precision; - } - - /** - * Set the default morph key type for migrations. - * - * @param string $type - * @return void - * - * @throws \InvalidArgumentException - */ - public static function defaultMorphKeyType(string $type) - { - if (! in_array($type, ['int', 'uuid', 'ulid'])) { - throw new InvalidArgumentException("Morph key type must be 'int', 'uuid', or 'ulid'."); - } - - static::$defaultMorphKeyType = $type; - } - - /** - * Set the default morph key type for migrations to UUIDs. - * - * @return void - */ - public static function morphUsingUuids() - { - static::defaultMorphKeyType('uuid'); - } - - /** - * Set the default morph key type for migrations to ULIDs. - * - * @return void - */ - public static function morphUsingUlids() - { - static::defaultMorphKeyType('ulid'); - } - - /** - * Create a database in the schema. - * - * @param string $name - * @return bool - */ - public function createDatabase($name) - { - return $this->connection->statement( - $this->grammar->compileCreateDatabase($name) - ); - } - - /** - * Drop a database from the schema if the database exists. - * - * @param string $name - * @return bool - */ - public function dropDatabaseIfExists($name) - { - return $this->connection->statement( - $this->grammar->compileDropDatabaseIfExists($name) - ); - } - - /** - * Get the schemas that belong to the connection. - * - * @return list - */ - public function getSchemas() - { - return $this->connection->getPostProcessor()->processSchemas( - $this->connection->selectFromWriteConnection($this->grammar->compileSchemas()) - ); - } - /** * Determine if the given table exists. * @@ -168,96 +48,11 @@ public function getSchemas() */ public function hasTable($table) { - [$schema, $table] = $this->parseSchemaAndTable($table); + $sql = $this->grammar->compileTableExists(); $table = $this->connection->getTablePrefix().$table; - if ($sql = $this->grammar->compileTableExists($schema, $table)) { - return (bool) $this->connection->scalar($sql); - } - - foreach ($this->getTables($schema ?? $this->getCurrentSchemaName()) as $value) { - if (strtolower($table) === strtolower($value['name'])) { - return true; - } - } - - return false; - } - - /** - * Determine if the given view exists. - * - * @param string $view - * @return bool - */ - public function hasView($view) - { - [$schema, $view] = $this->parseSchemaAndTable($view); - - $view = $this->connection->getTablePrefix().$view; - - foreach ($this->getViews($schema ?? $this->getCurrentSchemaName()) as $value) { - if (strtolower($view) === strtolower($value['name'])) { - return true; - } - } - - return false; - } - - /** - * Get the tables that belong to the connection. - * - * @param string|string[]|null $schema - * @return list - */ - public function getTables($schema = null) - { - return $this->connection->getPostProcessor()->processTables( - $this->connection->selectFromWriteConnection($this->grammar->compileTables($schema)) - ); - } - - /** - * Get the names of the tables that belong to the connection. - * - * @param string|string[]|null $schema - * @param bool $schemaQualified - * @return list - */ - public function getTableListing($schema = null, $schemaQualified = true) - { - return array_column( - $this->getTables($schema), - $schemaQualified ? 'schema_qualified_name' : 'name' - ); - } - - /** - * Get the views that belong to the connection. - * - * @param string|string[]|null $schema - * @return list - */ - public function getViews($schema = null) - { - return $this->connection->getPostProcessor()->processViews( - $this->connection->selectFromWriteConnection($this->grammar->compileViews($schema)) - ); - } - - /** - * Get the user-defined types that belong to the connection. - * - * @param string|string[]|null $schema - * @return list - */ - public function getTypes($schema = null) - { - return $this->connection->getPostProcessor()->processTypes( - $this->connection->selectFromWriteConnection($this->grammar->compileTypes($schema)) - ); + return count($this->connection->select($sql, [$table])) > 0; } /** @@ -269,21 +64,21 @@ public function getTypes($schema = null) */ public function hasColumn($table, $column) { - return in_array( - strtolower($column), array_map(strtolower(...), $this->getColumnListing($table)) - ); + $column = strtolower($column); + + return in_array($column, array_map('strtolower', $this->getColumnListing($table))); } /** * Determine if the given table has given columns. * * @param string $table - * @param array $columns + * @param array $columns * @return bool */ public function hasColumns($table, array $columns) { - $tableColumns = array_map(strtolower(...), $this->getColumnListing($table)); + $tableColumns = array_map('strtolower', $this->getColumnListing($table)); foreach ($columns as $column) { if (! in_array(strtolower($column), $tableColumns)) { @@ -294,202 +89,41 @@ public function hasColumns($table, array $columns) return true; } - /** - * Execute a table builder callback if the given table has a given column. - * - * @param string $table - * @param string $column - * @param \Closure $callback - * @return void - */ - public function whenTableHasColumn(string $table, string $column, Closure $callback) - { - if ($this->hasColumn($table, $column)) { - $this->table($table, fn (Blueprint $table) => $callback($table)); - } - } - - /** - * Execute a table builder callback if the given table doesn't have a given column. - * - * @param string $table - * @param string $column - * @param \Closure $callback - * @return void - */ - public function whenTableDoesntHaveColumn(string $table, string $column, Closure $callback) - { - if (! $this->hasColumn($table, $column)) { - $this->table($table, fn (Blueprint $table) => $callback($table)); - } - } - - /** - * Execute a table builder callback if the given table has a given index. - * - * @param string $table - * @param string|array $index - * @param \Closure $callback - * @param string|null $type - * @return void - */ - public function whenTableHasIndex(string $table, string|array $index, Closure $callback, ?string $type = null) - { - if ($this->hasIndex($table, $index, $type)) { - $this->table($table, fn (Blueprint $table) => $callback($table)); - } - } - - /** - * Execute a table builder callback if the given table doesn't have a given index. - * - * @param string $table - * @param string|array $index - * @param \Closure $callback - * @param string|null $type - * @return void - */ - public function whenTableDoesntHaveIndex(string $table, string|array $index, Closure $callback, ?string $type = null) - { - if (! $this->hasIndex($table, $index, $type)) { - $this->table($table, fn (Blueprint $table) => $callback($table)); - } - } - /** * Get the data type for the given column name. * * @param string $table * @param string $column - * @param bool $fullDefinition * @return string - * - * @throws \InvalidArgumentException */ - public function getColumnType($table, $column, $fullDefinition = false) + public function getColumnType($table, $column) { - $columns = $this->getColumns($table); - - foreach ($columns as $value) { - if (strtolower($value['name']) === strtolower($column)) { - return $fullDefinition ? $value['type'] : $value['type_name']; - } - } + $table = $this->connection->getTablePrefix().$table; - throw new InvalidArgumentException("There is no column with name '$column' on table '$table'."); + return $this->connection->getDoctrineColumn($table, $column)->getType()->getName(); } /** * Get the column listing for a given table. * * @param string $table - * @return list + * @return array */ public function getColumnListing($table) { - return array_column($this->getColumns($table), 'name'); - } - - /** - * Get the columns for a given table. - * - * @param string $table - * @return list - */ - public function getColumns($table) - { - [$schema, $table] = $this->parseSchemaAndTable($table); - $table = $this->connection->getTablePrefix().$table; - return $this->connection->getPostProcessor()->processColumns( - $this->connection->selectFromWriteConnection( - $this->grammar->compileColumns($schema, $table) - ) - ); - } + $results = $this->connection->select($this->grammar->compileColumnExists($table)); - /** - * Get the indexes for a given table. - * - * @param string $table - * @return list, type: string, unique: bool, primary: bool}> - */ - public function getIndexes($table) - { - [$schema, $table] = $this->parseSchemaAndTable($table); - - $table = $this->connection->getTablePrefix().$table; - - return $this->connection->getPostProcessor()->processIndexes( - $this->connection->selectFromWriteConnection( - $this->grammar->compileIndexes($schema, $table) - ) - ); - } - - /** - * Get the names of the indexes for a given table. - * - * @param string $table - * @return list - */ - public function getIndexListing($table) - { - return array_column($this->getIndexes($table), 'name'); - } - - /** - * Determine if the given table has a given index. - * - * @param string $table - * @param string|array $index - * @param string|null $type - * @return bool - */ - public function hasIndex($table, $index, $type = null) - { - $type = is_null($type) ? $type : strtolower($type); - - foreach ($this->getIndexes($table) as $value) { - $typeMatches = is_null($type) - || ($type === 'primary' && $value['primary']) - || ($type === 'unique' && $value['unique']) - || $type === $value['type']; - - if (($value['name'] === $index || $value['columns'] === $index) && $typeMatches) { - return true; - } - } - - return false; - } - - /** - * Get the foreign keys for a given table. - * - * @param string $table - * @return array - */ - public function getForeignKeys($table) - { - [$schema, $table] = $this->parseSchemaAndTable($table); - - $table = $this->connection->getTablePrefix().$table; - - return $this->connection->getPostProcessor()->processForeignKeys( - $this->connection->selectFromWriteConnection( - $this->grammar->compileForeignKeys($schema, $table) - ) - ); + return $this->connection->getPostProcessor()->processColumnListing($results); } /** * Modify a table on the schema. * - * @param string $table + * @param string $table * @param \Closure $callback - * @return void + * @return \Illuminate\Database\Schema\Blueprint */ public function table($table, Closure $callback) { @@ -499,93 +133,49 @@ public function table($table, Closure $callback) /** * Create a new table on the schema. * - * @param string $table + * @param string $table * @param \Closure $callback - * @return void + * @return \Illuminate\Database\Schema\Blueprint */ public function create($table, Closure $callback) { - $this->build(tap($this->createBlueprint($table), function ($blueprint) use ($callback) { - $blueprint->create(); + $blueprint = $this->createBlueprint($table); + + $blueprint->create(); + + $callback($blueprint); - $callback($blueprint); - })); + $this->build($blueprint); } /** * Drop a table from the schema. * * @param string $table - * @return void + * @return \Illuminate\Database\Schema\Blueprint */ public function drop($table) { - $this->build(tap($this->createBlueprint($table), function ($blueprint) { - $blueprint->drop(); - })); + $blueprint = $this->createBlueprint($table); + + $blueprint->drop(); + + $this->build($blueprint); } /** * Drop a table from the schema if it exists. * * @param string $table - * @return void + * @return \Illuminate\Database\Schema\Blueprint */ public function dropIfExists($table) { - $this->build(tap($this->createBlueprint($table), function ($blueprint) { - $blueprint->dropIfExists(); - })); - } + $blueprint = $this->createBlueprint($table); - /** - * Drop columns from a table schema. - * - * @param string $table - * @param string|array $columns - * @return void - */ - public function dropColumns($table, $columns) - { - $this->table($table, function (Blueprint $blueprint) use ($columns) { - $blueprint->dropColumn($columns); - }); - } + $blueprint->dropIfExists(); - /** - * Drop all tables from the database. - * - * @return void - * - * @throws \LogicException - */ - public function dropAllTables() - { - throw new LogicException('This database driver does not support dropping all tables.'); - } - - /** - * Drop all views from the database. - * - * @return void - * - * @throws \LogicException - */ - public function dropAllViews() - { - throw new LogicException('This database driver does not support dropping all views.'); - } - - /** - * Drop all types from the database. - * - * @return void - * - * @throws \LogicException - */ - public function dropAllTypes() - { - throw new LogicException('This database driver does not support dropping all types.'); + $this->build($blueprint); } /** @@ -593,13 +183,15 @@ public function dropAllTypes() * * @param string $from * @param string $to - * @return void + * @return \Illuminate\Database\Schema\Blueprint */ public function rename($from, $to) { - $this->build(tap($this->createBlueprint($from), function ($blueprint) use ($to) { - $blueprint->rename($to); - })); + $blueprint = $this->createBlueprint($from); + + $blueprint->rename($to); + + $this->build($blueprint); } /** @@ -626,59 +218,6 @@ public function disableForeignKeyConstraints() ); } - /** - * Disable foreign key constraints during the execution of a callback. - * - * @template TReturn - * - * @param (\Closure(): TReturn) $callback - * @return TReturn - */ - public function withoutForeignKeyConstraints(Closure $callback) - { - $this->disableForeignKeyConstraints(); - - try { - return $callback(); - } finally { - $this->enableForeignKeyConstraints(); - } - } - - /** - * Create the vector extension on the schema if it does not exist. - * - * @param string|null $schema - * @return void - */ - public function ensureVectorExtensionExists($schema = null) - { - $this->ensureExtensionExists('vector', $schema); - } - - /** - * Create a new extension on the schema if it does not exist. - * - * @param string $name - * @param string|null $schema - * @return void - * - * @throws \RuntimeException - */ - public function ensureExtensionExists($name, $schema = null) - { - if (! $this->getConnection() instanceof PostgresConnection) { - throw new RuntimeException('Extensions are only supported by Postgres.'); - } - - $name = $this->getConnection()->getSchemaGrammar()->wrap($name); - - $this->getConnection()->statement(match (filled($schema)) { - true => "create extension if not exists {$name} schema {$this->getConnection()->getSchemaGrammar()->wrap($schema)}", - false => "create extension if not exists {$name}", - }); - } - /** * Execute the blueprint to build / modify the table. * @@ -687,7 +226,7 @@ public function ensureExtensionExists($name, $schema = null) */ protected function build(Blueprint $blueprint) { - $blueprint->build(); + $blueprint->build($this->connection, $this->grammar); } /** @@ -697,82 +236,42 @@ protected function build(Blueprint $blueprint) * @param \Closure|null $callback * @return \Illuminate\Database\Schema\Blueprint */ - protected function createBlueprint($table, ?Closure $callback = null) + protected function createBlueprint($table, Closure $callback = null) { - $connection = $this->connection; - if (isset($this->resolver)) { - return call_user_func($this->resolver, $connection, $table, $callback); + return call_user_func($this->resolver, $table, $callback); } - return Container::getInstance()->make(Blueprint::class, compact('connection', 'table', 'callback')); - } - - /** - * Get the names of the current schemas for the connection. - * - * @return string[]|null - */ - public function getCurrentSchemaListing() - { - return null; + return new Blueprint($table, $callback); } /** - * Get the default schema name for the connection. + * Get the database connection instance. * - * @return string|null + * @return \Illuminate\Database\Connection */ - public function getCurrentSchemaName() + public function getConnection() { - return $this->getCurrentSchemaListing()[0] ?? null; + return $this->connection; } /** - * Parse the given database object reference and extract the schema and table. + * Set the database connection instance. * - * @param string $reference - * @param string|bool|null $withDefaultSchema - * @return array{string|null, string} - * - * @throws \InvalidArgumentException + * @param \Illuminate\Database\Connection $connection + * @return $this */ - public function parseSchemaAndTable($reference, $withDefaultSchema = null) + public function setConnection(Connection $connection) { - $segments = explode('.', $reference); - - if (count($segments) > 2) { - throw new InvalidArgumentException( - "Using three-part references is not supported, you may use `Schema::connection('{$segments[0]}')` instead." - ); - } - - $table = $segments[1] ?? $segments[0]; - - $schema = match (true) { - isset($segments[1]) => $segments[0], - is_string($withDefaultSchema) => $withDefaultSchema, - $withDefaultSchema => $this->getCurrentSchemaName(), - default => null, - }; - - return [$schema, $table]; - } + $this->connection = $connection; - /** - * Get the database connection instance. - * - * @return \Illuminate\Database\Connection - */ - public function getConnection() - { - return $this->connection; + return $this; } /** * Set the Schema Blueprint resolver callback. * - * @param \Closure(\Illuminate\Database\Connection, string, \Closure|null): \Illuminate\Database\Schema\Blueprint $resolver + * @param \Closure $resolver * @return void */ public function blueprintResolver(Closure $resolver) diff --git a/Schema/ColumnDefinition.php b/Schema/ColumnDefinition.php deleted file mode 100644 index 05618db57c..0000000000 --- a/Schema/ColumnDefinition.php +++ /dev/null @@ -1,41 +0,0 @@ -blueprint = $blueprint; - } - - /** - * Create a foreign key constraint on this column referencing the "id" column of the conventionally related table. - * - * @param string|null $table - * @param string|null $column - * @param string|null $indexName - * @return \Illuminate\Database\Schema\ForeignKeyDefinition - */ - public function constrained($table = null, $column = null, $indexName = null) - { - $table ??= $this->table; - $column ??= $this->referencesModelColumn ?? 'id'; - - return $this->references($column, $indexName)->on($table ?? (new Stringable($this->name))->beforeLast('_'.$column)->plural()); - } - - /** - * Specify which column this foreign ID references on another table. - * - * @param string $column - * @param string|null $indexName - * @return \Illuminate\Database\Schema\ForeignKeyDefinition - */ - public function references($column, $indexName = null) - { - return $this->blueprint->foreign($this->name, $indexName)->references($column); - } -} diff --git a/Schema/ForeignKeyDefinition.php b/Schema/ForeignKeyDefinition.php deleted file mode 100644 index cd17896560..0000000000 --- a/Schema/ForeignKeyDefinition.php +++ /dev/null @@ -1,97 +0,0 @@ -onUpdate('cascade'); - } - - /** - * Indicate that updates should be restricted. - * - * @return $this - */ - public function restrictOnUpdate() - { - return $this->onUpdate('restrict'); - } - - /** - * Indicate that updates should set the foreign key value to null. - * - * @return $this - */ - public function nullOnUpdate() - { - return $this->onUpdate('set null'); - } - - /** - * Indicate that updates should have "no action". - * - * @return $this - */ - public function noActionOnUpdate() - { - return $this->onUpdate('no action'); - } - - /** - * Indicate that deletes should cascade. - * - * @return $this - */ - public function cascadeOnDelete() - { - return $this->onDelete('cascade'); - } - - /** - * Indicate that deletes should be restricted. - * - * @return $this - */ - public function restrictOnDelete() - { - return $this->onDelete('restrict'); - } - - /** - * Indicate that deletes should set the foreign key value to null. - * - * @return $this - */ - public function nullOnDelete() - { - return $this->onDelete('set null'); - } - - /** - * Indicate that deletes should have "no action". - * - * @return $this - */ - public function noActionOnDelete() - { - return $this->onDelete('no action'); - } -} diff --git a/Schema/Grammars/Grammar.php b/Schema/Grammars/Grammar.php index 5a1fd468e1..72009426ce 100755 --- a/Schema/Grammars/Grammar.php +++ b/Schema/Grammars/Grammar.php @@ -2,242 +2,73 @@ namespace Illuminate\Database\Schema\Grammars; -use Illuminate\Contracts\Database\Query\Expression; -use Illuminate\Database\Concerns\CompilesJsonPaths; -use Illuminate\Database\Grammar as BaseGrammar; -use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Fluent; use RuntimeException; -use UnitEnum; - -use function Illuminate\Support\enum_value; +use Doctrine\DBAL\Types\Type; +use Illuminate\Support\Fluent; +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Schema\Column; +use Doctrine\DBAL\Schema\TableDiff; +use Illuminate\Database\Connection; +use Doctrine\DBAL\Schema\Comparator; +use Illuminate\Database\Query\Expression; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Grammar as BaseGrammar; +use Doctrine\DBAL\Schema\AbstractSchemaManager as SchemaManager; abstract class Grammar extends BaseGrammar { - use CompilesJsonPaths; - /** - * The possible column modifiers. - * - * @var string[] - */ - protected $modifiers = []; - - /** - * If this Grammar supports schema changes wrapped in a transaction. - * - * @var bool - */ - protected $transactions = false; - - /** - * The commands to be executed outside of create or alter command. - * - * @var array - */ - protected $fluentCommands = []; - - /** - * Compile a create database command. - * - * @param string $name - * @return string - */ - public function compileCreateDatabase($name) - { - return sprintf('create database %s', - $this->wrapValue($name), - ); - } - - /** - * Compile a drop database if exists command. - * - * @param string $name - * @return string - */ - public function compileDropDatabaseIfExists($name) - { - return sprintf('drop database if exists %s', - $this->wrapValue($name) - ); - } - - /** - * Compile the query to determine the schemas. - * - * @return string - * - * @throws \RuntimeException - */ - public function compileSchemas() - { - throw new RuntimeException('This database driver does not support retrieving schemas.'); - } - - /** - * Compile the query to determine if the given table exists. - * - * @param string|null $schema - * @param string $table - * @return string|null - */ - public function compileTableExists($schema, $table) - { - // - } - - /** - * Compile the query to determine the tables. - * - * @param string|string[]|null $schema - * @return string + * Compile a rename column command. * - * @throws \RuntimeException + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @param \Illuminate\Database\Connection $connection + * @return array */ - public function compileTables($schema) + public function compileRenameColumn(Blueprint $blueprint, Fluent $command, Connection $connection) { - throw new RuntimeException('This database driver does not support retrieving tables.'); - } + $schema = $connection->getDoctrineSchemaManager(); - /** - * Compile the query to determine the views. - * - * @param string|string[]|null $schema - * @return string - * - * @throws \RuntimeException - */ - public function compileViews($schema) - { - throw new RuntimeException('This database driver does not support retrieving views.'); - } + $table = $this->getTablePrefix().$blueprint->getTable(); - /** - * Compile the query to determine the user-defined types. - * - * @param string|string[]|null $schema - * @return string - * - * @throws \RuntimeException - */ - public function compileTypes($schema) - { - throw new RuntimeException('This database driver does not support retrieving user-defined types.'); - } + $column = $connection->getDoctrineColumn($table, $command->from); - /** - * Compile the query to determine the columns. - * - * @param string|null $schema - * @param string $table - * @return string - * - * @throws \RuntimeException - */ - public function compileColumns($schema, $table) - { - throw new RuntimeException('This database driver does not support retrieving columns.'); - } + $tableDiff = $this->getRenamedDiff($blueprint, $command, $column, $schema); - /** - * Compile the query to determine the indexes. - * - * @param string|null $schema - * @param string $table - * @return string - * - * @throws \RuntimeException - */ - public function compileIndexes($schema, $table) - { - throw new RuntimeException('This database driver does not support retrieving indexes.'); + return (array) $schema->getDatabasePlatform()->getAlterTableSQL($tableDiff); } /** - * Compile a vector index key command. + * Get a new column instance with the new column name. * * @param \Illuminate\Database\Schema\Blueprint $blueprint * @param \Illuminate\Support\Fluent $command - * @return void - * - * @throws \RuntimeException + * @param \Doctrine\DBAL\Schema\Column $column + * @param \Doctrine\DBAL\Schema\AbstractSchemaManager $schema + * @return \Doctrine\DBAL\Schema\TableDiff */ - public function compileVectorIndex(Blueprint $blueprint, Fluent $command) + protected function getRenamedDiff(Blueprint $blueprint, Fluent $command, Column $column, SchemaManager $schema) { - throw new RuntimeException('The database driver in use does not support vector indexes.'); - } + $tableDiff = $this->getDoctrineTableDiff($blueprint, $schema); - /** - * Compile the query to determine the foreign keys. - * - * @param string|null $schema - * @param string $table - * @return string - * - * @throws \RuntimeException - */ - public function compileForeignKeys($schema, $table) - { - throw new RuntimeException('This database driver does not support retrieving foreign keys.'); + return $this->setRenamedColumns($tableDiff, $command, $column); } /** - * Compile a rename column command. + * Set the renamed columns on the table diff. * - * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Doctrine\DBAL\Schema\TableDiff $tableDiff * @param \Illuminate\Support\Fluent $command - * @return list|string + * @param \Doctrine\DBAL\Schema\Column $column + * @return \Doctrine\DBAL\Schema\TableDiff */ - public function compileRenameColumn(Blueprint $blueprint, Fluent $command) + protected function setRenamedColumns(TableDiff $tableDiff, Fluent $command, Column $column) { - return sprintf('alter table %s rename column %s to %s', - $this->wrapTable($blueprint), - $this->wrap($command->from), - $this->wrap($command->to) - ); - } + $newColumn = new Column($command->to, $column->getType(), $column->toArray()); - /** - * Compile a change column command into a series of SQL statements. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return list|string - * - * @throws \RuntimeException - */ - public function compileChange(Blueprint $blueprint, Fluent $command) - { - throw new RuntimeException('This database driver does not support modifying columns.'); - } - - /** - * Compile a fulltext index key command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - * - * @throws \RuntimeException - */ - public function compileFulltext(Blueprint $blueprint, Fluent $command) - { - throw new RuntimeException('This database driver does not support fulltext index creation.'); - } + $tableDiff->renamedColumns = [$command->from => $newColumn]; - /** - * Compile a drop fulltext index command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - * - * @throws \RuntimeException - */ - public function compileDropFullText(Blueprint $blueprint, Fluent $command) - { - throw new RuntimeException('This database driver does not support fulltext index removal.'); + return $tableDiff; } /** @@ -249,22 +80,22 @@ public function compileDropFullText(Blueprint $blueprint, Fluent $command) */ public function compileForeign(Blueprint $blueprint, Fluent $command) { + $table = $this->wrapTable($blueprint); + + $index = $this->wrap($command->index); + + $on = $this->wrapTable($command->on); + // We need to prepare several of the elements of the foreign key definition // before we can create the SQL, such as wrapping the tables and convert // an array of columns to comma-delimited strings for the SQL queries. - $sql = sprintf('alter table %s add constraint %s ', - $this->wrapTable($blueprint), - $this->wrap($command->index) - ); - - // Once we have the initial portion of the SQL statement we will add on the - // key name, table name, and referenced columns. These will complete the - // main portion of the SQL statement and this SQL will almost be done. - $sql .= sprintf('foreign key (%s) references %s (%s)', - $this->columnize($command->columns), - $this->wrapTable($command->on), - $this->columnize((array) $command->references) - ); + $columns = $this->columnize($command->columns); + + $onColumns = $this->columnize((array) $command->references); + + $sql = "alter table {$table} add constraint {$index} "; + + $sql .= "foreign key ({$columns}) references {$on} ({$onColumns})"; // Once we have the basic foreign key creation statement constructed we can // build out the syntax for what should happen on an update or delete of @@ -281,51 +112,74 @@ public function compileForeign(Blueprint $blueprint, Fluent $command) } /** - * Compile a drop foreign key command. + * Compile the blueprint's column definitions. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @return array + */ + protected function getColumns(Blueprint $blueprint) + { + $columns = []; + + foreach ($blueprint->getAddedColumns() as $column) { + // Each of the column types have their own compiler functions which are tasked + // with turning the column definition into its SQL format for this platform + // used by the connection. The column's modifiers are compiled and added. + $sql = $this->wrap($column).' '.$this->getType($column); + + $columns[] = $this->addModifiers($sql, $blueprint, $column); + } + + return $columns; + } + + /** + * Add the column modifiers to the definition. * + * @param string $sql * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command + * @param \Illuminate\Support\Fluent $column * @return string - * - * @throws \RuntimeException */ - public function compileDropForeign(Blueprint $blueprint, Fluent $command) + protected function addModifiers($sql, Blueprint $blueprint, Fluent $column) { - throw new RuntimeException('This database driver does not support dropping foreign keys.'); + foreach ($this->modifiers as $modifier) { + if (method_exists($this, $method = "modify{$modifier}")) { + $sql .= $this->{$method}($blueprint, $column); + } + } + + return $sql; } /** - * Compile the blueprint's added column definitions. + * Get the primary key command if it exists on the blueprint. * * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @return array + * @param string $name + * @return \Illuminate\Support\Fluent|null */ - protected function getColumns(Blueprint $blueprint) + protected function getCommandByName(Blueprint $blueprint, $name) { - $columns = []; + $commands = $this->getCommandsByName($blueprint, $name); - foreach ($blueprint->getAddedColumns() as $column) { - $columns[] = $this->getColumn($blueprint, $column); + if (count($commands) > 0) { + return reset($commands); } - - return $columns; } /** - * Compile the column definition. + * Get all of the commands with a given name. * * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Database\Schema\ColumnDefinition $column - * @return string + * @param string $name + * @return array */ - protected function getColumn(Blueprint $blueprint, $column) + protected function getCommandsByName(Blueprint $blueprint, $name) { - // Each of the column types has their own compiler functions, which are tasked - // with turning the column definition into its SQL format for this platform - // used by the connection. The column's modifiers are compiled and added. - $sql = $this->wrap($column).' '.$this->getType($column); - - return $this->addModifiers($sql, $blueprint, $column); + return array_filter($blueprint->getCommands(), function ($value) use ($name) { + return $value->name == $name; + }); } /** @@ -340,189 +194,273 @@ protected function getType(Fluent $column) } /** - * Create the column definition for a generated, computed column type. - * - * @param \Illuminate\Support\Fluent $column - * @return void + * Add a prefix to an array of values. * - * @throws \RuntimeException + * @param string $prefix + * @param array $values + * @return array */ - protected function typeComputed(Fluent $column) + public function prefixArray($prefix, array $values) { - throw new RuntimeException('This database driver does not support the computed type.'); + return array_map(function ($value) use ($prefix) { + return $prefix.' '.$value; + + }, $values); } /** - * Create the column definition for a vector type. + * Wrap a table in keyword identifiers. * - * @param \Illuminate\Support\Fluent $column + * @param mixed $table * @return string - * - * @throws \RuntimeException */ - protected function typeVector(Fluent $column) + public function wrapTable($table) + { + if ($table instanceof Blueprint) { + $table = $table->getTable(); + } + + return parent::wrapTable($table); + } + + /** + * {@inheritdoc} + */ + public function wrap($value, $prefixAlias = false) { - throw new RuntimeException('This database driver does not support the vector type.'); + if ($value instanceof Fluent) { + $value = $value->name; + } + + return parent::wrap($value, $prefixAlias); } /** - * Create the column definition for a raw column type. + * Format a value so that it can be used in "default" clauses. * - * @param \Illuminate\Support\Fluent $column + * @param mixed $value * @return string */ - protected function typeRaw(Fluent $column) + protected function getDefaultValue($value) { - return $column->offsetGet('definition'); + if ($value instanceof Expression) { + return $value; + } + + if (is_bool($value)) { + return "'".(int) $value."'"; + } + + return "'".strval($value)."'"; } /** - * Add the column modifiers to the definition. + * Create an empty Doctrine DBAL TableDiff from the Blueprint. * - * @param string $sql * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $column - * @return string + * @param \Doctrine\DBAL\Schema\AbstractSchemaManager $schema + * @return \Doctrine\DBAL\Schema\TableDiff */ - protected function addModifiers($sql, Blueprint $blueprint, Fluent $column) + protected function getDoctrineTableDiff(Blueprint $blueprint, SchemaManager $schema) { - foreach ($this->modifiers as $modifier) { - if (method_exists($this, $method = "modify{$modifier}")) { - $sql .= $this->{$method}($blueprint, $column); - } - } + $table = $this->getTablePrefix().$blueprint->getTable(); - return $sql; + $tableDiff = new TableDiff($table); + + $tableDiff->fromTable = $schema->listTableDetails($table); + + return $tableDiff; } /** - * Get the command with a given name if it exists on the blueprint. + * Compile a change column command into a series of SQL statements. * * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param string $name - * @return \Illuminate\Support\Fluent|null + * @param \Illuminate\Support\Fluent $command + * @param \Illuminate\Database\Connection $connection + * @return array + * + * @throws \RuntimeException */ - protected function getCommandByName(Blueprint $blueprint, $name) + public function compileChange(Blueprint $blueprint, Fluent $command, Connection $connection) { - $commands = $this->getCommandsByName($blueprint, $name); + if (! $connection->isDoctrineAvailable()) { + throw new RuntimeException(sprintf( + 'Changing columns for table "%s" requires Doctrine DBAL; install "doctrine/dbal".', + $blueprint->getTable() + )); + } - if (count($commands) > 0) { - return array_first($commands); + $schema = $connection->getDoctrineSchemaManager(); + + $tableDiff = $this->getChangedDiff($blueprint, $schema); + + if ($tableDiff !== false) { + return (array) $schema->getDatabasePlatform()->getAlterTableSQL($tableDiff); } + + return []; } /** - * Get all of the commands with a given name. + * Get the Doctrine table difference for the given changes. * * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param string $name - * @return array + * @param \Doctrine\DBAL\Schema\AbstractSchemaManager $schema + * @return \Doctrine\DBAL\Schema\TableDiff|bool */ - protected function getCommandsByName(Blueprint $blueprint, $name) + protected function getChangedDiff(Blueprint $blueprint, SchemaManager $schema) { - return array_filter($blueprint->getCommands(), function ($value) use ($name) { - return $value->name == $name; - }); + $table = $schema->listTableDetails($this->getTablePrefix().$blueprint->getTable()); + + return (new Comparator)->diffTable($table, $this->getTableWithColumnChanges($blueprint, $table)); } - /* - * Determine if a command with a given name exists on the blueprint. + /** + * Get a copy of the given Doctrine table after making the column changes. * * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param string $name - * @return bool + * @param \Doctrine\DBAL\Schema\Table $table + * @return \Doctrine\DBAL\Schema\TableDiff */ - protected function hasCommand(Blueprint $blueprint, $name) + protected function getTableWithColumnChanges(Blueprint $blueprint, Table $table) { - foreach ($blueprint->getCommands() as $command) { - if ($command->name === $name) { - return true; + $table = clone $table; + + foreach ($blueprint->getChangedColumns() as $fluent) { + $column = $this->getDoctrineColumnForChange($table, $fluent); + + // Here we will spin through each fluent column definition and map it to the proper + // Doctrine column definitions - which is necessary because Laravel and Doctrine + // use some different terminology for various column attributes on the tables. + foreach ($fluent->getAttributes() as $key => $value) { + if (! is_null($option = $this->mapFluentOptionToDoctrine($key))) { + if (method_exists($column, $method = 'set'.ucfirst($option))) { + $column->{$method}($this->mapFluentValueToDoctrine($option, $value)); + } + } } } - return false; + return $table; } /** - * Add a prefix to an array of values. + * Get the Doctrine column instance for a column change. * - * @param string $prefix - * @param array $values - * @return array + * @param \Doctrine\DBAL\Schema\Table $table + * @param \Illuminate\Support\Fluent $fluent + * @return \Doctrine\DBAL\Schema\Column */ - public function prefixArray($prefix, array $values) + protected function getDoctrineColumnForChange(Table $table, Fluent $fluent) { - return array_map(function ($value) use ($prefix) { - return $prefix.' '.$value; - }, $values); + return $table->changeColumn( + $fluent['name'], $this->getDoctrineColumnChangeOptions($fluent) + )->getColumn($fluent['name']); } /** - * Wrap a table in keyword identifiers. + * Get the Doctrine column change options. * - * @param mixed $table - * @param string|null $prefix - * @return string + * @param \Illuminate\Support\Fluent $fluent + * @return array */ - public function wrapTable($table, $prefix = null) + protected function getDoctrineColumnChangeOptions(Fluent $fluent) { - return parent::wrapTable( - $table instanceof Blueprint ? $table->getTable() : $table, - $prefix - ); + $options = ['type' => $this->getDoctrineColumnType($fluent['type'])]; + + if (in_array($fluent['type'], ['text', 'mediumText', 'longText'])) { + $options['length'] = $this->calculateDoctrineTextLength($fluent['type']); + } + + return $options; } /** - * Wrap a value in keyword identifiers. + * Get the doctrine column type. * - * @param \Illuminate\Support\Fluent|\Illuminate\Contracts\Database\Query\Expression|string $value - * @return string + * @param string $type + * @return \Doctrine\DBAL\Types\Type */ - public function wrap($value) + protected function getDoctrineColumnType($type) { - return parent::wrap( - $value instanceof Fluent ? $value->name : $value, - ); + $type = strtolower($type); + + switch ($type) { + case 'biginteger': + $type = 'bigint'; + break; + case 'smallinteger': + $type = 'smallint'; + break; + case 'mediumtext': + case 'longtext': + $type = 'text'; + break; + case 'binary': + $type = 'blob'; + break; + } + + return Type::getType($type); } /** - * Format a value so that it can be used in "default" clauses. + * Calculate the proper column length to force the Doctrine text type. * - * @param mixed $value - * @return string + * @param string $type + * @return int */ - protected function getDefaultValue($value) + protected function calculateDoctrineTextLength($type) { - if ($value instanceof Expression) { - return $this->getValue($value); - } + switch ($type) { + case 'mediumText': + return 65535 + 1; - if ($value instanceof UnitEnum) { - return "'".str_replace("'", "''", enum_value($value))."'"; - } + case 'longText': + return 16777215 + 1; - return is_bool($value) - ? "'".(int) $value."'" - : "'".str_replace("'", "''", $value)."'"; + default: + return 255 + 1; + } } /** - * Get the fluent commands for the grammar. + * Get the matching Doctrine option for a given Fluent attribute name. * - * @return array + * @param string $attribute + * @return string|null */ - public function getFluentCommands() + protected function mapFluentOptionToDoctrine($attribute) { - return $this->fluentCommands; + switch ($attribute) { + case 'type': + case 'name': + return; + + case 'nullable': + return 'notnull'; + + case 'total': + return 'precision'; + + case 'places': + return 'scale'; + + default: + return $attribute; + } } /** - * Check if this Grammar supports schema changes wrapped in a transaction. + * Get the matching Doctrine value for a given Fluent attribute. * - * @return bool + * @param string $option + * @param mixed $value + * @return mixed */ - public function supportsSchemaTransactions() + protected function mapFluentValueToDoctrine($option, $value) { - return $this->transactions; + return $option == 'notnull' ? ! $value : $value; } } diff --git a/Schema/Grammars/MariaDbGrammar.php b/Schema/Grammars/MariaDbGrammar.php deleted file mode 100755 index ec15f50c78..0000000000 --- a/Schema/Grammars/MariaDbGrammar.php +++ /dev/null @@ -1,67 +0,0 @@ -connection->getServerVersion(), '10.5.2', '<')) { - return $this->compileLegacyRenameColumn($blueprint, $command); - } - - return parent::compileRenameColumn($blueprint, $command); - } - - /** - * Create the column definition for a uuid type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeUuid(Fluent $column) - { - if (version_compare($this->connection->getServerVersion(), '10.7.0', '<')) { - return 'char(36)'; - } - - return 'uuid'; - } - - /** - * Create the column definition for a spatial Geometry type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeGeometry(Fluent $column) - { - $subtype = $column->subtype ? strtolower($column->subtype) : null; - - if (! in_array($subtype, ['point', 'linestring', 'polygon', 'geometrycollection', 'multipoint', 'multilinestring', 'multipolygon'])) { - $subtype = null; - } - - return sprintf('%s%s', - $subtype ?? 'geometry', - $column->srid ? ' ref_system_id='.$column->srid : '' - ); - } - - /** - * Wrap the given JSON selector. - * - * @param string $value - * @return string - */ - protected function wrapJsonSelector($value) - { - [$field, $path] = $this->wrapJsonFieldAndPath($value); - - return 'json_value('.$field.$path.')'; - } -} diff --git a/Schema/Grammars/MySqlGrammar.php b/Schema/Grammars/MySqlGrammar.php index 30b559eb6e..97d8940032 100755 --- a/Schema/Grammars/MySqlGrammar.php +++ b/Schema/Grammars/MySqlGrammar.php @@ -2,201 +2,47 @@ namespace Illuminate\Database\Schema\Grammars; -use Illuminate\Database\Query\Expression; -use Illuminate\Database\Schema\Blueprint; -use Illuminate\Database\Schema\ColumnDefinition; -use Illuminate\Support\Collection; use Illuminate\Support\Fluent; -use RuntimeException; +use Illuminate\Database\Connection; +use Illuminate\Database\Schema\Blueprint; class MySqlGrammar extends Grammar { /** * The possible column modifiers. * - * @var string[] + * @var array */ protected $modifiers = [ - 'Unsigned', 'Charset', 'Collate', 'VirtualAs', 'StoredAs', 'Nullable', - 'Default', 'OnUpdate', 'Invisible', 'Increment', 'Comment', 'After', 'First', + 'VirtualAs', 'StoredAs', 'Unsigned', 'Charset', 'Collate', 'Nullable', + 'Default', 'Increment', 'Comment', 'After', 'First', ]; /** * The possible column serials. * - * @var string[] + * @var array */ protected $serials = ['bigInteger', 'integer', 'mediumInteger', 'smallInteger', 'tinyInteger']; /** - * The commands to be executed outside of create or alter commands. - * - * @var string[] - */ - protected $fluentCommands = ['AutoIncrementStartingValues']; - - /** - * Compile a create database command. - * - * @param string $name - * @return string - */ - public function compileCreateDatabase($name) - { - $sql = parent::compileCreateDatabase($name); - - if ($charset = $this->connection->getConfig('charset')) { - $sql .= sprintf(' default character set %s', $this->wrapValue($charset)); - } - - if ($collation = $this->connection->getConfig('collation')) { - $sql .= sprintf(' default collate %s', $this->wrapValue($collation)); - } - - return $sql; - } - - /** - * Compile the query to determine the schemas. - * - * @return string - */ - public function compileSchemas() - { - return 'select schema_name as name, schema_name = schema() as `default` from information_schema.schemata where ' - .$this->compileSchemaWhereClause(null, 'schema_name') - .' order by schema_name'; - } - - /** - * Compile the query to determine if the given table exists. - * - * @param string|null $schema - * @param string $table - * @return string - */ - public function compileTableExists($schema, $table) - { - return sprintf( - 'select exists (select 1 from information_schema.tables where ' - ."table_schema = %s and table_name = %s and table_type in ('BASE TABLE', 'SYSTEM VERSIONED')) as `exists`", - $schema ? $this->quoteString($schema) : 'schema()', - $this->quoteString($table) - ); - } - - /** - * Compile the query to determine the tables. - * - * @param string|string[]|null $schema - * @return string - */ - public function compileTables($schema) - { - return sprintf( - 'select table_name as `name`, table_schema as `schema`, (data_length + index_length) as `size`, ' - .'table_comment as `comment`, engine as `engine`, table_collation as `collation` ' - ."from information_schema.tables where table_type in ('BASE TABLE', 'SYSTEM VERSIONED') and " - .$this->compileSchemaWhereClause($schema, 'table_schema') - .' order by table_schema, table_name', - $this->quoteString($schema) - ); - } - - /** - * Compile the query to determine the views. - * - * @param string|string[]|null $schema - * @return string - */ - public function compileViews($schema) - { - return 'select table_name as `name`, table_schema as `schema`, view_definition as `definition` ' - .'from information_schema.views where ' - .$this->compileSchemaWhereClause($schema, 'table_schema') - .' order by table_schema, table_name'; - } - - /** - * Compile the query to compare the schema. - * - * @param string|string[]|null $schema - * @param string $column - * @return string - */ - protected function compileSchemaWhereClause($schema, $column) - { - return $column.(match (true) { - ! empty($schema) && is_array($schema) => ' in ('.$this->quoteString($schema).')', - ! empty($schema) => ' = '.$this->quoteString($schema), - default => " not in ('information_schema', 'mysql', 'ndbinfo', 'performance_schema', 'sys')", - }); - } - - /** - * Compile the query to determine the columns. + * Compile the query to determine the list of tables. * - * @param string|null $schema - * @param string $table * @return string */ - public function compileColumns($schema, $table) + public function compileTableExists() { - return sprintf( - 'select column_name as `name`, data_type as `type_name`, column_type as `type`, ' - .'collation_name as `collation`, is_nullable as `nullable`, ' - .'column_default as `default`, column_comment as `comment`, ' - .'generation_expression as `expression`, extra as `extra` ' - .'from information_schema.columns where table_schema = %s and table_name = %s ' - .'order by ordinal_position asc', - $schema ? $this->quoteString($schema) : 'schema()', - $this->quoteString($table) - ); + return 'select * from information_schema.tables where table_schema = ? and table_name = ?'; } /** - * Compile the query to determine the indexes. + * Compile the query to determine the list of columns. * - * @param string|null $schema - * @param string $table * @return string */ - public function compileIndexes($schema, $table) + public function compileColumnExists() { - return sprintf( - 'select index_name as `name`, group_concat(column_name order by seq_in_index) as `columns`, ' - .'index_type as `type`, not non_unique as `unique` ' - .'from information_schema.statistics where table_schema = %s and table_name = %s ' - .'group by index_name, index_type, non_unique', - $schema ? $this->quoteString($schema) : 'schema()', - $this->quoteString($table) - ); - } - - /** - * Compile the query to determine the foreign keys. - * - * @param string|null $schema - * @param string $table - * @return string - */ - public function compileForeignKeys($schema, $table) - { - return sprintf( - 'select kc.constraint_name as `name`, ' - .'group_concat(kc.column_name order by kc.ordinal_position) as `columns`, ' - .'kc.referenced_table_schema as `foreign_schema`, ' - .'kc.referenced_table_name as `foreign_table`, ' - .'group_concat(kc.referenced_column_name order by kc.ordinal_position) as `foreign_columns`, ' - .'rc.update_rule as `on_update`, ' - .'rc.delete_rule as `on_delete` ' - .'from information_schema.key_column_usage kc join information_schema.referential_constraints rc ' - .'on kc.constraint_schema = rc.constraint_schema and kc.constraint_name = rc.constraint_name ' - .'where kc.table_schema = %s and kc.table_name = %s and kc.referenced_table_name is not null ' - .'group by kc.constraint_name, kc.referenced_table_schema, kc.referenced_table_name, rc.update_rule, rc.delete_rule', - $schema ? $this->quoteString($schema) : 'schema()', - $this->quoteString($table) - ); + return 'select column_name from information_schema.columns where table_schema = ? and table_name = ?'; } /** @@ -204,98 +50,51 @@ public function compileForeignKeys($schema, $table) * * @param \Illuminate\Database\Schema\Blueprint $blueprint * @param \Illuminate\Support\Fluent $command + * @param \Illuminate\Database\Connection $connection * @return string */ - public function compileCreate(Blueprint $blueprint, Fluent $command) + public function compileCreate(Blueprint $blueprint, Fluent $command, Connection $connection) { - $sql = $this->compileCreateTable( - $blueprint, $command - ); + $columns = implode(', ', $this->getColumns($blueprint)); + + $sql = $blueprint->temporary ? 'create temporary' : 'create'; + + $sql .= ' table '.$this->wrapTable($blueprint)." ($columns)"; // Once we have the primary SQL, we can add the encoding option to the SQL for // the table. Then, we can check if a storage engine has been supplied for // the table. If so, we will add the engine declaration to the SQL query. - $sql = $this->compileCreateEncoding( - $sql, $blueprint - ); - - // Finally, we will append the engine configuration onto this SQL statement as - // the final thing we do before returning this finished SQL. Once this gets - // added the query will be ready to execute against the real connections. - return $this->compileCreateEngine($sql, $blueprint); - } + $sql = $this->compileCreateEncoding($sql, $connection, $blueprint); - /** - * Create the main create table clause. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - protected function compileCreateTable($blueprint, $command) - { - $tableStructure = $this->getColumns($blueprint); - - if ($primaryKey = $this->getCommandByName($blueprint, 'primary')) { - $tableStructure[] = sprintf( - 'primary key %s(%s)', - $primaryKey->algorithm ? 'using '.$primaryKey->algorithm : '', - $this->columnize($primaryKey->columns) - ); - - $primaryKey->shouldBeSkipped = true; + if (isset($blueprint->engine)) { + $sql .= ' engine = '.$blueprint->engine; + } elseif (! is_null($engine = $connection->getConfig('engine'))) { + $sql .= ' engine = '.$engine; } - return sprintf('%s table %s (%s)', - $blueprint->temporary ? 'create temporary' : 'create', - $this->wrapTable($blueprint), - implode(', ', $tableStructure) - ); + return $sql; } /** * Append the character set specifications to a command. * * @param string $sql + * @param \Illuminate\Database\Connection $connection * @param \Illuminate\Database\Schema\Blueprint $blueprint * @return string */ - protected function compileCreateEncoding($sql, Blueprint $blueprint) + protected function compileCreateEncoding($sql, Connection $connection, Blueprint $blueprint) { - // First we will set the character set if one has been set on either the create - // blueprint itself or on the root configuration for the connection that the - // table is being created on. We will add these to the create table query. if (isset($blueprint->charset)) { $sql .= ' default character set '.$blueprint->charset; - } elseif (! is_null($charset = $this->connection->getConfig('charset'))) { + } elseif (! is_null($charset = $connection->getConfig('charset'))) { $sql .= ' default character set '.$charset; } - // Next we will add the collation to the create table statement if one has been - // added to either this create table blueprint or the configuration for this - // connection that the query is targeting. We'll add it to this SQL query. if (isset($blueprint->collation)) { - $sql .= " collate '{$blueprint->collation}'"; - } elseif (! is_null($collation = $this->connection->getConfig('collation'))) { - $sql .= " collate '{$collation}'"; - } - - return $sql; - } - - /** - * Append the engine specifications to a command. - * - * @param string $sql - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @return string - */ - protected function compileCreateEngine($sql, Blueprint $blueprint) - { - if (isset($blueprint->engine)) { - return $sql.' engine = '.$blueprint->engine; - } elseif (! is_null($engine = $this->connection->getConfig('engine'))) { - return $sql.' engine = '.$engine; + $sql .= ' collate '.$blueprint->collation; + } elseif (! is_null($collation = $connection->getConfig('collation'))) { + $sql .= ' collate '.$collation; } return $sql; @@ -310,112 +109,11 @@ protected function compileCreateEngine($sql, Blueprint $blueprint) */ public function compileAdd(Blueprint $blueprint, Fluent $command) { - return sprintf('alter table %s add %s%s%s', - $this->wrapTable($blueprint), - $this->getColumn($blueprint, $command->column), - $command->column->instant ? ', algorithm=instant' : '', - $command->column->lock ? ', lock='.$command->column->lock : '' - ); - } - - /** - * Compile the auto-incrementing column starting values. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileAutoIncrementStartingValues(Blueprint $blueprint, Fluent $command) - { - if ($command->column->autoIncrement - && $value = $command->column->get('startingValue', $command->column->get('from'))) { - return 'alter table '.$this->wrapTable($blueprint).' auto_increment = '.$value; - } - } - - /** @inheritDoc */ - public function compileRenameColumn(Blueprint $blueprint, Fluent $command) - { - $isMaria = $this->connection->isMaria(); - $version = $this->connection->getServerVersion(); + $table = $this->wrapTable($blueprint); - if (($isMaria && version_compare($version, '10.5.2', '<')) || - (! $isMaria && version_compare($version, '8.0.3', '<'))) { - return $this->compileLegacyRenameColumn($blueprint, $command); - } + $columns = $this->prefixArray('add', $this->getColumns($blueprint)); - return parent::compileRenameColumn($blueprint, $command); - } - - /** - * Compile a rename column command for legacy versions of MySQL. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - protected function compileLegacyRenameColumn(Blueprint $blueprint, Fluent $command) - { - $column = (new Collection($this->connection->getSchemaBuilder()->getColumns($blueprint->getTable()))) - ->firstWhere('name', $command->from); - - $modifiers = $this->addModifiers($column['type'], $blueprint, new ColumnDefinition([ - 'change' => true, - 'type' => match ($column['type_name']) { - 'bigint' => 'bigInteger', - 'int' => 'integer', - 'mediumint' => 'mediumInteger', - 'smallint' => 'smallInteger', - 'tinyint' => 'tinyInteger', - default => $column['type_name'], - }, - 'nullable' => $column['nullable'], - 'default' => $column['default'] && (str_starts_with(strtolower($column['default']), 'current_timestamp') || $column['default'] === 'NULL') - ? new Expression($column['default']) - : $column['default'], - 'autoIncrement' => $column['auto_increment'], - 'collation' => $column['collation'], - 'comment' => $column['comment'], - 'virtualAs' => ! is_null($column['generation']) && $column['generation']['type'] === 'virtual' - ? $column['generation']['expression'] - : null, - 'storedAs' => ! is_null($column['generation']) && $column['generation']['type'] === 'stored' - ? $column['generation']['expression'] - : null, - ])); - - return sprintf('alter table %s change %s %s %s', - $this->wrapTable($blueprint), - $this->wrap($command->from), - $this->wrap($command->to), - $modifiers - ); - } - - /** @inheritDoc */ - public function compileChange(Blueprint $blueprint, Fluent $command) - { - $column = $command->column; - - $sql = sprintf('alter table %s %s %s%s %s', - $this->wrapTable($blueprint), - is_null($column->renameTo) ? 'modify' : 'change', - $this->wrap($column), - is_null($column->renameTo) ? '' : ' '.$this->wrap($column->renameTo), - $this->getType($column) - ); - - $sql = $this->addModifiers($sql, $blueprint, $column); - - if ($column->instant) { - $sql .= ', algorithm=instant'; - } - - if ($column->lock) { - $sql .= ', lock='.$column->lock; - } - - return $sql; + return 'alter table '.$table.' '.implode(', ', $columns); } /** @@ -427,12 +125,9 @@ public function compileChange(Blueprint $blueprint, Fluent $command) */ public function compilePrimary(Blueprint $blueprint, Fluent $command) { - return sprintf('alter table %s add primary key %s(%s)%s', - $this->wrapTable($blueprint), - $command->algorithm ? 'using '.$command->algorithm : '', - $this->columnize($command->columns), - $command->lock ? ', lock='.$command->lock : '' - ); + $command->name(null); + + return $this->compileKey($blueprint, $command, 'primary key'); } /** @@ -459,30 +154,6 @@ public function compileIndex(Blueprint $blueprint, Fluent $command) return $this->compileKey($blueprint, $command, 'index'); } - /** - * Compile a fulltext index key command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileFullText(Blueprint $blueprint, Fluent $command) - { - return $this->compileKey($blueprint, $command, 'fulltext'); - } - - /** - * Compile a spatial index key command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileSpatialIndex(Blueprint $blueprint, Fluent $command) - { - return $this->compileKey($blueprint, $command, 'spatial index'); - } - /** * Compile an index creation command. * @@ -493,14 +164,15 @@ public function compileSpatialIndex(Blueprint $blueprint, Fluent $command) */ protected function compileKey(Blueprint $blueprint, Fluent $command, $type) { - return sprintf('alter table %s add %s %s%s(%s)%s', - $this->wrapTable($blueprint), - $type, - $this->wrap($command->index), - $command->algorithm ? ' using '.$command->algorithm : '', - $this->columnize($command->columns), - $command->lock ? ', lock='.$command->lock : '' - ); + $columns = $this->columnize($command->columns); + + $table = $this->wrapTable($blueprint); + + $index = $this->wrap($command->index); + + $algorithm = $command->algorithm ? ' using '.$command->algorithm : ''; + + return "alter table {$table} add {$type} {$index}{$algorithm}($columns)"; } /** @@ -538,17 +210,9 @@ public function compileDropColumn(Blueprint $blueprint, Fluent $command) { $columns = $this->prefixArray('drop', $this->wrapArray($command->columns)); - $sql = 'alter table '.$this->wrapTable($blueprint).' '.implode(', ', $columns); - - if ($command->instant) { - $sql .= ', algorithm=instant'; - } - - if ($command->lock) { - $sql .= ', lock='.$command->lock; - } + $table = $this->wrapTable($blueprint); - return $sql; + return 'alter table '.$table.' '.implode(', ', $columns); } /** @@ -572,9 +236,11 @@ public function compileDropPrimary(Blueprint $blueprint, Fluent $command) */ public function compileDropUnique(Blueprint $blueprint, Fluent $command) { + $table = $this->wrapTable($blueprint); + $index = $this->wrap($command->index); - return "alter table {$this->wrapTable($blueprint)} drop index {$index}"; + return "alter table {$table} drop index {$index}"; } /** @@ -586,51 +252,11 @@ public function compileDropUnique(Blueprint $blueprint, Fluent $command) */ public function compileDropIndex(Blueprint $blueprint, Fluent $command) { - $index = $this->wrap($command->index); + $table = $this->wrapTable($blueprint); - return "alter table {$this->wrapTable($blueprint)} drop index {$index}"; - } - - /** - * Compile a drop fulltext index command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileDropFullText(Blueprint $blueprint, Fluent $command) - { - return $this->compileDropIndex($blueprint, $command); - } - - /** - * Compile a drop spatial index command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileDropSpatialIndex(Blueprint $blueprint, Fluent $command) - { - return $this->compileDropIndex($blueprint, $command); - } - - /** - * Compile a foreign key command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileForeign(Blueprint $blueprint, Fluent $command) - { - $sql = parent::compileForeign($blueprint, $command); - - if ($command->lock) { - $sql .= ', lock='.$command->lock; - } + $index = $this->wrap($command->index); - return $sql; + return "alter table {$table} drop index {$index}"; } /** @@ -642,9 +268,11 @@ public function compileForeign(Blueprint $blueprint, Fluent $command) */ public function compileDropForeign(Blueprint $blueprint, Fluent $command) { + $table = $this->wrapTable($blueprint); + $index = $this->wrap($command->index); - return "alter table {$this->wrapTable($blueprint)} drop foreign key {$index}"; + return "alter table {$table} drop foreign key {$index}"; } /** @@ -661,44 +289,6 @@ public function compileRename(Blueprint $blueprint, Fluent $command) return "rename table {$from} to ".$this->wrapTable($command->to); } - /** - * Compile a rename index command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileRenameIndex(Blueprint $blueprint, Fluent $command) - { - return sprintf('alter table %s rename index %s to %s', - $this->wrapTable($blueprint), - $this->wrap($command->from), - $this->wrap($command->to) - ); - } - - /** - * Compile the SQL needed to drop all tables. - * - * @param array $tables - * @return string - */ - public function compileDropAllTables($tables) - { - return 'drop table '.implode(', ', $this->escapeNames($tables)); - } - - /** - * Compile the SQL needed to drop all views. - * - * @param array $views - * @return string - */ - public function compileDropAllViews($views) - { - return 'drop view '.implode(', ', $this->escapeNames($views)); - } - /** * Compile the command to enable foreign key constraints. * @@ -719,35 +309,6 @@ public function compileDisableForeignKeyConstraints() return 'SET FOREIGN_KEY_CHECKS=0;'; } - /** - * Compile a table comment command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileTableComment(Blueprint $blueprint, Fluent $command) - { - return sprintf('alter table %s comment = %s', - $this->wrapTable($blueprint), - "'".str_replace("'", "''", $command->comment)."'" - ); - } - - /** - * Quote-escape the given tables, views, or types. - * - * @param array $names - * @return array - */ - public function escapeNames($names) - { - return array_map( - fn ($name) => (new Collection(explode('.', $name)))->map($this->wrapValue(...))->implode('.'), - $names - ); - } - /** * Create the column definition for a char type. * @@ -770,17 +331,6 @@ protected function typeString(Fluent $column) return "varchar({$column->length})"; } - /** - * Create the column definition for a tiny text type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeTinyText(Fluent $column) - { - return 'tinytext'; - } - /** * Create the column definition for a text type. * @@ -826,7 +376,7 @@ protected function typeBigInteger(Fluent $column) } /** - * Create the column definition for an integer type. + * Create the column definition for a integer type. * * @param \Illuminate\Support\Fluent $column * @return string @@ -877,11 +427,7 @@ protected function typeSmallInteger(Fluent $column) */ protected function typeFloat(Fluent $column) { - if ($column->precision) { - return "float({$column->precision})"; - } - - return 'float'; + return $this->typeDouble($column); } /** @@ -892,6 +438,10 @@ protected function typeFloat(Fluent $column) */ protected function typeDouble(Fluent $column) { + if ($column->total && $column->places) { + return "double({$column->total}, {$column->places})"; + } + return 'double'; } @@ -918,25 +468,14 @@ protected function typeBoolean(Fluent $column) } /** - * Create the column definition for an enumeration type. + * Create the column definition for an enum type. * * @param \Illuminate\Support\Fluent $column * @return string */ protected function typeEnum(Fluent $column) { - return sprintf('enum(%s)', $this->quoteString($column->allowed)); - } - - /** - * Create the column definition for a set enumeration type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeSet(Fluent $column) - { - return sprintf('set(%s)', $this->quoteString($column->allowed)); + return "enum('".implode("', '", $column->allowed)."')"; } /** @@ -969,16 +508,6 @@ protected function typeJsonb(Fluent $column) */ protected function typeDate(Fluent $column) { - $isMaria = $this->connection->isMaria(); - $version = $this->connection->getServerVersion(); - - if ($isMaria || - (! $isMaria && version_compare($version, '8.0.13', '>='))) { - if ($column->useCurrent) { - $column->default(new Expression('(CURDATE())')); - } - } - return 'date'; } @@ -990,28 +519,18 @@ protected function typeDate(Fluent $column) */ protected function typeDateTime(Fluent $column) { - $current = $column->precision ? "CURRENT_TIMESTAMP($column->precision)" : 'CURRENT_TIMESTAMP'; - - if ($column->useCurrent) { - $column->default(new Expression($current)); - } - - if ($column->useCurrentOnUpdate) { - $column->onUpdate(new Expression($current)); - } - - return $column->precision ? "datetime($column->precision)" : 'datetime'; + return 'datetime'; } /** - * Create the column definition for a date-time (with time zone) type. + * Create the column definition for a date-time type. * * @param \Illuminate\Support\Fluent $column * @return string */ protected function typeDateTimeTz(Fluent $column) { - return $this->typeDateTime($column); + return 'datetime'; } /** @@ -1022,18 +541,18 @@ protected function typeDateTimeTz(Fluent $column) */ protected function typeTime(Fluent $column) { - return $column->precision ? "time($column->precision)" : 'time'; + return 'time'; } /** - * Create the column definition for a time (with time zone) type. + * Create the column definition for a time type. * * @param \Illuminate\Support\Fluent $column * @return string */ protected function typeTimeTz(Fluent $column) { - return $this->typeTime($column); + return 'time'; } /** @@ -1044,49 +563,26 @@ protected function typeTimeTz(Fluent $column) */ protected function typeTimestamp(Fluent $column) { - $current = $column->precision ? "CURRENT_TIMESTAMP($column->precision)" : 'CURRENT_TIMESTAMP'; - if ($column->useCurrent) { - $column->default(new Expression($current)); - } - - if ($column->useCurrentOnUpdate) { - $column->onUpdate(new Expression($current)); + return 'timestamp default CURRENT_TIMESTAMP'; } - return $column->precision ? "timestamp($column->precision)" : 'timestamp'; + return 'timestamp'; } /** - * Create the column definition for a timestamp (with time zone) type. + * Create the column definition for a timestamp type. * * @param \Illuminate\Support\Fluent $column * @return string */ protected function typeTimestampTz(Fluent $column) { - return $this->typeTimestamp($column); - } - - /** - * Create the column definition for a year type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeYear(Fluent $column) - { - $isMaria = $this->connection->isMaria(); - $version = $this->connection->getServerVersion(); - - if ($isMaria || - (! $isMaria && version_compare($version, '8.0.13', '>='))) { - if ($column->useCurrent) { - $column->default(new Expression('(YEAR(CURDATE()))')); - } + if ($column->useCurrent) { + return 'timestamp default CURRENT_TIMESTAMP'; } - return 'year'; + return 'timestamp'; } /** @@ -1097,10 +593,6 @@ protected function typeYear(Fluent $column) */ protected function typeBinary(Fluent $column) { - if ($column->length) { - return $column->fixed ? "binary({$column->length})" : "varbinary({$column->length})"; - } - return 'blob'; } @@ -1137,67 +629,6 @@ protected function typeMacAddress(Fluent $column) return 'varchar(17)'; } - /** - * Create the column definition for a spatial Geometry type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeGeometry(Fluent $column) - { - $subtype = $column->subtype ? strtolower($column->subtype) : null; - - if (! in_array($subtype, ['point', 'linestring', 'polygon', 'geometrycollection', 'multipoint', 'multilinestring', 'multipolygon'])) { - $subtype = null; - } - - return sprintf('%s%s', - $subtype ?? 'geometry', - match (true) { - $column->srid && $this->connection->isMaria() => ' ref_system_id='.$column->srid, - (bool) $column->srid => ' srid '.$column->srid, - default => '', - } - ); - } - - /** - * Create the column definition for a spatial Geography type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeGeography(Fluent $column) - { - return $this->typeGeometry($column); - } - - /** - * Create the column definition for a generated, computed column type. - * - * @param \Illuminate\Support\Fluent $column - * @return void - * - * @throws \RuntimeException - */ - protected function typeComputed(Fluent $column) - { - throw new RuntimeException('This database driver requires a type, see the virtualAs / storedAs modifiers.'); - } - - /** - * Create the column definition for a vector type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeVector(Fluent $column) - { - return isset($column->dimensions) && $column->dimensions !== '' - ? "vector({$column->dimensions})" - : 'vector'; - } - /** * Get the SQL for a generated virtual column modifier. * @@ -1207,16 +638,8 @@ protected function typeVector(Fluent $column) */ protected function modifyVirtualAs(Blueprint $blueprint, Fluent $column) { - if (! is_null($virtualAs = $column->virtualAsJson)) { - if ($this->isJsonSelector($virtualAs)) { - $virtualAs = $this->wrapJsonSelector($virtualAs); - } - - return " as ({$virtualAs})"; - } - - if (! is_null($virtualAs = $column->virtualAs)) { - return " as ({$this->getValue($virtualAs)})"; + if (! is_null($column->virtualAs)) { + return " as ({$column->virtualAs})"; } } @@ -1229,16 +652,8 @@ protected function modifyVirtualAs(Blueprint $blueprint, Fluent $column) */ protected function modifyStoredAs(Blueprint $blueprint, Fluent $column) { - if (! is_null($storedAs = $column->storedAsJson)) { - if ($this->isJsonSelector($storedAs)) { - $storedAs = $this->wrapJsonSelector($storedAs); - } - - return " as ({$storedAs}) stored"; - } - - if (! is_null($storedAs = $column->storedAs)) { - return " as ({$this->getValue($storedAs)}) stored"; + if (! is_null($column->storedAs)) { + return " as ({$column->storedAs}) stored"; } } @@ -1280,7 +695,7 @@ protected function modifyCharset(Blueprint $blueprint, Fluent $column) protected function modifyCollate(Blueprint $blueprint, Fluent $column) { if (! is_null($column->collation)) { - return " collate '{$column->collation}'"; + return ' collate '.$column->collation; } } @@ -1293,30 +708,7 @@ protected function modifyCollate(Blueprint $blueprint, Fluent $column) */ protected function modifyNullable(Blueprint $blueprint, Fluent $column) { - if (is_null($column->virtualAs) && - is_null($column->virtualAsJson) && - is_null($column->storedAs) && - is_null($column->storedAsJson)) { - return $column->nullable ? ' null' : ' not null'; - } - - if ($column->nullable === false) { - return ' not null'; - } - } - - /** - * Get the SQL for an invisible column modifier. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $column - * @return string|null - */ - protected function modifyInvisible(Blueprint $blueprint, Fluent $column) - { - if (! is_null($column->invisible)) { - return ' invisible'; - } + return $column->nullable ? ' null' : ' not null'; } /** @@ -1333,20 +725,6 @@ protected function modifyDefault(Blueprint $blueprint, Fluent $column) } } - /** - * Get the SQL for an "on update" column modifier. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $column - * @return string|null - */ - protected function modifyOnUpdate(Blueprint $blueprint, Fluent $column) - { - if (! is_null($column->onUpdate)) { - return ' on update '.$this->getValue($column->onUpdate); - } - } - /** * Get the SQL for an auto-increment column modifier. * @@ -1357,9 +735,7 @@ protected function modifyOnUpdate(Blueprint $blueprint, Fluent $column) protected function modifyIncrement(Blueprint $blueprint, Fluent $column) { if (in_array($column->type, $this->serials) && $column->autoIncrement) { - return $this->hasCommand($blueprint, 'primary') || ($column->change && ! $column->primary) - ? ' auto_increment' - : ' auto_increment primary key'; + return ' auto_increment primary key'; } } @@ -1401,7 +777,7 @@ protected function modifyAfter(Blueprint $blueprint, Fluent $column) protected function modifyComment(Blueprint $blueprint, Fluent $column) { if (! is_null($column->comment)) { - return " comment '".addslashes($column->comment)."'"; + return ' comment "'.$column->comment.'"'; } } @@ -1413,23 +789,10 @@ protected function modifyComment(Blueprint $blueprint, Fluent $column) */ protected function wrapValue($value) { - if ($value !== '*') { - return '`'.str_replace('`', '``', $value).'`'; + if ($value === '*') { + return $value; } - return $value; - } - - /** - * Wrap the given JSON selector. - * - * @param string $value - * @return string - */ - protected function wrapJsonSelector($value) - { - [$field, $path] = $this->wrapJsonFieldAndPath($value); - - return 'json_unquote(json_extract('.$field.$path.'))'; + return '`'.str_replace('`', '``', $value).'`'; } } diff --git a/Schema/Grammars/PostgresGrammar.php b/Schema/Grammars/PostgresGrammar.php index 4a1660a20e..03e03d8877 100755 --- a/Schema/Grammars/PostgresGrammar.php +++ b/Schema/Grammars/PostgresGrammar.php @@ -2,229 +2,44 @@ namespace Illuminate\Database\Schema\Grammars; -use Illuminate\Database\Query\Expression; -use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Collection; use Illuminate\Support\Fluent; -use LogicException; +use Illuminate\Database\Schema\Blueprint; class PostgresGrammar extends Grammar { - /** - * If this Grammar supports schema changes wrapped in a transaction. - * - * @var bool - */ - protected $transactions = true; - /** * The possible column modifiers. * - * @var string[] + * @var array */ - protected $modifiers = ['Collate', 'Nullable', 'Default', 'VirtualAs', 'StoredAs', 'GeneratedAs', 'Increment']; + protected $modifiers = ['Increment', 'Nullable', 'Default']; /** * The columns available as serials. * - * @var string[] + * @var array */ protected $serials = ['bigInteger', 'integer', 'mediumInteger', 'smallInteger', 'tinyInteger']; /** - * The commands to be executed outside of create or alter command. - * - * @var string[] - */ - protected $fluentCommands = ['AutoIncrementStartingValues', 'Comment']; - - /** - * Compile a create database command. - * - * @param string $name - * @return string - */ - public function compileCreateDatabase($name) - { - $sql = parent::compileCreateDatabase($name); - - if ($charset = $this->connection->getConfig('charset')) { - $sql .= sprintf(' encoding %s', $this->wrapValue($charset)); - } - - return $sql; - } - - /** - * Compile the query to determine the schemas. - * - * @return string - */ - public function compileSchemas() - { - return 'select nspname as name, nspname = current_schema() as "default" from pg_namespace where ' - .$this->compileSchemaWhereClause(null, 'nspname') - .' order by nspname'; - } - - /** - * Compile the query to determine if the given table exists. - * - * @param string|null $schema - * @param string $table - * @return string - */ - public function compileTableExists($schema, $table) - { - return sprintf( - 'select exists (select 1 from pg_class c, pg_namespace n where ' - ."n.nspname = %s and c.relname = %s and c.relkind in ('r', 'p') and n.oid = c.relnamespace)", - $schema ? $this->quoteString($schema) : 'current_schema()', - $this->quoteString($table) - ); - } - - /** - * Compile the query to determine the tables. - * - * @param string|string[]|null $schema - * @return string - */ - public function compileTables($schema) - { - return 'select c.relname as name, n.nspname as schema, pg_total_relation_size(c.oid) as size, ' - ."obj_description(c.oid, 'pg_class') as comment from pg_class c, pg_namespace n " - ."where c.relkind in ('r', 'p') and n.oid = c.relnamespace and " - .$this->compileSchemaWhereClause($schema, 'n.nspname') - .' order by n.nspname, c.relname'; - } - - /** - * Compile the query to determine the views. - * - * @param string|string[]|null $schema - * @return string - */ - public function compileViews($schema) - { - return 'select viewname as name, schemaname as schema, definition from pg_views where ' - .$this->compileSchemaWhereClause($schema, 'schemaname') - .' order by schemaname, viewname'; - } - - /** - * Compile the query to determine the user-defined types. - * - * @param string|string[]|null $schema - * @return string - */ - public function compileTypes($schema) - { - return 'select t.typname as name, n.nspname as schema, t.typtype as type, t.typcategory as category, ' - ."((t.typinput = 'array_in'::regproc and t.typoutput = 'array_out'::regproc) or t.typtype = 'm') as implicit " - .'from pg_type t join pg_namespace n on n.oid = t.typnamespace ' - .'left join pg_class c on c.oid = t.typrelid ' - .'left join pg_type el on el.oid = t.typelem ' - .'left join pg_class ce on ce.oid = el.typrelid ' - ."where ((t.typrelid = 0 and (ce.relkind = 'c' or ce.relkind is null)) or c.relkind = 'c') " - ."and not exists (select 1 from pg_depend d where d.objid in (t.oid, t.typelem) and d.deptype = 'e') and " - .$this->compileSchemaWhereClause($schema, 'n.nspname'); - } - - /** - * Compile the query to compare the schema. - * - * @param string|string[]|null $schema - * @param string $column - * @return string - */ - protected function compileSchemaWhereClause($schema, $column) - { - return $column.(match (true) { - ! empty($schema) && is_array($schema) => ' in ('.$this->quoteString($schema).')', - ! empty($schema) => ' = '.$this->quoteString($schema), - default => " <> 'information_schema' and $column not like 'pg\_%'", - }); - } - - /** - * Compile the query to determine the columns. - * - * @param string|null $schema - * @param string $table - * @return string - */ - public function compileColumns($schema, $table) - { - return sprintf( - 'select a.attname as name, t.typname as type_name, format_type(a.atttypid, a.atttypmod) as type, ' - .'(select tc.collcollate from pg_catalog.pg_collation tc where tc.oid = a.attcollation) as collation, ' - .'not a.attnotnull as nullable, ' - .'(select pg_get_expr(adbin, adrelid) from pg_attrdef where c.oid = pg_attrdef.adrelid and pg_attrdef.adnum = a.attnum) as default, ' - .(version_compare($this->connection->getServerVersion(), '12.0', '<') ? "'' as generated, " : 'a.attgenerated as generated, ') - .'col_description(c.oid, a.attnum) as comment ' - .'from pg_attribute a, pg_class c, pg_type t, pg_namespace n ' - .'where c.relname = %s and n.nspname = %s and a.attnum > 0 and a.attrelid = c.oid and a.atttypid = t.oid and n.oid = c.relnamespace ' - .'order by a.attnum', - $this->quoteString($table), - $schema ? $this->quoteString($schema) : 'current_schema()' - ); - } - - /** - * Compile the query to determine the indexes. + * Compile the query to determine if a table exists. * - * @param string|null $schema - * @param string $table * @return string */ - public function compileIndexes($schema, $table) + public function compileTableExists() { - return sprintf( - "select ic.relname as name, string_agg(a.attname, ',' order by indseq.ord) as columns, " - .'am.amname as "type", i.indisunique as "unique", i.indisprimary as "primary" ' - .'from pg_index i ' - .'join pg_class tc on tc.oid = i.indrelid ' - .'join pg_namespace tn on tn.oid = tc.relnamespace ' - .'join pg_class ic on ic.oid = i.indexrelid ' - .'join pg_am am on am.oid = ic.relam ' - .'join lateral unnest(i.indkey) with ordinality as indseq(num, ord) on true ' - .'left join pg_attribute a on a.attrelid = i.indrelid and a.attnum = indseq.num ' - .'where tc.relname = %s and tn.nspname = %s ' - .'group by ic.relname, am.amname, i.indisunique, i.indisprimary', - $this->quoteString($table), - $schema ? $this->quoteString($schema) : 'current_schema()' - ); + return 'select * from information_schema.tables where table_schema = ? and table_name = ?'; } /** - * Compile the query to determine the foreign keys. + * Compile the query to determine the list of columns. * - * @param string|null $schema * @param string $table * @return string */ - public function compileForeignKeys($schema, $table) + public function compileColumnExists($table) { - return sprintf( - 'select c.conname as name, ' - ."string_agg(la.attname, ',' order by conseq.ord) as columns, " - .'fn.nspname as foreign_schema, fc.relname as foreign_table, ' - ."string_agg(fa.attname, ',' order by conseq.ord) as foreign_columns, " - .'c.confupdtype as on_update, c.confdeltype as on_delete ' - .'from pg_constraint c ' - .'join pg_class tc on c.conrelid = tc.oid ' - .'join pg_namespace tn on tn.oid = tc.relnamespace ' - .'join pg_class fc on c.confrelid = fc.oid ' - .'join pg_namespace fn on fn.oid = fc.relnamespace ' - .'join lateral unnest(c.conkey) with ordinality as conseq(num, ord) on true ' - .'join pg_attribute la on la.attrelid = c.conrelid and la.attnum = conseq.num ' - .'join pg_attribute fa on fa.attrelid = c.confrelid and fa.attnum = c.confkey[conseq.ord] ' - ."where c.contype = 'f' and tc.relname = %s and tn.nspname = %s " - .'group by c.conname, fn.nspname, fc.relname, c.confupdtype, c.confdeltype', - $this->quoteString($table), - $schema ? $this->quoteString($schema) : 'current_schema()' - ); + return "select column_name from information_schema.columns where table_name = '$table'"; } /** @@ -236,11 +51,13 @@ public function compileForeignKeys($schema, $table) */ public function compileCreate(Blueprint $blueprint, Fluent $command) { - return sprintf('%s table %s (%s)', - $blueprint->temporary ? 'create temporary' : 'create', - $this->wrapTable($blueprint), - implode(', ', $this->getColumns($blueprint)) - ); + $columns = implode(', ', $this->getColumns($blueprint)); + + $sql = $blueprint->temporary ? 'create temporary' : 'create'; + + $sql .= ' table '.$this->wrapTable($blueprint)." ($columns)"; + + return $sql; } /** @@ -252,57 +69,11 @@ public function compileCreate(Blueprint $blueprint, Fluent $command) */ public function compileAdd(Blueprint $blueprint, Fluent $command) { - return sprintf('alter table %s add column %s', - $this->wrapTable($blueprint), - $this->getColumn($blueprint, $command->column) - ); - } - - /** - * Compile the auto-incrementing column starting values. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileAutoIncrementStartingValues(Blueprint $blueprint, Fluent $command) - { - if ($command->column->autoIncrement - && $value = $command->column->get('startingValue', $command->column->get('from'))) { - return sprintf( - 'select setval(pg_get_serial_sequence(%s, %s), %s, false)', - $this->quoteString($this->wrapTable($blueprint)), - $this->quoteString($command->column->name), - $value - ); - } - } - - /** @inheritDoc */ - public function compileChange(Blueprint $blueprint, Fluent $command) - { - $column = $command->column; + $table = $this->wrapTable($blueprint); - $changes = ['type '.$this->getType($column).$this->modifyCollate($blueprint, $column)]; + $columns = $this->prefixArray('add column', $this->getColumns($blueprint)); - foreach ($this->modifiers as $modifier) { - if ($modifier === 'Collate') { - continue; - } - - if (method_exists($this, $method = "modify{$modifier}")) { - $constraints = (array) $this->{$method}($blueprint, $column); - - foreach ($constraints as $constraint) { - $changes[] = $constraint; - } - } - } - - return sprintf('alter table %s %s', - $this->wrapTable($blueprint), - implode(', ', $this->prefixArray('alter column '.$this->wrap($column), $changes)) - ); + return 'alter table '.$table.' '.implode(', ', $columns); } /** @@ -324,49 +95,17 @@ public function compilePrimary(Blueprint $blueprint, Fluent $command) * * @param \Illuminate\Database\Schema\Blueprint $blueprint * @param \Illuminate\Support\Fluent $command - * @return string[] + * @return string */ public function compileUnique(Blueprint $blueprint, Fluent $command) { - $uniqueStatement = 'unique'; + $table = $this->wrapTable($blueprint); - if (! is_null($command->nullsNotDistinct)) { - $uniqueStatement .= ' nulls '.($command->nullsNotDistinct ? 'not distinct' : 'distinct'); - } - - if ($command->online || $command->algorithm) { - $createIndexSql = sprintf('create unique index %s%s on %s%s (%s)', - $command->online ? 'concurrently ' : '', - $this->wrap($command->index), - $this->wrapTable($blueprint), - $command->algorithm ? ' using '.$command->algorithm : '', - $this->columnize($command->columns) - ); - - $sql = sprintf('alter table %s add constraint %s unique using index %s', - $this->wrapTable($blueprint), - $this->wrap($command->index), - $this->wrap($command->index) - ); - } else { - $sql = sprintf( - 'alter table %s add constraint %s %s (%s)', - $this->wrapTable($blueprint), - $this->wrap($command->index), - $uniqueStatement, - $this->columnize($command->columns) - ); - } - - if (! is_null($command->deferrable)) { - $sql .= $command->deferrable ? ' deferrable' : ' not deferrable'; - } + $index = $this->wrap($command->index); - if ($command->deferrable && ! is_null($command->initiallyImmediate)) { - $sql .= $command->initiallyImmediate ? ' initially immediate' : ' initially deferred'; - } + $columns = $this->columnize($command->columns); - return isset($createIndexSql) ? [$createIndexSql, $sql] : [$sql]; + return "alter table $table add constraint {$index} unique ($columns)"; } /** @@ -378,128 +117,13 @@ public function compileUnique(Blueprint $blueprint, Fluent $command) */ public function compileIndex(Blueprint $blueprint, Fluent $command) { - return sprintf('create index %s%s on %s%s (%s)', - $command->online ? 'concurrently ' : '', - $this->wrap($command->index), - $this->wrapTable($blueprint), - $command->algorithm ? ' using '.$command->algorithm : '', - $this->columnize($command->columns) - ); - } - - /** - * Compile a fulltext index key command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - * - * @throws \RuntimeException - */ - public function compileFulltext(Blueprint $blueprint, Fluent $command) - { - $language = $command->language ?: 'english'; - - $columns = array_map(function ($column) use ($language) { - return "to_tsvector({$this->quoteString($language)}, {$this->wrap($column)})"; - }, $command->columns); - - return sprintf('create index %s%s on %s using gin ((%s))', - $command->online ? 'concurrently ' : '', - $this->wrap($command->index), - $this->wrapTable($blueprint), - implode(' || ', $columns) - ); - } - - /** - * Compile a spatial index key command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileSpatialIndex(Blueprint $blueprint, Fluent $command) - { - $command->algorithm = 'gist'; - - if (! is_null($command->operatorClass)) { - return $this->compileIndexWithOperatorClass($blueprint, $command); - } - - return $this->compileIndex($blueprint, $command); - } - - /** - * Compile a vector index key command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileVectorIndex(Blueprint $blueprint, Fluent $command) - { - return $this->compileIndexWithOperatorClass($blueprint, $command); - } - - /** - * Compile a spatial index with operator class key command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - protected function compileIndexWithOperatorClass(Blueprint $blueprint, Fluent $command) - { - $columns = $this->columnizeWithOperatorClass($command->columns, $command->operatorClass); - - return sprintf('create index %s%s on %s%s (%s)', - $command->online ? 'concurrently ' : '', - $this->wrap($command->index), - $this->wrapTable($blueprint), - $command->algorithm ? ' using '.$command->algorithm : '', - $columns - ); - } - - /** - * Convert an array of column names to a delimited string with operator class. - * - * @param array $columns - * @param string $operatorClass - * @return string - */ - protected function columnizeWithOperatorClass(array $columns, $operatorClass) - { - return implode(', ', array_map(function ($column) use ($operatorClass) { - return $this->wrap($column).' '.$operatorClass; - }, $columns)); - } - - /** - * Compile a foreign key command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileForeign(Blueprint $blueprint, Fluent $command) - { - $sql = parent::compileForeign($blueprint, $command); - - if (! is_null($command->deferrable)) { - $sql .= $command->deferrable ? ' deferrable' : ' not deferrable'; - } + $columns = $this->columnize($command->columns); - if ($command->deferrable && ! is_null($command->initiallyImmediate)) { - $sql .= $command->initiallyImmediate ? ' initially immediate' : ' initially deferred'; - } + $index = $this->wrap($command->index); - if (! is_null($command->notValid)) { - $sql .= ' not valid'; - } + $algorithm = $command->algorithm ? ' using '.$command->algorithm : ''; - return $sql; + return "create index {$index} on ".$this->wrapTable($blueprint).$algorithm." ({$columns})"; } /** @@ -526,50 +150,6 @@ public function compileDropIfExists(Blueprint $blueprint, Fluent $command) return 'drop table if exists '.$this->wrapTable($blueprint); } - /** - * Compile the SQL needed to drop all tables. - * - * @param array $tables - * @return string - */ - public function compileDropAllTables($tables) - { - return 'drop table '.implode(', ', $this->escapeNames($tables)).' cascade'; - } - - /** - * Compile the SQL needed to drop all views. - * - * @param array $views - * @return string - */ - public function compileDropAllViews($views) - { - return 'drop view '.implode(', ', $this->escapeNames($views)).' cascade'; - } - - /** - * Compile the SQL needed to drop all types. - * - * @param array $types - * @return string - */ - public function compileDropAllTypes($types) - { - return 'drop type '.implode(', ', $this->escapeNames($types)).' cascade'; - } - - /** - * Compile the SQL needed to drop all domains. - * - * @param array $domains - * @return string - */ - public function compileDropAllDomains($domains) - { - return 'drop domain '.implode(', ', $this->escapeNames($domains)).' cascade'; - } - /** * Compile a drop column command. * @@ -581,7 +161,9 @@ public function compileDropColumn(Blueprint $blueprint, Fluent $command) { $columns = $this->prefixArray('drop column', $this->wrapArray($command->columns)); - return 'alter table '.$this->wrapTable($blueprint).' '.implode(', ', $columns); + $table = $this->wrapTable($blueprint); + + return 'alter table '.$table.' '.implode(', ', $columns); } /** @@ -593,8 +175,9 @@ public function compileDropColumn(Blueprint $blueprint, Fluent $command) */ public function compileDropPrimary(Blueprint $blueprint, Fluent $command) { - [, $table] = $this->connection->getSchemaBuilder()->parseSchemaAndTable($blueprint->getTable()); - $index = $this->wrap("{$this->connection->getTablePrefix()}{$table}_pkey"); + $table = $blueprint->getTable(); + + $index = $this->wrap("{$table}_pkey"); return 'alter table '.$this->wrapTable($blueprint)." drop constraint {$index}"; } @@ -608,9 +191,11 @@ public function compileDropPrimary(Blueprint $blueprint, Fluent $command) */ public function compileDropUnique(Blueprint $blueprint, Fluent $command) { + $table = $this->wrapTable($blueprint); + $index = $this->wrap($command->index); - return "alter table {$this->wrapTable($blueprint)} drop constraint {$index}"; + return "alter table {$table} drop constraint {$index}"; } /** @@ -622,31 +207,9 @@ public function compileDropUnique(Blueprint $blueprint, Fluent $command) */ public function compileDropIndex(Blueprint $blueprint, Fluent $command) { - return "drop index {$this->wrap($command->index)}"; - } - - /** - * Compile a drop fulltext index command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileDropFullText(Blueprint $blueprint, Fluent $command) - { - return $this->compileDropIndex($blueprint, $command); - } + $index = $this->wrap($command->index); - /** - * Compile a drop spatial index command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileDropSpatialIndex(Blueprint $blueprint, Fluent $command) - { - return $this->compileDropIndex($blueprint, $command); + return "drop index {$index}"; } /** @@ -658,38 +221,11 @@ public function compileDropSpatialIndex(Blueprint $blueprint, Fluent $command) */ public function compileDropForeign(Blueprint $blueprint, Fluent $command) { - $index = $this->wrap($command->index); - - return "alter table {$this->wrapTable($blueprint)} drop constraint {$index}"; - } - - /** - * Compile a rename table command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileRename(Blueprint $blueprint, Fluent $command) - { - $from = $this->wrapTable($blueprint); + $table = $this->wrapTable($blueprint); - return "alter table {$from} rename to ".$this->wrapTable($command->to); - } + $index = $this->wrap($command->index); - /** - * Compile a rename index command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileRenameIndex(Blueprint $blueprint, Fluent $command) - { - return sprintf('alter index %s rename to %s', - $this->wrap($command->from), - $this->wrap($command->to) - ); + return "alter table {$table} drop constraint {$index}"; } /** @@ -713,50 +249,17 @@ public function compileDisableForeignKeyConstraints() } /** - * Compile a comment command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileComment(Blueprint $blueprint, Fluent $command) - { - if (! is_null($comment = $command->column->comment) || $command->column->change) { - return sprintf('comment on column %s.%s is %s', - $this->wrapTable($blueprint), - $this->wrap($command->column->name), - is_null($comment) ? 'NULL' : "'".str_replace("'", "''", $comment)."'" - ); - } - } - - /** - * Compile a table comment command. + * Compile a rename table command. * * @param \Illuminate\Database\Schema\Blueprint $blueprint * @param \Illuminate\Support\Fluent $command * @return string */ - public function compileTableComment(Blueprint $blueprint, Fluent $command) + public function compileRename(Blueprint $blueprint, Fluent $command) { - return sprintf('comment on table %s is %s', - $this->wrapTable($blueprint), - "'".str_replace("'", "''", $command->comment)."'" - ); - } + $from = $this->wrapTable($blueprint); - /** - * Quote-escape the given tables, views, or types. - * - * @param array $names - * @return array - */ - public function escapeNames($names) - { - return array_map( - fn ($name) => (new Collection(explode('.', $name)))->map($this->wrapValue(...))->implode('.'), - $names - ); + return "alter table {$from} rename to ".$this->wrapTable($command->to); } /** @@ -767,11 +270,7 @@ public function escapeNames($names) */ protected function typeChar(Fluent $column) { - if ($column->length) { - return "char({$column->length})"; - } - - return 'char'; + return "char({$column->length})"; } /** @@ -782,22 +281,7 @@ protected function typeChar(Fluent $column) */ protected function typeString(Fluent $column) { - if ($column->length) { - return "varchar({$column->length})"; - } - - return 'varchar'; - } - - /** - * Create the column definition for a tiny text type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeTinyText(Fluent $column) - { - return 'varchar(255)'; + return "varchar({$column->length})"; } /** @@ -834,14 +318,14 @@ protected function typeLongText(Fluent $column) } /** - * Create the column definition for an integer type. + * Create the column definition for a integer type. * * @param \Illuminate\Support\Fluent $column * @return string */ protected function typeInteger(Fluent $column) { - return $column->autoIncrement && is_null($column->generatedAs) && ! $column->change ? 'serial' : 'integer'; + return $column->autoIncrement ? 'serial' : 'integer'; } /** @@ -852,7 +336,7 @@ protected function typeInteger(Fluent $column) */ protected function typeBigInteger(Fluent $column) { - return $column->autoIncrement && is_null($column->generatedAs) && ! $column->change ? 'bigserial' : 'bigint'; + return $column->autoIncrement ? 'bigserial' : 'bigint'; } /** @@ -863,7 +347,7 @@ protected function typeBigInteger(Fluent $column) */ protected function typeMediumInteger(Fluent $column) { - return $this->typeInteger($column); + return $column->autoIncrement ? 'serial' : 'integer'; } /** @@ -874,7 +358,7 @@ protected function typeMediumInteger(Fluent $column) */ protected function typeTinyInteger(Fluent $column) { - return $this->typeSmallInteger($column); + return $column->autoIncrement ? 'smallserial' : 'smallint'; } /** @@ -885,7 +369,7 @@ protected function typeTinyInteger(Fluent $column) */ protected function typeSmallInteger(Fluent $column) { - return $column->autoIncrement && is_null($column->generatedAs) && ! $column->change ? 'smallserial' : 'smallint'; + return $column->autoIncrement ? 'smallserial' : 'smallint'; } /** @@ -896,11 +380,7 @@ protected function typeSmallInteger(Fluent $column) */ protected function typeFloat(Fluent $column) { - if ($column->precision) { - return "float({$column->precision})"; - } - - return 'float'; + return $this->typeDouble($column); } /** @@ -914,17 +394,6 @@ protected function typeDouble(Fluent $column) return 'double precision'; } - /** - * Create the column definition for a real type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeReal(Fluent $column) - { - return 'real'; - } - /** * Create the column definition for a decimal type. * @@ -948,18 +417,18 @@ protected function typeBoolean(Fluent $column) } /** - * Create the column definition for an enumeration type. + * Create the column definition for an enum type. * * @param \Illuminate\Support\Fluent $column * @return string */ protected function typeEnum(Fluent $column) { - return sprintf( - 'varchar(255) check ("%s" in (%s))', - $column->name, - $this->quoteString($column->allowed) - ); + $allowed = array_map(function ($a) { + return "'".$a."'"; + }, $column->allowed); + + return "varchar(255) check (\"{$column->name}\" in (".implode(', ', $allowed).'))'; } /** @@ -992,10 +461,6 @@ protected function typeJsonb(Fluent $column) */ protected function typeDate(Fluent $column) { - if ($column->useCurrent) { - $column->default(new Expression('CURRENT_DATE')); - } - return 'date'; } @@ -1007,18 +472,18 @@ protected function typeDate(Fluent $column) */ protected function typeDateTime(Fluent $column) { - return $this->typeTimestamp($column); + return 'timestamp(0) without time zone'; } /** - * Create the column definition for a date-time (with time zone) type. + * Create the column definition for a date-time type. * * @param \Illuminate\Support\Fluent $column * @return string */ protected function typeDateTimeTz(Fluent $column) { - return $this->typeTimestampTz($column); + return 'timestamp(0) with time zone'; } /** @@ -1029,18 +494,18 @@ protected function typeDateTimeTz(Fluent $column) */ protected function typeTime(Fluent $column) { - return 'time'.(is_null($column->precision) ? '' : "($column->precision)").' without time zone'; + return 'time(0) without time zone'; } /** - * Create the column definition for a time (with time zone) type. + * Create the column definition for a time type. * * @param \Illuminate\Support\Fluent $column * @return string */ protected function typeTimeTz(Fluent $column) { - return 'time'.(is_null($column->precision) ? '' : "($column->precision)").' with time zone'; + return 'time(0) with time zone'; } /** @@ -1052,14 +517,14 @@ protected function typeTimeTz(Fluent $column) protected function typeTimestamp(Fluent $column) { if ($column->useCurrent) { - $column->default(new Expression('CURRENT_TIMESTAMP')); + return 'timestamp(0) without time zone default CURRENT_TIMESTAMP(0)'; } - return 'timestamp'.(is_null($column->precision) ? '' : "($column->precision)").' without time zone'; + return 'timestamp(0) without time zone'; } /** - * Create the column definition for a timestamp (with time zone) type. + * Create the column definition for a timestamp type. * * @param \Illuminate\Support\Fluent $column * @return string @@ -1067,25 +532,10 @@ protected function typeTimestamp(Fluent $column) protected function typeTimestampTz(Fluent $column) { if ($column->useCurrent) { - $column->default(new Expression('CURRENT_TIMESTAMP')); + return 'timestamp(0) with time zone default CURRENT_TIMESTAMP(0)'; } - return 'timestamp'.(is_null($column->precision) ? '' : "($column->precision)").' with time zone'; - } - - /** - * Create the column definition for a year type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeYear(Fluent $column) - { - if ($column->useCurrent) { - $column->default(new Expression('EXTRACT(YEAR FROM CURRENT_DATE)')); - } - - return $this->typeInteger($column); + return 'timestamp(0) with time zone'; } /** @@ -1132,69 +582,6 @@ protected function typeMacAddress(Fluent $column) return 'macaddr'; } - /** - * Create the column definition for a spatial Geometry type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeGeometry(Fluent $column) - { - if ($column->subtype) { - return sprintf('geometry(%s%s)', - strtolower($column->subtype), - $column->srid ? ','.$column->srid : '' - ); - } - - return 'geometry'; - } - - /** - * Create the column definition for a spatial Geography type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeGeography(Fluent $column) - { - if ($column->subtype) { - return sprintf('geography(%s%s)', - strtolower($column->subtype), - $column->srid ? ','.$column->srid : '' - ); - } - - return 'geography'; - } - - /** - * Create the column definition for a vector type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeVector(Fluent $column) - { - return isset($column->dimensions) && $column->dimensions !== '' - ? "vector({$column->dimensions})" - : 'vector'; - } - - /** - * Get the SQL for a collation column modifier. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $column - * @return string|null - */ - protected function modifyCollate(Blueprint $blueprint, Fluent $column) - { - if (! is_null($column->collation)) { - return ' collate '.$this->wrapValue($column->collation); - } - } - /** * Get the SQL for a nullable column modifier. * @@ -1204,10 +591,6 @@ protected function modifyCollate(Blueprint $blueprint, Fluent $column) */ protected function modifyNullable(Blueprint $blueprint, Fluent $column) { - if ($column->change) { - return $column->nullable ? 'drop not null' : 'set not null'; - } - return $column->nullable ? ' null' : ' not null'; } @@ -1220,14 +603,6 @@ protected function modifyNullable(Blueprint $blueprint, Fluent $column) */ protected function modifyDefault(Blueprint $blueprint, Fluent $column) { - if ($column->change) { - if (! $column->autoIncrement || ! is_null($column->generatedAs)) { - return is_null($column->default) ? 'drop default' : 'set default '.$this->getDefaultValue($column->default); - } - - return null; - } - if (! is_null($column->default)) { return ' default '.$this->getDefaultValue($column->default); } @@ -1242,95 +617,8 @@ protected function modifyDefault(Blueprint $blueprint, Fluent $column) */ protected function modifyIncrement(Blueprint $blueprint, Fluent $column) { - if (! $column->change - && ! $this->hasCommand($blueprint, 'primary') - && (in_array($column->type, $this->serials) || ($column->generatedAs !== null)) - && $column->autoIncrement) { + if (in_array($column->type, $this->serials) && $column->autoIncrement) { return ' primary key'; } } - - /** - * Get the SQL for a generated virtual column modifier. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $column - * @return string|null - * - * @throws \LogicException - */ - protected function modifyVirtualAs(Blueprint $blueprint, Fluent $column) - { - if ($column->change) { - if (array_key_exists('virtualAs', $column->getAttributes())) { - return is_null($column->virtualAs) - ? 'drop expression if exists' - : throw new LogicException('This database driver does not support modifying generated columns.'); - } - - return null; - } - - if (! is_null($column->virtualAs)) { - return " generated always as ({$this->getValue($column->virtualAs)}) virtual"; - } - } - - /** - * Get the SQL for a generated stored column modifier. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $column - * @return string|null - * - * @throws \LogicException - */ - protected function modifyStoredAs(Blueprint $blueprint, Fluent $column) - { - if ($column->change) { - if (array_key_exists('storedAs', $column->getAttributes())) { - return is_null($column->storedAs) - ? 'drop expression if exists' - : throw new LogicException('This database driver does not support modifying generated columns.'); - } - - return null; - } - - if (! is_null($column->storedAs)) { - return " generated always as ({$this->getValue($column->storedAs)}) stored"; - } - } - - /** - * Get the SQL for an identity column modifier. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $column - * @return string|list|null - */ - protected function modifyGeneratedAs(Blueprint $blueprint, Fluent $column) - { - $sql = null; - - if (! is_null($column->generatedAs)) { - $sql = sprintf( - ' generated %s as identity%s', - $column->always ? 'always' : 'by default', - ! is_bool($column->generatedAs) && ! empty($column->generatedAs) ? " ({$column->generatedAs})" : '' - ); - } - - if ($column->change) { - $changes = $column->autoIncrement && is_null($sql) ? [] : ['drop identity if exists']; - - if (! is_null($sql)) { - $changes[] = 'add '.$sql; - } - - return $changes; - } - - return $sql; - } } diff --git a/Schema/Grammars/SQLiteGrammar.php b/Schema/Grammars/SQLiteGrammar.php old mode 100644 new mode 100755 index c472266c43..9c040be5d0 --- a/Schema/Grammars/SQLiteGrammar.php +++ b/Schema/Grammars/SQLiteGrammar.php @@ -2,257 +2,100 @@ namespace Illuminate\Database\Schema\Grammars; -use Illuminate\Database\Query\Expression; -use Illuminate\Database\Schema\Blueprint; -use Illuminate\Database\Schema\IndexDefinition; -use Illuminate\Support\Arr; -use Illuminate\Support\Collection; use Illuminate\Support\Fluent; -use RuntimeException; +use Illuminate\Database\Connection; +use Illuminate\Database\Schema\Blueprint; class SQLiteGrammar extends Grammar { /** * The possible column modifiers. * - * @var string[] + * @var array */ - protected $modifiers = ['Increment', 'Nullable', 'Default', 'Collate', 'VirtualAs', 'StoredAs']; + protected $modifiers = ['Nullable', 'Default', 'Increment']; /** * The columns available as serials. * - * @var string[] + * @var array */ protected $serials = ['bigInteger', 'integer', 'mediumInteger', 'smallInteger', 'tinyInteger']; /** - * Get the commands to be compiled on the alter command. - * - * @return array - */ - public function getAlterCommands() - { - $alterCommands = ['change', 'primary', 'dropPrimary', 'foreign', 'dropForeign']; - - if (version_compare($this->connection->getServerVersion(), '3.35', '<')) { - $alterCommands[] = 'dropColumn'; - } - - return $alterCommands; - } - - /** - * Compile the query to determine the SQL text that describes the given object. - * - * @param string|null $schema - * @param string $name - * @param string $type - * @return string - */ - public function compileSqlCreateStatement($schema, $name, $type = 'table') - { - return sprintf('select "sql" from %s.sqlite_master where type = %s and name = %s', - $this->wrapValue($schema ?? 'main'), - $this->quoteString($type), - $this->quoteString($name) - ); - } - - /** - * Compile the query to determine if the dbstat table is available. - * - * @return string - */ - public function compileDbstatExists() - { - return "select exists (select 1 from pragma_compile_options where compile_options = 'ENABLE_DBSTAT_VTAB') as enabled"; - } - - /** - * Compile the query to determine the schemas. + * Compile the query to determine if a table exists. * * @return string */ - public function compileSchemas() + public function compileTableExists() { - return 'select name, file as path, name = \'main\' as "default" from pragma_database_list order by name'; + return "select * from sqlite_master where type = 'table' and name = ?"; } /** - * Compile the query to determine if the given table exists. + * Compile the query to determine the list of columns. * - * @param string|null $schema * @param string $table * @return string */ - public function compileTableExists($schema, $table) - { - return sprintf( - 'select exists (select 1 from %s.sqlite_master where name = %s and type = \'table\') as "exists"', - $this->wrapValue($schema ?? 'main'), - $this->quoteString($table) - ); - } - - /** - * Compile the query to determine the tables. - * - * @param string|string[]|null $schema - * @param bool $withSize - * @return string - */ - public function compileTables($schema, $withSize = false) + public function compileColumnExists($table) { - return 'select tl.name as name, tl.schema as schema' - .($withSize ? ', (select sum(s.pgsize) ' - .'from (select tl.name as name union select il.name as name from pragma_index_list(tl.name, tl.schema) as il) as es ' - .'join dbstat(tl.schema) as s on s.name = es.name) as size' : '') - .' from pragma_table_list as tl where' - .(match (true) { - ! empty($schema) && is_array($schema) => ' tl.schema in ('.$this->quoteString($schema).') and', - ! empty($schema) => ' tl.schema = '.$this->quoteString($schema).' and', - default => '', - }) - ." tl.type in ('table', 'virtual') and tl.name not like 'sqlite\_%' escape '\' " - .'order by tl.schema, tl.name'; + return 'pragma table_info('.str_replace('.', '__', $table).')'; } /** - * Compile the query for legacy versions of SQLite to determine the tables. + * Compile a create table command. * - * @param string $schema - * @param bool $withSize + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command * @return string */ - public function compileLegacyTables($schema, $withSize = false) + public function compileCreate(Blueprint $blueprint, Fluent $command) { - return $withSize - ? sprintf( - 'select m.tbl_name as name, %s as schema, sum(s.pgsize) as size from %s.sqlite_master as m ' - .'join dbstat(%s) as s on s.name = m.name ' - ."where m.type in ('table', 'index') and m.tbl_name not like 'sqlite\_%%' escape '\' " - .'group by m.tbl_name ' - .'order by m.tbl_name', - $this->quoteString($schema), - $this->wrapValue($schema), - $this->quoteString($schema) - ) - : sprintf( - 'select name, %s as schema from %s.sqlite_master ' - ."where type = 'table' and name not like 'sqlite\_%%' escape '\' order by name", - $this->quoteString($schema), - $this->wrapValue($schema) - ); - } + $columns = implode(', ', $this->getColumns($blueprint)); - /** - * Compile the query to determine the views. - * - * @param string $schema - * @return string - */ - public function compileViews($schema) - { - return sprintf( - "select name, %s as schema, sql as definition from %s.sqlite_master where type = 'view' order by name", - $this->quoteString($schema), - $this->wrapValue($schema) - ); - } + $sql = $blueprint->temporary ? 'create temporary' : 'create'; - /** - * Compile the query to determine the columns. - * - * @param string|null $schema - * @param string $table - * @return string - */ - public function compileColumns($schema, $table) - { - return sprintf( - 'select name, type, not "notnull" as "nullable", dflt_value as "default", pk as "primary", hidden as "extra" ' - .'from pragma_table_xinfo(%s, %s) order by cid asc', - $this->quoteString($table), - $this->quoteString($schema ?? 'main') - ); - } + $sql .= ' table '.$this->wrapTable($blueprint)." ($columns"; - /** - * Compile the query to determine the indexes. - * - * @param string|null $schema - * @param string $table - * @return string - */ - public function compileIndexes($schema, $table) - { - return sprintf( - 'select \'primary\' as name, group_concat(col) as columns, 1 as "unique", 1 as "primary" ' - .'from (select name as col from pragma_table_xinfo(%s, %s) where pk > 0 order by pk, cid) group by name ' - .'union select name, group_concat(col) as columns, "unique", origin = \'pk\' as "primary" ' - .'from (select il.*, ii.name as col from pragma_index_list(%s, %s) il, pragma_index_info(il.name, %s) ii order by il.seq, ii.seqno) ' - .'group by name, "unique", "primary"', - $table = $this->quoteString($table), - $schema = $this->quoteString($schema ?? 'main'), - $table, - $schema, - $schema - ); - } + // SQLite forces primary keys to be added when the table is initially created + // so we will need to check for a primary key commands and add the columns + // to the table's declaration here so they can be created on the tables. + $sql .= (string) $this->addForeignKeys($blueprint); - /** - * Compile the query to determine the foreign keys. - * - * @param string|null $schema - * @param string $table - * @return string - */ - public function compileForeignKeys($schema, $table) - { - return sprintf( - 'select group_concat("from") as columns, %s as foreign_schema, "table" as foreign_table, ' - .'group_concat("to") as foreign_columns, on_update, on_delete ' - .'from (select * from pragma_foreign_key_list(%s, %s) order by id desc, seq) ' - .'group by id, "table", on_update, on_delete', - $schema = $this->quoteString($schema ?? 'main'), - $this->quoteString($table), - $schema - ); - } + $sql .= (string) $this->addPrimaryKeys($blueprint); - /** - * Compile a create table command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileCreate(Blueprint $blueprint, Fluent $command) - { - return sprintf('%s table %s (%s%s%s)', - $blueprint->temporary ? 'create temporary' : 'create', - $this->wrapTable($blueprint), - implode(', ', $this->getColumns($blueprint)), - $this->addForeignKeys($this->getCommandsByName($blueprint, 'foreign')), - $this->addPrimaryKeys($this->getCommandByName($blueprint, 'primary')) - ); + return $sql.')'; } /** * Get the foreign key syntax for a table creation statement. * - * @param \Illuminate\Database\Schema\ForeignKeyDefinition[] $foreignKeys + * @param \Illuminate\Database\Schema\Blueprint $blueprint * @return string|null */ - protected function addForeignKeys($foreignKeys) + protected function addForeignKeys(Blueprint $blueprint) { - return (new Collection($foreignKeys))->reduce(function ($sql, $foreign) { - // Once we have all the foreign key commands for the table creation statement - // we'll loop through each of them and add them to the create table SQL we - // are building, since SQLite needs foreign keys on the tables creation. - return $sql.$this->getForeignKey($foreign); - }, ''); + $sql = ''; + + $foreigns = $this->getCommandsByName($blueprint, 'foreign'); + + // Once we have all the foreign key commands for the table creation statement + // we'll loop through each of them and add them to the create table SQL we + // are building, since SQLite needs foreign keys on the tables creation. + foreach ($foreigns as $foreign) { + $sql .= $this->getForeignKey($foreign); + + if (! is_null($foreign->onDelete)) { + $sql .= " on delete {$foreign->onDelete}"; + } + + if (! is_null($foreign->onUpdate)) { + $sql .= " on update {$foreign->onUpdate}"; + } + } + + return $sql; } /** @@ -263,39 +106,32 @@ protected function addForeignKeys($foreignKeys) */ protected function getForeignKey($foreign) { + $on = $this->wrapTable($foreign->on); + // We need to columnize the columns that the foreign key is being defined for // so that it is a properly formatted list. Once we have done this, we can // return the foreign key SQL declaration to the calling method for use. - $sql = sprintf(', foreign key(%s) references %s(%s)', - $this->columnize($foreign->columns), - $this->wrapTable($foreign->on), - $this->columnize((array) $foreign->references) - ); - - if (! is_null($foreign->onDelete)) { - $sql .= " on delete {$foreign->onDelete}"; - } + $columns = $this->columnize($foreign->columns); - // If this foreign key specifies the action to be taken on update we will add - // that to the statement here. We'll append it to this SQL and then return - // this SQL so we can keep adding any other foreign constraints to this. - if (! is_null($foreign->onUpdate)) { - $sql .= " on update {$foreign->onUpdate}"; - } + $onColumns = $this->columnize((array) $foreign->references); - return $sql; + return ", foreign key($columns) references $on($onColumns)"; } /** * Get the primary key syntax for a table creation statement. * - * @param \Illuminate\Support\Fluent|null $primary + * @param \Illuminate\Database\Schema\Blueprint $blueprint * @return string|null */ - protected function addPrimaryKeys($primary) + protected function addPrimaryKeys(Blueprint $blueprint) { + $primary = $this->getCommandByName($blueprint, 'primary'); + if (! is_null($primary)) { - return ", primary key ({$this->columnize($primary->columns)})"; + $columns = $this->columnize($primary->columns); + + return ", primary key ({$columns})"; } } @@ -304,88 +140,21 @@ protected function addPrimaryKeys($primary) * * @param \Illuminate\Database\Schema\Blueprint $blueprint * @param \Illuminate\Support\Fluent $command - * @return string + * @return array */ public function compileAdd(Blueprint $blueprint, Fluent $command) { - return sprintf('alter table %s add column %s', - $this->wrapTable($blueprint), - $this->getColumn($blueprint, $command->column) - ); - } - - /** - * Compile alter table command into a series of SQL statements. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return list|string - */ - public function compileAlter(Blueprint $blueprint, Fluent $command) - { - $columnNames = []; - $autoIncrementColumn = null; - - $columns = (new Collection($blueprint->getState()->getColumns())) - ->map(function ($column) use ($blueprint, &$columnNames, &$autoIncrementColumn) { - $name = $this->wrap($column); - - $autoIncrementColumn = $column->autoIncrement ? $column->name : $autoIncrementColumn; - - if (is_null($column->virtualAs) && is_null($column->virtualAsJson) && - is_null($column->storedAs) && is_null($column->storedAsJson)) { - $columnNames[] = $name; - } - - return $this->addModifiers( - $this->wrap($column).' '.($column->full_type_definition ?? $this->getType($column)), - $blueprint, - $column - ); - })->all(); - - $indexes = (new Collection($blueprint->getState()->getIndexes())) - ->reject(fn ($index) => str_starts_with('sqlite_', $index->index)) - ->map(fn ($index) => $this->{'compile'.ucfirst($index->name)}($blueprint, $index)) - ->all(); - - [, $tableName] = $this->connection->getSchemaBuilder()->parseSchemaAndTable($blueprint->getTable()); - $tempTable = $this->wrapTable($blueprint, '__temp__'.$this->connection->getTablePrefix()); $table = $this->wrapTable($blueprint); - $columnNames = implode(', ', $columnNames); - - $foreignKeyConstraintsEnabled = $this->connection->scalar($this->pragma('foreign_keys')); - - return array_filter(array_merge([ - $foreignKeyConstraintsEnabled ? $this->compileDisableForeignKeyConstraints() : null, - sprintf('create table %s (%s%s%s)', - $tempTable, - implode(', ', $columns), - $this->addForeignKeys($blueprint->getState()->getForeignKeys()), - $autoIncrementColumn ? '' : $this->addPrimaryKeys($blueprint->getState()->getPrimaryKey()) - ), - sprintf('insert into %s (%s) select %s from %s', $tempTable, $columnNames, $columnNames, $table), - sprintf('drop table %s', $table), - sprintf('alter table %s rename to %s', $tempTable, $this->wrapTable($tableName)), - ], $indexes, [$foreignKeyConstraintsEnabled ? $this->compileEnableForeignKeyConstraints() : null])); - } - /** @inheritDoc */ - public function compileChange(Blueprint $blueprint, Fluent $command) - { - // Handled on table alteration... - } + $columns = $this->prefixArray('add column', $this->getColumns($blueprint)); - /** - * Compile a primary key command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compilePrimary(Blueprint $blueprint, Fluent $command) - { - // Handled on table creation or alteration... + $statements = []; + + foreach ($columns as $column) { + $statements[] = 'alter table '.$table.' '.$column; + } + + return $statements; } /** @@ -397,14 +166,13 @@ public function compilePrimary(Blueprint $blueprint, Fluent $command) */ public function compileUnique(Blueprint $blueprint, Fluent $command) { - [$schema, $table] = $this->connection->getSchemaBuilder()->parseSchemaAndTable($blueprint->getTable()); - - return sprintf('create unique index %s%s on %s (%s)', - $schema ? $this->wrapValue($schema).'.' : '', - $this->wrap($command->index), - $this->wrapTable($table), - $this->columnize($command->columns) - ); + $columns = $this->columnize($command->columns); + + $table = $this->wrapTable($blueprint); + + $index = $this->wrap($command->index); + + return "create unique index {$index} on {$table} ({$columns})"; } /** @@ -416,28 +184,13 @@ public function compileUnique(Blueprint $blueprint, Fluent $command) */ public function compileIndex(Blueprint $blueprint, Fluent $command) { - [$schema, $table] = $this->connection->getSchemaBuilder()->parseSchemaAndTable($blueprint->getTable()); - - return sprintf('create index %s%s on %s (%s)', - $schema ? $this->wrapValue($schema).'.' : '', - $this->wrap($command->index), - $this->wrapTable($table), - $this->columnize($command->columns) - ); - } + $columns = $this->columnize($command->columns); - /** - * Compile a spatial index key command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return void - * - * @throws \RuntimeException - */ - public function compileSpatialIndex(Blueprint $blueprint, Fluent $command) - { - throw new RuntimeException('The database driver in use does not support spatial indexes.'); + $table = $this->wrapTable($blueprint); + + $index = $this->wrap($command->index); + + return "create index {$index} on {$table} ({$columns})"; } /** @@ -445,11 +198,11 @@ public function compileSpatialIndex(Blueprint $blueprint, Fluent $command) * * @param \Illuminate\Database\Schema\Blueprint $blueprint * @param \Illuminate\Support\Fluent $command - * @return string|null + * @return string */ public function compileForeign(Blueprint $blueprint, Fluent $command) { - // Handled on table creation or alteration... + // Handled on table creation... } /** @@ -476,77 +229,27 @@ public function compileDropIfExists(Blueprint $blueprint, Fluent $command) return 'drop table if exists '.$this->wrapTable($blueprint); } - /** - * Compile the SQL needed to drop all tables. - * - * @param string|null $schema - * @return string - */ - public function compileDropAllTables($schema = null) - { - return sprintf("delete from %s.sqlite_master where type in ('table', 'index', 'trigger')", - $this->wrapValue($schema ?? 'main') - ); - } - - /** - * Compile the SQL needed to drop all views. - * - * @param string|null $schema - * @return string - */ - public function compileDropAllViews($schema = null) - { - return sprintf("delete from %s.sqlite_master where type in ('view')", - $this->wrapValue($schema ?? 'main') - ); - } - - /** - * Compile the SQL needed to rebuild the database. - * - * @param string|null $schema - * @return string - */ - public function compileRebuild($schema = null) - { - return sprintf('vacuum %s', - $this->wrapValue($schema ?? 'main') - ); - } - /** * Compile a drop column command. * * @param \Illuminate\Database\Schema\Blueprint $blueprint * @param \Illuminate\Support\Fluent $command - * @return list|null + * @param \Illuminate\Database\Connection $connection + * @return array */ - public function compileDropColumn(Blueprint $blueprint, Fluent $command) + public function compileDropColumn(Blueprint $blueprint, Fluent $command, Connection $connection) { - if (version_compare($this->connection->getServerVersion(), '3.35', '<')) { - // Handled on table alteration... + $schema = $connection->getDoctrineSchemaManager(); - return null; - } + $tableDiff = $this->getDoctrineTableDiff($blueprint, $schema); - $table = $this->wrapTable($blueprint); + foreach ($command->columns as $name) { + $column = $connection->getDoctrineColumn($blueprint->getTable(), $name); - $columns = $this->prefixArray('drop column', $this->wrapArray($command->columns)); + $tableDiff->removedColumns[$name] = $column; + } - return (new Collection($columns))->map(fn ($column) => 'alter table '.$table.' '.$column)->all(); - } - - /** - * Compile a drop primary key command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileDropPrimary(Blueprint $blueprint, Fluent $command) - { - // Handled on table alteration... + return (array) $schema->getDatabasePlatform()->getAlterTableSQL($tableDiff); } /** @@ -558,7 +261,9 @@ public function compileDropPrimary(Blueprint $blueprint, Fluent $command) */ public function compileDropUnique(Blueprint $blueprint, Fluent $command) { - return $this->compileDropIndex($blueprint, $command); + $index = $this->wrap($command->index); + + return "drop index {$index}"; } /** @@ -570,44 +275,9 @@ public function compileDropUnique(Blueprint $blueprint, Fluent $command) */ public function compileDropIndex(Blueprint $blueprint, Fluent $command) { - [$schema] = $this->connection->getSchemaBuilder()->parseSchemaAndTable($blueprint->getTable()); - - return sprintf('drop index %s%s', - $schema ? $this->wrapValue($schema).'.' : '', - $this->wrap($command->index) - ); - } - - /** - * Compile a drop spatial index command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return void - * - * @throws \RuntimeException - */ - public function compileDropSpatialIndex(Blueprint $blueprint, Fluent $command) - { - throw new RuntimeException('The database driver in use does not support spatial indexes.'); - } - - /** - * Compile a drop foreign key command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return array - * - * @throws \RuntimeException - */ - public function compileDropForeign(Blueprint $blueprint, Fluent $command) - { - if (empty($command->columns)) { - throw new RuntimeException('This database driver does not support dropping foreign keys by name.'); - } + $index = $this->wrap($command->index); - // Handled on table alteration... + return "drop index {$index}"; } /** @@ -624,46 +294,6 @@ public function compileRename(Blueprint $blueprint, Fluent $command) return "alter table {$from} rename to ".$this->wrapTable($command->to); } - /** - * Compile a rename index command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return array - * - * @throws \RuntimeException - */ - public function compileRenameIndex(Blueprint $blueprint, Fluent $command) - { - $indexes = $this->connection->getSchemaBuilder()->getIndexes($blueprint->getTable()); - - $index = Arr::first($indexes, fn ($index) => $index['name'] === $command->from); - - if (! $index) { - throw new RuntimeException("Index [{$command->from}] does not exist."); - } - - if ($index['primary']) { - throw new RuntimeException('SQLite does not support altering primary keys.'); - } - - if ($index['unique']) { - return [ - $this->compileDropUnique($blueprint, new IndexDefinition(['index' => $index['name']])), - $this->compileUnique($blueprint, - new IndexDefinition(['index' => $command->to, 'columns' => $index['columns']]) - ), - ]; - } - - return [ - $this->compileDropIndex($blueprint, new IndexDefinition(['index' => $index['name']])), - $this->compileIndex($blueprint, - new IndexDefinition(['index' => $command->to, 'columns' => $index['columns']]) - ), - ]; - } - /** * Compile the command to enable foreign key constraints. * @@ -671,7 +301,7 @@ public function compileRenameIndex(Blueprint $blueprint, Fluent $command) */ public function compileEnableForeignKeyConstraints() { - return $this->pragma('foreign_keys', 1); + return 'PRAGMA foreign_keys = ON;'; } /** @@ -681,22 +311,7 @@ public function compileEnableForeignKeyConstraints() */ public function compileDisableForeignKeyConstraints() { - return $this->pragma('foreign_keys', 0); - } - - /** - * Get the SQL to get or set a PRAGMA value. - * - * @param string $key - * @param mixed $value - * @return string - */ - public function pragma(string $key, mixed $value = null): string - { - return sprintf('pragma %s%s', - $key, - is_null($value) ? '' : ' = '.$value - ); + return 'PRAGMA foreign_keys = OFF;'; } /** @@ -721,17 +336,6 @@ protected function typeString(Fluent $column) return 'varchar'; } - /** - * Create the column definition for a tiny text type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeTinyText(Fluent $column) - { - return 'text'; - } - /** * Create the column definition for a text type. * @@ -766,7 +370,7 @@ protected function typeLongText(Fluent $column) } /** - * Create the column definition for an integer type. + * Create the column definition for a integer type. * * @param \Illuminate\Support\Fluent $column * @return string @@ -839,7 +443,7 @@ protected function typeFloat(Fluent $column) */ protected function typeDouble(Fluent $column) { - return 'double'; + return 'float'; } /** @@ -865,18 +469,14 @@ protected function typeBoolean(Fluent $column) } /** - * Create the column definition for an enumeration type. + * Create the column definition for an enum type. * * @param \Illuminate\Support\Fluent $column * @return string */ protected function typeEnum(Fluent $column) { - return sprintf( - 'varchar check ("%s" in (%s))', - $column->name, - $this->quoteString($column->allowed) - ); + return 'varchar'; } /** @@ -887,7 +487,7 @@ protected function typeEnum(Fluent $column) */ protected function typeJson(Fluent $column) { - return $this->connection->getConfig('use_native_json') ? 'json' : 'text'; + return 'text'; } /** @@ -898,7 +498,7 @@ protected function typeJson(Fluent $column) */ protected function typeJsonb(Fluent $column) { - return $this->connection->getConfig('use_native_jsonb') ? 'jsonb' : 'text'; + return 'text'; } /** @@ -909,10 +509,6 @@ protected function typeJsonb(Fluent $column) */ protected function typeDate(Fluent $column) { - if ($column->useCurrent) { - $column->default(new Expression('CURRENT_DATE')); - } - return 'date'; } @@ -924,14 +520,13 @@ protected function typeDate(Fluent $column) */ protected function typeDateTime(Fluent $column) { - return $this->typeTimestamp($column); + return 'datetime'; } /** - * Create the column definition for a date-time (with time zone) type. + * Create the column definition for a date-time type. * * Note: "SQLite does not have a storage class set aside for storing dates and/or times." - * * @link https://www.sqlite.org/datatype3.html * * @param \Illuminate\Support\Fluent $column @@ -939,7 +534,7 @@ protected function typeDateTime(Fluent $column) */ protected function typeDateTimeTz(Fluent $column) { - return $this->typeDateTime($column); + return 'datetime'; } /** @@ -954,14 +549,14 @@ protected function typeTime(Fluent $column) } /** - * Create the column definition for a time (with time zone) type. + * Create the column definition for a time type. * * @param \Illuminate\Support\Fluent $column * @return string */ protected function typeTimeTz(Fluent $column) { - return $this->typeTime($column); + return 'time'; } /** @@ -973,36 +568,25 @@ protected function typeTimeTz(Fluent $column) protected function typeTimestamp(Fluent $column) { if ($column->useCurrent) { - $column->default(new Expression('CURRENT_TIMESTAMP')); + return 'datetime default CURRENT_TIMESTAMP'; } return 'datetime'; } /** - * Create the column definition for a timestamp (with time zone) type. + * Create the column definition for a timestamp type. * * @param \Illuminate\Support\Fluent $column * @return string */ protected function typeTimestampTz(Fluent $column) - { - return $this->typeTimestamp($column); - } - - /** - * Create the column definition for a year type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeYear(Fluent $column) { if ($column->useCurrent) { - $column->default(new Expression("(CAST(strftime('%Y', 'now') AS INTEGER))")); + return 'datetime default CURRENT_TIMESTAMP'; } - return $this->typeInteger($column); + return 'datetime'; } /** @@ -1049,85 +633,6 @@ protected function typeMacAddress(Fluent $column) return 'varchar'; } - /** - * Create the column definition for a spatial Geometry type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeGeometry(Fluent $column) - { - return 'geometry'; - } - - /** - * Create the column definition for a spatial Geography type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeGeography(Fluent $column) - { - return $this->typeGeometry($column); - } - - /** - * Create the column definition for a generated, computed column type. - * - * @param \Illuminate\Support\Fluent $column - * @return void - * - * @throws \RuntimeException - */ - protected function typeComputed(Fluent $column) - { - throw new RuntimeException('This database driver requires a type, see the virtualAs / storedAs modifiers.'); - } - - /** - * Get the SQL for a generated virtual column modifier. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $column - * @return string|null - */ - protected function modifyVirtualAs(Blueprint $blueprint, Fluent $column) - { - if (! is_null($virtualAs = $column->virtualAsJson)) { - if ($this->isJsonSelector($virtualAs)) { - $virtualAs = $this->wrapJsonSelector($virtualAs); - } - - return " as ({$virtualAs})"; - } - - if (! is_null($virtualAs = $column->virtualAs)) { - return " as ({$this->getValue($virtualAs)})"; - } - } - - /** - * Get the SQL for a generated stored column modifier. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $column - * @return string|null - */ - protected function modifyStoredAs(Blueprint $blueprint, Fluent $column) - { - if (! is_null($storedAs = $column->storedAsJson)) { - if ($this->isJsonSelector($storedAs)) { - $storedAs = $this->wrapJsonSelector($storedAs); - } - - return " as ({$storedAs}) stored"; - } - - if (! is_null($storedAs = $column->storedAs)) { - return " as ({$this->getValue($column->storedAs)}) stored"; - } - } - /** * Get the SQL for a nullable column modifier. * @@ -1137,16 +642,7 @@ protected function modifyStoredAs(Blueprint $blueprint, Fluent $column) */ protected function modifyNullable(Blueprint $blueprint, Fluent $column) { - if (is_null($column->virtualAs) && - is_null($column->virtualAsJson) && - is_null($column->storedAs) && - is_null($column->storedAsJson)) { - return $column->nullable ? '' : ' not null'; - } - - if ($column->nullable === false) { - return ' not null'; - } + return $column->nullable ? ' null' : ' not null'; } /** @@ -1158,7 +654,7 @@ protected function modifyNullable(Blueprint $blueprint, Fluent $column) */ protected function modifyDefault(Blueprint $blueprint, Fluent $column) { - if (! is_null($column->default) && is_null($column->virtualAs) && is_null($column->virtualAsJson) && is_null($column->storedAs)) { + if (! is_null($column->default)) { return ' default '.$this->getDefaultValue($column->default); } } @@ -1176,31 +672,4 @@ protected function modifyIncrement(Blueprint $blueprint, Fluent $column) return ' primary key autoincrement'; } } - - /** - * Get the SQL for a collation column modifier. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $column - * @return string|null - */ - protected function modifyCollate(Blueprint $blueprint, Fluent $column) - { - if (! is_null($column->collation)) { - return " collate '{$column->collation}'"; - } - } - - /** - * Wrap the given JSON selector. - * - * @param string $value - * @return string - */ - protected function wrapJsonSelector($value) - { - [$field, $path] = $this->wrapJsonFieldAndPath($value); - - return 'json_extract('.$field.$path.')'; - } } diff --git a/Schema/Grammars/SqlServerGrammar.php b/Schema/Grammars/SqlServerGrammar.php index 28b5e5a7a1..c527660b61 100755 --- a/Schema/Grammars/SqlServerGrammar.php +++ b/Schema/Grammars/SqlServerGrammar.php @@ -2,198 +2,46 @@ namespace Illuminate\Database\Schema\Grammars; -use Illuminate\Database\Query\Expression; -use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Fluent; +use Illuminate\Database\Schema\Blueprint; class SqlServerGrammar extends Grammar { - /** - * If this Grammar supports schema changes wrapped in a transaction. - * - * @var bool - */ - protected $transactions = true; - /** * The possible column modifiers. * - * @var string[] + * @var array */ - protected $modifiers = ['Collate', 'Nullable', 'Default', 'Persisted', 'Increment']; + protected $modifiers = ['Increment', 'Nullable', 'Default']; /** * The columns available as serials. * - * @var string[] + * @var array */ protected $serials = ['tinyInteger', 'smallInteger', 'mediumInteger', 'integer', 'bigInteger']; /** - * The commands to be executed outside of create or alter command. - * - * @var string[] - */ - protected $fluentCommands = ['Default']; - - /** - * Compile the query to determine the schemas. - * - * @return string - */ - public function compileSchemas() - { - return 'select name, iif(schema_id = schema_id(), 1, 0) as [default] from sys.schemas ' - ."where name not in ('information_schema', 'sys') and name not like 'db[_]%' order by name"; - } - - /** - * Compile the query to determine if the given table exists. - * - * @param string|null $schema - * @param string $table - * @return string - */ - public function compileTableExists($schema, $table) - { - return sprintf( - 'select (case when object_id(%s, \'U\') is null then 0 else 1 end) as [exists]', - $this->quoteString($schema ? $schema.'.'.$table : $table) - ); - } - - /** - * Compile the query to determine the tables. - * - * @param string|string[]|null $schema - * @return string - */ - public function compileTables($schema) - { - return 'select t.name as name, schema_name(t.schema_id) as [schema], sum(u.total_pages) * 8 * 1024 as size ' - .'from sys.tables as t ' - .'join sys.partitions as p on p.object_id = t.object_id ' - .'join sys.allocation_units as u on u.container_id = p.hobt_id ' - ."where t.is_ms_shipped = 0 and t.name <> 'sysdiagrams'" - .$this->compileSchemaWhereClause($schema, 'schema_name(t.schema_id)') - .' group by t.name, t.schema_id ' - .'order by [schema], t.name'; - } - - /** - * Compile the query to determine the views. - * - * @param string|string[]|null $schema - * @return string - */ - public function compileViews($schema) - { - return 'select name, schema_name(v.schema_id) as [schema], definition from sys.views as v ' - .'inner join sys.sql_modules as m on v.object_id = m.object_id ' - .'where v.is_ms_shipped = 0' - .$this->compileSchemaWhereClause($schema, 'schema_name(v.schema_id)') - .' order by [schema], name'; - } - - /** - * Compile the query to compare the schema. - * - * @param string|string[]|null $schema - * @param string $column - * @return string - */ - protected function compileSchemaWhereClause($schema, $column) - { - return match (true) { - ! empty($schema) && is_array($schema) => " and $column in (".$this->quoteString($schema).')', - ! empty($schema) => " and $column = ".$this->quoteString($schema), - default => '', - }; - } - - /** - * Compile the query to determine the columns. + * Compile the query to determine if a table exists. * - * @param string|null $schema - * @param string $table * @return string */ - public function compileColumns($schema, $table) + public function compileTableExists() { - return sprintf( - 'select col.name, type.name as type_name, ' - .'col.max_length as length, col.precision as precision, col.scale as places, ' - .'col.is_nullable as nullable, def.definition as [default], ' - .'col.is_identity as autoincrement, col.collation_name as collation, ' - .'com.definition as [expression], is_persisted as [persisted], ' - .'cast(prop.value as nvarchar(max)) as comment ' - .'from sys.columns as col ' - .'join sys.types as type on col.user_type_id = type.user_type_id ' - .'join sys.objects as obj on col.object_id = obj.object_id ' - .'join sys.schemas as scm on obj.schema_id = scm.schema_id ' - .'left join sys.default_constraints def on col.default_object_id = def.object_id and col.object_id = def.parent_object_id ' - ."left join sys.extended_properties as prop on obj.object_id = prop.major_id and col.column_id = prop.minor_id and prop.name = 'MS_Description' " - .'left join sys.computed_columns as com on col.column_id = com.column_id and col.object_id = com.object_id ' - ."where obj.type in ('U', 'V') and obj.name = %s and scm.name = %s " - .'order by col.column_id', - $this->quoteString($table), - $schema ? $this->quoteString($schema) : 'schema_name()', - ); + return "select * from sysobjects where type = 'U' and name = ?"; } /** - * Compile the query to determine the indexes. + * Compile the query to determine the list of columns. * - * @param string|null $schema * @param string $table * @return string */ - public function compileIndexes($schema, $table) + public function compileColumnExists($table) { - return sprintf( - "select idx.name as name, string_agg(col.name, ',') within group (order by idxcol.key_ordinal) as columns, " - .'idx.type_desc as [type], idx.is_unique as [unique], idx.is_primary_key as [primary] ' - .'from sys.indexes as idx ' - .'join sys.tables as tbl on idx.object_id = tbl.object_id ' - .'join sys.schemas as scm on tbl.schema_id = scm.schema_id ' - .'join sys.index_columns as idxcol on idx.object_id = idxcol.object_id and idx.index_id = idxcol.index_id ' - .'join sys.columns as col on idxcol.object_id = col.object_id and idxcol.column_id = col.column_id ' - .'where tbl.name = %s and scm.name = %s ' - .'group by idx.name, idx.type_desc, idx.is_unique, idx.is_primary_key', - $this->quoteString($table), - $schema ? $this->quoteString($schema) : 'schema_name()', - ); - } - - /** - * Compile the query to determine the foreign keys. - * - * @param string|null $schema - * @param string $table - * @return string - */ - public function compileForeignKeys($schema, $table) - { - return sprintf( - 'select fk.name as name, ' - ."string_agg(lc.name, ',') within group (order by fkc.constraint_column_id) as columns, " - .'fs.name as foreign_schema, ft.name as foreign_table, ' - ."string_agg(fc.name, ',') within group (order by fkc.constraint_column_id) as foreign_columns, " - .'fk.update_referential_action_desc as on_update, ' - .'fk.delete_referential_action_desc as on_delete ' - .'from sys.foreign_keys as fk ' - .'join sys.foreign_key_columns as fkc on fkc.constraint_object_id = fk.object_id ' - .'join sys.tables as lt on lt.object_id = fk.parent_object_id ' - .'join sys.schemas as ls on lt.schema_id = ls.schema_id ' - .'join sys.columns as lc on fkc.parent_object_id = lc.object_id and fkc.parent_column_id = lc.column_id ' - .'join sys.tables as ft on ft.object_id = fk.referenced_object_id ' - .'join sys.schemas as fs on ft.schema_id = fs.schema_id ' - .'join sys.columns as fc on fkc.referenced_object_id = fc.object_id and fkc.referenced_column_id = fc.column_id ' - .'where lt.name = %s and ls.name = %s ' - .'group by fk.name, fs.name, ft.name, fk.update_referential_action_desc, fk.delete_referential_action_desc', - $this->quoteString($table), - $schema ? $this->quoteString($schema) : 'schema_name()', - ); + return "select col.name from sys.columns as col + join sys.objects as obj on col.object_id = obj.object_id + where obj.type = 'U' and obj.name = '$table'"; } /** @@ -205,10 +53,9 @@ public function compileForeignKeys($schema, $table) */ public function compileCreate(Blueprint $blueprint, Fluent $command) { - return sprintf('create table %s (%s)', - $this->wrapTable($blueprint, $blueprint->temporary ? '#'.$this->connection->getTablePrefix() : null), - implode(', ', $this->getColumns($blueprint)) - ); + $columns = implode(', ', $this->getColumns($blueprint)); + + return 'create table '.$this->wrapTable($blueprint)." ($columns)"; } /** @@ -220,31 +67,11 @@ public function compileCreate(Blueprint $blueprint, Fluent $command) */ public function compileAdd(Blueprint $blueprint, Fluent $command) { - return sprintf('alter table %s add %s', - $this->wrapTable($blueprint), - $this->getColumn($blueprint, $command->column) - ); - } + $table = $this->wrapTable($blueprint); - /** @inheritDoc */ - public function compileRenameColumn(Blueprint $blueprint, Fluent $command) - { - return sprintf("sp_rename %s, %s, N'COLUMN'", - $this->quoteString($this->wrapTable($blueprint).'.'.$this->wrap($command->from)), - $this->wrap($command->to) - ); - } + $columns = $this->getColumns($blueprint); - /** @inheritDoc */ - public function compileChange(Blueprint $blueprint, Fluent $command) - { - return [ - $this->compileDropDefaultConstraint($blueprint, $command), - sprintf('alter table %s alter column %s', - $this->wrapTable($blueprint), - $this->getColumn($blueprint, $command->column), - ), - ]; + return 'alter table '.$table.' add '.implode(', ', $columns); } /** @@ -256,11 +83,13 @@ public function compileChange(Blueprint $blueprint, Fluent $command) */ public function compilePrimary(Blueprint $blueprint, Fluent $command) { - return sprintf('alter table %s add constraint %s primary key (%s)', - $this->wrapTable($blueprint), - $this->wrap($command->index), - $this->columnize($command->columns) - ); + $columns = $this->columnize($command->columns); + + $table = $this->wrapTable($blueprint); + + $index = $this->wrap($command->index); + + return "alter table {$table} add constraint {$index} primary key ({$columns})"; } /** @@ -272,12 +101,13 @@ public function compilePrimary(Blueprint $blueprint, Fluent $command) */ public function compileUnique(Blueprint $blueprint, Fluent $command) { - return sprintf('create unique index %s on %s (%s)%s', - $this->wrap($command->index), - $this->wrapTable($blueprint), - $this->columnize($command->columns), - $command->online ? ' with (online = on)' : '' - ); + $columns = $this->columnize($command->columns); + + $table = $this->wrapTable($blueprint); + + $index = $this->wrap($command->index); + + return "create unique index {$index} on {$table} ({$columns})"; } /** @@ -289,46 +119,13 @@ public function compileUnique(Blueprint $blueprint, Fluent $command) */ public function compileIndex(Blueprint $blueprint, Fluent $command) { - return sprintf('create index %s on %s (%s)%s', - $this->wrap($command->index), - $this->wrapTable($blueprint), - $this->columnize($command->columns), - $command->online ? ' with (online = on)' : '' - ); - } + $columns = $this->columnize($command->columns); - /** - * Compile a spatial index key command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileSpatialIndex(Blueprint $blueprint, Fluent $command) - { - return sprintf('create spatial index %s on %s (%s)', - $this->wrap($command->index), - $this->wrapTable($blueprint), - $this->columnize($command->columns) - ); - } + $table = $this->wrapTable($blueprint); - /** - * Compile a default command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string|null - */ - public function compileDefault(Blueprint $blueprint, Fluent $command) - { - if ($command->column->change && ! is_null($command->column->default)) { - return sprintf('alter table %s add default %s for %s', - $this->wrapTable($blueprint), - $this->getDefaultValue($command->column->default), - $this->wrap($command->column) - ); - } + $index = $this->wrap($command->index); + + return "create index {$index} on {$table} ({$columns})"; } /** @@ -352,20 +149,7 @@ public function compileDrop(Blueprint $blueprint, Fluent $command) */ public function compileDropIfExists(Blueprint $blueprint, Fluent $command) { - return sprintf('if object_id(%s, \'U\') is not null drop table %s', - $this->quoteString($this->wrapTable($blueprint)), - $this->wrapTable($blueprint) - ); - } - - /** - * Compile the SQL needed to drop all tables. - * - * @return string - */ - public function compileDropAllTables() - { - return "EXEC sp_msforeachtable 'DROP TABLE ?'"; + return 'if exists (select * from INFORMATION_SCHEMA.TABLES where TABLE_NAME = \''.$blueprint->getTable().'\') drop table ['.$blueprint->getTable().']'; } /** @@ -379,34 +163,9 @@ public function compileDropColumn(Blueprint $blueprint, Fluent $command) { $columns = $this->wrapArray($command->columns); - $dropExistingConstraintsSql = $this->compileDropDefaultConstraint($blueprint, $command).';'; - - return $dropExistingConstraintsSql.'alter table '.$this->wrapTable($blueprint).' drop column '.implode(', ', $columns); - } - - /** - * Compile a drop default constraint command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileDropDefaultConstraint(Blueprint $blueprint, Fluent $command) - { - $columns = $command->name === 'change' - ? "'".$command->column->name."'" - : "'".implode("', '", $command->columns)."'"; - $table = $this->wrapTable($blueprint); - $tableName = $this->quoteString($this->wrapTable($blueprint)); - $sql = "DECLARE @sql NVARCHAR(MAX) = '';"; - $sql .= "SELECT @sql += 'ALTER TABLE $table DROP CONSTRAINT ' + OBJECT_NAME([default_object_id]) + ';' "; - $sql .= 'FROM sys.columns '; - $sql .= "WHERE [object_id] = OBJECT_ID($tableName) AND [name] in ($columns) AND [default_object_id] <> 0;"; - $sql .= 'EXEC(@sql)'; - - return $sql; + return 'alter table '.$table.' drop column '.implode(', ', $columns); } /** @@ -418,9 +177,11 @@ public function compileDropDefaultConstraint(Blueprint $blueprint, Fluent $comma */ public function compileDropPrimary(Blueprint $blueprint, Fluent $command) { + $table = $this->wrapTable($blueprint); + $index = $this->wrap($command->index); - return "alter table {$this->wrapTable($blueprint)} drop constraint {$index}"; + return "alter table {$table} drop constraint {$index}"; } /** @@ -432,9 +193,11 @@ public function compileDropPrimary(Blueprint $blueprint, Fluent $command) */ public function compileDropUnique(Blueprint $blueprint, Fluent $command) { + $table = $this->wrapTable($blueprint); + $index = $this->wrap($command->index); - return "drop index {$index} on {$this->wrapTable($blueprint)}"; + return "drop index {$index} on {$table}"; } /** @@ -446,21 +209,11 @@ public function compileDropUnique(Blueprint $blueprint, Fluent $command) */ public function compileDropIndex(Blueprint $blueprint, Fluent $command) { - $index = $this->wrap($command->index); + $table = $this->wrapTable($blueprint); - return "drop index {$index} on {$this->wrapTable($blueprint)}"; - } + $index = $this->wrap($command->index); - /** - * Compile a drop spatial index command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileDropSpatialIndex(Blueprint $blueprint, Fluent $command) - { - return $this->compileDropIndex($blueprint, $command); + return "drop index {$index} on {$table}"; } /** @@ -472,9 +225,11 @@ public function compileDropSpatialIndex(Blueprint $blueprint, Fluent $command) */ public function compileDropForeign(Blueprint $blueprint, Fluent $command) { + $table = $this->wrapTable($blueprint); + $index = $this->wrap($command->index); - return "alter table {$this->wrapTable($blueprint)} drop constraint {$index}"; + return "alter table {$table} drop constraint {$index}"; } /** @@ -486,25 +241,9 @@ public function compileDropForeign(Blueprint $blueprint, Fluent $command) */ public function compileRename(Blueprint $blueprint, Fluent $command) { - return sprintf('sp_rename %s, %s', - $this->quoteString($this->wrapTable($blueprint)), - $this->wrapTable($command->to) - ); - } + $from = $this->wrapTable($blueprint); - /** - * Compile a rename index command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileRenameIndex(Blueprint $blueprint, Fluent $command) - { - return sprintf("sp_rename %s, %s, N'INDEX'", - $this->quoteString($this->wrapTable($blueprint).'.'.$this->wrap($command->from)), - $this->wrap($command->to) - ); + return "sp_rename {$from}, ".$this->wrapTable($command->to); } /** @@ -527,36 +266,6 @@ public function compileDisableForeignKeyConstraints() return 'EXEC sp_msforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT all";'; } - /** - * Compile the command to drop all foreign keys. - * - * @return string - */ - public function compileDropAllForeignKeys() - { - return "DECLARE @sql NVARCHAR(MAX) = N''; - SELECT @sql += 'ALTER TABLE ' - + QUOTENAME(OBJECT_SCHEMA_NAME(parent_object_id)) + '.' + + QUOTENAME(OBJECT_NAME(parent_object_id)) - + ' DROP CONSTRAINT ' + QUOTENAME(name) + ';' - FROM sys.foreign_keys; - - EXEC sp_executesql @sql;"; - } - - /** - * Compile the command to drop all views. - * - * @return string - */ - public function compileDropAllViews() - { - return "DECLARE @sql NVARCHAR(MAX) = N''; - SELECT @sql += 'DROP VIEW ' + QUOTENAME(OBJECT_SCHEMA_NAME(object_id)) + '.' + QUOTENAME(name) + ';' - FROM sys.views; - - EXEC sp_executesql @sql;"; - } - /** * Create the column definition for a char type. * @@ -579,17 +288,6 @@ protected function typeString(Fluent $column) return "nvarchar({$column->length})"; } - /** - * Create the column definition for a tiny text type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeTinyText(Fluent $column) - { - return 'nvarchar(255)'; - } - /** * Create the column definition for a text type. * @@ -624,7 +322,7 @@ protected function typeLongText(Fluent $column) } /** - * Create the column definition for an integer type. + * Create the column definition for a integer type. * * @param \Illuminate\Support\Fluent $column * @return string @@ -686,10 +384,6 @@ protected function typeSmallInteger(Fluent $column) */ protected function typeFloat(Fluent $column) { - if ($column->precision) { - return "float({$column->precision})"; - } - return 'float'; } @@ -701,7 +395,7 @@ protected function typeFloat(Fluent $column) */ protected function typeDouble(Fluent $column) { - return 'double precision'; + return 'float'; } /** @@ -727,18 +421,14 @@ protected function typeBoolean(Fluent $column) } /** - * Create the column definition for an enumeration type. + * Create the column definition for an enum type. * * @param \Illuminate\Support\Fluent $column * @return string */ protected function typeEnum(Fluent $column) { - return sprintf( - 'nvarchar(255) check ("%s" in (%s))', - $column->name, - $this->quoteString($column->allowed) - ); + return 'nvarchar(255)'; } /** @@ -771,10 +461,6 @@ protected function typeJsonb(Fluent $column) */ protected function typeDate(Fluent $column) { - if ($column->useCurrent) { - $column->default(new Expression('CAST(GETDATE() AS DATE)')); - } - return 'date'; } @@ -786,18 +472,18 @@ protected function typeDate(Fluent $column) */ protected function typeDateTime(Fluent $column) { - return $this->typeTimestamp($column); + return 'datetime'; } /** - * Create the column definition for a date-time (with time zone) type. + * Create the column definition for a date-time type. * * @param \Illuminate\Support\Fluent $column * @return string */ protected function typeDateTimeTz(Fluent $column) { - return $this->typeTimestampTz($column); + return 'datetimeoffset(0)'; } /** @@ -808,18 +494,18 @@ protected function typeDateTimeTz(Fluent $column) */ protected function typeTime(Fluent $column) { - return $column->precision ? "time($column->precision)" : 'time'; + return 'time'; } /** - * Create the column definition for a time (with time zone) type. + * Create the column definition for a time type. * * @param \Illuminate\Support\Fluent $column * @return string */ protected function typeTimeTz(Fluent $column) { - return $this->typeTime($column); + return 'time'; } /** @@ -831,16 +517,16 @@ protected function typeTimeTz(Fluent $column) protected function typeTimestamp(Fluent $column) { if ($column->useCurrent) { - $column->default(new Expression('CURRENT_TIMESTAMP')); + return 'datetime default CURRENT_TIMESTAMP'; } - return $column->precision ? "datetime2($column->precision)" : 'datetime'; + return 'datetime'; } /** - * Create the column definition for a timestamp (with time zone) type. + * Create the column definition for a timestamp type. * - * @link https://docs.microsoft.com/en-us/sql/t-sql/data-types/datetimeoffset-transact-sql?view=sql-server-ver15 + * @link https://msdn.microsoft.com/en-us/library/bb630289(v=sql.120).aspx * * @param \Illuminate\Support\Fluent $column * @return string @@ -848,25 +534,10 @@ protected function typeTimestamp(Fluent $column) protected function typeTimestampTz(Fluent $column) { if ($column->useCurrent) { - $column->default(new Expression('CURRENT_TIMESTAMP')); - } - - return $column->precision ? "datetimeoffset($column->precision)" : 'datetimeoffset'; - } - - /** - * Create the column definition for a year type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeYear(Fluent $column) - { - if ($column->useCurrent) { - $column->default(new Expression('CAST(YEAR(GETDATE()) AS INTEGER)')); + return 'datetimeoffset(0) default CURRENT_TIMESTAMP'; } - return $this->typeInteger($column); + return 'datetimeoffset(0)'; } /** @@ -877,10 +548,6 @@ protected function typeYear(Fluent $column) */ protected function typeBinary(Fluent $column) { - if ($column->length) { - return $column->fixed ? "binary({$column->length})" : "varbinary({$column->length})"; - } - return 'varbinary(max)'; } @@ -917,53 +584,6 @@ protected function typeMacAddress(Fluent $column) return 'nvarchar(17)'; } - /** - * Create the column definition for a spatial Geometry type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeGeometry(Fluent $column) - { - return 'geometry'; - } - - /** - * Create the column definition for a spatial Geography type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeGeography(Fluent $column) - { - return 'geography'; - } - - /** - * Create the column definition for a generated, computed column type. - * - * @param \Illuminate\Support\Fluent $column - * @return string|null - */ - protected function typeComputed(Fluent $column) - { - return "as ({$this->getValue($column->expression)})"; - } - - /** - * Get the SQL for a collation column modifier. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $column - * @return string|null - */ - protected function modifyCollate(Blueprint $blueprint, Fluent $column) - { - if (! is_null($column->collation)) { - return ' collate '.$column->collation; - } - } - /** * Get the SQL for a nullable column modifier. * @@ -973,9 +593,7 @@ protected function modifyCollate(Blueprint $blueprint, Fluent $column) */ protected function modifyNullable(Blueprint $blueprint, Fluent $column) { - if ($column->type !== 'computed') { - return $column->nullable ? ' null' : ' not null'; - } + return $column->nullable ? ' null' : ' not null'; } /** @@ -987,7 +605,7 @@ protected function modifyNullable(Blueprint $blueprint, Fluent $column) */ protected function modifyDefault(Blueprint $blueprint, Fluent $column) { - if (! $column->change && ! is_null($column->default)) { + if (! is_null($column->default)) { return ' default '.$this->getDefaultValue($column->default); } } @@ -1001,45 +619,8 @@ protected function modifyDefault(Blueprint $blueprint, Fluent $column) */ protected function modifyIncrement(Blueprint $blueprint, Fluent $column) { - if (! $column->change && in_array($column->type, $this->serials) && $column->autoIncrement) { - return $this->hasCommand($blueprint, 'primary') ? ' identity' : ' identity primary key'; - } - } - - /** - * Get the SQL for a generated stored column modifier. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $column - * @return string|null - */ - protected function modifyPersisted(Blueprint $blueprint, Fluent $column) - { - if ($column->change) { - if ($column->type === 'computed') { - return $column->persisted ? ' add persisted' : ' drop persisted'; - } - - return null; + if (in_array($column->type, $this->serials) && $column->autoIncrement) { + return ' identity primary key'; } - - if ($column->persisted) { - return ' persisted'; - } - } - - /** - * Quote the given string literal. - * - * @param string|array $value - * @return string - */ - public function quoteString($value) - { - if (is_array($value)) { - return implode(', ', array_map([$this, __FUNCTION__], $value)); - } - - return "N'$value'"; } } diff --git a/Schema/IndexDefinition.php b/Schema/IndexDefinition.php deleted file mode 100644 index 2ebe07a0ea..0000000000 --- a/Schema/IndexDefinition.php +++ /dev/null @@ -1,19 +0,0 @@ -detectClientVersion(); - - $command = 'mariadb '.$this->connectionString($versionInfo).' --database="${:LARAVEL_LOAD_DATABASE}" < "${:LARAVEL_LOAD_PATH}"'; - - $process = $this->makeProcess($command)->setTimeout(null); - - $process->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ - 'LARAVEL_LOAD_PATH' => $path, - ])); - } - - /** - * Get the base dump command arguments for MariaDB as a string. - * - * @return string - */ - protected function baseDumpCommand() - { - $versionInfo = $this->detectClientVersion(); - - $command = 'mariadb-dump '.$this->connectionString($versionInfo).' --no-tablespaces --skip-add-locks --skip-comments --skip-set-charset --tz-utc'; - - return $command.' "${:LARAVEL_LOAD_DATABASE}"'; - } -} diff --git a/Schema/MySqlBuilder.php b/Schema/MySqlBuilder.php index 6676411225..e0b0ade967 100755 --- a/Schema/MySqlBuilder.php +++ b/Schema/MySqlBuilder.php @@ -5,54 +5,38 @@ class MySqlBuilder extends Builder { /** - * Drop all tables from the database. + * Determine if the given table exists. * - * @return void + * @param string $table + * @return bool */ - public function dropAllTables() + public function hasTable($table) { - $tables = $this->getTableListing($this->getCurrentSchemaListing()); + $sql = $this->grammar->compileTableExists(); - if (empty($tables)) { - return; - } + $database = $this->connection->getDatabaseName(); - $this->disableForeignKeyConstraints(); + $table = $this->connection->getTablePrefix().$table; - try { - $this->connection->statement( - $this->grammar->compileDropAllTables($tables) - ); - } finally { - $this->enableForeignKeyConstraints(); - } + return count($this->connection->select($sql, [$database, $table])) > 0; } /** - * Drop all views from the database. + * Get the column listing for a given table. * - * @return void + * @param string $table + * @return array */ - public function dropAllViews() + public function getColumnListing($table) { - $views = array_column($this->getViews($this->getCurrentSchemaListing()), 'schema_qualified_name'); + $sql = $this->grammar->compileColumnExists(); - if (empty($views)) { - return; - } + $database = $this->connection->getDatabaseName(); - $this->connection->statement( - $this->grammar->compileDropAllViews($views) - ); - } + $table = $this->connection->getTablePrefix().$table; - /** - * Get the names of current schemas for the connection. - * - * @return string[]|null - */ - public function getCurrentSchemaListing() - { - return [$this->connection->getDatabaseName()]; + $results = $this->connection->select($sql, [$database, $table]); + + return $this->connection->getPostProcessor()->processColumnListing($results); } } diff --git a/Schema/MySqlSchemaState.php b/Schema/MySqlSchemaState.php deleted file mode 100644 index dbfa3ccaef..0000000000 --- a/Schema/MySqlSchemaState.php +++ /dev/null @@ -1,235 +0,0 @@ -executeDumpProcess($this->makeProcess( - $this->baseDumpCommand().' --routines --result-file="${:LARAVEL_LOAD_PATH}" --no-data' - ), $this->output, array_merge($this->baseVariables($this->connection->getConfig()), [ - 'LARAVEL_LOAD_PATH' => $path, - ])); - - $this->removeAutoIncrementingState($path); - - if ($this->hasMigrationTable()) { - $this->appendMigrationData($path); - } - } - - /** - * Remove the auto-incrementing state from the given schema dump. - * - * @param string $path - * @return void - */ - protected function removeAutoIncrementingState(string $path) - { - $this->files->put($path, preg_replace( - '/\s+AUTO_INCREMENT=[0-9]+/iu', - '', - $this->files->get($path) - )); - } - - /** - * Append the migration data to the schema dump. - * - * @param string $path - * @return void - */ - protected function appendMigrationData(string $path) - { - $process = $this->executeDumpProcess($this->makeProcess( - $this->baseDumpCommand().' '.$this->getMigrationTable().' --no-create-info --skip-extended-insert --skip-routines --compact --complete-insert' - ), null, array_merge($this->baseVariables($this->connection->getConfig()), [ - // - ])); - - $this->files->append($path, $process->getOutput()); - } - - /** - * Load the given schema file into the database. - * - * @param string $path - * @return void - */ - public function load($path) - { - $versionInfo = $this->detectClientVersion(); - - $command = 'mysql '.$this->connectionString($versionInfo).' --database="${:LARAVEL_LOAD_DATABASE}" < "${:LARAVEL_LOAD_PATH}"'; - - $process = $this->makeProcess($command)->setTimeout(null); - - $process->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ - 'LARAVEL_LOAD_PATH' => $path, - ])); - } - - /** - * Get the base dump command arguments for MySQL as a string. - * - * @return string - */ - protected function baseDumpCommand() - { - $versionInfo = $this->detectClientVersion(); - - $command = 'mysqldump '.$this->connectionString($versionInfo).' --no-tablespaces --skip-add-locks --skip-comments --skip-set-charset --tz-utc --column-statistics=0'; - - if (! $this->connection->isMaria()) { - $command .= ' --set-gtid-purged=OFF'; - } - - return $command.' "${:LARAVEL_LOAD_DATABASE}"'; - } - - /** - * Generate a basic connection string (--socket, --host, --port, --user, --password) for the database. - * - * @param array{version: string, isMariaDb: bool} $versionInfo - * @return string - */ - protected function connectionString(array $versionInfo) - { - $value = ' --user="${:LARAVEL_LOAD_USER}" --password="${:LARAVEL_LOAD_PASSWORD}"'; - - $config = $this->connection->getConfig(); - - $value .= $config['unix_socket'] ?? false - ? ' --socket="${:LARAVEL_LOAD_SOCKET}"' - : ' --host="${:LARAVEL_LOAD_HOST}" --port="${:LARAVEL_LOAD_PORT}"'; - - /** @phpstan-ignore class.notFound */ - if (isset($config['options'][PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA])) { - $value .= ' --ssl-ca="${:LARAVEL_LOAD_SSL_CA}"'; - } - - /** @phpstan-ignore class.notFound */ - if (isset($config['options'][PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CERT : \PDO::MYSQL_ATTR_SSL_CERT])) { - $value .= ' --ssl-cert="${:LARAVEL_LOAD_SSL_CERT}"'; - } - - /** @phpstan-ignore class.notFound */ - if (isset($config['options'][PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_KEY : \PDO::MYSQL_ATTR_SSL_KEY])) { - $value .= ' --ssl-key="${:LARAVEL_LOAD_SSL_KEY}"'; - } - - /** @phpstan-ignore class.notFound */ - $verifyCertOption = PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_VERIFY_SERVER_CERT : \PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT; - - if (isset($config['options'][$verifyCertOption]) && $config['options'][$verifyCertOption] === false) { - if (version_compare($versionInfo['version'], '5.7.11', '>=') && ! $versionInfo['isMariaDb']) { - $value .= ' --ssl-mode=DISABLED'; - } else { - $value .= ' --ssl=off'; - } - } - - return $value; - } - - /** - * Get the base variables for a dump / load command. - * - * @param array $config - * @return array - */ - protected function baseVariables(array $config) - { - $config['host'] ??= ''; - - return [ - 'LARAVEL_LOAD_SOCKET' => $config['unix_socket'] ?? '', - '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_DATABASE' => $config['database'], - 'LARAVEL_LOAD_SSL_CA' => $config['options'][PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA] ?? '', // @phpstan-ignore class.notFound - 'LARAVEL_LOAD_SSL_CERT' => $config['options'][PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CERT : \PDO::MYSQL_ATTR_SSL_CERT] ?? '', // @phpstan-ignore class.notFound - 'LARAVEL_LOAD_SSL_KEY' => $config['options'][PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_KEY : \PDO::MYSQL_ATTR_SSL_KEY] ?? '', // @phpstan-ignore class.notFound - ]; - } - - /** - * Execute the given dump process. - * - * @param \Symfony\Component\Process\Process $process - * @param callable $output - * @param array $variables - * @param int $depth - * @return \Symfony\Component\Process\Process - * - * @throws \Throwable - */ - protected function executeDumpProcess(Process $process, $output, array $variables, int $depth = 0) - { - if ($depth > 30) { - throw new Exception('Dump execution exceeded maximum depth of 30.'); - } - - try { - $process->setTimeout(null)->mustRun($output, $variables); - } catch (Exception $e) { - if (Str::contains($e->getMessage(), ['column-statistics', 'column_statistics'])) { - return $this->executeDumpProcess(Process::fromShellCommandLine( - str_replace(' --column-statistics=0', '', $process->getCommandLine()) - ), $output, $variables, $depth + 1); - } - - if (str_contains($e->getMessage(), 'set-gtid-purged')) { - return $this->executeDumpProcess(Process::fromShellCommandLine( - str_replace(' --set-gtid-purged=OFF', '', $process->getCommandLine()) - ), $output, $variables, $depth + 1); - } - - throw $e; - } - - return $process; - } - - /** - * Detect the MySQL client version. - * - * @return array{version: string, isMariaDb: bool} - */ - protected function detectClientVersion(): array - { - [$version, $isMariaDb] = ['8.0.0', false]; - - try { - $versionOutput = $this->makeProcess('mysql --version')->mustRun()->getOutput(); - - if (preg_match('/(\d+\.\d+\.\d+)/', $versionOutput, $matches)) { - $version = $matches[1]; - } - - $isMariaDb = stripos($versionOutput, 'mariadb') !== false; - } catch (ProcessFailedException) { - } - - return [ - 'version' => $version, - 'isMariaDb' => $isMariaDb, - ]; - } -} diff --git a/Schema/PostgresBuilder.php b/Schema/PostgresBuilder.php index 66f3117427..20abc0fe1e 100755 --- a/Schema/PostgresBuilder.php +++ b/Schema/PostgresBuilder.php @@ -2,99 +2,22 @@ namespace Illuminate\Database\Schema; -use Illuminate\Database\Concerns\ParsesSearchPath; - class PostgresBuilder extends Builder { - use ParsesSearchPath; - - /** - * Drop all tables from the database. - * - * @return void - */ - public function dropAllTables() - { - $tables = []; - - $excludedTables = $this->connection->getConfig('dont_drop') ?? ['spatial_ref_sys']; - - foreach ($this->getTables($this->getCurrentSchemaListing()) as $table) { - if (empty(array_intersect([$table['name'], $table['schema_qualified_name']], $excludedTables))) { - $tables[] = $table['schema_qualified_name']; - } - } - - if (empty($tables)) { - return; - } - - $this->connection->statement( - $this->grammar->compileDropAllTables($tables) - ); - } - - /** - * Drop all views from the database. - * - * @return void - */ - public function dropAllViews() - { - $views = array_column($this->getViews($this->getCurrentSchemaListing()), 'schema_qualified_name'); - - if (empty($views)) { - return; - } - - $this->connection->statement( - $this->grammar->compileDropAllViews($views) - ); - } - /** - * Drop all types from the database. + * Determine if the given table exists. * - * @return void + * @param string $table + * @return bool */ - public function dropAllTypes() + public function hasTable($table) { - $types = []; - $domains = []; + $sql = $this->grammar->compileTableExists(); - foreach ($this->getTypes($this->getCurrentSchemaListing()) as $type) { - if (! $type['implicit']) { - if ($type['type'] === 'domain') { - $domains[] = $type['schema_qualified_name']; - } else { - $types[] = $type['schema_qualified_name']; - } - } - } + $schema = $this->connection->getConfig('schema'); - if (! empty($types)) { - $this->connection->statement($this->grammar->compileDropAllTypes($types)); - } + $table = $this->connection->getTablePrefix().$table; - if (! empty($domains)) { - $this->connection->statement($this->grammar->compileDropAllDomains($domains)); - } - } - - /** - * Get the current schemas for the connection. - * - * @return string[] - */ - public function getCurrentSchemaListing() - { - return array_map( - fn ($schema) => $schema === '$user' ? $this->connection->getConfig('username') : $schema, - $this->parseSearchPath( - $this->connection->getConfig('search_path') - ?: $this->connection->getConfig('schema') - ?: 'public' - ) - ); + return count($this->connection->select($sql, [$schema, $table])) > 0; } } diff --git a/Schema/PostgresSchemaState.php b/Schema/PostgresSchemaState.php deleted file mode 100644 index 25da812e61..0000000000 --- a/Schema/PostgresSchemaState.php +++ /dev/null @@ -1,95 +0,0 @@ -baseDumpCommand().' --schema-only > '.$path, - ]); - - if ($this->hasMigrationTable()) { - $commands->push($this->baseDumpCommand().' -t '.$this->getMigrationTable().' --data-only >> '.$path); - } - - $commands->map(function ($command, $path) { - $this->makeProcess($command)->mustRun($this->output, array_merge($this->baseVariables($this->connection->getConfig()), [ - 'LARAVEL_LOAD_PATH' => $path, - ])); - }); - } - - /** - * Load the given schema file into the database. - * - * @param string $path - * @return void - */ - public function load($path) - { - $command = 'pg_restore --no-owner --no-acl --clean --if-exists --host="${:LARAVEL_LOAD_HOST}" --port="${:LARAVEL_LOAD_PORT}" --username="${:LARAVEL_LOAD_USER}" --dbname="${:LARAVEL_LOAD_DATABASE}" "${:LARAVEL_LOAD_PATH}"'; - - if (str_ends_with($path, '.sql')) { - $command = 'psql --file="${:LARAVEL_LOAD_PATH}" --host="${:LARAVEL_LOAD_HOST}" --port="${:LARAVEL_LOAD_PORT}" --username="${:LARAVEL_LOAD_USER}" --dbname="${:LARAVEL_LOAD_DATABASE}"'; - } - - $process = $this->makeProcess($command); - - $process->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ - 'LARAVEL_LOAD_PATH' => $path, - ])); - } - - /** - * Get the name of the application's migration table. - * - * @return string - */ - protected function getMigrationTable(): string - { - [$schema, $table] = $this->connection->getSchemaBuilder()->parseSchemaAndTable($this->migrationTable, withDefaultSchema: true); - - return $schema.'.'.$this->connection->getTablePrefix().$table; - } - - /** - * Get the base dump command arguments for PostgreSQL as a string. - * - * @return string - */ - protected function baseDumpCommand() - { - return 'pg_dump --no-owner --no-acl --host="${:LARAVEL_LOAD_HOST}" --port="${:LARAVEL_LOAD_PORT}" --username="${:LARAVEL_LOAD_USER}" --dbname="${:LARAVEL_LOAD_DATABASE}"'; - } - - /** - * Get the base variables for a dump / load command. - * - * @param array $config - * @return array - */ - protected function baseVariables(array $config) - { - $config['host'] ??= ''; - - return [ - 'LARAVEL_LOAD_HOST' => is_array($config['host']) ? $config['host'][0] : $config['host'], - 'LARAVEL_LOAD_PORT' => $config['port'] ?? '', - 'LARAVEL_LOAD_USER' => $config['username'], - 'PGPASSWORD' => $config['password'], - 'LARAVEL_LOAD_DATABASE' => $config['database'], - ]; - } -} diff --git a/Schema/SQLiteBuilder.php b/Schema/SQLiteBuilder.php deleted file mode 100644 index f750e97edf..0000000000 --- a/Schema/SQLiteBuilder.php +++ /dev/null @@ -1,173 +0,0 @@ -connection->scalar($this->grammar->compileDbstatExists()); - } catch (QueryException) { - $withSize = false; - } - - if (version_compare($this->connection->getServerVersion(), '3.37.0', '<')) { - $schema ??= array_column($this->getSchemas(), 'name'); - - $tables = []; - - foreach (Arr::wrap($schema) as $name) { - $tables = array_merge($tables, $this->connection->selectFromWriteConnection( - $this->grammar->compileLegacyTables($name, $withSize) - )); - } - - return $this->connection->getPostProcessor()->processTables($tables); - } - - return $this->connection->getPostProcessor()->processTables( - $this->connection->selectFromWriteConnection( - $this->grammar->compileTables($schema) - ) - ); - } - - /** @inheritDoc */ - public function getViews($schema = null) - { - $schema ??= array_column($this->getSchemas(), 'name'); - - $views = []; - - foreach (Arr::wrap($schema) as $name) { - $views = array_merge($views, $this->connection->selectFromWriteConnection( - $this->grammar->compileViews($name) - )); - } - - return $this->connection->getPostProcessor()->processViews($views); - } - - /** @inheritDoc */ - public function getColumns($table) - { - [$schema, $table] = $this->parseSchemaAndTable($table); - - $table = $this->connection->getTablePrefix().$table; - - return $this->connection->getPostProcessor()->processColumns( - $this->connection->selectFromWriteConnection($this->grammar->compileColumns($schema, $table)), - $this->connection->scalar($this->grammar->compileSqlCreateStatement($schema, $table)) - ); - } - - /** - * Drop all tables from the database. - * - * @return void - */ - public function dropAllTables() - { - foreach ($this->getCurrentSchemaListing() as $schema) { - $database = $schema === 'main' - ? $this->connection->getDatabaseName() - : (array_column($this->getSchemas(), 'path', 'name')[$schema] ?: ':memory:'); - - if ($database !== ':memory:' && - ! str_contains($database, '?mode=memory') && - ! str_contains($database, '&mode=memory') - ) { - $this->refreshDatabaseFile($database); - } else { - $this->pragma('writable_schema', 1); - - $this->connection->statement($this->grammar->compileDropAllTables($schema)); - - $this->pragma('writable_schema', 0); - - $this->connection->statement($this->grammar->compileRebuild($schema)); - } - } - } - - /** - * Drop all views from the database. - * - * @return void - */ - public function dropAllViews() - { - foreach ($this->getCurrentSchemaListing() as $schema) { - $this->pragma('writable_schema', 1); - - $this->connection->statement($this->grammar->compileDropAllViews($schema)); - - $this->pragma('writable_schema', 0); - - $this->connection->statement($this->grammar->compileRebuild($schema)); - } - } - - /** - * Get the value for the given pragma name or set the given value. - * - * @param string $key - * @param mixed $value - * @return mixed - */ - public function pragma($key, $value = null) - { - return is_null($value) - ? $this->connection->scalar($this->grammar->pragma($key)) - : $this->connection->statement($this->grammar->pragma($key, $value)); - } - - /** - * Empty the database file. - * - * @param string|null $path - * @return void - */ - public function refreshDatabaseFile($path = null) - { - file_put_contents($path ?? $this->connection->getDatabaseName(), ''); - } - - /** - * Get the names of current schemas for the connection. - * - * @return string[]|null - */ - public function getCurrentSchemaListing() - { - return ['main']; - } -} diff --git a/Schema/SchemaState.php b/Schema/SchemaState.php deleted file mode 100644 index be792138f7..0000000000 --- a/Schema/SchemaState.php +++ /dev/null @@ -1,141 +0,0 @@ -connection = $connection; - - $this->files = $files ?: new Filesystem; - - $this->processFactory = $processFactory ?: function (...$arguments) { - return Process::fromShellCommandline(...$arguments)->setTimeout(null); - }; - - $this->handleOutputUsing(function () { - // - }); - } - - /** - * Dump the database's schema into a file. - * - * @param \Illuminate\Database\Connection $connection - * @param string $path - * @return void - */ - abstract public function dump(Connection $connection, $path); - - /** - * Load the given schema file into the database. - * - * @param string $path - * @return void - */ - abstract public function load($path); - - /** - * Create a new process instance. - * - * @param mixed ...$arguments - * @return \Symfony\Component\Process\Process - */ - public function makeProcess(...$arguments) - { - return call_user_func($this->processFactory, ...$arguments); - } - - /** - * Determine if the current connection has a migration table. - * - * @return bool - */ - public function hasMigrationTable(): bool - { - return $this->connection->getSchemaBuilder()->hasTable($this->migrationTable); - } - - /** - * Get the name of the application's migration table. - * - * @return string - */ - protected function getMigrationTable(): string - { - return $this->connection->getTablePrefix().$this->migrationTable; - } - - /** - * Specify the name of the application's migration table. - * - * @param string $table - * @return $this - */ - public function withMigrationTable(string $table) - { - $this->migrationTable = $table; - - return $this; - } - - /** - * Specify the callback that should be used to handle process output. - * - * @param callable $output - * @return $this - */ - public function handleOutputUsing(callable $output) - { - $this->output = $output; - - return $this; - } -} diff --git a/Schema/SqlServerBuilder.php b/Schema/SqlServerBuilder.php deleted file mode 100644 index 9161bc61af..0000000000 --- a/Schema/SqlServerBuilder.php +++ /dev/null @@ -1,40 +0,0 @@ -connection->statement($this->grammar->compileDropAllForeignKeys()); - - $this->connection->statement($this->grammar->compileDropAllTables()); - } - - /** - * Drop all views from the database. - * - * @return void - */ - public function dropAllViews() - { - $this->connection->statement($this->grammar->compileDropAllViews()); - } - - /** - * Get the default schema name for the connection. - * - * @return string|null - */ - public function getCurrentSchemaName() - { - return Arr::first($this->getSchemas(), fn ($schema) => $schema['default'])['name']; - } -} diff --git a/Schema/SqliteSchemaState.php b/Schema/SqliteSchemaState.php deleted file mode 100644 index 3d954a39de..0000000000 --- a/Schema/SqliteSchemaState.php +++ /dev/null @@ -1,103 +0,0 @@ -makeProcess($this->baseCommand().' ".schema --indent"') - ->setTimeout(null) - ->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ - // - ])); - - $migrations = preg_replace('/CREATE TABLE sqlite_.+?\);[\r\n]+/is', '', $process->getOutput()); - - $this->files->put($path, $migrations.PHP_EOL); - - if ($this->hasMigrationTable()) { - $this->appendMigrationData($path); - } - } - - /** - * Append the migration data to the schema dump. - * - * @param string $path - * @return void - */ - protected function appendMigrationData(string $path) - { - $process = $this->makeProcess( - $this->baseCommand().' ".dump \''.$this->getMigrationTable().'\'"' - )->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ - // - ])); - - $migrations = (new Collection(preg_split("/\r\n|\n|\r/", $process->getOutput()))) - ->filter(fn ($line) => preg_match('/^\s*(--|INSERT\s)/iu', $line) === 1 && strlen($line) > 0) - ->all(); - - $this->files->append($path, implode(PHP_EOL, $migrations).PHP_EOL); - } - - /** - * Load the given schema file into the database. - * - * @param string $path - * @return void - */ - public function load($path) - { - $database = $this->connection->getDatabaseName(); - - if ($database === ':memory:' || - str_contains($database, '?mode=memory') || - str_contains($database, '&mode=memory') - ) { - $this->connection->getPdo()->exec($this->files->get($path)); - - return; - } - - $process = $this->makeProcess($this->baseCommand().' < "${:LARAVEL_LOAD_PATH}"'); - - $process->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ - 'LARAVEL_LOAD_PATH' => $path, - ])); - } - - /** - * Get the base sqlite command arguments as a string. - * - * @return string - */ - protected function baseCommand() - { - return 'sqlite3 "${:LARAVEL_LOAD_DATABASE}"'; - } - - /** - * Get the base variables for a dump / load command. - * - * @param array $config - * @return array - */ - protected function baseVariables(array $config) - { - return [ - 'LARAVEL_LOAD_DATABASE' => $config['database'], - ]; - } -} diff --git a/SeedServiceProvider.php b/SeedServiceProvider.php new file mode 100755 index 0000000000..417e86adb9 --- /dev/null +++ b/SeedServiceProvider.php @@ -0,0 +1,54 @@ +app->singleton('seeder', function () { + return new Seeder; + }); + + $this->registerSeedCommand(); + + $this->commands('command.seed'); + } + + /** + * Register the seed console command. + * + * @return void + */ + protected function registerSeedCommand() + { + $this->app->singleton('command.seed', function ($app) { + return new SeedCommand($app['db']); + }); + } + + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides() + { + return ['seeder', 'command.seed']; + } +} diff --git a/Seeder.php b/Seeder.php index cac50afb57..a9d9ef691c 100755 --- a/Seeder.php +++ b/Seeder.php @@ -3,18 +3,14 @@ namespace Illuminate\Database; use Illuminate\Console\Command; -use Illuminate\Console\View\Components\TwoColumnDetail; -use Illuminate\Contracts\Container\Container; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; -use Illuminate\Support\Arr; -use InvalidArgumentException; +use Illuminate\Container\Container; abstract class Seeder { /** * The container instance. * - * @var \Illuminate\Contracts\Container\Container + * @var \Illuminate\Container\Container */ protected $container; @@ -26,94 +22,24 @@ abstract class Seeder protected $command; /** - * Seeders that have been called at least one time. - * - * @var array - */ - protected static $called = []; - - /** - * Run the given seeder class. - * - * @param array|string $class - * @param bool $silent - * @param array $parameters - * @return $this - */ - public function call($class, $silent = false, array $parameters = []) - { - $classes = Arr::wrap($class); - - foreach ($classes as $class) { - $seeder = $this->resolve($class); - - $name = get_class($seeder); - - if ($silent === false && isset($this->command)) { - (new TwoColumnDetail($this->command->getOutput())) - ->render($name, 'RUNNING'); - } - - $startTime = microtime(true); - - $seeder->__invoke($parameters); - - if ($silent === false && isset($this->command)) { - $runTime = number_format((microtime(true) - $startTime) * 1000); - - (new TwoColumnDetail($this->command->getOutput())) - ->render($name, "$runTime ms DONE"); - - $this->command->getOutput()->writeln(''); - } - - static::$called[] = $class; - } - - return $this; - } - - /** - * Run the given seeder class. - * - * @param array|string $class - * @param array $parameters - * @return void - */ - public function callWith($class, array $parameters = []) - { - $this->call($class, false, $parameters); - } - - /** - * Silently run the given seeder class. + * Run the database seeds. * - * @param array|string $class - * @param array $parameters * @return void */ - public function callSilent($class, array $parameters = []) - { - $this->call($class, true, $parameters); - } + abstract public function run(); /** - * Run the given seeder class once. + * Seed the given connection from the given path. * - * @param array|string $class - * @param bool $silent + * @param string $class * @return void */ - public function callOnce($class, $silent = false, array $parameters = []) + public function call($class) { - $classes = Arr::wrap($class); - - foreach ($classes as $class) { - if (in_array($class, static::$called)) { - continue; - } + $this->resolve($class)->run(); - $this->call($class, $silent, $parameters); + if (isset($this->command)) { + $this->command->getOutput()->writeln("Seeded: $class"); } } @@ -143,7 +69,7 @@ protected function resolve($class) /** * Set the IoC container instance. * - * @param \Illuminate\Contracts\Container\Container $container + * @param \Illuminate\Container\Container $container * @return $this */ public function setContainer(Container $container) @@ -165,31 +91,4 @@ public function setCommand(Command $command) return $this; } - - /** - * Run the database seeds. - * - * @param array $parameters - * @return mixed - * - * @throws \InvalidArgumentException - */ - public function __invoke(array $parameters = []) - { - if (! method_exists($this, 'run')) { - throw new InvalidArgumentException('Method [run] missing from '.get_class($this)); - } - - $callback = fn () => isset($this->container) - ? $this->container->call([$this, 'run'], $parameters) - : $this->run(...$parameters); - - $uses = array_flip(class_uses_recursive(static::class)); - - if (isset($uses[WithoutModelEvents::class])) { - $callback = $this->withoutModelEvents($callback); - } - - return $callback(); - } } diff --git a/SqlServerConnection.php b/SqlServerConnection.php index 7b3d0c5f01..2d848a851d 100755 --- a/SqlServerConnection.php +++ b/SqlServerConnection.php @@ -4,86 +4,53 @@ use Closure; use Exception; -use Illuminate\Database\Query\Grammars\SqlServerGrammar as QueryGrammar; +use Throwable; +use Doctrine\DBAL\Driver\PDOSqlsrv\Driver as DoctrineDriver; use Illuminate\Database\Query\Processors\SqlServerProcessor; +use Illuminate\Database\Query\Grammars\SqlServerGrammar as QueryGrammar; use Illuminate\Database\Schema\Grammars\SqlServerGrammar as SchemaGrammar; -use Illuminate\Database\Schema\SqlServerBuilder; -use Illuminate\Filesystem\Filesystem; -use RuntimeException; -use Throwable; class SqlServerConnection extends Connection { - /** - * {@inheritdoc} - */ - public function getDriverTitle() - { - return 'SQL Server'; - } - /** * Execute a Closure within a transaction. * * @param \Closure $callback - * @param int $attempts * @return mixed * - * @throws \Throwable + * @throws \Exception|\Throwable */ - public function transaction(Closure $callback, $attempts = 1) + public function transaction(Closure $callback) { - for ($a = 1; $a <= $attempts; $a++) { - if ($this->getDriverName() === 'sqlsrv') { - return parent::transaction($callback, $attempts); - } + if ($this->getDriverName() == 'sqlsrv') { + return parent::transaction($callback); + } - $this->getPdo()->exec('BEGIN TRAN'); + $this->getPdo()->exec('BEGIN TRAN'); - // We'll simply execute the given callback within a try / catch block - // and if we catch any exception we can rollback the transaction - // so that none of the changes are persisted to the database. - try { - $result = $callback($this); + // We'll simply execute the given callback within a try / catch block + // and if we catch any exception we can rollback the transaction + // so that none of the changes are persisted to the database. + try { + $result = $callback($this); - $this->getPdo()->exec('COMMIT TRAN'); - } + $this->getPdo()->exec('COMMIT TRAN'); + } - // If we catch an exception, we will rollback so nothing gets messed - // up in the database. Then we'll re-throw the exception so it can - // be handled how the developer sees fit for their applications. - catch (Throwable $e) { - $this->getPdo()->exec('ROLLBACK TRAN'); + // If we catch an exception, we will roll back so nothing gets messed + // up in the database. Then we'll re-throw the exception so it can + // be handled how the developer sees fit for their applications. + catch (Exception $e) { + $this->getPdo()->exec('ROLLBACK TRAN'); - throw $e; - } + throw $e; + } catch (Throwable $e) { + $this->getPdo()->exec('ROLLBACK TRAN'); - return $result; + throw $e; } - } - - /** - * Escape a binary value for safe SQL embedding. - * - * @param string $value - * @return string - */ - protected function escapeBinary($value) - { - $hex = bin2hex($value); - return "0x{$hex}"; - } - - /** - * Determine if the given database exception was caused by a unique constraint violation. - * - * @param \Exception $exception - * @return bool - */ - protected function isUniqueConstraintError(Exception $exception) - { - return (bool) preg_match('#Cannot insert duplicate key row in object#i', $exception->getMessage()); + return $result; } /** @@ -93,21 +60,7 @@ protected function isUniqueConstraintError(Exception $exception) */ protected function getDefaultQueryGrammar() { - return new QueryGrammar($this); - } - - /** - * Get a schema builder instance for the connection. - * - * @return \Illuminate\Database\Schema\SqlServerBuilder - */ - public function getSchemaBuilder() - { - if (is_null($this->schemaGrammar)) { - $this->useDefaultSchemaGrammar(); - } - - return new SqlServerBuilder($this); + return $this->withTablePrefix(new QueryGrammar); } /** @@ -117,29 +70,26 @@ public function getSchemaBuilder() */ protected function getDefaultSchemaGrammar() { - return new SchemaGrammar($this); + return $this->withTablePrefix(new SchemaGrammar); } /** - * Get the schema state for the connection. - * - * @param \Illuminate\Filesystem\Filesystem|null $files - * @param callable|null $processFactory + * Get the default post processor instance. * - * @throws \RuntimeException + * @return \Illuminate\Database\Query\Processors\SqlServerProcessor */ - public function getSchemaState(?Filesystem $files = null, ?callable $processFactory = null) + protected function getDefaultPostProcessor() { - throw new RuntimeException('Schema dumping is not supported when using SQL Server.'); + return new SqlServerProcessor; } /** - * Get the default post processor instance. + * Get the Doctrine DBAL driver. * - * @return \Illuminate\Database\Query\Processors\SqlServerProcessor + * @return \Doctrine\DBAL\Driver\PDOSqlsrv\Driver */ - protected function getDefaultPostProcessor() + protected function getDoctrineDriver() { - return new SqlServerProcessor; + return new DoctrineDriver; } } diff --git a/UniqueConstraintViolationException.php b/UniqueConstraintViolationException.php deleted file mode 100644 index 13b705b77c..0000000000 --- a/UniqueConstraintViolationException.php +++ /dev/null @@ -1,7 +0,0 @@ -=5.6.4", + "illuminate/container": "5.3.*", + "illuminate/contracts": "5.3.*", + "illuminate/support": "5.3.*", + "nesbot/carbon": "~1.20" }, - "minimum-stability": "dev", "autoload": { "psr-4": { "Illuminate\\Database\\": "" } }, - "config": { - "sort-packages": true - }, "extra": { "branch-alias": { - "dev-master": "13.0.x-dev" + "dev-master": "5.3-dev" } - } + }, + "suggest": { + "doctrine/dbal": "Required to rename columns and drop SQLite columns (~2.4).", + "fzaninotto/faker": "Required to use the eloquent factory builder (~1.4).", + "illuminate/console": "Required to use the database commands (5.3.*).", + "illuminate/events": "Required to use the observers with Eloquent (5.3.*).", + "illuminate/filesystem": "Required to use the migrations (5.3.*).", + "illuminate/pagination": "Required to paginate the result set (5.3.*)." + }, + "minimum-stability": "dev" }