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..b82a792ce7 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,8 +25,9 @@ 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); @@ -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 */ @@ -191,7 +191,7 @@ 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) 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 index 537594e085..e56e020e82 100644 --- a/Concerns/BuildsQueries.php +++ b/Concerns/BuildsQueries.php @@ -2,194 +2,26 @@ namespace Illuminate\Database\Concerns; -use Illuminate\Container\Container; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\MultipleRecordsFoundException; -use Illuminate\Database\Query\Expression; -use Illuminate\Database\RecordNotFoundException; -use Illuminate\Database\RecordsNotFoundException; -use Illuminate\Pagination\Cursor; -use Illuminate\Pagination\CursorPaginator; -use Illuminate\Pagination\LengthAwarePaginator; -use Illuminate\Pagination\Paginator; -use Illuminate\Support\Collection; -use Illuminate\Support\LazyCollection; -use Illuminate\Support\Str; -use Illuminate\Support\Traits\Conditionable; -use InvalidArgumentException; -use RuntimeException; - -/** - * @template TValue - * - * @mixin \Illuminate\Database\Query\Builder - */ trait BuildsQueries { - use Conditionable; - /** * Chunk the results of the query. * * @param int $count - * @param callable(\Illuminate\Support\Collection, int): mixed $callback + * @param callable $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(); - } + $results = $this->forPage($page, $count)->get(); $countResults = $results->count(); @@ -197,25 +29,13 @@ public function orderedChunkById($count, callable $callback, $column = null, $al 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) { + if ($callback($results) === 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); @@ -223,393 +43,50 @@ public function orderedChunkById($count, callable $callback, $column = null, $al } /** - * Execute a callback over each item while chunking by ID. + * Execute a callback over each item while chunking. * - * @param callable(TValue, int): mixed $callback + * @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) + public function each(callable $callback, $count = 1000) { - return $this->chunkById($count, function ($results, $page) use ($callback, $count) { + return $this->chunk($count, function ($results) use ($callback) { foreach ($results as $key => $value) { - if ($callback($value, (($page - 1) * $count) + $key) === false) { + if ($callback($value, $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 + * @param array $columns + * @return mixed */ 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(), - ]); + return $this->take(1)->get($columns)->first(); } /** - * Get the original column name of the given column, without any aliasing. + * Apply the callback's query changes if the given "value" is true. * - * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*> $builder - * @param string $parameter - * @return string + * @param mixed $value + * @param \Closure $callback + * @param \Closure $default + * @return mixed */ - protected function getOriginalColumnNameForCursorPagination($builder, string $parameter) + public function when($value, $callback, $default = null) { - $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; - } - } - } + if ($value) { + return $callback($this, $value) ?: $this; + } elseif ($default) { + return $default($this, $value) ?: $this; } - 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 index 9874727d26..9b1dcd16ca 100644 --- a/Concerns/ManagesTransactions.php +++ b/Concerns/ManagesTransactions.php @@ -3,25 +3,19 @@ namespace Illuminate\Database\Concerns; use Closure; -use Illuminate\Database\DeadlockException; -use RuntimeException; +use Exception; use Throwable; -/** - * @mixin \Illuminate\Database\Connection - */ trait ManagesTransactions { /** - * @template TReturn of mixed - * * Execute a Closure within a transaction. * - * @param (\Closure(static): TReturn) $callback + * @param \Closure $callback * @param int $attempts - * @return TReturn + * @return mixed * - * @throws \Throwable + * @throws \Exception|\Throwable */ public function transaction(Closure $callback, $attempts = 1) { @@ -32,73 +26,46 @@ public function transaction(Closure $callback, $attempts = 1) // 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); + return tap($callback($this), function ($result) { + $this->commit(); + }); } // 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) { + // exception back out and let the developer handle an uncaught exceptions. + catch (Exception $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 - ); + $this->rollBack(); - continue; + throw $e; } - - $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 \Exception $e * @param int $currentAttempt * @param int $maxAttempts * @return void * - * @throws \Throwable + * @throws \Exception */ - protected function handleTransactionException(Throwable $e, $currentAttempt, $maxAttempts) + protected function handleTransactionException($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) && + if ($this->causedByDeadlock($e) && $this->transactions > 1) { - $this->transactions--; - - $this->transactionsManager?->rollback( - $this->getName(), $this->transactions - ); + --$this->transactions; - throw new DeadlockException($e->getMessage(), is_int($e->getCode()) ? $e->getCode() : 0, $e); + throw $e; } // If there was an exception we will rollback this transaction and then we @@ -106,7 +73,7 @@ protected function handleTransactionException(Throwable $e, $currentAttempt, $ma // if we haven't we will return and try this query again in our loop. $this->rollBack(); - if ($this->causedByConcurrencyError($e) && + if ($this->causedByDeadlock($e) && $currentAttempt < $maxAttempts) { return; } @@ -118,22 +85,13 @@ protected function handleTransactionException(Throwable $e, $currentAttempt, $ma * Start a new database transaction. * * @return void - * - * @throws \Throwable + * @throws \Exception */ public function beginTransaction() { - foreach ($this->beforeStartingTransaction as $callback) { - $callback($this); - } - $this->createTransaction(); - $this->transactions++; - - $this->transactionsManager?->begin( - $this->getName(), $this->transactions - ); + ++$this->transactions; $this->fireConnectionEvent('beganTransaction'); } @@ -142,17 +100,13 @@ public function beginTransaction() * 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->getPdo()->beginTransaction(); + } catch (Exception $e) { $this->handleBeginTransactionException($e); } } elseif ($this->transactions >= 1 && $this->queryGrammar->supportsSavepoints()) { @@ -164,8 +118,6 @@ protected function createTransaction() * Create a save point within the database. * * @return void - * - * @throws \Throwable */ protected function createSavepoint() { @@ -177,17 +129,17 @@ protected function createSavepoint() /** * Handle an exception from a transaction beginning. * - * @param \Throwable $e + * @param \Exception $e * @return void * - * @throws \Throwable + * @throws \Exception */ - protected function handleBeginTransactionException(Throwable $e) + protected function handleBeginTransactionException($e) { if ($this->causedByLostConnection($e)) { $this->reconnect(); - $this->executeBeginTransactionStatement(); + $this->pdo->beginTransaction(); } else { throw $e; } @@ -197,51 +149,16 @@ protected function handleBeginTransactionException(Throwable $e) * Commit the active database transaction. * * @return void - * - * @throws \Throwable */ public function commit() { - if ($this->transactionLevel() == 1) { - $this->fireConnectionEvent('committing'); + if ($this->transactions == 1) { $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; + $this->fireConnectionEvent('committed'); } /** @@ -249,8 +166,6 @@ protected function handleCommitTransactionException(Throwable $e, $currentAttemp * * @param int|null $toLevel * @return void - * - * @throws \Throwable */ public function rollBack($toLevel = null) { @@ -258,8 +173,8 @@ public function rollBack($toLevel = null) // 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; + ? $this->transactions - 1 + : $toLevel; if ($toLevel < 0 || $toLevel >= $this->transactions) { return; @@ -268,18 +183,10 @@ public function rollBack($toLevel = null) // 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->performRollBack($toLevel); $this->transactions = $toLevel; - $this->transactionsManager?->rollback( - $this->getName(), $this->transactions - ); - $this->fireConnectionEvent('rollingBack'); } @@ -288,17 +195,11 @@ public function rollBack($toLevel = null) * * @param int $toLevel * @return void - * - * @throws \Throwable */ protected function performRollBack($toLevel) { if ($toLevel == 0) { - $pdo = $this->getPdo(); - - if ($pdo->inTransaction()) { - $pdo->rollBack(); - } + $this->getPdo()->rollBack(); } elseif ($this->queryGrammar->supportsSavepoints()) { $this->getPdo()->exec( $this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1)) @@ -306,27 +207,6 @@ protected function performRollBack($toLevel) } } - /** - * 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. * @@ -336,38 +216,4 @@ 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 +130,27 @@ class Connection implements ConnectionInterface protected $pretending = false; /** - * All of the callbacks that should be invoked before a transaction is started. + * The instance of Doctrine connection. * - * @var \Closure[] + * @var \Doctrine\DBAL\Connection */ - protected $beforeStartingTransaction = []; - - /** - * All of the callbacks that should be invoked before a query is executed. - * - * @var (\Closure(string, array, \Illuminate\Database\Connection): mixed)[] - */ - protected $beforeExecutingCallbacks = []; + protected $doctrineConnection; /** * The connection resolvers. * - * @var \Closure[] + * @var array */ protected static $resolvers = []; - /** - * The last retrieved PDO read / write type. - * - * @var null|'read'|'write' - */ - protected $latestPdoTypeRetrieved = null; - /** * 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 +190,7 @@ public function useDefaultQueryGrammar() */ protected function getDefaultQueryGrammar() { - return new QueryGrammar($this); + return new QueryGrammar; } /** @@ -279,7 +206,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 +250,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); } /** @@ -348,7 +274,7 @@ public function query() * Run a select statement and return a single result. * * @param string $query - * @param array $bindings + * @param array $bindings * @param bool $useReadPdo * @return mixed */ @@ -359,38 +285,11 @@ public function selectOne($query, $bindings = [], $useReadPdo = true) return array_shift($records); } - /** - * 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) - { - $record = $this->selectOne($query, $bindings, $useReadPdo); - - if (is_null($record)) { - return null; - } - - $record = (array) $record; - - if (count($record) > 1) { - throw new MultipleColumnsSelectedException; - } - - return array_first($record); - } - /** * Run a select statement against the database. * * @param string $query - * @param array $bindings + * @param array $bindings * @return array */ public function selectFromWriteConnection($query, $bindings = []) @@ -404,12 +303,11 @@ 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) { + return $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo) { if ($this->pretending()) { return []; } @@ -417,49 +315,14 @@ public function select($query, $bindings = [], $useReadPdo = true, array $fetchU // 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->prepared($this->getPdoForSelect($useReadPdo) + ->prepare($query)); $this->bindValues($statement, $this->prepareBindings($bindings)); $statement->execute(); - $sets = []; - - do { - $sets[] = $statement->fetchAll(...$fetchUsing); - } while ($statement->nextRowset()); - - return $sets; + return $statement->fetchAll(); }); } @@ -469,10 +332,9 @@ public function selectResultSets($query, $bindings = [], $useReadPdo = true, arr * @param string $query * @param array $bindings * @param bool $useReadPdo - * @param array $fetchUsing - * @return \Generator + * @return \Generator */ - public function cursor($query, $bindings = [], $useReadPdo = true, array $fetchUsing = []) + public function cursor($query, $bindings = [], $useReadPdo = true) { $statement = $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo) { if ($this->pretending()) { @@ -483,7 +345,7 @@ public function cursor($query, $bindings = [], $useReadPdo = true, array $fetchU // 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)); + ->prepare($query)); $this->bindValues( $statement, $this->prepareBindings($bindings) @@ -497,7 +359,7 @@ public function cursor($query, $bindings = [], $useReadPdo = true, array $fetchU return $statement; }); - while ($record = $statement->fetch(...$fetchUsing)) { + while ($record = $statement->fetch()) { yield $record; } } @@ -512,7 +374,9 @@ protected function prepared(PDOStatement $statement) { $statement->setFetchMode($this->fetchMode); - $this->event(new StatementPrepared($this, $statement)); + $this->event(new Events\StatementPrepared( + $this, $statement + )); return $statement; } @@ -532,7 +396,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 +408,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 +420,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,7 +432,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 = []) @@ -582,8 +446,6 @@ public function statement($query, $bindings = []) $this->bindValues($statement, $this->prepareBindings($bindings)); - $this->recordsHaveBeenModified(); - return $statement->execute(); }); } @@ -592,7 +454,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 = []) @@ -611,11 +473,7 @@ public function affectingStatement($query, $bindings = []) $statement->execute(); - $this->recordsHaveBeenModified( - ($count = $statement->rowCount()) > 0 - ); - - return $count; + return $statement->rowCount(); }); } @@ -632,76 +490,37 @@ public function unprepared($query) return true; } - $this->recordsHaveBeenModified( - $change = $this->getPdo()->exec($query) !== false - ); - - return $change; + return (bool) $this->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. * - * @param (\Closure(\Illuminate\Database\Connection): mixed) $callback - * @return array{query: string, bindings: array, time: float|null}[] + * @param \Closure $callback + * @return array */ public function pretend(Closure $callback) { 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); - - return $this->queryLog; - } finally { - $this->pretending = false; - } - }); - } - - /** - * Execute the given callback without "pretending". - * - * @param \Closure $callback - * @return mixed - */ - public function withoutPretending(Closure $callback) - { - if (! $this->pretending) { - return $callback(); - } + // 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->pretending = false; - try { - return $callback(); - } finally { - $this->pretending = true; - } + return $this->queryLog; + }); } /** * Execute the given callback in "dry run" mode. * - * @param (\Closure(): array{query: string, bindings: array, time: float|null}[]) $callback - * @return array{query: string, bindings: array, time: float|null}[] + * @param \Closure $callback + * @return array */ protected function withFreshQueryLog($callback) { @@ -716,7 +535,7 @@ protected function withFreshQueryLog($callback) // 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. + // value of hte callback so the original callers can have the results. $result = $callback(); $this->loggingQueries = $loggingQueries; @@ -727,7 +546,7 @@ protected function withFreshQueryLog($callback) /** * Bind values to their parameters in the given statement. * - * @param \PDOStatement $statement + * @param \PDOStatement $statement * @param array $bindings * @return void */ @@ -735,13 +554,8 @@ public function bindValues($statement, $bindings) { 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 - }, + is_string($key) ? $key : $key + 1, $value, + is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR ); } } @@ -762,8 +576,8 @@ public function prepareBindings(array $bindings) // 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; + } elseif ($value === false) { + $bindings[$key] = 0; } } @@ -773,8 +587,8 @@ public function prepareBindings(array $bindings) /** * 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 +596,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); @@ -814,8 +624,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,68 +637,42 @@ 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($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; + return $result; } /** * Log a query in the connection's query log. * * @param string $query - * @param array $bindings + * @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; + $this->event(new QueryExecuted($query, $bindings, $time, $this)); if ($this->loggingQueries) { - $this->queryLog[] = compact('query', 'bindings', 'time', 'readWriteType'); + $this->queryLog[] = compact('query', 'bindings', 'time'); } } /** - * Get the elapsed time in milliseconds since a given starting point. + * Get the elapsed time since a given starting point. * - * @param float $start + * @param int $start * @return float */ protected function getElapsedTime($start) @@ -896,83 +680,16 @@ 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 \Exception $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) + protected function handleQueryException($e, $query, $bindings, Closure $callback) { if ($this->transactions >= 1) { throw $e; @@ -987,8 +704,8 @@ protected function handleQueryException(QueryException $e, $query, $bindings, Cl * 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 * @@ -1008,9 +725,9 @@ protected function tryAgainIfCausedByLostConnection(QueryException $e, $query, $ /** * Reconnect to the database. * - * @return mixed|false + * @return void * - * @throws \Illuminate\Database\LostConnectionException + * @throws \LogicException */ public function reconnect() { @@ -1018,7 +735,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,7 +743,7 @@ public function reconnect() * * @return void */ - public function reconnectIfMissingConnection() + protected function reconnectIfMissingConnection() { if (is_null($this->pdo)) { $this->reconnect(); @@ -1043,58 +760,39 @@ 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) - { - $this->beforeExecutingCallbacks[] = $callback; - - return $this; - } - /** * 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 + * @return void */ 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, - }); + if (! isset($this->events)) { + return; + } + + switch ($event) { + case 'beganTransaction': + return $this->events->dispatch(new Events\TransactionBeginning($this)); + case 'committed': + return $this->events->dispatch(new Events\TransactionCommitted($this)); + case 'rollingBack': + return $this->events->dispatch(new Events\TransactionRolledBack($this)); + } } /** @@ -1105,14 +803,16 @@ protected function fireConnectionEvent($event) */ protected function event($event) { - $this->events?->dispatch($event); + if (isset($this->events)) { + $this->events->dispatch($event); + } } /** * Get a new raw query expression. * * @param mixed $value - * @return \Illuminate\Contracts\Database\Query\Expression + * @return \Illuminate\Database\Query\Expression */ public function raw($value) { @@ -1120,131 +820,55 @@ public function raw($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.'); - } - - return $this->escapeString($value); - } - } - - /** - * Escape a string value for safe SQL embedding. - * - * @param string $value - * @return string - */ - protected function escapeString($value) - { - return $this->getReadPdo()->quote($value); - } - - /** - * Escape a boolean value for safe SQL embedding. - * - * @param bool $value - * @return string - */ - protected function escapeBool($value) - { - return $value ? '1' : '0'; - } - - /** - * Escape a binary value for safe SQL embedding. - * - * @param string $value - * @return string - * - * @throws \RuntimeException - */ - protected function escapeBinary($value) - { - throw new RuntimeException('The database connection does not support escaping binary values.'); - } - - /** - * Determine if the database connection has modified any database records. + * Is Doctrine available? * * @return bool */ - public function hasModifiedRecords() + public function isDoctrineAvailable() { - return $this->recordsModified; + return class_exists('Doctrine\DBAL\Connection'); } /** - * Indicate if any records have been modified. + * Get a Doctrine Schema Column instance. * - * @param bool $value - * @return void + * @param string $table + * @param string $column + * @return \Doctrine\DBAL\Schema\Column */ - public function recordsHaveBeenModified($value = true) + public function getDoctrineColumn($table, $column) { - if (! $this->recordsModified) { - $this->recordsModified = $value; - } - } + $schema = $this->getDoctrineSchemaManager(); - /** - * Set the record modification state. - * - * @param bool $value - * @return $this - */ - public function setRecordModificationState(bool $value) - { - $this->recordsModified = $value; - - return $this; + return $schema->listTableDetails($table)->getColumn($column); } /** - * Reset the record modification state. + * Get the Doctrine DBAL schema manager for the connection. * - * @return void + * @return \Doctrine\DBAL\Schema\AbstractSchemaManager */ - public function forgetRecordModificationState() + public function getDoctrineSchemaManager() { - $this->recordsModified = false; + return $this->getDoctrineDriver()->getSchemaManager($this->getDoctrineConnection()); } /** - * Indicate that the connection should use the write PDO connection for reads. + * Get the Doctrine DBAL database connection instance. * - * @param bool $value - * @return $this + * @return \Doctrine\DBAL\Connection */ - public function useWriteConnectionWhenReading($value = true) + public function getDoctrineConnection() { - $this->readOnWriteConnection = $value; + if (is_null($this->doctrineConnection)) { + $data = ['pdo' => $this->getPdo(), 'dbname' => $this->getConfig('database')]; - return $this; + $this->doctrineConnection = new DoctrineConnection( + $data, $this->getDoctrineDriver() + ); + } + + return $this->doctrineConnection; } /** @@ -1254,8 +878,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 +885,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,17 +892,10 @@ 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); } @@ -1298,20 +903,10 @@ public function getReadPdo() 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 */ public function setPdo($pdo) @@ -1326,7 +921,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 +931,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,18 +954,6 @@ 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. * @@ -1395,27 +965,6 @@ public function getConfig($option = null) 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 +975,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 +989,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 +1010,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 +1031,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,60 +1052,15 @@ 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. - * - * @return void - */ - public function unsetEventDispatcher() - { - $this->events = null; - } - - /** - * Run the statement to start a new transaction. - * - * @return void - */ - protected function executeBeginTransactionStatement() - { - $this->getPdo()->beginTransaction(); } /** - * Set the transaction manager instance on the connection. - * - * @param \Illuminate\Database\DatabaseTransactionsManager $manager - * @return $this - */ - public function setTransactionManager($manager) - { - $this->transactionsManager = $manager; - - return $this; - } - - /** - * Unset the transaction manager for this connection. - * - * @return void - */ - public function unsetTransactionManager() - { - $this->transactionsManager = null; - } - - /** - * Determine if the connection is in a "dry run". + * Determine if the connection in a "dry run". * * @return bool */ @@ -1584,29 +1072,13 @@ public function pretending() /** * 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 +1133,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,42 +1154,26 @@ 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; + $this->getQueryGrammar()->setTablePrefix($prefix); } /** - * Execute the given callback without table prefix. + * Set the table prefix and return the grammar. * - * @param \Closure $callback - * @return mixed + * @param \Illuminate\Database\Grammar $grammar + * @return \Illuminate\Database\Grammar */ - public function withoutTablePrefix(Closure $callback): mixed + public function withTablePrefix(Grammar $grammar) { - $tablePrefix = $this->getTablePrefix(); + $grammar->setTablePrefix($this->tablePrefix); - $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); + return $grammar; } /** @@ -1761,25 +1192,11 @@ public static function resolverFor($driver, Closure $callback) * Get the connection resolver for the given driver. * * @param string $driver - * @return \Closure|null + * @return mixed */ public static function getResolver($driver) { - return static::$resolvers[$driver] ?? null; - } - - /** - * Prepare the instance for cloning. - * - * @return void - */ - public function __clone() - { - // When cloning, re-initialize grammars to reference cloned connection... - $this->useDefaultQueryGrammar(); - - if (! is_null($this->schemaGrammar)) { - $this->useDefaultSchemaGrammar(); - } + return isset(static::$resolvers[$driver]) ? + static::$resolvers[$driver] : null; } } diff --git a/ConnectionInterface.php b/ConnectionInterface.php index 69e1fafa38..9262d6fdfe 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 = []); @@ -174,11 +147,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..709fa3aa9a 100755 --- a/Connectors/ConnectionFactory.php +++ b/Connectors/ConnectionFactory.php @@ -2,16 +2,16 @@ namespace Illuminate\Database\Connectors; -use Illuminate\Contracts\Container\Container; +use PDOException; +use Illuminate\Support\Arr; +use InvalidArgumentException; use Illuminate\Database\Connection; -use Illuminate\Database\MariaDbConnection; 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; +use Illuminate\Contracts\Debug\ExceptionHandler; class ConnectionFactory { @@ -26,6 +26,7 @@ class ConnectionFactory * Create a new connection factory instance. * * @param \Illuminate\Contracts\Container\Container $container + * @return void */ public function __construct(Container $container) { @@ -35,8 +36,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) @@ -53,7 +54,7 @@ public function make(array $config, $name = null) /** * Parse and prepare the database configuration. * - * @param array $config + * @param array $config * @param string $name * @return array */ @@ -78,7 +79,7 @@ protected function createSingleConnection(array $config) } /** - * Create a read / write database connection instance. + * Create a single database connection instance. * * @param array $config * @return \Illuminate\Database\Connection @@ -87,9 +88,7 @@ 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)); } /** @@ -117,7 +116,7 @@ protected function getReadConfig(array $config) } /** - * Get the write configuration for a read / write connection. + * Get the read configuration for a read / write connection. * * @param array $config * @return array @@ -132,15 +131,15 @@ protected function getWriteConfig(array $config) /** * 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]; + ? $config[$type][array_rand($config[$type])] + : $config[$type]; } /** @@ -164,8 +163,8 @@ protected function mergeReadWriteConfig(array $config, array $merge) protected function createPdoResolver(array $config) { return array_key_exists('host', $config) - ? $this->createPdoResolverWithHosts($config) - : $this->createPdoResolverWithoutHosts($config); + ? $this->createPdoResolverWithHosts($config) + : $this->createPdoResolverWithoutHosts($config); } /** @@ -173,25 +172,23 @@ protected function createPdoResolver(array $config) * * @param array $config * @return \Closure - * - * @throws \PDOException */ protected function createPdoResolverWithHosts(array $config) { return function () use ($config) { - foreach (Arr::shuffle($this->parseHosts($config)) as $host) { + foreach (Arr::shuffle($hosts = $this->parseHosts($config)) as $key => $host) { $config['host'] = $host; try { return $this->createConnector($config)->connect($config); } catch (PDOException $e) { - continue; + if (count($hosts) - 1 === $key && $this->container->bound(ExceptionHandler::class)) { + $this->container->make(ExceptionHandler::class)->report($e); + } } } - if (isset($e)) { - throw $e; - } + throw $e; }; } @@ -200,8 +197,6 @@ protected function createPdoResolverWithHosts(array $config) * * @param array $config * @return array - * - * @throws \InvalidArgumentException */ protected function parseHosts(array $config) { @@ -222,7 +217,9 @@ protected function parseHosts(array $config) */ protected function createPdoResolverWithoutHosts(array $config) { - return fn () => $this->createConnector($config)->connect($config); + return function () use ($config) { + return $this->createConnector($config)->connect($config); + }; } /** @@ -243,24 +240,28 @@ 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 @@ -271,13 +272,17 @@ protected function createConnection($driver, $connection, $database, $prefix = ' return $resolver($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..a56fe95a47 100755 --- a/Connectors/Connector.php +++ b/Connectors/Connector.php @@ -2,10 +2,11 @@ namespace Illuminate\Database\Connectors; +use PDO; use Exception; +use Illuminate\Support\Arr; +use Doctrine\DBAL\Driver\PDOConnection; use Illuminate\Database\DetectsLostConnections; -use PDO; -use Throwable; class Connector { @@ -28,16 +29,14 @@ class Connector * Create a new PDO connection. * * @param string $dsn - * @param array $config - * @param array $options + * @param array $config + * @param array $options * @return \PDO - * - * @throws \Exception */ public function createConnection($dsn, array $config, array $options) { - [$username, $password] = [ - $config['username'] ?? null, $config['password'] ?? null, + list($username, $password) = [ + Arr::get($config, 'username'), Arr::get($config, 'password'), ]; try { @@ -60,26 +59,40 @@ public function createConnection($dsn, array $config, array $options) * @param array $options * @return \PDO */ - protected function createPdoConnection($dsn, $username, #[\SensitiveParameter] $password, $options) + protected function createPdoConnection($dsn, $username, $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) */ + if (class_exists(PDOConnection::class) && ! $this->isPersistentConnection($options)) { + return new PDOConnection($dsn, $username, $password, $options); + } + + return new PDO($dsn, $username, $password, $options); + } + + /** + * Determine if the connection is persistent. + * + * @param array $options + * @return bool + */ + protected function isPersistentConnection($options) + { + return isset($options[PDO::ATTR_PERSISTENT]) && + $options[PDO::ATTR_PERSISTENT]; } /** * Handle an exception that occurred during connect execution. * - * @param \Throwable $e + * @param \Exception $e * @param string $dsn * @param string $username * @param string $password - * @param array $options + * @param array $options * @return \PDO * - * @throws \Throwable + * @throws \Exception */ - protected function tryAgainIfCausedByLostConnection(Throwable $e, $dsn, $username, #[\SensitiveParameter] $password, $options) + protected function tryAgainIfCausedByLostConnection(Exception $e, $dsn, $username, $password, $options) { if ($this->causedByLostConnection($e)) { return $this->createPdoConnection($dsn, $username, $password, $options); @@ -96,7 +109,7 @@ protected function tryAgainIfCausedByLostConnection(Throwable $e, $dsn, $usernam */ public function getOptions(array $config) { - $options = $config['options'] ?? []; + $options = Arr::get($config, 'options', []); return array_diff_key($this->options, $options) + $options; } 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 (! empty($config['database'])) { $connection->exec("use `{$config['database']}`;"); } - $this->configureConnection($connection, $config); + $this->configureEncoding($connection, $config); + + // 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); + + $this->setModes($connection, $config); return $connection; } + /** + * Set the connection character set and collation. + * + * @param \PDO $connection + * @param array $config + * @return void + */ + protected function configureEncoding($connection, array $config) + { + if (! isset($config['charset'])) { + return $connection; + } + + $connection->prepare( + "set names '{$config['charset']}'".$this->getCollation($config) + )->execute(); + } + + /** + * Get the collation for the configuration. + * + * @param array $config + * @return string + */ + protected function getCollation(array $config) + { + return ! is_null($config['collation']) ? " collate '{$config['collation']}'" : ''; + } + + /** + * Set the timezone on the connection. + * + * @param \PDO $connection + * @param array $config + * @return void + */ + protected function configureTimezone($connection, array $config) + { + if (isset($config['timezone'])) { + $connection->prepare('set time_zone="'.$config['timezone'].'"')->execute(); + } + } + /** * Create a DSN string from a configuration. * * 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); + ? $this->getSocketDsn($config) + : $this->getHostDsn($config); } /** @@ -79,76 +127,54 @@ 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['charset'])) { - if (isset($config['collation'])) { - $statements[] = sprintf("NAMES '%s' COLLATE '%s'", $config['charset'], $config['collation']); + if (isset($config['modes'])) { + $this->setCustomModes($connection, $config); + } elseif (isset($config['strict'])) { + if ($config['strict']) { + $connection->prepare($this->strictMode())->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. + * Set the custom modes on the connection. * * @param \PDO $connection * @param array $config - * @return string|null + * @return void */ - protected function getSqlMode(PDO $connection, array $config) + protected function setCustomModes(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); + $modes = implode(',', $config['modes']); - 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'; - } + $connection->prepare("set session sql_mode='{$modes}'")->execute(); + } - 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'; + /** + * Get the query to enable strict mode. + * + * @return string + */ + protected function strictMode() + { + return "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'"; } } diff --git a/Connectors/PostgresConnector.php b/Connectors/PostgresConnector.php index 31d2ff4732..0581b8b2aa 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. * @@ -36,152 +33,142 @@ public function connect(array $config) $this->getDsn($config), $config, $this->getOptions($config) ); - $this->configureIsolationLevel($connection, $config); + $this->configureEncoding($connection, $config); // 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); - $this->configureSearchPath($connection, $config); + $this->configureSchema($connection, $config); - $this->configureSynchronousCommit($connection, $config); + // 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. + $this->configureApplicationName($connection, $config); return $connection; } /** - * Create a DSN string from a configuration. + * Set the connection character set and collation. * + * @param \PDO $connection * @param array $config - * @return string + * @return void */ - protected function getDsn(array $config) + protected function configureEncoding($connection, $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. - extract($config, EXTR_SKIP); - - $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}'"; - - // 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)) { - $dsn .= ";port={$port}"; - } + $charset = $config['charset']; - 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); + $connection->prepare("set names '$charset'")->execute(); } /** - * Add the SSL options to the DSN. + * Set the timezone on the connection. * - * @param string $dsn + * @param \PDO $connection * @param array $config - * @return string + * @return void */ - protected function addSslOptions($dsn, array $config) + protected function configureTimezone($connection, array $config) { - foreach (['sslmode', 'sslcert', 'sslkey', 'sslrootcert'] as $option) { - if (isset($config[$option])) { - $dsn .= ";{$option}={$config[$option]}"; - } - } + if (isset($config['timezone'])) { + $timezone = $config['timezone']; - return $dsn; + $connection->prepare("set time zone '{$timezone}'")->execute(); + } } /** - * Set the connection transaction isolation level. + * Set the schema on the connection. * * @param \PDO $connection * @param array $config * @return void */ - protected function configureIsolationLevel($connection, array $config) + protected function configureSchema($connection, $config) { - if (isset($config['isolation_level'])) { - $connection->prepare("set session characteristics as transaction isolation level {$config['isolation_level']}")->execute(); + if (isset($config['schema'])) { + $schema = $this->formatSchema($config['schema']); + + $connection->prepare("set search_path to {$schema}")->execute(); } } /** - * Set the timezone on the connection. + * Format the schema for the DSN. * - * @param \PDO $connection - * @param array $config - * @return void + * @param array|string $schema + * @return string */ - protected function configureTimezone($connection, array $config) + protected function formatSchema($schema) { - if (isset($config['timezone'])) { - $timezone = $config['timezone']; - - $connection->prepare("set time zone '{$timezone}'")->execute(); + if (is_array($schema)) { + return '"'.implode('", "', $schema).'"'; + } else { + return '"'.$schema.'"'; } } /** - * Set the "search_path" on the database connection. + * Set the schema on the connection. * * @param \PDO $connection * @param array $config * @return void */ - protected function configureSearchPath($connection, $config) + protected function configureApplicationName($connection, $config) { - if (isset($config['search_path']) || isset($config['schema'])) { - $searchPath = $this->quoteSearchPath( - $this->parseSearchPath($config['search_path'] ?? $config['schema']) - ); + if (isset($config['application_name'])) { + $applicationName = $config['application_name']; - $connection->prepare("set search_path to {$searchPath}")->execute(); + $connection->prepare("set application_name to '$applicationName'")->execute(); } } /** - * Format the search path for the DSN. + * Create a DSN string from a configuration. * - * @param array $searchPath + * @param array $config * @return string */ - protected function quoteSearchPath($searchPath) + protected function getDsn(array $config) { - return count($searchPath) === 1 ? '"'.$searchPath[0].'"' : '"'.implode('", "', $searchPath).'"'; + // 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. + extract($config, EXTR_SKIP); + + $host = isset($host) ? "host={$host};" : ''; + + $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 (isset($config['port'])) { + $dsn .= ";port={$port}"; + } + + return $this->addSslOptions($dsn, $config); } /** - * Configure the synchronous_commit setting. + * Add the SSL options to the DSN. * - * @param \PDO $connection + * @param string $dsn * @param array $config - * @return void + * @return string */ - protected function configureSynchronousCommit($connection, array $config) + protected function addSslOptions($dsn, array $config) { - if (isset($config['synchronous_commit'])) { - $connection->prepare("set synchronous_commit to '{$config['synchronous_commit']}'")->execute(); + foreach (['sslmode', 'sslcert', 'sslkey', 'sslrootcert'] as $option) { + if (isset($config[$option])) { + $dsn .= ";{$option}={$config[$option]}"; + } } + + return $dsn; } } 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..a1b8447ab6 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,14 +43,12 @@ 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); } } @@ -87,7 +61,7 @@ protected function getDsn(array $config) protected function prefersOdbc(array $config) { return in_array('odbc', $this->getAvailableDrivers()) && - ($config['odbc'] ?? null) === true; + array_get($config, 'odbc') === true; } /** @@ -101,7 +75,7 @@ protected function getDblibDsn(array $config) return $this->buildConnectString('dblib', array_merge([ 'host' => $this->buildHostString($config, ':'), 'dbname' => $config['database'], - ], Arr::only($config, ['appname', 'charset', 'version']))); + ], Arr::only($config, ['appname', 'charset']))); } /** @@ -113,8 +87,7 @@ protected function getDblibDsn(array $config) protected function getOdbcDsn(array $config) { return isset($config['odbc_datasource_name']) - ? 'odbc:'.$config['odbc_datasource_name'] - : ''; + ? 'odbc:'.$config['odbc_datasource_name'] : ''; } /** @@ -153,42 +126,6 @@ protected function getSqlSrvDsn(array $config) $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['authentication'])) { - $arguments['Authentication'] = $config['authentication']; - } - return $this->buildConnectString('sqlsrv', $arguments); } @@ -215,11 +152,11 @@ protected function buildConnectString($driver, array $arguments) */ protected function buildHostString(array $config, $separator) { - if (empty($config['port'])) { + if (isset($config['port']) && ! empty($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..81ca3cc75e 100755 --- a/Console/Migrations/BaseCommand.php +++ b/Console/Migrations/BaseCommand.php @@ -3,14 +3,13 @@ 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[] + * @return array */ protected function getMigrationPaths() { @@ -18,28 +17,16 @@ protected function getMigrationPaths() // 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; + return collect($this->option('path'))->map(function ($path) { + return $this->laravel->basePath().'/'.$path; })->all(); } return array_merge( - $this->migrator->paths(), [$this->getMigrationPath()] + [$this->getMigrationPath()], $this->migrator->paths() ); } - /** - * 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 index 723d3c2298..68e8d2d68f 100644 --- a/Console/Migrations/FreshCommand.php +++ b/Console/Migrations/FreshCommand.php @@ -4,17 +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 Illuminate\Database\Migrations\Migrator; -use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputOption; -#[AsCommand(name: 'migrate:fresh')] class FreshCommand extends Command { - use ConfirmableTrait, Prohibitable; + use ConfirmableTrait; /** * The console command name. @@ -30,74 +24,45 @@ class FreshCommand extends Command */ protected $description = 'Drop all tables and re-run all migrations'; - /** - * The migrator instance. - * - * @var \Illuminate\Database\Migrations\Migrator - */ - protected $migrator; - - /** - * Create a new fresh command instance. - * - * @param \Illuminate\Database\Migrations\Migrator $migrator - */ - public function __construct(Migrator $migrator) - { - parent::__construct(); - - $this->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; } - $database = $this->input->getOption('database'); - - $this->migrator->usingConnection($database, function () use ($database) { - if ($this->migrator->repositoryExists()) { - $this->newLine(); + $this->dropAllTables( + $database = $this->input->getOption('database') + ); - $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->info('Dropped all tables successfully.'); - $this->newLine(); - - $this->call('migrate', array_filter([ + $this->call('migrate', [ '--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()) - ); - } + '--force' => $this->input->getOption('force'), + ]); if ($this->needsSeeding()) { $this->runSeeder($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(); } /** @@ -118,11 +83,11 @@ protected function needsSeeding() */ protected function runSeeder($database) { - $this->call('db:seed', array_filter([ + $this->call('db:seed', [ '--database' => $database, - '--class' => $this->option('seeder') ?: 'Database\\Seeders\\DatabaseSeeder', - '--force' => true, - ])); + '--class' => $this->option('seeder') ?: 'DatabaseSeeder', + '--force' => $this->option('force'), + ]); } /** @@ -133,16 +98,15 @@ protected function runSeeder($database) 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'], + ['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/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..092a7d2426 100755 --- a/Console/Migrations/MigrateCommand.php +++ b/Console/Migrations/MigrateCommand.php @@ -3,22 +3,9 @@ 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 function Laravel\Prompts\confirm; - -#[AsCommand(name: 'migrate')] -class MigrateCommand extends BaseCommand implements Isolatable +class MigrateCommand extends BaseCommand { use ConfirmableTrait; @@ -27,16 +14,12 @@ class MigrateCommand extends BaseCommand implements Isolatable * * @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 $signature = 'migrate {--database= : The database connection to use.} + {--force : Force the operation to run when in production.} + {--path= : The path of migrations files to be executed.} + {--pretend : Dump the SQL queries that would be run.} + {--seed : Indicates if the seed task should be re-run.} + {--step : Force the migrations to be run so they can be rolled back individually.}'; /** * The console command description. @@ -52,84 +35,53 @@ 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; - } + // 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->run($this->getMigrationPaths(), [ + 'pretend' => $this->option('pretend'), + 'step' => $this->option('step'), + ]); - throw $e; + // 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); } - return 0; - } - - /** - * Run the pending migrations. - * - * @return void - */ - protected function runMigrations() - { - $this->migrator->usingConnection($this->option('database'), function () { - $this->prepareDatabase(); - - // 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'), - ]); - - // 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->option('seed')) { + $this->call('db:seed', ['--force' => true]); + } } /** @@ -139,205 +91,12 @@ 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')); + $this->migrator->setConnection($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->components->warn('The SQLite database configured for this application does not exist: '.$path); - - 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.'); - } - - return touch($path); - } - - /** - * Create a missing MySQL or Postgres database. - * - * @param \Illuminate\Database\Connection $connection - * @return bool - * - * @throws \RuntimeException - */ - protected function createMissingMySqlOrPgsqlDatabase($connection) - { - 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."); - - 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.'); - } - } - try { - $this->laravel['config']->set( - "database.connections.{$connection->getName()}.database", - match ($connection->getDriverName()) { - 'mysql', 'mariadb' => null, - 'pgsql' => 'postgres', - }, + if (! $this->migrator->repositoryExists()) { + $this->call( + 'migrate:install', ['--database' => $this->option('database')] ); - - $this->laravel['db']->purge(); - - $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; - } - - return database_path('schema/'.$connection->getName().'-schema.sql'); - } } diff --git a/Console/Migrations/MigrateMakeCommand.php b/Console/Migrations/MigrateMakeCommand.php index ac5077f58d..fa2024afd5 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,12 +58,12 @@ 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'); @@ -85,17 +78,12 @@ public function handle() $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 +91,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( + $file = pathinfo($this->creator->create( $name, $this->getMigrationPath(), $table, $create - ); + ), PATHINFO_FILENAME); - if (windows_os()) { - $file = str_replace('/', '\\', $file); - } - - $this->components->info(sprintf('Migration [%s] created successfully.', $file)); + $this->line("Created Migration: {$file}"); } /** @@ -127,23 +111,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..4d2772b970 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,13 +27,12 @@ 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 @@ -48,38 +42,31 @@ public function handle() $path = $this->input->getOption('path'); + $force = $this->input->getOption('force'); + // 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); + $this->runRollback($database, $path, $step, $force); } else { - $this->runReset($database, $path); + $this->runReset($database, $path, $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, '--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()) - ); - } + '--force' => $force, + ]); if ($this->needsSeeding()) { $this->runSeeder($database); } - - return 0; } /** @@ -87,18 +74,18 @@ public function handle() * * @param string $database * @param string $path - * @param int $step + * @param bool $step + * @param bool $force * @return void */ - protected function runRollback($database, $path, $step) + protected function runRollback($database, $path, $step, $force) { - $this->call('migrate:rollback', array_filter([ + $this->call('migrate:rollback', [ '--database' => $database, '--path' => $path, - '--realpath' => $this->input->getOption('realpath'), '--step' => $step, - '--force' => true, - ])); + '--force' => $force, + ]); } /** @@ -106,16 +93,16 @@ protected function runRollback($database, $path, $step) * * @param string $database * @param string $path + * @param bool $force * @return void */ - protected function runReset($database, $path) + protected function runReset($database, $path, $force) { - $this->call('migrate:reset', array_filter([ + $this->call('migrate:reset', [ '--database' => $database, '--path' => $path, - '--realpath' => $this->input->getOption('realpath'), - '--force' => true, - ])); + '--force' => $force, + ]); } /** @@ -136,11 +123,11 @@ protected function needsSeeding() */ protected function runSeeder($database) { - $this->call('db:seed', array_filter([ + $this->call('db:seed', [ '--database' => $database, - '--class' => $this->option('seeder') ?: 'Database\\Seeders\\DatabaseSeeder', - '--force' => true, - ])); + '--class' => $this->option('seeder') ?: 'DatabaseSeeder', + '--force' => $this->option('force'), + ]); } /** @@ -151,13 +138,17 @@ 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.'], + + ['step', null, InputOption::VALUE_OPTIONAL, 'The number of migrations to be reverted & re-run.'], ]; } } diff --git a/Console/Migrations/ResetCommand.php b/Console/Migrations/ResetCommand.php index 787801bab2..3b8dab3ea6 100755 --- a/Console/Migrations/ResetCommand.php +++ b/Console/Migrations/ResetCommand.php @@ -2,17 +2,13 @@ namespace Illuminate\Database\Console\Migrations; -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 { - use ConfirmableTrait, Prohibitable; + use ConfirmableTrait; /** * The console command name. @@ -39,6 +35,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 +47,33 @@ 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->option('database')); + + // 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->comment('Migration table not found.'); + } + + $this->migrator->reset( + $this->getMigrationPaths(), $this->option('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,13 @@ 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'], + ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use.'], - ['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to the migrations files to be executed'], + ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production.'], - ['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'], + ['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) of migrations files to be executed.'], - ['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..4b1a75faed 100755 --- a/Console/Migrations/RollbackCommand.php +++ b/Console/Migrations/RollbackCommand.php @@ -2,17 +2,13 @@ namespace Illuminate\Database\Console\Migrations; -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 { - use ConfirmableTrait, Prohibitable; + use ConfirmableTrait; /** * The console command name. @@ -39,6 +35,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 +47,29 @@ 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->option('database')); + + $this->migrator->rollback( + $this->getMigrationPaths(), [ + 'pretend' => $this->option('pretend'), + 'step' => (int) $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); + } } /** @@ -80,13 +80,15 @@ 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.'], + + ['path', null, InputOption::VALUE_OPTIONAL, 'The path of migrations files to be executed.'], + + ['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.'], ]; } } diff --git a/Console/Migrations/StatusCommand.php b/Console/Migrations/StatusCommand.php index cbb16a133c..34b98f27b2 100644 --- a/Console/Migrations/StatusCommand.php +++ b/Console/Migrations/StatusCommand.php @@ -2,13 +2,10 @@ 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 Illuminate\Database\Migrations\Migrator; use Symfony\Component\Console\Input\InputOption; -#[AsCommand(name: 'migrate:status')] class StatusCommand extends BaseCommand { /** @@ -35,7 +32,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,72 +45,41 @@ 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.'); - - return 1; - } - - $ran = $this->migrator->getRepository()->getRan(); - - $batches = $this->migrator->getRepository()->getMigrationBatches(); - - $migrations = $this->getStatusFor($ran, $batches) - ->when($this->option('pending') !== false, fn ($collection) => $collection->filter(function ($migration) { - return (new Stringable($migration[1]))->contains('Pending'); - })); + $this->migrator->setConnection($this->option('database')); - if (count($migrations) > 0) { - $this->newLine(); + if (! $this->migrator->repositoryExists()) { + return $this->error('No migrations found.'); + } - $this->components->twoColumnDetail('Migration name', 'Batch / Status'); + $ran = $this->migrator->getRepository()->getRan(); - $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 = $this->getStatusFor($ran)) > 0) { + $this->table(['Ran?', 'Migration'], $migrations); + } else { + $this->error('No migrations found'); + } } /** - * Get the status for the given run migrations. + * Get the status for the given ran migrations. * * @param array $ran - * @param array $batches * @return \Illuminate\Support\Collection */ - protected function getStatusFor(array $ran, array $batches) + protected function getStatusFor(array $ran) { - 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]; - }); + return Collection::make($this->getAllMigrationFiles()) + ->map(function ($migration) use ($ran) { + $migrationName = $this->migrator->getMigrationName($migration); + + return in_array($migrationName, $ran) + ? ['Y', $migrationName] + : ['N', $migrationName]; + }); } /** @@ -133,10 +100,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..cf866e0457 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(); }); - - 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..89b793122e 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 class name and format according to the root namespace. * + * @param string $name * @return string */ - protected function rootNamespace() + protected function qualifyClass($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..cc9c8dbe6e 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,130 +27,54 @@ 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($database, $type) = $this->parseConnectionName($name); + + $name = $name ?: $database; // 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($database), $type ); - - $this->dispatchConnectionEstablishedEvent($this->connections[$name]); } return $this->connections[$name]; } - /** - * Build a database connection instance from the given configuration. - * - * @param array $config - * @return \Illuminate\Database\ConnectionInterface - */ - public function build(array $config) - { - $config['name'] ??= static::calculateDynamicConnectionName($config); - - $this->dynamicConnectionConfigurations[$config['name']] = $config; - - return $this->connectUsing($config['name'], $config, true); - } - - /** - * Calculate the dynamic connection name for an on-demand connection based on its configuration. - * - * @param array $config - * @return string - */ - public static function calculateDynamicConnectionName(array $config) - { - return 'dynamic_'.md5((new Collection($config))->map(function ($value, $key) { - return $key.(is_string($value) || is_int($value) ? $value : ''); - })->implode('')); - } - - /** - * Get a database connection instance from the given configuration. - * - * @param \UnitEnum|string $name - * @param array $config - * @param bool $force - * @return \Illuminate\Database\ConnectionInterface - * - * @throws \RuntimeException - */ - public function connectUsing(string $name, array $config, bool $force = false) - { - 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."); - } - - $connection = $this->configure( - $this->factory->make($config, $name), null - ); - - $this->dispatchConnectionEstablishedEvent($connection); - - return tap($connection, fn ($connection) => $this->connections[$name] = $connection); - } - /** * Parse the connection into an array of the name and read / write type. * @@ -173,9 +83,10 @@ public function connectUsing(string $name, array $config, bool $force = false) */ protected function parseConnectionName($name) { + $name = $name ?: $this->getDefaultConnection(); + return Str::endsWith($name, ['::read', '::write']) - ? explode('::', $name, 2) - : [$name, null]; + ? explode('::', $name, 2) : [$name, null]; } /** @@ -215,16 +126,18 @@ protected function makeConnection($name) */ protected function configuration($name) { - $connections = $this->app['config']['database.connections']; + $name = $name ?: $this->getDefaultConnection(); - $config = $this->dynamicConnectionConfigurations[$name] ?? Arr::get($connections, $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']; - if (is_null($config)) { - throw new InvalidArgumentException("Database connection [{$name}] not configured."); + if (is_null($config = Arr::get($connections, $name))) { + throw new InvalidArgumentException("Database [$name] not configured."); } - return (new ConfigurationUrlParser) - ->parseConfiguration($config); + return $config; } /** @@ -236,7 +149,7 @@ protected function configuration($name) */ protected function configure(Connection $connection, $type) { - $connection = $this->setPdoForType($connection, $type)->setReadWriteType($type); + $connection = $this->setPdoForType($connection, $type); // First we'll set the fetch mode and a few other dependencies of the database // connection. This method basically just configures and prepares it to get @@ -245,47 +158,28 @@ protected function configure(Connection $connection, $type) $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. * * @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()); } @@ -295,12 +189,14 @@ protected function setPdoForType(Connection $connection, $type = null) /** * Disconnect from the given database and remove from local cache. * - * @param \UnitEnum|string|null $name + * @param string $name * @return void */ public function purge($name = null) { - $this->disconnect($name = enum_value($name) ?: $this->getDefaultConnection()); + $name = $name ?: $this->getDefaultConnection(); + + $this->disconnect($name); unset($this->connections[$name]); } @@ -308,12 +204,12 @@ public function purge($name = null) /** * Disconnect from the given database. * - * @param \UnitEnum|string|null $name + * @param string $name * @return void */ public function disconnect($name = null) { - if (isset($this->connections[$name = enum_value($name) ?: $this->getDefaultConnection()])) { + if (isset($this->connections[$name = $name ?: $this->getDefaultConnection()])) { $this->connections[$name]->disconnect(); } } @@ -321,40 +217,18 @@ public function disconnect($name = null) /** * Reconnect to the given database. * - * @param \UnitEnum|string|null $name + * @param string $name * @return \Illuminate\Database\Connection */ public function reconnect($name = null) { - $this->disconnect($name = enum_value($name) ?: $this->getDefaultConnection()); + $this->disconnect($name = $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 \UnitEnum|string $name - * @param callable $callback - * @return mixed - */ - public function usingConnection($name, callable $callback) - { - $previousName = $this->getDefaultConnection(); - - $this->setDefaultConnection($name = enum_value($name)); - - try { - return $callback(); - } finally { - $this->setDefaultConnection($previousName); - } + return $this->refreshPdoConnections($name); } /** @@ -365,15 +239,11 @@ public function usingConnection($name, callable $callback) */ protected function refreshPdoConnections($name) { - [$database, $type] = $this->parseConnectionName($name); - - $fresh = $this->configure( - $this->makeConnection($database), $type - ); + $fresh = $this->makeConnection($name); return $this->connections[$name] - ->setPdo($fresh->getRawPdo()) - ->setReadPdo($fresh->getRawReadPdo()); + ->setPdo($fresh->getPdo()) + ->setReadPdo($fresh->getReadPdo()); } /** @@ -398,19 +268,19 @@ 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() { @@ -423,7 +293,7 @@ public function availableDrivers() /** * Register an extension connection resolver. * - * @param string $name + * @param string $name * @param callable $resolver * @return void */ @@ -432,64 +302,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); } } diff --git a/DatabaseServiceProvider.php b/DatabaseServiceProvider.php index 8a6115ba1e..a8ee7b030b 100755 --- a/DatabaseServiceProvider.php +++ b/DatabaseServiceProvider.php @@ -4,23 +4,15 @@ 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\Database\Eloquent\Model; +use Illuminate\Support\ServiceProvider; 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\Factory as EloquentFactory; class DatabaseServiceProvider extends ServiceProvider { - /** - * The array of resolved Faker instances. - * - * @var array - */ - protected static $fakers = []; - /** * Bootstrap the application events. * @@ -43,7 +35,9 @@ public function register() Model::clearBootedModels(); $this->registerConnectionServices(); - $this->registerFakerGenerator(); + + $this->registerEloquentFactory(); + $this->registerQueueableEntityResolver(); } @@ -71,45 +65,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); - } - - static::$fakers[$locale]->unique(true); + $this->app->singleton(FakerGenerator::class, function ($app) { + return FakerFactory::create($app['config']->get('app.faker_locale', 'en_US')); + }); - return static::$fakers[$locale]; + $this->app->singleton(EloquentFactory::class, function ($app) { + return EloquentFactory::construct( + $app->make(FakerGenerator::class), $this->app->databasePath('factories') + ); }); } 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/DetectsDeadlocks.php b/DetectsDeadlocks.php new file mode 100644 index 0000000000..dcbbd00bbe --- /dev/null +++ b/DetectsDeadlocks.php @@ -0,0 +1,30 @@ +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', + ]); + } +} diff --git a/DetectsLostConnections.php b/DetectsLostConnections.php index ba649afe2a..bee348203f 100644 --- a/DetectsLostConnections.php +++ b/DetectsLostConnections.php @@ -2,26 +2,32 @@ 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', + '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 BuildsQueries, Concerns\QueriesRelationships; /** * The base query builder instance. @@ -49,17 +30,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. * @@ -88,53 +62,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 +86,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 +140,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. * @@ -275,90 +171,31 @@ public function removedScopes() */ public function whereKey($id) { - if ($id instanceof Model) { - $id = $id->getKey(); - } - 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->whereIn($this->model->getQualifiedKeyName(), $id); return $this; } - if ($id !== null && $this->model->getKeyType() === 'string') { - $id = (string) $id; - } - return $this->where($this->model->getQualifiedKeyName(), '=', $id); } - /** - * Add a where clause on the primary key to the query. - * - * @param mixed $id - * @return $this - */ - public function whereKeyNot($id) - { - if ($id instanceof Model) { - $id = $id->getKey(); - } - - 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); - } - - return $this; - } - - if ($id !== null && $this->model->getKeyType() === 'string') { - $id = (string) $id; - } - - return $this->where($this->model->getQualifiedKeyName(), '!=', $id); - } - - /** - * Exclude the given models from the query results. - * - * @param iterable|mixed $models - * @return static - */ - public function except($models) - { - return $this->whereKeyNot( - $models instanceof Model - ? $models->getKey() - : Collection::wrap($models)->modelKeys() - ); - } - /** * Add a basic where clause to the query. * - * @param (\Closure(static): mixed)|string|array|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator + * @param string|\Closure $column + * @param string $operator * @param mixed $value * @param string $boolean * @return $this */ public function where($column, $operator = null, $value = null, $boolean = 'and') { - if ($column instanceof Closure && is_null($operator)) { - $column($query = $this->model->newQueryWithoutRelationships()); + if ($column instanceof Closure) { + $query = $this->model->newQueryWithoutScopes(); - $this->eagerLoad = array_merge($this->eagerLoad, $query->getEagerLoads()); + $column($query); - $this->withoutGlobalScopes( - $query->removedScopes() - ); $this->query->addNestedWhereQuery($query->getQuery(), $boolean); } else { $this->query->where(...func_get_args()); @@ -367,186 +204,40 @@ public function where($column, $operator = null, $value = null, $boolean = 'and' return $this; } - /** - * Add a basic where clause to the query, and return 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 TModel|null - */ - public function firstWhere($column, $operator = null, $value = null, $boolean = 'and') - { - return $this->where(...func_get_args())->first(); - } - /** * Add an "or where" clause to the query. * - * @param (\Closure(static): mixed)|array|string|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator + * @param string|\Closure $column + * @param string $operator * @param mixed $value - * @return $this + * @return \Illuminate\Database\Eloquent\Builder|static */ public function orWhere($column, $operator = null, $value = null) { - [$value, $operator] = $this->query->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - return $this->where($column, $operator, $value, 'or'); } - /** - * Add a basic "where not" clause to the query. - * - * @param (\Closure(static): mixed)|string|array|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @param string $boolean - * @return $this - */ - public function whereNot($column, $operator = null, $value = null, $boolean = 'and') - { - return $this->where($column, $operator, $value, $boolean.' not'); - } - - /** - * Add an "or where not" clause to the query. - * - * @param (\Closure(static): mixed)|array|string|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @return $this - */ - public function orWhereNot($column, $operator = null, $value = null) - { - return $this->whereNot($column, $operator, $value, 'or'); - } - - /** - * Add an "order by" clause for a timestamp to the query. - * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @return $this - */ - public function latest($column = null) - { - if (is_null($column)) { - $column = $this->model->getCreatedAtColumn() ?? 'created_at'; - } - - $this->query->latest($column); - - return $this; - } - - /** - * Add an "order by" clause for a timestamp to the query. - * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @return $this - */ - public function oldest($column = null) - { - if (is_null($column)) { - $column = $this->model->getCreatedAtColumn() ?? 'created_at'; - } - - $this->query->oldest($column); - - return $this; - } - /** * Create a collection of models from plain arrays. * * @param array $items - * @return \Illuminate\Database\Eloquent\Collection + * @return \Illuminate\Database\Eloquent\Collection */ public function hydrate(array $items) { $instance = $this->newModelInstance(); - return $instance->newCollection(array_map(function ($item) use ($items, $instance) { - $model = $instance->newFromBuilder($item); - - if (count($items) > 1) { - $model->preventsLazyLoading = Model::preventsLazyLoading(); - } - - return $model; + return $instance->newCollection(array_map(function ($item) use ($instance) { + return $instance->newFromBuilder($item); }, $items)); } - /** - * Insert into the database after merging the model's default attributes, setting timestamps, and casting values. - * - * @param array> $values - * @return bool - */ - public function fillAndInsert(array $values) - { - return $this->insert($this->fillForInsert($values)); - } - - /** - * 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)); - } - - /** - * Insert a record into the database and get its ID after merging the model's default attributes, setting timestamps, and casting values. - * - * @param array $values - * @return int - */ - public function fillAndInsertGetId(array $values) - { - return $this->insertGetId($this->fillForInsert([$values])[0]); - } - - /** - * Enrich the given values by merging in the model's default attributes, adding timestamps, and casting values. - * - * @param array> $values - * @return array> - */ - public function fillForInsert(array $values) - { - if (empty($values)) { - return []; - } - - if (! is_array(array_first($values))) { - $values = [$values]; - } - - $this->model->unguarded(function () use (&$values) { - foreach ($values as $key => $rowValues) { - $values[$key] = tap( - $this->newModelInstance($rowValues), - fn ($model) => $model->setUniqueIds() - )->getAttributes(); - } - }); - - return $this->addTimestampsToUpsertValues($values); - } - /** * Create a collection of models from a raw query. * * @param string $query * @param array $bindings - * @return \Illuminate\Database\Eloquent\Collection + * @return \Illuminate\Database\Eloquent\Collection */ public function fromQuery($query, $bindings = []) { @@ -559,44 +250,27 @@ public function fromQuery($query, $bindings = []) * Find a model by its primary key. * * @param mixed $id - * @param array|string $columns - * @return ($id is (\Illuminate\Contracts\Support\Arrayable|array) ? \Illuminate\Database\Eloquent\Collection : TModel|null) + * @param array $columns + * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|static[]|static|null */ public function find($id, $columns = ['*']) { - if (is_array($id) || $id instanceof Arrayable) { + if (is_array($id)) { return $this->findMany($id, $columns); } return $this->whereKey($id)->first($columns); } - /** - * Find a sole model by its primary key. - * - * @param mixed $id - * @param array|string $columns - * @return TModel - * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - * @throws \Illuminate\Database\MultipleRecordsFoundException - */ - public function findSole($id, $columns = ['*']) - { - return $this->whereKey($id)->sole($columns); - } - /** * Find multiple models by their primary keys. * - * @param \Illuminate\Contracts\Support\Arrayable|array $ids - * @param array|string $columns - * @return \Illuminate\Database\Eloquent\Collection + * @param 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->model->newCollection(); } @@ -608,42 +282,34 @@ public function findMany($ids, $columns = ['*']) * 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) + * @param array $columns + * @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))) { - throw (new ModelNotFoundException)->setModel( - get_class($this->model), array_diff($id, $result->modelKeys()) - ); + if (count($result) == count(array_unique($id))) { + return $result; } - + } elseif (! is_null($result)) { return $result; } - if (is_null($result)) { - throw (new ModelNotFoundException)->setModel( - get_class($this->model), $id - ); - } - - return $result; + throw (new ModelNotFoundException)->setModel( + get_class($this->model), $id + ); } /** * 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) + * @param array $columns + * @return \Illuminate\Database\Eloquent\Model */ public function findOrNew($id, $columns = ['*']) { @@ -654,83 +320,38 @@ public function findOrNew($id, $columns = ['*']) 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 + * @return \Illuminate\Database\Eloquent\Model */ - public function firstOrNew(array $attributes = [], array $values = []) + public function firstOrNew(array $attributes, array $values = []) { if (! is_null($instance = $this->where($attributes)->first())) { return $instance; } - return $this->newModelInstance(array_merge($attributes, $values)); + return $this->newModelInstance($attributes + $values); } /** - * Get the first record matching the attributes. If the record is not found, create it. + * Get the first record matching the attributes or create it. * * @param array $attributes - * @param (\Closure(): array)|array $values - * @return TModel + * @param array $values + * @return \Illuminate\Database\Eloquent\Model */ - public function firstOrCreate(array $attributes = [], Closure|array $values = []) + public function firstOrCreate(array $attributes, array $values = []) { - if (! is_null($instance = (clone $this)->where($attributes)->first())) { + if (! is_null($instance = $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 tap($this->newModelInstance($attributes + $values), function ($instance) { + $instance->save(); + }); } /** @@ -738,43 +359,22 @@ public function createOrFirst(array $attributes = [], Closure|array $values = [] * * @param array $attributes * @param array $values - * @return TModel + * @return \Illuminate\Database\Eloquent\Model */ 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); - } + return tap($this->firstOrNew($attributes), function ($instance) use ($values) { + $instance->fill($values)->save(); }); } /** * Execute the query and get the first result or throw an exception. * - * @param array|string $columns - * @return TModel + * @param array $columns + * @return \Illuminate\Database\Eloquent\Model|static * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException */ public function firstOrFail($columns = ['*']) { @@ -788,13 +388,11 @@ public function firstOrFail($columns = ['*']) /** * 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 + * @param \Closure|array $columns + * @param \Closure|null $callback + * @return \Illuminate\Database\Eloquent\Model|static|mixed */ - public function firstOr($columns = ['*'], ?Closure $callback = null) + public function firstOr($columns = ['*'], Closure $callback = null) { if ($columns instanceof Closure) { $callback = $columns; @@ -806,78 +404,27 @@ public function firstOr($columns = ['*'], ?Closure $callback = null) return $model; } - return $callback(); - } - - /** - * 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 call_user_func($callback); } /** * Get a single column's value from the first result of a query. * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column + * @param string $column * @return mixed */ public function value($column) { if ($result = $this->first([$column])) { - $column = $column instanceof Expression ? $column->getValue($this->getGrammar()) : $column; - - return $result->{Str::afterLast($column, '.')}; + return $result->{$column}; } } - /** - * 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, '.')}; - } - - /** - * Get a single column's value from the first result of the query or throw an exception. - * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @return mixed - * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - */ - public function valueOrFail($column) - { - $column = $column instanceof Expression ? $column->getValue($this->getGrammar()) : $column; - - return $this->firstOrFail([$column])->{Str::afterLast($column, '.')}; - } - /** * Execute the query as a "select" statement. * - * @param array|string $columns - * @return \Illuminate\Database\Eloquent\Collection + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection|static[] */ public function get($columns = ['*']) { @@ -890,16 +437,14 @@ public function get($columns = ['*']) $models = $builder->eagerLoadRelations($models); } - return $this->applyAfterQueryCallbacks( - $builder->getModel()->newCollection($models) - ); + return $builder->getModel()->newCollection($models); } /** * 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 = ['*']) { @@ -911,8 +456,8 @@ public function getModels($columns = ['*']) /** * Eager load the relationships for the models. * - * @param array $models - * @return array + * @param array $models + * @return array */ public function eagerLoadRelations(array $models) { @@ -920,7 +465,7 @@ 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, '.')) { + if (strpos($name, '.') === false) { $models = $this->eagerLoadRelation($models, $name, $constraints); } } @@ -960,7 +505,7 @@ protected function eagerLoadRelation(array $models, $name, Closure $constraints) * 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) { @@ -969,8 +514,8 @@ public function getRelation($name) // and error prone. We don't want constraints because we add eager ones. $relation = Relation::noConstraints(function () use ($name) { try { - return $this->getModel()->newInstance()->$name(); - } catch (BadMethodCallException) { + return $this->getModel()->{$name}(); + } catch (BadMethodCallException $e) { throw RelationNotFoundException::make($this->getModel(), $name); } }); @@ -1018,49 +563,63 @@ protected function relationsNestedUnder($relation) */ protected function isNestedUnder($relation, $name) { - return str_contains($name, '.') && str_starts_with($name, $relation.'.'); + return Str::contains($name, '.') && Str::startsWith($name, $relation.'.'); } /** - * Register a closure to be invoked after the query is executed. + * Get a generator for the given query. * - * @param \Closure $callback - * @return $this + * @return \Generator */ - public function afterQuery(Closure $callback) + public function cursor() { - $this->afterQueryCallbacks[] = $callback; - - return $this; + foreach ($this->applyScopes()->query->cursor() as $record) { + yield $this->model->newFromBuilder($record); + } } /** - * Invoke the "after query" modification callbacks. + * Chunk the results of a query by comparing numeric IDs. * - * @param mixed $result - * @return mixed + * @param int $count + * @param callable $callback + * @param string $column + * @param string|null $alias + * @return bool */ - public function applyAfterQueryCallbacks($result) + public function chunkById($count, callable $callback, $column = null, $alias = null) { - foreach ($this->afterQueryCallbacks as $afterQueryCallback) { - $result = $afterQueryCallback($result) ?: $result; - } + $column = is_null($column) ? $this->getModel()->getKeyName() : $column; - return $result; - } + $alias = is_null($alias) ? $column : $alias; - /** - * Get a lazy collection for the given query. - * - * @return \Illuminate\Support\LazyCollection - */ - public function cursor() - { - return $this->applyScopes()->query->cursor()->map(function ($record) { - $model = $this->newModelInstance()->newFromBuilder($record); + $lastId = 0; + + do { + $clone = clone $this; - return $this->applyAfterQueryCallbacks($this->newModelInstance()->newCollection([$model]))->first(); - })->reject(fn ($model) => is_null($model)); + // 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. + $results = $clone->forPageAfterId($count, $lastId, $column)->get(); + + $countResults = $results->count(); + + if ($countResults == 0) { + break; + } + + // 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) === false) { + return false; + } + + $lastId = $results->last()->{$alias}; + } while ($countResults == $count); + + return true; } /** @@ -1076,61 +635,52 @@ protected function enforceOrderBy() } /** - * Get a collection with the values of a given column. + * Get an array with the values of a given column. * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column + * @param string $column * @param string|null $key - * @return \Illuminate\Support\Collection + * @return \Illuminate\Support\Collection */ public function pluck($column, $key = null) { $results = $this->toBase()->pluck($column, $key); - $column = $column instanceof Expression ? $column->getValue($this->getGrammar()) : $column; - - $column = Str::after($column, "{$this->model->getTable()}."); - // 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) && + if (! $this->model->hasGetMutator($column) && ! $this->model->hasCast($column) && ! in_array($column, $this->model->getDates())) { - return $this->applyAfterQueryCallbacks($results); + return $results; } - return $this->applyAfterQueryCallbacks( - $results->map(function ($value) use ($column) { - return $this->model->newFromBuilder([$column => $value])->{$column}; - }) - ); + return $results->map(function ($value) use ($column) { + return $this->model->newFromBuilder([$column => $value])->{$column}; + }); } /** * Paginate the given query. * - * @param int|null|\Closure $perPage - * @param array|string $columns + * @param int $perPage + * @param array $columns * @param string $pageName * @param int|null $page - * @param \Closure|int|null $total - * @return \Illuminate\Pagination\LengthAwarePaginator + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator * * @throws \InvalidArgumentException */ - public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null, $total = null) + public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) { $page = $page ?: Paginator::resolveCurrentPage($pageName); - $total = value($total) ?? $this->toBase()->getCountForPagination(); - - $perPage = value($perPage, $total) ?: $this->model->getPerPage(); + $perPage = $perPage ?: $this->model->getPerPage(); - $results = $total - ? $this->forPage($page, $perPage)->get($columns) - : $this->model->newCollection(); + $results = ($total = $this->toBase()->getCountForPagination()) + ? $this->forPage($page, $perPage)->get($columns) + : $this->model->newCollection(); - return $this->paginator($results, $total, $perPage, $page, [ + return new LengthAwarePaginator($results, $total, $perPage, $page, [ 'path' => Paginator::resolveCurrentPath(), 'pageName' => $pageName, ]); @@ -1139,8 +689,8 @@ public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', /** * Paginate the given query into a simple paginator. * - * @param int|null $perPage - * @param array|string $columns + * @param int $perPage + * @param array $columns * @param string $pageName * @param int|null $page * @return \Illuminate\Contracts\Pagination\Paginator @@ -1154,69 +704,19 @@ public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'p // 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); + $this->skip(($page - 1) * $perPage)->take($perPage + 1); - return $this->simplePaginator($this->get($columns), $perPage, $page, [ + return new Paginator($this->get($columns), $perPage, $page, [ 'path' => Paginator::resolveCurrentPath(), 'pageName' => $pageName, ]); } - /** - * Paginate the given query into a cursor paginator. - * - * @param int|null $perPage - * @param array|string $columns - * @param string $cursorName - * @param \Illuminate\Pagination\Cursor|string|null $cursor - * @return \Illuminate\Contracts\Pagination\CursorPaginator - */ - public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null) - { - $perPage = $perPage ?: $this->model->getPerPage(); - - 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->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(); - } - /** * Save a new model and return the instance. * * @param array $attributes - * @return TModel + * @return \Illuminate\Database\Eloquent\Model */ public function create(array $attributes = []) { @@ -1225,22 +725,11 @@ public function create(array $attributes = []) }); } - /** - * Save a new model and return the instance without raising model events. - * - * @param array $attributes - * @return TModel - */ - public function createQuietly(array $attributes = []) - { - return Model::withoutEvents(fn () => $this->create($attributes)); - } - /** * Save a new model and return the instance. Allow mass-assignment. * * @param array $attributes - * @return TModel + * @return \Illuminate\Database\Eloquent\Model */ public function forceCreate(array $attributes) { @@ -1250,18 +739,7 @@ public function forceCreate(array $attributes) } /** - * 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)); - } - - /** - * Update records in the database. + * Update a record in the database. * * @param array $values * @return int @@ -1271,63 +749,11 @@ public function update(array $values) return $this->toBase()->update($this->addUpdatedAtColumn($values)); } - /** - * 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)) { - return 0; - } - - if (! is_array(array_first($values))) { - $values = [$values]; - } - - if (is_null($update)) { - $update = array_keys(array_first($values)); - } - - return $this->toBase()->upsert( - $this->addTimestampsToUpsertValues($this->addUniqueIdsToUpsertValues($values)), - $uniqueBy, - $this->addUpdatedAtToUpsertColumns($update) - ); - } - - /** - * Update the column's update timestamp. - * - * @param string|null $column - * @return int|false - */ - public function touch($column = null) - { - $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; - } - - return $this->toBase()->update([$column => $time]); - } - /** * Increment a column's value by a given amount. * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param float|int $amount + * @param string $column + * @param int $amount * @param array $extra * @return int */ @@ -1339,138 +765,40 @@ public function increment($column, $amount = 1, array $extra = []) } /** - * Decrement 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 decrement($column, $amount = 1, array $extra = []) - { - return $this->toBase()->decrement( - $column, $amount, $this->addUpdatedAtColumn($extra) - ); - } - - /** - * Add the "updated at" column to an array of values. - * - * @param array $values - * @return array - */ - protected function addUpdatedAtColumn(array $values) - { - 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); - } - - $segments = preg_split('/\s+as\s+/i', $this->query->from); - - $qualifiedColumn = array_last($segments).'.'.$column; - - $values[$qualifiedColumn] = Arr::get($values, $qualifiedColumn, $values[$column]); - - unset($values[$column]); - - return $values; - } - - /** - * Add unique IDs to the inserted values. - * - * @param array $values - * @return array - */ - protected function addUniqueIdsToUpsertValues(array $values) - { - 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; - } - - /** - * Add timestamps to the inserted values. - * - * @param array $values - * @return array - */ - protected function addTimestampsToUpsertValues(array $values) - { - if (! $this->model->usesTimestamps()) { - return $values; - } - - $timestamp = $this->model->freshTimestampString(); - - $columns = array_filter([ - $this->model->getCreatedAtColumn(), - $this->model->getUpdatedAtColumn(), - ]); - - foreach ($columns as $column) { - foreach ($values as &$row) { - $row = array_merge([$column => $timestamp], $row); - } - } - - return $values; + * Decrement a column's value by a given amount. + * + * @param string $column + * @param int $amount + * @param array $extra + * @return int + */ + public function decrement($column, $amount = 1, array $extra = []) + { + return $this->toBase()->decrement( + $column, $amount, $this->addUpdatedAtColumn($extra) + ); } /** - * Add the "updated at" column to the updated columns. + * Add the "updated at" column to an array of values. * - * @param array $update + * @param array $values * @return array */ - protected function addUpdatedAtToUpsertColumns(array $update) + protected function addUpdatedAtColumn(array $values) { 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; + return $values; } - return $update; + return Arr::add( + $values, $this->model->getUpdatedAtColumn(), + $this->model->freshTimestampString() + ); } /** - * Delete records from the database. + * Delete a record from the database. * * @return mixed */ @@ -1506,40 +834,30 @@ public function onDelete(Closure $callback) $this->onDelete = $callback; } - /** - * Determine if the given model has a scope. - * - * @param string $scope - * @return bool - */ - public function hasNamedScope($scope) - { - return $this->model && $this->model->hasNamedScope($scope); - } - /** * Call the given local model scopes. * - * @param array|string $scopes - * @return static|mixed + * @param array $scopes + * @return mixed */ - public function scopes($scopes) + public function scopes(array $scopes) { $builder = $this; - foreach (Arr::wrap($scopes) as $scope => $parameters) { + foreach ($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, []]; + list($scope, $parameters) = [$parameters, []]; } // 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 + // care of groping the "wheres" correctly 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) + $builder = $builder->callScope( + [$this->model, 'scope'.ucfirst($scope)], + (array) $parameters ); } @@ -1549,7 +867,7 @@ public function scopes($scopes) /** * Apply the scopes to the Eloquent builder instance and return it. * - * @return static + * @return \Illuminate\Database\Eloquent\Builder|static */ public function applyScopes() { @@ -1559,12 +877,8 @@ public function applyScopes() $builder = clone $this; - foreach ($this->scopes as $identifier => $scope) { - if (! isset($builder->scopes[$identifier])) { - continue; - } - - $builder->callScope(function (self $builder) use ($scope) { + foreach ($this->scopes as $scope) { + $builder->callScope(function (Builder $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. @@ -1591,7 +905,7 @@ public function applyScopes() * @param array $parameters * @return mixed */ - protected function callScope(callable $scope, array $parameters = []) + protected function callScope(callable $scope, $parameters = []) { array_unshift($parameters, $this); @@ -1600,33 +914,17 @@ protected function callScope(callable $scope, array $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 = is_null($query->wheres) - ? 0 - : count($query->wheres); + $originalWhereCount = count($query->wheres); - $result = $scope(...$parameters) ?? $this; + $result = $scope(...array_values($parameters)) ?? $this; - if (count((array) $query->wheres) > $originalWhereCount) { + if (count($query->wheres) > $originalWhereCount) { $this->addNewWheresWithinGroup($query, $originalWhereCount); } return $result; } - /** - * Apply the given named scope on the current builder instance. - * - * @param string $scope - * @param array $parameters - * @return mixed - */ - protected function callNamedScope($scope, array $parameters = []) - { - return $this->callScope(function (...$parameters) use ($scope) { - return $this->model->callNamedScope($scope, $parameters); - }, $parameters); - } - /** * Nest where conditions by slicing them at the given where count. * @@ -1661,14 +959,14 @@ protected function addNewWheresWithinGroup(QueryBuilder $query, $originalWhereCo */ protected function groupWhereSliceForScope(QueryBuilder $query, $whereSlice) { - $whereBooleans = (new BaseCollection($whereSlice))->pluck('boolean'); + $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'))) { + if ($whereBooleans->contains('or')) { $query->wheres[] = $this->createNestedWhere( - $whereSlice, str_replace(' not', '', $whereBooleans->first()) + $whereSlice, $whereBooleans->first() ); } else { $query->wheres = array_merge($query->wheres, $whereSlice); @@ -1692,19 +990,14 @@ protected function createNestedWhere($whereSlice, $boolean = 'and') } /** - * Specify relationships that should be eager loaded. + * Set the relationships that should be eager loaded. * - * @param array): mixed)|string>|string $relations - * @param (\Closure(\Illuminate\Database\Eloquent\Relations\Relation<*,*,*>): mixed)|string|null $callback + * @param mixed $relations * @return $this */ - public function with($relations, $callback = null) + public function with($relations) { - if ($callback instanceof Closure) { - $eagerLoad = $this->parseWithRelations([$relations => $callback]); - } else { - $eagerLoad = $this->parseWithRelations(is_string($relations) ? func_get_args() : $relations); - } + $eagerLoad = $this->parseWithRelations(is_string($relations) ? func_get_args() : $relations); $this->eagerLoad = array_merge($this->eagerLoad, $eagerLoad); @@ -1726,29 +1019,14 @@ public function without($relations) return $this; } - /** - * Set the relationships that should be eager loaded while removing any previously added eager loading specifications. - * - * @param array): mixed)|string>|string $relations - * @return $this - */ - public function withOnly($relations) - { - $this->eagerLoad = []; - - return $this->with($relations); - } - /** * Create a new instance of the model being queried. * * @param array $attributes - * @return TModel + * @return \Illuminate\Database\Eloquent\Model */ public function newModelInstance($attributes = []) { - $attributes = array_merge($this->pendingAttributes, $attributes); - return $this->model->newInstance($attributes)->setConnection( $this->query->getConnection()->getName() ); @@ -1762,16 +1040,25 @@ public function newModelInstance($attributes = []) */ protected function parseWithRelations(array $relations) { - if ($relations === []) { - return []; - } - $results = []; - foreach ($this->prepareNestedWithRelationships($relations) as $name => $constraints) { - // We need to separate out any nested includes, which allows the developers + 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)) { + $name = $constraints; + + list($name, $constraints) = Str::contains($name, ':') + ? $this->createSelectWithConstraint($name) + : [$name, function () { + // + }]; + } + + // 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. + // the relationship with its own key in the array of eager load names. $results = $this->addNestedWiths($name, $results); $results[$name] = $constraints; @@ -1780,91 +1067,6 @@ protected function parseWithRelations(array $relations) return $results; } - /** - * Prepare nested with relationships. - * - * @param array $relations - * @param string $prefix - * @return array - */ - protected function prepareNestedWithRelationships($relations, $prefix = '') - { - $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. * @@ -1873,12 +1075,8 @@ protected function parseNameAndAttributeSelectionConstraint($name) */ 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]))); + return [explode(':', $name)[0], function ($query) use ($name) { + $query->select(explode(',', explode(':', $name)[1])); }]; } @@ -1900,7 +1098,7 @@ protected function addNestedWiths($name, $results) $progress[] = $segment; if (! isset($results[$last = implode('.', $progress)])) { - $results[$last] = static function () { + $results[$last] = function () { // }; } @@ -1909,73 +1107,6 @@ protected function addNestedWiths($name, $results) 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. * @@ -2032,63 +1163,10 @@ public function setEagerLoads(array $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 + * @return \Illuminate\Database\Eloquent\Model */ public function getModel() { @@ -2098,10 +1176,8 @@ public function getModel() /** * 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) { @@ -2112,30 +1188,6 @@ public function setModel(Model $model) return $this; } - /** - * 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. - * - * @param array|\Illuminate\Contracts\Database\Query\Expression $columns - * @return array - */ - public function qualifyColumns($columns) - { - return $this->model->qualifyColumns($columns); - } - /** * Get the given macro by name. * @@ -2147,60 +1199,6 @@ 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."); - } - /** * Dynamically handle calls into the query instance. * @@ -2216,31 +1214,29 @@ public function __call($method, $parameters) return; } - if ($this->hasMacro($method)) { + if (isset($this->localMacros[$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); - } + if (isset(static::$macros[$method]) and static::$macros[$method] instanceof Closure) { + return call_user_func_array(static::$macros[$method]->bindTo($this, static::class), $parameters); + } - return $callable(...$parameters); + if (isset(static::$macros[$method])) { + return call_user_func_array(static::$macros[$method]->bindTo($this, static::class), $parameters); } - if ($this->hasNamedScope($method)) { - return $this->callNamedScope($method, $parameters); + if (method_exists($this->model, $scope = 'scope'.ucfirst($method))) { + return $this->callScope([$this->model, $scope], $parameters); } - if (in_array(strtolower($method), $this->passthru)) { + if (in_array($method, $this->passthru)) { return $this->toBase()->{$method}(...$parameters); } - $this->forwardCallTo($this->query, $method, $parameters); + $this->query->{$method}(...$parameters); return $this; } @@ -2262,64 +1258,15 @@ public static function __callStatic($method, $parameters) return; } - if ($method === 'mixin') { - return static::registerMixin($parameters[0], $parameters[1] ?? true); + if (! isset(static::$macros[$method])) { + throw new BadMethodCallException("Method {$method} does not exist."); } - if (! static::hasGlobalMacro($method)) { - static::throwBadMethodCallException($method); + if (static::$macros[$method] instanceof Closure) { + return call_user_func_array(Closure::bind(static::$macros[$method], null, static::class), $parameters); } - $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; - - return $this; + return call_user_func_array(static::$macros[$method], $parameters); } /** @@ -2330,9 +1277,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; - /** * 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|static */ public function find($key, $default = null) { @@ -35,10 +22,6 @@ 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; @@ -47,54 +30,25 @@ public function find($key, $default = null) 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; + return Arr::first($this->items, function ($model) use ($key) { + return $model->getKey() == $key; + }, $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 +57,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,7 +72,7 @@ public function loadMorphCount($relation, $relations) /** * Determine if a key exists in the collection. * - * @param (callable(TModel, TKey): bool)|TModel|string|int $key + * @param mixed $key * @param mixed $operator * @param mixed $value * @return bool @@ -357,40 +83,29 @@ public function contains($key, $operator = null, $value = null) return parent::contains(...func_get_args()); } - if ($key instanceof Model) { - return parent::contains(fn ($model) => $model->is($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,7 +113,7 @@ 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)); @@ -407,64 +122,22 @@ public function merge($items) /** * Run a map over each of the items. * - * @template TMapValue - * - * @param callable(TModel, TKey): TMapValue $callback - * @return \Illuminate\Support\Collection|static + * @param callable $callback + * @return \Illuminate\Support\Collection */ 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()]); + return $result->contains(function ($item) { + return ! $item instanceof Model; + }) ? $result->toBase() : $result; } /** * Diff the collection with the given items. * - * @param iterable $items + * @param \ArrayAccess|array $items * @return static */ public function diff($items) @@ -474,7 +147,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 +158,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,9 +179,9 @@ public function intersect($items) /** * Return only unique items from the collection. * - * @param (callable(TModel, TKey): mixed)|string|null $key + * @param string|callable|null $key * @param bool $strict - * @return static + * @return static|\Illuminate\Support\Collection */ public function unique($key = null, $strict = false) { @@ -526,7 +195,7 @@ 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) @@ -535,7 +204,7 @@ public function only($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 +212,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 +225,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 +261,7 @@ public function getDictionary($items = null) $dictionary = []; foreach ($items as $value) { - $dictionary[$this->getDictionaryKey($value->getKey())] = $value; + $dictionary[$value->getKey()] = $value; } return $dictionary; @@ -679,153 +272,84 @@ 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} + * Zip the collection together with one or more arrays. * - * @template TPadValue - * - * @return \Illuminate\Support\Collection + * @param mixed ...$items + * @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} - * - * @return \Illuminate\Support\Collection - */ - #[\Override] - public function pluck($value, $key = null) + public function zip($items) { - return $this->toBase()->pluck($value, $key); + return call_user_func_array([$this->toBase(), 'zip'], func_get_args()); } /** - * {@inheritDoc} - * - * @template TZipValue + * Collapse the collection of items into a single array. * - * @return \Illuminate\Support\Collection> + * @return \Illuminate\Support\Collection */ - #[\Override] - public function zip($items) + public function collapse() { - return $this->toBase()->zip(...func_get_args()); + return $this->toBase()->collapse(); } /** - * Get the comparison function to detect duplicates. + * Get a flattened array of the items in the collection. * - * @return callable(TModel, TModel): bool + * @param int $depth + * @return \Illuminate\Support\Collection */ - protected function duplicateComparator($strict) + public function flatten($depth = INF) { - return fn ($a, $b) => $a->is($b); + return $this->toBase()->flatten($depth); } /** - * Enable relationship autoloading for all models in this collection. + * Flip the items in the collection. * - * @return $this + * @return \Illuminate\Support\Collection */ - public function withRelationshipAutoloading() + public function flip() { - $callback = fn ($tuples) => $this->loadMissingRelationshipChain($tuples); - - foreach ($this as $model) { - if (! $model->hasRelationAutoloadCallback()) { - $model->autoloadRelationsUsing($callback, $this); - } - } - - return $this; + return $this->toBase()->flip(); } /** * Get the type of the entities being queued. * * @return string|null - * - * @throws \LogicException */ public function getQueueableClass() { - if ($this->isEmpty()) { + if ($this->count() === 0) { return; } - $class = $this->getQueueableModelClass($this->first()); + $class = get_class($this->first()); $this->each(function ($model) use ($class) { - if ($this->getQueueableModelClass($model) !== $class) { + if (get_class($model) !== $class) { throw new LogicException('Queueing collections with multiple model types is not supported.'); } }); @@ -833,102 +357,13 @@ public function getQueueableClass() 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); - } - /** * Get the identifiers for all of the entities. * - * @return array + * @return array */ public function getQueueableIds() { - if ($this->isEmpty()) { - return []; - } - - return $this->first() instanceof QueueableEntity - ? $this->map->getQueueableId()->all() - : $this->modelKeys(); - } - - /** - * Get the relationships of the entities being queued. - * - * @return array - */ - public function getQueueableRelations() - { - 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)); - } - } - - /** - * Get the connection of the entities being queued. - * - * @return string|null - * - * @throws \LogicException - */ - public function getQueueableConnection() - { - 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; - } - - /** - * Get the Eloquent query builder from the collection. - * - * @return \Illuminate\Database\Eloquent\Builder - * - * @throws \LogicException - */ - public function toQuery() - { - $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 $this->modelKeys(); } } diff --git a/Eloquent/Concerns/GuardsAttributes.php b/Eloquent/Concerns/GuardsAttributes.php index aa9b2c60c9..96317cf571 100644 --- a/Eloquent/Concerns/GuardsAttributes.php +++ b/Eloquent/Concerns/GuardsAttributes.php @@ -2,24 +2,21 @@ namespace Illuminate\Database\Eloquent\Concerns; -use Illuminate\Database\Eloquent\Attributes\Fillable; -use Illuminate\Database\Eloquent\Attributes\Guarded; -use Illuminate\Database\Eloquent\Attributes\Initialize; -use Illuminate\Database\Eloquent\Attributes\Unguarded; +use Illuminate\Support\Str; trait GuardsAttributes { /** * The attributes that are mass assignable. * - * @var array + * @var array */ protected $fillable = []; /** * The attributes that aren't mass assignable. * - * @var array + * @var array */ protected $guarded = ['*']; @@ -30,38 +27,10 @@ trait GuardsAttributes */ 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 + * @return array */ public function getFillable() { @@ -71,7 +40,7 @@ public function getFillable() /** * Set the fillable attributes for the model. * - * @param array $fillable + * @param array $fillable * @return $this */ public function fillable(array $fillable) @@ -81,35 +50,20 @@ public function fillable(array $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 + * @return array */ public function getGuarded() { - return self::$unguarded === true - ? [] - : $this->guarded; + return $this->guarded; } /** * Set the guarded attributes for the model. * - * @param array $guarded + * @param array $guarded * @return $this */ public function guard(array $guarded) @@ -119,19 +73,6 @@ public function guard(array $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. * @@ -154,7 +95,7 @@ public static function reguard() } /** - * Determine if the current state is "unguarded". + * Determine if current state is "unguarded". * * @return bool */ @@ -166,10 +107,8 @@ public static function isUnguarded() /** * Run the given callable while being unguarded. * - * @template TReturn - * - * @param callable(): TReturn $callback - * @return TReturn + * @param callable $callback + * @return mixed */ public static function unguarded(callable $callback) { @@ -213,8 +152,7 @@ public function isFillable($key) } return empty($this->getFillable()) && - ! str_contains($key, '.') && - ! str_starts_with($key, '_'); + ! Str::startsWith($key, '_'); } /** @@ -225,40 +163,7 @@ public function isFillable($key) */ 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)]); + return in_array($key, $this->getGuarded()) || $this->getGuarded() == ['*']; } /** @@ -268,14 +173,14 @@ protected function isGuardableColumn($key) */ public function totallyGuarded() { - return count($this->getFillable()) === 0 && $this->getGuarded() == ['*']; + return count($this->getFillable()) == 0 && $this->getGuarded() == ['*']; } /** * Get the fillable attributes of a given array. * - * @param array $attributes - * @return array + * @param array $attributes + * @return array */ protected function fillableFromArray(array $attributes) { diff --git a/Eloquent/Concerns/HasAttributes.php b/Eloquent/Concerns/HasAttributes.php index a5717d2c30..36843e9dd2 100644 --- a/Eloquent/Concerns/HasAttributes.php +++ b/Eloquent/Concerns/HasAttributes.php @@ -2,143 +2,50 @@ namespace Illuminate\Database\Eloquent\Concerns; -use BackedEnum; -use Brick\Math\BigDecimal; -use Brick\Math\Exception\MathException as BrickMathException; -use Brick\Math\RoundingMode; -use Carbon\CarbonImmutable; -use Carbon\CarbonInterface; -use DateTimeImmutable; +use Carbon\Carbon; +use LogicException; use DateTimeInterface; -use Illuminate\Contracts\Database\Eloquent\Castable; -use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes; +use Illuminate\Support\Arr; +use Illuminate\Support\Str; use Illuminate\Contracts\Support\Arrayable; -use Illuminate\Database\Eloquent\Attributes\Appends; -use Illuminate\Database\Eloquent\Attributes\Initialize; -use Illuminate\Database\Eloquent\Attributes\Table; -use Illuminate\Database\Eloquent\Casts\AsArrayObject; -use Illuminate\Database\Eloquent\Casts\AsCollection; -use Illuminate\Database\Eloquent\Casts\AsEncryptedArrayObject; -use Illuminate\Database\Eloquent\Casts\AsEncryptedCollection; -use Illuminate\Database\Eloquent\Casts\AsEnumArrayObject; -use Illuminate\Database\Eloquent\Casts\AsEnumCollection; -use Illuminate\Database\Eloquent\Casts\Attribute; -use Illuminate\Database\Eloquent\Casts\Json; -use Illuminate\Database\Eloquent\InvalidCastException; -use Illuminate\Database\Eloquent\JsonEncodingException; -use Illuminate\Database\Eloquent\MissingAttributeException; use Illuminate\Database\Eloquent\Relations\Relation; -use Illuminate\Database\LazyLoadingViolationException; -use Illuminate\Support\Arr; -use Illuminate\Support\Carbon; -use Illuminate\Support\Collection; use Illuminate\Support\Collection as BaseCollection; -use Illuminate\Support\Exceptions\MathException; -use Illuminate\Support\Facades\Crypt; -use Illuminate\Support\Facades\Date; -use Illuminate\Support\Facades\Hash; -use Illuminate\Support\Str; -use InvalidArgumentException; -use LogicException; -use ReflectionClass; -use ReflectionMethod; -use ReflectionNamedType; -use RuntimeException; -use Stringable; -use ValueError; - -use function Illuminate\Support\enum_value; +use Illuminate\Database\Eloquent\JsonEncodingException; trait HasAttributes { /** * The model's attributes. * - * @var array + * @var array */ protected $attributes = []; /** * The model attribute's original state. * - * @var array + * @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. + * The attributes that should be cast to native types. * * @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. + * The attributes that should be mutated to dates. * * @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', - ]; + protected $dates = []; /** * The storage format of the model's date columns. * - * @var string|null + * @var string */ protected $dateFormat; @@ -163,63 +70,10 @@ trait HasAttributes */ 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 + * @return array */ public function attributesToArray() { @@ -254,13 +108,13 @@ public function attributesToArray() /** * Add the date attributes to the attributes array. * - * @param array $attributes - * @return array + * @param array $attributes + * @return array */ protected function addDateAttributesToArray(array $attributes) { foreach ($this->getDates() as $key) { - if (is_null($key) || ! isset($attributes[$key])) { + if (! isset($attributes[$key])) { continue; } @@ -275,9 +129,9 @@ protected function addDateAttributesToArray(array $attributes) /** * Add the mutated attributes to the attributes array. * - * @param array $attributes - * @param array $mutatedAttributes - * @return array + * @param array $attributes + * @param array $mutatedAttributes + * @return array */ protected function addMutatedAttributesToArray(array $attributes, array $mutatedAttributes) { @@ -303,15 +157,14 @@ protected function addMutatedAttributesToArray(array $attributes, array $mutated /** * Add the casted attributes to the attributes array. * - * @param array $attributes - * @param array $mutatedAttributes - * @return 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)) { + if (! array_key_exists($key, $attributes) || in_array($key, $mutatedAttributes)) { continue; } @@ -323,33 +176,12 @@ protected function addCastAttributesToArray(array $attributes, array $mutatedAtt ); // 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 + // a string. This allows the developers to customize hwo 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)) { + if ($attributes[$key] && + ($value === 'date' || $value === 'datetime')) { $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; @@ -358,11 +190,11 @@ protected function addCastAttributesToArray(array $attributes, array $mutatedAtt /** * Get an attribute array of all arrayable attributes. * - * @return array + * @return array */ protected function getArrayableAttributes() { - return $this->getArrayableItems($this->getAttributes()); + return $this->getArrayableItems($this->attributes); } /** @@ -372,14 +204,12 @@ protected function getArrayableAttributes() */ protected function getArrayableAppends() { - $appends = $this->getAppends(); - - if (! count($appends)) { + if (! count($this->appends)) { return []; } return $this->getArrayableItems( - array_combine($appends, $appends) + array_combine($this->appends, $this->appends) ); } @@ -393,7 +223,7 @@ public function relationsToArray() $attributes = []; foreach ($this->getArrayableRelations() as $key => $value) { - // If the values implement the Arrayable interface we can just call this + // 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) { @@ -401,8 +231,8 @@ public function relationsToArray() } // 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. + // 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; } @@ -417,8 +247,8 @@ public function relationsToArray() // 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; + if (isset($relation) || is_null($value)) { + $attributes[$key] = $relation; } unset($relation); @@ -456,25 +286,6 @@ protected function getArrayableItems(array $values) 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. * @@ -490,54 +301,54 @@ public function getAttribute($key) // 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)) { + if (array_key_exists($key, $this->attributes) || + $this->hasGetMutator($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 + // since we do not want to treat any of those methods are relationships since // they are all intended as helper methods and none of these are relations. if (method_exists(self::class, $key)) { - return $this->throwMissingAttributeExceptionIfApplicable($key); + return; } - return $this->isRelation($key) || $this->relationLoaded($key) - ? $this->getRelationValue($key) - : $this->throwMissingAttributeExceptionIfApplicable($key); + return $this->getRelationValue($key); } /** - * Either throw a missing attribute exception or return null depending on Eloquent's configuration. + * Get a plain attribute (not a relationship). * * @param string $key - * @return null - * - * @throws \Illuminate\Database\Eloquent\MissingAttributeException + * @return mixed */ - protected function throwMissingAttributeExceptionIfApplicable($key) + public function getAttributeValue($key) { - if ($this->exists && - ! $this->wasRecentlyCreated && - static::preventsAccessingMissingAttributes()) { - if (isset(static::$missingAttributeViolationCallback)) { - return call_user_func(static::$missingAttributeViolationCallback, $this, $key); - } + $value = $this->getAttributeFromArray($key); - throw new MissingAttributeException($this, $key); + // 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); } - return null; - } + // 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); + } - /** - * Get a plain attribute (not a relationship). - * - * @param string $key - * @return mixed - */ - public function getAttributeValue($key) - { - return $this->transformModelValue($key, $this->getAttributeFromArray($key)); + // 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 $value; } /** @@ -548,9 +359,9 @@ public function getAttributeValue($key) */ protected function getAttributeFromArray($key) { - $this->mergeAttributeFromCachedCasts($key); - - return $this->attributes[$key] ?? null; + if (isset($this->attributes[$key])) { + return $this->attributes[$key]; + } } /** @@ -568,59 +379,12 @@ public function getRelationValue($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; + if (method_exists($this, $key)) { + return $this->getRelationshipFromMethod($key); } - - throw new LazyLoadingViolationException($this, $key); } /** @@ -636,15 +400,7 @@ 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 - )); + throw new LogicException(get_class($this).'::'.$method.' must return a relationship instance.'); } return tap($relation->getResults(), function ($results) use ($method) { @@ -663,59 +419,6 @@ 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. * @@ -725,41 +428,9 @@ public function hasAnyGetMutator($key) */ 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. * @@ -769,74 +440,11 @@ protected function mutateAttributeMarkedAttribute($key, $value) */ 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); - } + $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. * @@ -846,31 +454,18 @@ protected function ensureCastsAreStringValues($casts) */ protected function castAttribute($key, $value) { - $castType = $this->getCastType($key); - - if (is_null($value) && in_array($castType, static::$primitiveCastTypes)) { + if (is_null($value)) { 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) { + switch ($this->getCastType($key)) { 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]); + return (float) $value; case 'string': return (string) $value; case 'bool': @@ -880,195 +475,29 @@ protected function castAttribute($key, $value) 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); + default: + return $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. + * Get the type of cast for a model attribute. * * @param string $key - * @param mixed $value - * @return mixed + * @return string */ - protected function getClassCastableAttributeValue($key, $value) + protected function getCastType($key) { - $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:'); + return trim(strtolower($this->getCasts()[$key])); } /** @@ -1076,57 +505,37 @@ protected function isDecimalCast($cast) * * @param string $key * @param mixed $value - * @return mixed + * @return $this */ 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. + // the model, such as "json_encoding" an listing of data for storage. if ($this->hasSetMutator($key)) { - return $this->setMutatedAttributeValue($key, $value); - } elseif ($this->hasAttributeSetMutator($key)) { - return $this->setAttributeMarkedMutatedAttributeValue($key, $value); + $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 (! is_null($value) && $this->isDateAttribute($key)) { + elseif ($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)) { + if ($this->isJsonCastable($key) && ! is_null($value)) { $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, '->')) { + 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; @@ -1143,79 +552,6 @@ 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. * @@ -1224,8 +560,8 @@ protected function setAttributeMarkedMutatedAttributeValue($key, $value) */ protected function isDateAttribute($key) { - return in_array($key, $this->getDates(), true) || - $this->isDateCastable($key); + return in_array($key, $this->getDates()) || + $this->isDateCastable($key); } /** @@ -1237,111 +573,22 @@ protected function isDateAttribute($key) */ public function fillJsonAttribute($key, $value) { - [$key, $path] = explode('->', $key, 2); + list($key, $path) = explode('->', $key, 2); - $value = $this->asJson($this->getArrayAttributeWithValue( + $this->attributes[$key] = $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 + * @return $this */ protected function getArrayAttributeWithValue($path, $key, $value) { @@ -1358,15 +605,8 @@ protected function getArrayAttributeWithValue($path, $key, $value) */ protected function getArrayAttributeByKey($key) { - if (! isset($this->attributes[$key])) { - return []; - } - - return $this->fromJson( - $this->isEncryptedCastable($key) - ? $this->fromEncryptedString($this->attributes[$key]) - : $this->attributes[$key] - ); + return isset($this->attributes[$key]) ? + $this->fromJson($this->attributes[$key]) : []; } /** @@ -1375,12 +615,10 @@ protected function getArrayAttributeByKey($key) * @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)); + $value = $this->asJson($value); if ($value === false) { throw JsonEncodingException::forAttribute( @@ -1391,900 +629,309 @@ protected function castAttributeAsJson($key, $value) 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) + protected function asJson($value) { - return Json::encode($value, $flags); + return json_encode($value); } /** * Decode the given JSON back into an array or object. * - * @param string|null $value + * @param string $value * @param bool $asObject * @return mixed */ public function fromJson($value, $asObject = false) { - if ($value === null || $value === '') { - return null; - } - - return Json::decode($value, ! $asObject); + return json_decode($value, ! $asObject); } /** - * Decrypt the given encrypted string. + * Return a timestamp as DateTime object with time set to 00:00:00. * - * @param string $value - * @return mixed + * @param mixed $value + * @return \Carbon\Carbon */ - public function fromEncryptedString($value) + protected function asDate($value) { - return static::currentEncrypter()->decrypt($value, false); + return $this->asDateTime($value)->startOfDay(); } /** - * Cast the given attribute to an encrypted string. + * Return a timestamp as DateTime object. * - * @param string $key * @param mixed $value - * @return string + * @return \Carbon\Carbon */ - protected function castAttributeAsEncryptedString($key, #[\SensitiveParameter] $value) + protected function asDateTime($value) { - return static::currentEncrypter()->encrypt($value, false); - } + // 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; + } - /** - * 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; + // 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 ($this->isStandardDateFormat($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 + ); } /** - * Get the current encrypter being used by the model. + * Determine if the given value is a standard date format. * - * @return \Illuminate\Contracts\Encryption\Encrypter + * @param string $value + * @return bool */ - public static function currentEncrypter() + protected function isStandardDateFormat($value) { - return static::$encrypter ?? Crypt::getFacadeRoot(); + return preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $value); } /** - * Cast the given attribute to a hashed string. + * Convert a DateTime to a storable string. * - * @param string $key - * @param mixed $value + * @param \DateTime|int $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() + public function fromDateTime($value) { - $this->mergeAttributesFromCachedCasts(); - - return $this->attributes; + return $this->asDateTime($value)->format( + $this->getDateFormat() + ); } /** - * Get all of the current attributes on the model for an insert operation. + * Return a timestamp as unix timestamp. * - * @return array + * @param mixed $value + * @return int */ - protected function getAttributesForInsert() + protected function asTimestamp($value) { - return $this->getAttributes(); + return $this->asDateTime($value)->getTimestamp(); } /** - * Set the array of model attributes. No checking is done. + * Prepare a date for array / JSON serialization. * - * @param array $attributes - * @param bool $sync - * @return $this + * @param \DateTimeInterface $date + * @return string */ - public function setRawAttributes(array $attributes, $sync = false) + protected function serializeDate(DateTimeInterface $date) { - $this->attributes = $attributes; - - if ($sync) { - $this->syncOriginal(); - } - - $this->classCastCache = []; - $this->attributeCastCache = []; - - return $this; + return $date->format($this->getDateFormat()); } /** - * Get the model's original attribute values. + * Get the attributes that should be converted to dates. * - * @param string|null $key - * @param mixed $default - * @return ($key is null ? array : mixed) + * @return array */ - public function getOriginal($key = null, $default = null) + public function getDates() { - return (new static)->setRawAttributes( - $this->original, $sync = true - )->getOriginalWithoutRewindingModel($key, $default); + $defaults = [static::CREATED_AT, static::UPDATED_AT]; + + return $this->usesTimestamps() ? array_merge($this->dates, $defaults) : $this->dates; } /** - * Get the model's original attribute values. + * Get the format for database stored dates. * - * @param string|null $key - * @param mixed $default - * @return ($key is null ? array : mixed) + * @return string */ - protected function getOriginalWithoutRewindingModel($key = null, $default = null) + protected function getDateFormat() { - 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(); + return $this->dateFormat ?: $this->getConnection()->getQueryGrammar()->getDateFormat(); } /** - * Get the model's raw original attribute values. + * Set the date format used by the model. * - * @param string|null $key - * @param mixed $default - * @return ($key is null ? array : mixed) + * @param string $format + * @return $this */ - public function getRawOriginal($key = null, $default = null) + public function setDateFormat($format) { - return Arr::get($this->original, $key, $default); + $this->dateFormat = $format; + + return $this; } /** - * Get a subset of the model's attributes. + * Determine whether an attribute should be cast to a native type. * - * @param array|mixed $attributes - * @return array + * @param string $key + * @param array|string|null $types + * @return bool */ - public function only($attributes) + public function hasCast($key, $types = null) { - $results = []; - - foreach (is_array($attributes) ? $attributes : func_get_args() as $attribute) { - $results[$attribute] = $this->getAttribute($attribute); + if (array_key_exists($key, $this->getCasts())) { + return $types ? in_array($this->getCastType($key), (array) $types, true) : true; } - return $results; + return false; } /** - * Get all attributes except the given ones. + * Get the casts array. * - * @param array|mixed $attributes * @return array */ - public function except($attributes) + public function getCasts() { - $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); - } + if ($this->getIncrementing()) { + return array_merge([$this->getKeyName() => $this->getKeyType()], $this->casts); } - return $results; + return $this->casts; } /** - * Sync the original attributes with the current. + * Determine whether a value is Date / DateTime castable for inbound manipulation. * - * @return $this + * @param string $key + * @return bool */ - public function syncOriginal() + protected function isDateCastable($key) { - $this->original = $this->getAttributes(); - - return $this; + return $this->hasCast($key, ['date', 'datetime']); } /** - * Sync a single original attribute with its current value. + * Determine whether a value is JSON castable for inbound manipulation. * - * @param string $attribute - * @return $this + * @param string $key + * @return bool */ - public function syncOriginalAttribute($attribute) + protected function isJsonCastable($key) { - return $this->syncOriginalAttributes($attribute); + return $this->hasCast($key, ['array', 'json', 'object', 'collection']); } /** - * Sync multiple original attribute with their current values. + * Get all of the current attributes on the model. * - * @param array|string $attributes - * @return $this + * @return array */ - public function syncOriginalAttributes($attributes) + public function getAttributes() { - $attributes = is_array($attributes) ? $attributes : func_get_args(); - - $modelAttributes = $this->getAttributes(); - - foreach ($attributes as $attribute) { - $this->original[$attribute] = $modelAttributes[$attribute]; - } - - return $this; + return $this->attributes; } /** - * Sync the changed attributes. + * Set the array of model attributes. No checking is done. * + * @param array $attributes + * @param bool $sync * @return $this */ - public function syncChanges() + public function setRawAttributes(array $attributes, $sync = false) { - $this->changes = $this->getDirty(); - $this->previous = array_intersect_key($this->getRawOriginal(), $this->changes); + $this->attributes = $attributes; + + if ($sync) { + $this->syncOriginal(); + } return $this; } /** - * Determine if the model or any of the given attribute(s) have been modified. + * Get the model's original attribute values. * - * @param array|string|null $attributes - * @return bool + * @param string|null $key + * @param mixed $default + * @return mixed|array */ - public function isDirty($attributes = null) + public function getOriginal($key = null, $default = null) { - return $this->hasChanges( - $this->getDirty(), is_array($attributes) ? $attributes : func_get_args() - ); + return Arr::get($this->original, $key, $default); } /** - * Determine if the model or all the given attribute(s) have remained the same. + * Sync the original attributes with the current. * - * @param array|string|null $attributes - * @return bool + * @return $this */ - public function isClean($attributes = null) + public function syncOriginal() { - return ! $this->isDirty(...func_get_args()); + $this->original = $this->attributes; + + return $this; } /** - * Discard attribute changes and reset the attributes to their original state. + * Sync a single original attribute with its current value. * + * @param string $attribute * @return $this */ - public function discardChanges() + public function syncOriginalAttribute($attribute) { - [$this->attributes, $this->changes, $this->previous] = [$this->original, [], []]; - - $this->classCastCache = []; - $this->attributeCastCache = []; + $this->original[$attribute] = $this->attributes[$attribute]; return $this; } /** - * Determine if the model or any of the given attribute(s) were changed when the model was last saved. + * Determine if the model or given attribute(s) have been modified. * - * @param array|string|null $attributes + * @param array|string|null $attributes * @return bool */ - public function wasChanged($attributes = null) + public function isDirty($attributes = null) { - return $this->hasChanges( - $this->getChanges(), is_array($attributes) ? $attributes : func_get_args() - ); - } + $dirty = $this->getDirty(); - /** - * 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; + if (is_null($attributes)) { + return count($dirty) > 0; } + $attributes = is_array($attributes) + ? $attributes : func_get_args(); + // 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)) { + foreach ($attributes as $attribute) { + if (array_key_exists($attribute, $dirty)) { return true; } } @@ -2293,16 +940,27 @@ protected function hasChanges($changes, $attributes = null) } /** - * Get the attributes that have been changed since the last sync. + * Determine if the model or 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()); + } + + /** + * Get the attributes that have been changed since last sync. * - * @return array + * @return array */ public function getDirty() { $dirty = []; foreach ($this->getAttributes() as $key => $value) { - if (! $this->originalIsEquivalent($key)) { + if (! $this->originalIsEquivalent($key, $value)) { $dirty[$key] = $value; } } @@ -2310,158 +968,52 @@ public function getDirty() 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 + * @param string $key + * @param mixed $current * @return bool */ - public function originalIsEquivalent($key) + protected function originalIsEquivalent($key, $current) { if (! array_key_exists($key, $this->original)) { return false; } - $attribute = Arr::get($this->attributes, $key); - $original = Arr::get($this->original, $key); + $original = $this->getOriginal($key); - if ($attribute === $original) { + if ($current === $original) { return true; - } elseif (is_null($attribute)) { + } elseif (is_null($current)) { 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); + } elseif ($this->isDateAttribute($key)) { + return $this->fromDateTime($current) === + $this->fromDateTime($original); + } elseif ($this->hasCast($key)) { + return $this->castAttribute($key, $current) === + $this->castAttribute($key, $original); } - return $value; + return is_numeric($current) && is_numeric($original) + && strcmp((string) $current, (string) $original) === 0; } /** * Append attributes to query when building a query. * - * @param array|string $attributes + * @param array|string $attributes * @return $this */ public function append($attributes) { - $this->appends = array_values(array_unique( + $this->appends = 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. * @@ -2475,40 +1027,6 @@ public function setAppends(array $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. * @@ -2516,33 +1034,26 @@ public function withoutAppends() */ public function getMutatedAttributes() { - if (! isset(static::$mutatorCache[static::class])) { - static::cacheMutatedAttributes($this); + $class = static::class; + + if (! isset(static::$mutatorCache[$class])) { + static::cacheMutatedAttributes($class); } - return static::$mutatorCache[static::class]; + return static::$mutatorCache[$class]; } /** * Extract and cache all the mutated attributes of a class. * - * @param object|string $classOrInstance + * @param string $class * @return void */ - public static function cacheMutatedAttributes($classOrInstance) + public static function cacheMutatedAttributes($class) { - $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(); + static::$mutatorCache[$class] = collect(static::getMutatorMethods($class))->map(function ($match) { + return lcfirst(static::$snakeAttributes ? Str::snake($match) : $match); + })->all(); } /** @@ -2557,28 +1068,4 @@ protected static function getMutatorMethods($class) 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 index cc0368e67d..a769221734 100644 --- a/Eloquent/Concerns/HasEvents.php +++ b/Eloquent/Concerns/HasEvents.php @@ -3,13 +3,6 @@ namespace Illuminate\Database\Eloquent\Concerns; use Illuminate\Contracts\Events\Dispatcher; -use Illuminate\Database\Eloquent\Attributes\ObservedBy; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Events\NullDispatcher; -use Illuminate\Support\Arr; -use Illuminate\Support\Collection; -use InvalidArgumentException; -use ReflectionClass; trait HasEvents { @@ -18,7 +11,7 @@ trait HasEvents * * Allows for object-based events for native Eloquent events. * - * @var array + * @var array */ protected $dispatchesEvents = []; @@ -27,114 +20,44 @@ trait HasEvents * * These are extra user-defined events observers may subscribe to. * - * @var string[] + * @var array */ protected $observables = []; /** - * Boot the has event trait for a model. + * Register an observer with the Model. * + * @param object|string $class * @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) + public static function observe($class) { $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); + $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 ($this->getObservableEvents() as $event) { + foreach ($instance->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[] + * @return array */ public function getObservableEvents() { return array_merge( [ - 'retrieved', 'creating', 'created', 'updating', 'updated', - 'saving', 'saved', 'restoring', 'restored', 'replicating', - 'trashed', 'deleting', 'deleted', 'forceDeleting', 'forceDeleted', + 'creating', 'created', 'updating', 'updated', + 'deleting', 'deleted', 'saving', 'saved', + 'restoring', 'restored', ], $this->observables ); @@ -143,7 +66,7 @@ public function getObservableEvents() /** * Set the observable event names. * - * @param string[] $observables + * @param array $observables * @return $this */ public function setObservableEvents(array $observables) @@ -156,7 +79,7 @@ public function setObservableEvents(array $observables) /** * Add an observable event name. * - * @param string|string[] $observables + * @param array|mixed $observables * @return void */ public function addObservableEvents($observables) @@ -169,7 +92,7 @@ public function addObservableEvents($observables) /** * Remove an observable event name. * - * @param string|string[] $observables + * @param array|mixed $observables * @return void */ public function removeObservableEvents($observables) @@ -183,7 +106,7 @@ public function removeObservableEvents($observables) * Register a model event with the dispatcher. * * @param string $event - * @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback + * @param \Closure|string $callback * @return void */ protected static function registerModelEvent($event, $callback) @@ -211,7 +134,7 @@ protected function fireModelEvent($event, $halt = 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'; + $method = $halt ? 'until' : 'fire'; $result = $this->filterModelEventResults( $this->fireCustomModelEvent($event, $method) @@ -230,8 +153,8 @@ protected function fireModelEvent($event, $halt = true) * Fire a custom model event for the given event. * * @param string $event - * @param 'until'|'dispatch' $method - * @return array|null|void + * @param string $method + * @return mixed|null */ protected function fireCustomModelEvent($event, $method) { @@ -263,21 +186,10 @@ protected function filterModelEventResults($result) 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 + * @param \Closure|string $callback * @return void */ public static function saving($callback) @@ -288,7 +200,7 @@ public static function saving($callback) /** * Register a saved model event with the dispatcher. * - * @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback + * @param \Closure|string $callback * @return void */ public static function saved($callback) @@ -299,7 +211,7 @@ public static function saved($callback) /** * Register an updating model event with the dispatcher. * - * @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback + * @param \Closure|string $callback * @return void */ public static function updating($callback) @@ -310,7 +222,7 @@ public static function updating($callback) /** * Register an updated model event with the dispatcher. * - * @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback + * @param \Closure|string $callback * @return void */ public static function updated($callback) @@ -321,7 +233,7 @@ public static function updated($callback) /** * Register a creating model event with the dispatcher. * - * @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback + * @param \Closure|string $callback * @return void */ public static function creating($callback) @@ -332,7 +244,7 @@ public static function creating($callback) /** * Register a created model event with the dispatcher. * - * @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback + * @param \Closure|string $callback * @return void */ public static function created($callback) @@ -340,21 +252,10 @@ 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 + * @param \Closure|string $callback * @return void */ public static function deleting($callback) @@ -365,7 +266,7 @@ public static function deleting($callback) /** * Register a deleted model event with the dispatcher. * - * @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback + * @param \Closure|string $callback * @return void */ public static function deleted($callback) @@ -374,7 +275,7 @@ public static function deleted($callback) } /** - * Remove all the event listeners for the model. + * Remove all of the event listeners for the model. * * @return void */ @@ -389,26 +290,12 @@ public static function flushEventListeners() 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 + * @return \Illuminate\Contracts\Events\Dispatcher */ public static function getEventDispatcher() { @@ -435,27 +322,4 @@ 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 index 635ac8d1fe..97a549f6c7 100644 --- a/Eloquent/Concerns/HasGlobalScopes.php +++ b/Eloquent/Concerns/HasGlobalScopes.php @@ -3,85 +3,32 @@ namespace Illuminate\Database\Eloquent\Concerns; use Closure; -use Illuminate\Database\Eloquent\Attributes\ScopedBy; -use Illuminate\Database\Eloquent\Scope; use Illuminate\Support\Arr; -use Illuminate\Support\Collection; use InvalidArgumentException; -use ReflectionAttribute; -use ReflectionClass; +use Illuminate\Database\Eloquent\Scope; trait HasGlobalScopes { - /** - * Boot the has global scopes trait for a model. - * - * @return void - */ - public static function bootHasGlobalScopes() - { - static::addGlobalScopes(static::resolveGlobalScopeAttributes()); - } - - /** - * Resolve the global scope class names from the attributes. - * - * @return array - */ - public static function resolveGlobalScopeAttributes() - { - $reflectionClass = new ReflectionClass(static::class); - - $attributes = (new Collection($reflectionClass->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 + * @param \Illuminate\Database\Eloquent\Scope|\Closure|string $scope + * @param \Closure|null $implementation * @return mixed * * @throws \InvalidArgumentException */ - public static function addGlobalScope($scope, $implementation = null) + public static function addGlobalScope($scope, Closure $implementation = null) { - if (is_string($scope) && ($implementation instanceof Closure || $implementation instanceof Scope)) { + if (is_string($scope) && ! is_null($implementation)) { 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); - } - } + throw new InvalidArgumentException('Global scope must be an instance of Closure or Scope.'); } /** @@ -99,7 +46,7 @@ public static function hasGlobalScope($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 + * @return \Illuminate\Database\Eloquent\Scope|\Closure|null */ public static function getGlobalScope($scope) { @@ -112,27 +59,6 @@ public static function getGlobalScope($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. * diff --git a/Eloquent/Concerns/HasRelationships.php b/Eloquent/Concerns/HasRelationships.php index 9380eb10e0..6f054bd4a5 100644 --- a/Eloquent/Concerns/HasRelationships.php +++ b/Eloquent/Concerns/HasRelationships.php @@ -2,28 +2,20 @@ namespace Illuminate\Database\Eloquent\Concerns; -use Closure; -use Illuminate\Database\ClassMorphViolationException; -use Illuminate\Database\Eloquent\Attributes\Initialize; -use Illuminate\Database\Eloquent\Attributes\Touches; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; +use Illuminate\Support\Arr; +use Illuminate\Support\Str; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\PendingHasThroughRelationship; -use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Database\Eloquent\Relations\BelongsToMany; -use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Eloquent\Relations\HasManyThrough; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Relations\HasOne; -use Illuminate\Database\Eloquent\Relations\HasOneThrough; -use Illuminate\Database\Eloquent\Relations\MorphMany; -use Illuminate\Database\Eloquent\Relations\MorphOne; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphTo; -use Illuminate\Database\Eloquent\Relations\MorphToMany; -use Illuminate\Database\Eloquent\Relations\Pivot; +use Illuminate\Database\Eloquent\Relations\MorphOne; use Illuminate\Database\Eloquent\Relations\Relation; -use Illuminate\Support\Arr; -use Illuminate\Support\Str; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\MorphMany; +use Illuminate\Database\Eloquent\Relations\MorphToMany; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; trait HasRelationships { @@ -41,188 +33,23 @@ trait HasRelationships */ protected $touches = []; - /** - * The relationship autoloader callback. - * - * @var \Closure|null - */ - protected $relationAutoloadCallback = null; - - /** - * The relationship autoloader callback context. - * - * @var mixed - */ - protected $relationAutoloadContext = null; - /** * The many to many relationship methods. * - * @var string[] + * @var array */ public static $manyMethods = [ 'belongsToMany', 'morphToMany', 'morphedByMany', + 'guessBelongsToManyRelation', 'findFirstMethodThatIsntRelation', ]; - /** - * The relation resolver callbacks. - * - * @var array - */ - protected static $relationResolvers = []; - - /** - * Initialize the HasRelationships trait. - * - * @return void - */ - #[Initialize] - public function initializeHasRelationships() - { - if (empty($this->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 + * @param string $related + * @param string $foreignKey + * @param string $localKey + * @return \Illuminate\Database\Eloquent\Relations\HasOne */ public function hasOne($related, $foreignKey = null, $localKey = null) { @@ -232,131 +59,40 @@ public function hasOne($related, $foreignKey = null, $localKey = null) $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); + return new HasOne($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey); } /** * Define a polymorphic one-to-one relationship. * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param class-string $related + * @param string $related * @param string $name - * @param string|null $type - * @param string|null $id - * @param string|null $localKey - * @return \Illuminate\Database\Eloquent\Relations\MorphOne + * @param string $type + * @param string $id + * @param string $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); + list($type, $id) = $this->getMorphs($name, $type, $id); - $localKey = $localKey ?: $this->getKeyName(); + $table = $instance->getTable(); - return $this->newMorphOne($instance->newQuery(), $this, $instance->qualifyColumn($type), $instance->qualifyColumn($id), $localKey); - } + $localKey = $localKey ?: $this->getKeyName(); - /** - * 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); + return new MorphOne($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$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 + * @param string $related + * @param string $foreignKey + * @param string $ownerKey + * @param string $relation + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function belongsTo($related, $foreignKey = null, $ownerKey = null, $relation = null) { @@ -376,60 +112,41 @@ public function belongsTo($related, $foreignKey = null, $ownerKey = null, $relat $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. + // 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. $ownerKey = $ownerKey ?: $instance->getKeyName(); - return $this->newBelongsTo( + return new BelongsTo( $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> + * @param string $name + * @param string $type + * @param string $id + * @return \Illuminate\Database\Eloquent\Relations\MorphTo */ - public function morphTo($name = null, $type = null, $id = null, $ownerKey = null) + public function morphTo($name = null, $type = null, $id = 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( + list($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); + return empty($class = $this->{$type}) + ? $this->morphEagerTo($name, $type, $id) + : $this->morphInstanceTo($class, $name, $type, $id); } /** @@ -438,13 +155,12 @@ public function morphTo($name = null, $type = null, $id = null, $ownerKey = null * @param string $name * @param string $type * @param string $id - * @param string|null $ownerKey - * @return \Illuminate\Database\Eloquent\Relations\MorphTo<\Illuminate\Database\Eloquent\Model, $this> + * @return \Illuminate\Database\Eloquent\Relations\MorphTo */ - protected function morphEagerTo($name, $type, $id, $ownerKey) + protected function morphEagerTo($name, $type, $id) { - return $this->newMorphTo( - $this->newQuery()->setEagerLoads([]), $this, $id, $ownerKey, $type, $name + return new MorphTo( + $this->newQuery()->setEagerLoads([]), $this, $id, null, $type, $name ); } @@ -455,39 +171,19 @@ protected function morphEagerTo($name, $type, $id, $ownerKey) * @param string $name * @param string $type * @param string $id - * @param string|null $ownerKey - * @return \Illuminate\Database\Eloquent\Relations\MorphTo<\Illuminate\Database\Eloquent\Model, $this> + * @return \Illuminate\Database\Eloquent\Relations\MorphTo */ - protected function morphInstanceTo($target, $name, $type, $id, $ownerKey) + protected function morphInstanceTo($target, $name, $type, $id) { $instance = $this->newRelatedInstance( static::getActualClassNameForMorph($target) ); - return $this->newMorphTo( - $instance->newQuery(), $this, $id, $ownerKey ?? $instance->getKeyName(), $type, $name + return new MorphTo( + $instance->newQuery(), $this, $id, $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. * @@ -496,7 +192,7 @@ protected function newMorphTo(Builder $query, Model $parent, $foreignKey, $owner */ public static function getActualClassNameForMorph($class) { - return Arr::get(Relation::morphMap() ?: [], $class, $class); + return Arr::get(Relation::morphMap(), $class, $class); } /** @@ -506,45 +202,18 @@ public static function getActualClassNameForMorph($class) */ protected function guessBelongsToRelation() { - [, , $caller] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); + list($one, $two, $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 + * @param string $related + * @param string $foreignKey + * @param string $localKey + * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function hasMany($related, $foreignKey = null, $localKey = null) { @@ -554,93 +223,45 @@ public function hasMany($related, $foreignKey = null, $localKey = null) $localKey = $localKey ?: $this->getKeyName(); - return $this->newHasMany( - $instance->newQuery(), $this, $instance->qualifyColumn($foreignKey), $localKey + return new HasMany( + $instance->newQuery(), $this, $instance->getTable().'.'.$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 $related + * @param string $through * @param string|null $firstKey * @param string|null $secondKey * @param string|null $localKey - * @param string|null $secondLocalKey - * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough + * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough */ - public function hasManyThrough($related, $through, $firstKey = null, $secondKey = null, $localKey = null, $secondLocalKey = null) + public function hasManyThrough($related, $through, $firstKey = null, $secondKey = null, $localKey = null) { - $through = $this->newRelatedThroughInstance($through); + $through = new $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() - ); - } + $localKey = $localKey ?: $this->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); + $instance = $this->newRelatedInstance($related); + + return new HasManyThrough($instance->newQuery(), $this, $through, $firstKey, $secondKey, $localKey); } /** * Define a polymorphic one-to-many relationship. * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * - * @param class-string $related + * @param string $related * @param string $name - * @param string|null $type - * @param string|null $id - * @param string|null $localKey - * @return \Illuminate\Database\Eloquent\Relations\MorphMany + * @param string $type + * @param string $id + * @param string $localKey + * @return \Illuminate\Database\Eloquent\Relations\MorphMany */ public function morphMany($related, $name, $type = null, $id = null, $localKey = null) { @@ -649,54 +270,30 @@ public function morphMany($related, $name, $type = null, $id = null, $localKey = // 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); + list($type, $id) = $this->getMorphs($name, $type, $id); - $localKey = $localKey ?: $this->getKeyName(); + $table = $instance->getTable(); - return $this->newMorphMany($instance->newQuery(), $this, $instance->qualifyColumn($type), $instance->qualifyColumn($id), $localKey); - } + $localKey = $localKey ?: $this->getKeyName(); - /** - * 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); + return new MorphMany($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$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 + * @param string $related + * @param string $table + * @param string $foreignPivotKey + * @param string $relatedPivotKey + * @param string $parentKey + * @param string $relatedKey + * @param string $relation + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ - public function belongsToMany( - $related, - $table = null, - $foreignPivotKey = null, - $relatedPivotKey = null, - $parentKey = null, - $relatedKey = null, - $relation = null, - ) { + 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. @@ -717,78 +314,34 @@ public function belongsToMany( // 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); + $table = $this->joiningTable($related); } - return $this->newBelongsToMany( - $instance->newQuery(), - $this, - $table, - $foreignPivotKey, - $relatedPivotKey, - $parentKey ?: $this->getKeyName(), - $relatedKey ?: $instance->getKeyName(), - $relation, + return new BelongsToMany( + $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 + * Define a polymorphic many-to-many relationship. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param TDeclaringModel $parent - * @param string|class-string<\Illuminate\Database\Eloquent\Model> $table + * @param string $related + * @param string $name + * @param string $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 + * @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(); + public function morphToMany($related, $name, $table = null, $foreignPivotKey = null, + $relatedPivotKey = null, $parentKey = null, + $relatedKey = null, $inverse = false) + { + $caller = $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 @@ -799,100 +352,33 @@ public function morphToMany( $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); + // 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. + $table = $table ?: Str::plural($name); - $table = implode('', $words).Str::plural($lastWord); - } - - return $this->newMorphToMany( - $instance->newQuery(), - $this, - $name, - $table, - $foreignPivotKey, - $relatedPivotKey, - $parentKey ?: $this->getKeyName(), - $relatedKey ?: $instance->getKeyName(), - $relation, - $inverse, + return new MorphToMany( + $instance->newQuery(), $this, $name, $table, + $foreignPivotKey, $relatedPivotKey, $parentKey ?: $this->getKeyName(), + $relatedKey ?: $instance->getKeyName(), $caller, $inverse ); } /** - * Instantiate a new MorphToMany relationship. - * - * @template TRelatedModel of \Illuminate\Database\Eloquent\Model - * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model + * Define a polymorphic, inverse many-to-many relationship. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param TDeclaringModel $parent + * @param string $related * @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 + * @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, - ) { + public function morphedByMany($related, $name, $table = null, $foreignPivotKey = null, + $relatedPivotKey = null, $parentKey = null, $relatedKey = null) + { $foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey(); // For the inverse of the polymorphic many-to-many relations, we will change @@ -901,70 +387,47 @@ public function morphedByMany( $relatedPivotKey = $relatedPivotKey ?: $name.'_id'; return $this->morphToMany( - $related, - $name, - $table, - $foreignPivotKey, - $relatedPivotKey, - $parentKey, - $relatedKey, - $relation, - true, + $related, $name, $table, $foreignPivotKey, + $relatedPivotKey, $parentKey, $relatedKey, true ); } /** - * Get the relationship name of the belongsToMany relationship. + * Get the relationship name of the belongs to many. * - * @return string|null + * @return string */ 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 ! in_array($trace['function'], Model::$manyMethods); }); - return $caller['function'] ?? null; + return ! is_null($caller) ? $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) + public function joiningTable($related) { // 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(), + $models = [ + Str::snake(class_basename($related)), + Str::snake(class_basename($this)), ]; // 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); + sort($models); - 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)); + return strtolower(implode('_', $models)); } /** @@ -975,7 +438,7 @@ public function joiningTableSegment() */ public function touches($relation) { - return in_array($relation, $this->getTouchedRelations()); + return in_array($relation, $this->touches); } /** @@ -985,19 +448,19 @@ public function touches($relation) */ public function touchOwners() { - $this->withoutRecursion(function () { - foreach ($this->getTouchedRelations() as $relation) { - $this->$relation()->touch(); + foreach ($this->touches as $relation) { + $this->$relation()->touch(); - if ($this->$relation instanceof self) { - $this->$relation->fireModelEvent('saved', false); + if ($this->$relation instanceof self) { + $this->$relation->fireModelEvent('saved', false); - $this->$relation->touchOwners(); - } elseif ($this->$relation instanceof EloquentCollection) { - $this->$relation->each->touchOwners(); - } + $this->$relation->touchOwners(); + } elseif ($this->$relation instanceof Collection) { + $this->$relation->each(function (Model $relation) { + $relation->touchOwners(); + }); } - }); + } } /** @@ -1017,8 +480,6 @@ protected function getMorphs($name, $type, $id) * Get the class name for polymorphic relations. * * @return string - * - * @throws \Illuminate\Database\ClassMorphViolationException */ public function getMorphClass() { @@ -1028,47 +489,24 @@ public function getMorphClass() 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 + * @param string $class + * @return mixed */ protected function newRelatedInstance($class) { return tap(new $class, function ($instance) { if (! $instance->getConnectionName()) { - $instance->setConnection($this->getConnectionName()); + $instance->setConnection($this->connection); } }); } - /** - * 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. * @@ -1102,7 +540,7 @@ public function relationLoaded($key) } /** - * Set the given relationship on the model. + * Set the specific relationship in the model. * * @param string $relation * @param mixed $value @@ -1112,21 +550,6 @@ 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; } @@ -1143,42 +566,6 @@ public function setRelations(array $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. * diff --git a/Eloquent/Concerns/HasTimestamps.php b/Eloquent/Concerns/HasTimestamps.php index b16fe685c6..ba640fe4b5 100644 --- a/Eloquent/Concerns/HasTimestamps.php +++ b/Eloquent/Concerns/HasTimestamps.php @@ -2,9 +2,7 @@ namespace Illuminate\Database\Eloquent\Concerns; -use Illuminate\Database\Eloquent\Attributes\Initialize; -use Illuminate\Database\Eloquent\Attributes\Table; -use Illuminate\Support\Facades\Date; +use Carbon\Carbon; trait HasTimestamps { @@ -15,42 +13,13 @@ trait HasTimestamps */ public $timestamps = true; - /** - * The list of models classes that have timestamps temporarily disabled. - * - * @var array - */ - protected static $ignoreTimestampsOn = []; - - /** - * Initialize the HasTimestamps trait. - * - * @return void - */ - #[Initialize] - public function initializeHasTimestamps() - { - if ($this->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) + public function touch() { - if ($attribute) { - $this->$attribute = $this->freshTimestamp(); - - return $this->save(); - } - if (! $this->usesTimestamps()) { return false; } @@ -60,39 +29,22 @@ public function touch($attribute = null) 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 + * @return void */ - public function updateTimestamps() + protected function updateTimestamps() { $time = $this->freshTimestamp(); - $updatedAtColumn = $this->getUpdatedAtColumn(); - - if (! is_null($updatedAtColumn) && ! $this->isDirty($updatedAtColumn)) { + if (! $this->isDirty(static::UPDATED_AT)) { $this->setUpdatedAt($time); } - $createdAtColumn = $this->getCreatedAtColumn(); - - if (! $this->exists && ! is_null($createdAtColumn) && ! $this->isDirty($createdAtColumn)) { + if (! $this->exists && ! $this->isDirty(static::CREATED_AT)) { $this->setCreatedAt($time); } - - return $this; } /** @@ -103,7 +55,7 @@ public function updateTimestamps() */ public function setCreatedAt($value) { - $this->{$this->getCreatedAtColumn()} = $value; + $this->{static::CREATED_AT} = $value; return $this; } @@ -116,7 +68,7 @@ public function setCreatedAt($value) */ public function setUpdatedAt($value) { - $this->{$this->getUpdatedAtColumn()} = $value; + $this->{static::UPDATED_AT} = $value; return $this; } @@ -124,11 +76,11 @@ public function setUpdatedAt($value) /** * Get a fresh timestamp for the model. * - * @return \Illuminate\Support\Carbon + * @return \Carbon\Carbon */ public function freshTimestamp() { - return Date::now(); + return new Carbon; } /** @@ -148,13 +100,13 @@ public function freshTimestampString() */ public function usesTimestamps() { - return $this->timestamps && ! static::isIgnoringTimestamps($this::class); + return $this->timestamps; } /** * Get the name of the "created at" column. * - * @return string|null + * @return string */ public function getCreatedAtColumn() { @@ -164,86 +116,10 @@ public function getCreatedAtColumn() /** * Get the name of the "updated at" column. * - * @return string|null + * @return string */ 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 @@ - + * @var array */ protected $hidden = []; /** * The attributes that should be visible in serialization. * - * @var array + * @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 + * @return array */ public function getHidden() { @@ -52,7 +31,7 @@ public function getHidden() /** * Set the hidden attributes for the model. * - * @param array $hidden + * @param array $hidden * @return $this */ public function setHidden(array $hidden) @@ -63,22 +42,22 @@ public function setHidden(array $hidden) } /** - * Merge new hidden attributes with existing hidden attributes on the model. + * Add hidden attributes for the model. * - * @param array $hidden - * @return $this + * @param array|string|null $attributes + * @return void */ - public function mergeHidden(array $hidden) + public function addHidden($attributes = null) { - $this->hidden = array_values(array_unique(array_merge($this->hidden, $hidden))); - - return $this; + $this->hidden = array_merge( + $this->hidden, is_array($attributes) ? $attributes : func_get_args() + ); } /** * Get the visible attributes for the model. * - * @return array + * @return array */ public function getVisible() { @@ -88,7 +67,7 @@ public function getVisible() /** * Set the visible attributes for the model. * - * @param array $visible + * @param array $visible * @return $this */ public function setVisible(array $visible) @@ -99,73 +78,49 @@ public function setVisible(array $visible) } /** - * Merge new visible attributes with existing visible attributes on the model. + * Add visible attributes for the model. * - * @param array $visible - * @return $this + * @param array|string|null $attributes + * @return void */ - public function mergeVisible(array $visible) + public function addVisible($attributes = null) { - $this->visible = array_values(array_unique(array_merge($this->visible, $visible))); - - return $this; + $this->visible = array_merge( + $this->visible, is_array($attributes) ? $attributes : func_get_args() + ); } /** * Make the given, typically hidden, attributes visible. * - * @param array|string|null $attributes + * @param array|string $attributes * @return $this */ public function makeVisible($attributes) { - $attributes = is_array($attributes) ? $attributes : func_get_args(); - - $this->hidden = array_diff($this->hidden, $attributes); + $this->hidden = array_diff($this->hidden, (array) $attributes); if (! empty($this->visible)) { - $this->visible = array_values(array_unique(array_merge($this->visible, $attributes))); + $this->addVisible($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 + * @param array|string $attributes * @return $this */ public function makeHidden($attributes) { - $this->hidden = array_values(array_unique(array_merge( - $this->hidden, is_array($attributes) ? $attributes : func_get_args() - ))); + $attributes = (array) $attributes; - return $this; - } + $this->visible = array_diff($this->visible, $attributes); - /** - * 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; + $this->hidden = array_unique(array_merge($this->hidden, $attributes)); + + return $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 index 6072dcff3d..05a64d8f29 100644 --- a/Eloquent/Concerns/QueriesRelationships.php +++ b/Eloquent/Concerns/QueriesRelationships.php @@ -2,63 +2,43 @@ namespace Illuminate\Database\Eloquent\Concerns; -use BadMethodCallException; use Closure; +use Illuminate\Support\Arr; +use Illuminate\Support\Str; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; -use Illuminate\Database\Eloquent\RelationNotFoundException; -use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Database\Eloquent\Relations\BelongsToMany; -use Illuminate\Database\Eloquent\Relations\MorphTo; +use Illuminate\Database\Query\Expression; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Query\Builder as QueryBuilder; -use Illuminate\Database\Query\Expression; -use Illuminate\Support\Collection as BaseCollection; -use Illuminate\Support\Str; -use InvalidArgumentException; -use function Illuminate\Support\enum_value; - -/** @mixin \Illuminate\Database\Eloquent\Builder */ trait QueriesRelationships { /** * 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 $relation * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|int $count + * @param int $count * @param string $boolean - * @param (\Closure(\Illuminate\Database\Eloquent\Builder): mixed)|null $callback - * @return $this - * - * @throws \RuntimeException + * @param \Closure|null $callback + * @return \Illuminate\Database\Eloquent\Builder|static */ - public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', ?Closure $callback = null) + 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 (strpos($relation, '.') !== false) { + return $this->hasNested($relation, $operator, $count, $boolean, $callback); } - if ($relation instanceof MorphTo) { - return $this->hasMorph($relation, ['*'], $operator, $count, $boolean, $callback); - } + $relation = $this->getRelationWithoutConstraints($relation); // 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'; + ? 'getRelationExistenceQuery' + : 'getRelationExistenceCountQuery'; $hasQuery = $relation->{$method}( - $relation->getRelated()->newQueryWithoutRelationships(), $this + $relation->getRelated()->newQuery(), $this ); // Next we will call any given callback as an "anonymous" scope so they can get the @@ -80,32 +60,16 @@ public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', ? * * @param string $relations * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|int $count + * @param int $count * @param string $boolean - * @param (\Closure(\Illuminate\Database\Eloquent\Builder<*>): mixed)|null $callback - * @return $this + * @param \Closure|null $callback + * @return \Illuminate\Database\Eloquent\Builder|static */ 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); - } - + $closure = function ($q) use (&$closure, &$relations, $operator, $count, $boolean, $callback) { // 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. @@ -114,16 +78,16 @@ protected function hasNested($relations, $operator = '>=', $count = 1, $boolean : $q->has(array_shift($relations), $operator, $count, 'and', $callback); }; - return $this->has(array_shift($relations), $doesntHave ? '<' : '>=', 1, $boolean, $closure); + return $this->has(array_shift($relations), '>=', 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 $relation * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|int $count - * @return $this + * @param int $count + * @return \Illuminate\Database\Eloquent\Builder|static */ public function orHas($relation, $operator = '>=', $count = 1) { @@ -133,74 +97,40 @@ public function orHas($relation, $operator = '>=', $count = 1) /** * 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 $relation * @param string $boolean - * @param (\Closure(\Illuminate\Database\Eloquent\Builder): mixed)|null $callback - * @return $this + * @param \Closure|null $callback + * @return \Illuminate\Database\Eloquent\Builder|static */ - public function doesntHave($relation, $boolean = 'and', ?Closure $callback = null) + 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 \Closure|null $callback * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|int $count - * @return $this + * @param int $count + * @return \Illuminate\Database\Eloquent\Builder|static */ - public function withWhereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1) + public function whereHas($relation, Closure $callback = null, $operator = '>=', $count = 1) { - return $this->whereHas(Str::before($relation, ':'), $callback, $operator, $count) - ->with($callback ? [$relation => fn ($query) => $callback($query)] : $relation); + return $this->has($relation, $operator, $count, '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 - * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|int $count - * @return $this + * @param string $relation + * @param \Closure $callback + * @param string $operator + * @param int $count + * @return \Illuminate\Database\Eloquent\Builder|static */ - public function orWhereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1) + public function orWhereHas($relation, Closure $callback = null, $operator = '>=', $count = 1) { return $this->has($relation, $operator, $count, 'or', $callback); } @@ -208,857 +138,95 @@ public function orWhereHas($relation, ?Closure $callback = null, $operator = '>= /** * 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 + * @param string $relation + * @param \Closure|null $callback + * @return \Illuminate\Database\Eloquent\Builder|static */ - public function whereDoesntHave($relation, ?Closure $callback = null) + 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. + * Add subselect queries to count the relations. * * @param mixed $relations - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string|null $function * @return $this */ - public function withAggregate($relations, $column, $function = null) + public function withCount($relations) { - if (empty($relations)) { - return $this; - } - if (is_null($this->query->columns)) { $this->query->select([$this->query->from.'.*']); } - $relations = is_array($relations) ? $relations : [$relations]; + $relations = is_array($relations) ? $relations : func_get_args(); 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. + // the resulting column. This allows multiple counts on the same relationship name. $segments = explode(' ', $name); unset($alias); - if (count($segments) === 3 && Str::lower($segments[1]) === 'as') { - [$name, $alias] = [$segments[0], $segments[2]]; + if (count($segments) == 3 && Str::lower($segments[1]) == 'as') { + list($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 + // Here we will get the relationship count 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'); + // count query. We will normalize the relation name then append _count as the name. + $query = $relation->getRelationExistenceCountQuery( + $relation->getRelated()->newQuery(), $this + ); $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'] = []; - } + $query->mergeConstraintsFrom($relation->getQuery()); - // 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))) - ) - ); + // Finally we will add the proper result column alias to the query and run the subselect + // statement against the query builder. Then we will return the builder instance back + // to the developer for further constraint chaining that needs to take place on it. + $column = isset($alias) ? $alias : Str::snake($name.'_count'); - 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 - ); - } + $this->selectSub($query->toBase(), $column); } 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 \Illuminate\Database\Eloquent\Builder $hasQuery + * @param \Illuminate\Database\Eloquent\Relations\Relation $relation * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|int $count + * @param int $count * @param string $boolean - * @return $this + * @return \Illuminate\Database\Eloquent\Builder|static */ 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); + ? $this->addWhereExistsQuery($hasQuery->toBase(), $boolean, $not = ($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 + * @param \Illuminate\Database\Eloquent\Builder $from + * @return \Illuminate\Database\Eloquent\Builder|static */ 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; + $whereBindings = Arr::get( + $from->getQuery()->getRawBindings(), 'where', [] + ); // 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 @@ -1066,35 +234,16 @@ public function mergeConstraintsFrom(Builder $from) return $this->withoutGlobalScopes( $from->removedScopes() )->mergeWheres( - $wheres, $whereBindings + $from->getQuery()->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 \Illuminate\Database\Query\Builder $query * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|int $count + * @param int $count * @param string $boolean * @return $this */ @@ -1114,7 +263,7 @@ protected function addWhereCountQuery(QueryBuilder $query, $operator = '>=', $co * Get the "has relation" base query instance. * * @param string $relation - * @return \Illuminate\Database\Eloquent\Relations\Relation<*, *, *> + * @return \Illuminate\Database\Eloquent\Relations\Relation */ protected function getRelationWithoutConstraints($relation) { @@ -1127,7 +276,7 @@ protected function getRelationWithoutConstraints($relation) * Check if we can run an "exists" query to optimize performance. * * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|int $count + * @param int $count * @return bool */ protected function canUseExistsForExistenceCheck($operator, $count) 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..0b57576e17 --- /dev/null +++ b/Eloquent/Factory.php @@ -0,0 +1,253 @@ +faker = $faker; + } + + /** + * 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 $this + */ + 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 $this + */ + public function define($class, callable $attributes, $name = 'default') + { + $this->definitions[$class][$name] = $attributes; + + return $this; + } + + /** + * Define a state with a given set of attributes. + * + * @param string $class + * @param string $state + * @param callable $attributes + * @return $this + */ + public function state($class, $state, callable $attributes) + { + $this->states[$class][$state] = $attributes; + + return $this; + } + + /** + * 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); + } + + /** + * 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') + { + return array_merge( + call_user_func($this->definitions[$class][$name], $this->faker), $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->states, $this->faker); + } + + /** + * Load factories from path. + * + * @param string $path + * @return $this + */ + public function load($path) + { + $factory = $this; + + if (is_dir($path)) { + foreach (Finder::create()->files()->name('*.php')->in($path) as $file) { + require $file->getRealPath(); + } + } + + return $factory; + } + + /** + * 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..6a1aab0613 --- /dev/null +++ b/Eloquent/FactoryBuilder.php @@ -0,0 +1,276 @@ +name = $name; + $this->class = $class; + $this->faker = $faker; + $this->states = $states; + $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; + } + + /** + * Set the states to be applied to the model. + * + * @param array|dynamic $states + * @return $this + */ + public function states($states) + { + $this->activeStates = is_array($states) ? $states : func_get_args(); + + return $this; + } + + /** + * Create a model and persist it in the database if requested. + * + * @param array $attributes + * @return \Closure + */ + public function lazy(array $attributes = []) + { + return function () use ($attributes) { + return $this->create($attributes); + }; + } + + /** + * 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 ($results instanceof Model) { + $this->store(collect([$results])); + } else { + $this->store($results); + } + + return $results; + } + + /** + * Set the connection name on the results and store them. + * + * @param \Illuminate\Support\Collection $results + * @return void + */ + protected function store($results) + { + $results->each(function ($model) { + $model->setConnection($model->query()->getConnection()->getName()); + + $model->save(); + }); + } + + /** + * Create a collection of models. + * + * @param array $attributes + * @return mixed + */ + public function make(array $attributes = []) + { + if ($this->amount === null) { + return $this->makeInstance($attributes); + } + + if ($this->amount < 1) { + return (new $this->class)->newCollection(); + } + + return (new $this->class)->newCollection(array_map(function () use ($attributes) { + return $this->makeInstance($attributes); + }, range(1, $this->amount))); + } + + /** + * Create an array of raw attribute arrays. + * + * @param array $attributes + * @return mixed + */ + public function raw(array $attributes = []) + { + if ($this->amount === null) { + return $this->getRawAttributes($attributes); + } + + if ($this->amount < 1) { + return []; + } + + return array_map(function () use ($attributes) { + return $this->getRawAttributes($attributes); + }, range(1, $this->amount)); + } + + /** + * Get a raw attributes array for the model. + * + * @param array $attributes + * @return mixed + */ + protected function getRawAttributes(array $attributes = []) + { + $definition = call_user_func( + $this->definitions[$this->class][$this->name], + $this->faker, $attributes + ); + + return $this->expandAttributes( + array_merge($this->applyStates($definition, $attributes), $attributes) + ); + } + + /** + * 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}]."); + } + + return new $this->class( + $this->getRawAttributes($attributes) + ); + }); + } + + /** + * Apply the active states to the model definition array. + * + * @param array $definition + * @param array $attributes + * @return array + */ + protected function applyStates(array $definition, array $attributes = []) + { + foreach ($this->activeStates as $state) { + if (! isset($this->states[$this->class][$state])) { + throw new InvalidArgumentException("Unable to locate [{$state}] state for [{$this->class}]."); + } + + $definition = array_merge($definition, call_user_func( + $this->states[$this->class][$state], + $this->faker, $attributes + )); + } + + return $definition; + } + + /** + * Expand all attributes to their underlying values. + * + * @param array $attributes + * @return array + */ + protected function expandAttributes(array $attributes) + { + foreach ($attributes as &$attribute) { + if ($attribute instanceof Closure) { + $attribute = $attribute($attributes); + } elseif ($attribute instanceof static) { + $attribute = $attribute->create()->getKey(); + } elseif ($attribute instanceof Model) { + $attribute = $attribute->getKey(); + } + } + + 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 index f62abd4695..5878b0f7b7 100644 --- a/Eloquent/JsonEncodingException.php +++ b/Eloquent/JsonEncodingException.php @@ -18,26 +18,12 @@ public static function forModel($model, $message) return new static('Error encoding model ['.get_class($model).'] with ID ['.$model->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 + * @param string $message * @return static */ public static function forAttribute($model, $key, $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; + Concerns\GuardsAttributes; /** * 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,7 +47,7 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt protected $primaryKey = 'id'; /** - * The "type" of the primary key ID. + * The "type" of the auto-incrementing ID. * * @var string */ @@ -96,20 +67,6 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt */ protected $with = []; - /** - * The relationship counts that should be eager loaded on every query. - * - * @var array - */ - protected $withCount = []; - - /** - * Indicates whether lazy loading will be prevented on this model. - * - * @var bool - */ - public $preventsLazyLoading = false; - /** * The number of models to return for pagination. * @@ -125,19 +82,12 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt public $exists = false; /** - * Indicates if the model was inserted during the object's lifecycle. + * Indicates if the model was inserted during the current request lifecycle. * * @var bool */ public $wasRecentlyCreated = false; - /** - * Indicates that the object's string representation should be escaped when __toString is invoked. - * - * @var bool - */ - protected $escapeWhenCastingToString = false; - /** * The connection resolver instance. * @@ -148,17 +98,10 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt /** * The event dispatcher instance. * - * @var \Illuminate\Contracts\Events\Dispatcher|null + * @var \Illuminate\Contracts\Events\Dispatcher */ protected static $dispatcher; - /** - * The models that are currently being booted. - * - * @var array - */ - protected static $booting = []; - /** * The array of booted models. * @@ -166,20 +109,6 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt */ protected static $booted = []; - /** - * The callbacks that should be executed after the model has booted. - * - * @var array - */ - protected static $bootedCallbacks = []; - - /** - * The array of trait initializers that will be called on each new instance. - * - * @var array - */ - protected static $traitInitializers = []; - /** * The array of global scopes on the model. * @@ -187,136 +116,32 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt */ protected static $globalScopes = []; - /** - * The list of models classes that should not be affected with touch. - * - * @var array - */ - protected static $ignoreOnTouch = []; - - /** - * Indicates whether lazy loading should be restricted on all models. - * - * @var bool - */ - protected static $modelsShouldPreventLazyLoading = false; - - /** - * Indicates whether relations should be automatically loaded on all models when they are accessed. - * - * @var bool - */ - protected static $modelsShouldAutomaticallyEagerLoadRelationships = false; - - /** - * The callback that is responsible for handling lazy loading violations. - * - * @var (callable(self, string))|null - */ - protected static $lazyLoadingViolationCallback; - - /** - * Indicates if an exception should be thrown instead of silently discarding non-fillable attributes. - * - * @var bool - */ - protected static $modelsShouldPreventSilentlyDiscardingAttributes = false; - - /** - * The callback that is responsible for handling discarded attribute violations. - * - * @var (callable(self, array))|null - */ - protected static $discardedAttributeViolationCallback; - - /** - * Indicates if an exception should be thrown when trying to access a missing attribute on a retrieved model. - * - * @var bool - */ - protected static $modelsShouldPreventAccessingMissingAttributes = false; - - /** - * The callback that is responsible for handling missing attribute violations. - * - * @var (callable(self, string))|null - */ - protected static $missingAttributeViolationCallback; - - /** - * Indicates if broadcasting is currently enabled. - * - * @var bool - */ - protected static $isBroadcasting = true; - - /** - * The Eloquent query builder class to use for the model. - * - * @var class-string<\Illuminate\Database\Eloquent\Builder<*>> - */ - protected static string $builder = Builder::class; - - /** - * The Eloquent collection class to use for the model. - * - * @var class-string<\Illuminate\Database\Eloquent\Collection<*, *>> - */ - protected static string $collectionClass = Collection::class; - - /** - * Cache of soft deletable models. - * - * @var array, bool> - */ - protected static array $isSoftDeletable; - - /** - * Cache of prunable models. - * - * @var array, bool> - */ - protected static array $isPrunable; - - /** - * Cache of mass prunable models. - * - * @var array, bool> - */ - protected static array $isMassPrunable; - - /** - * Cache of resolved class attributes. - * - * @var array, array> - */ - protected static array $classAttributes = []; - /** * 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 +149,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,92 +182,11 @@ 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. - * - * @return void - */ - protected function initializeTraits() - { - foreach (static::$traitInitializers[static::class] as $method) { - $this->{$method}(); - } - } - - /** - * Initialize the model attributes from class attributes. - * - * @return void - */ - public function initializeModelAttributes() - { - $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 ($this->keyType === 'int' && $table && $table->keyType !== null) { - $this->keyType = $table->keyType; - } - - if ($this->incrementing === true && $table && $table->incrementing !== null) { - $this->incrementing = $table->incrementing; - } - } - - /** - * Perform any actions required after the model boots. - * - * @return void - */ - protected static function booted() - { - // - } - - /** - * Register a closure to be executed after the model has booted. - * - * @param \Closure $callback - * @return void - */ - protected static function whenBooted(Closure $callback) - { - static::$bootedCallbacks[static::class] ??= []; - - static::$bootedCallbacks[static::class][] = $callback; } /** @@ -481,183 +197,14 @@ protected static function whenBooted(Closure $callback) public static function clearBootedModels() { static::$booted = []; - static::$bootedCallbacks = []; - static::$classAttributes = []; - static::$globalScopes = []; - } - - /** - * Disables relationship model touching for the current class during given callback scope. - * - * @param callable $callback - * @return void - */ - public static function withoutTouching(callable $callback) - { - static::withoutTouchingOn([static::class], $callback); - } - - /** - * Disables relationship model touching for the given model classes during given callback scope. - * - * @param array $models - * @param callable $callback - * @return void - */ - public static function withoutTouchingOn(array $models, callable $callback) - { - static::$ignoreOnTouch = array_values(array_merge(static::$ignoreOnTouch, $models)); - - try { - $callback(); - } finally { - static::$ignoreOnTouch = array_values(array_diff(static::$ignoreOnTouch, $models)); - } - } - - /** - * Determine if the given model is ignoring touches. - * - * @param string|null $class - * @return bool - */ - public static function isIgnoringTouch($class = null) - { - $class = $class ?: static::class; - - if (! $class::UPDATED_AT) { - return true; - } - - $timestamps = static::resolveClassAttribute(Table::class, 'timestamps', $class) - ?? get_class_vars($class)['timestamps']; - - if (! $timestamps) { - return true; - } - - foreach (static::$ignoreOnTouch as $ignoredClass) { - if ($class === $ignoredClass || is_subclass_of($class, $ignoredClass)) { - return true; - } - } - - return false; - } - - /** - * Indicate that models should prevent lazy loading, silently discarding attributes, and accessing missing attributes. - * - * @param bool $shouldBeStrict - * @return void - */ - public static function shouldBeStrict(bool $shouldBeStrict = true) - { - static::preventLazyLoading($shouldBeStrict); - static::preventSilentlyDiscardingAttributes($shouldBeStrict); - static::preventAccessingMissingAttributes($shouldBeStrict); - } - - /** - * Prevent model relationships from being lazy loaded. - * - * @param bool $value - * @return void - */ - public static function preventLazyLoading($value = true) - { - static::$modelsShouldPreventLazyLoading = $value; - } - - /** - * Determine if model relationships should be automatically eager loaded when accessed. - * - * @param bool $value - * @return void - */ - public static function automaticallyEagerLoadRelationships($value = true) - { - static::$modelsShouldAutomaticallyEagerLoadRelationships = $value; - } - - /** - * Register a callback that is responsible for handling lazy loading violations. - * - * @param (callable(self, string))|null $callback - * @return void - */ - public static function handleLazyLoadingViolationUsing(?callable $callback) - { - static::$lazyLoadingViolationCallback = $callback; - } - - /** - * Prevent non-fillable attributes from being silently discarded. - * - * @param bool $value - * @return void - */ - public static function preventSilentlyDiscardingAttributes($value = true) - { - static::$modelsShouldPreventSilentlyDiscardingAttributes = $value; - } - - /** - * Register a callback that is responsible for handling discarded attribute violations. - * - * @param (callable(self, array))|null $callback - * @return void - */ - public static function handleDiscardedAttributeViolationUsing(?callable $callback) - { - static::$discardedAttributeViolationCallback = $callback; - } - /** - * Prevent accessing missing attributes on retrieved models. - * - * @param bool $value - * @return void - */ - public static function preventAccessingMissingAttributes($value = true) - { - static::$modelsShouldPreventAccessingMissingAttributes = $value; - } - - /** - * Register a callback that is responsible for handling missing attribute violations. - * - * @param (callable(self, string))|null $callback - * @return void - */ - public static function handleMissingAttributeViolationUsing(?callable $callback) - { - static::$missingAttributeViolationCallback = $callback; - } - - /** - * Execute a callback without broadcasting any model events for all model types. - * - * @param callable $callback - * @return mixed - */ - public static function withoutBroadcasting(callable $callback) - { - $isBroadcasting = static::$isBroadcasting; - - static::$isBroadcasting = false; - - try { - return $callback(); - } finally { - static::$isBroadcasting = $isBroadcasting; - } + static::$globalScopes = []; } /** * Fill the model with an array of attributes. * - * @param array $attributes + * @param array $attributes * @return $this * * @throws \Illuminate\Database\Eloquent\MassAssignmentException @@ -666,38 +213,16 @@ public function fill(array $attributes) { $totallyGuarded = $this->totallyGuarded(); - $fillable = $this->fillableFromArray($attributes); + foreach ($this->fillableFromArray($attributes) as $key => $value) { + $key = $this->removeTableFromKey($key); - 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) - )); - } - } - } - - if (count($attributes) !== count($fillable) && - static::preventsSilentlyDiscardingAttributes()) { - $keys = array_diff(array_keys($attributes), array_keys($fillable)); - - 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) - )); + } elseif ($totallyGuarded) { + throw new MassAssignmentException($key); } } @@ -707,46 +232,31 @@ public function fill(array $attributes) /** * Fill the model with an array of attributes. Force mass assignment. * - * @param array $attributes + * @param array $attributes * @return $this */ public function forceFill(array $attributes) { - return static::unguarded(fn () => $this->fill($attributes)); + return static::unguarded(function () use ($attributes) { + return $this->fill($attributes); + }); } /** - * Qualify the given column name by the model's table. + * Remove the table name from a given key. * - * @param string $column + * @param string $key * @return string */ - public function qualifyColumn($column) - { - if (str_contains($column, '.')) { - return $column; - } - - return $this->getTable().'.'.$column; - } - - /** - * Qualify the given columns with the model's table. - * - * @param array $columns - * @return array - */ - public function qualifyColumns($columns) + protected function removeTableFromKey($key) { - return (new BaseCollection($columns)) - ->map(fn ($column) => $this->qualifyColumn($column)) - ->all(); + return Str::contains($key, '.') ? last(explode('.', $key)) : $key; } /** * Create a new instance of the given model. * - * @param array $attributes + * @param array $attributes * @param bool $exists * @return static */ @@ -755,323 +265,113 @@ 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; + $model = new static((array) $attributes); $model->exists = $exists; - $model->setConnection( - $this->getConnectionName() - ); - - $model->setTable($this->getTable()); - - $model->mergeCasts($this->casts); - - $model->fill((array) $attributes); - - return $model; - } - - /** - * Create a new model instance that is existing. - * - * @param array $attributes - * @param \UnitEnum|string|null $connection - * @return static - */ - public function newFromBuilder($attributes = [], $connection = null) - { - $model = $this->newInstance([], true); - - $model->setRawAttributes((array) $attributes, true); - - $model->setConnection($connection ?? $this->getConnectionName()); - - $model->fireModelEvent('retrieved', false); - - return $model; - } - - /** - * Begin querying the model on a given connection. - * - * @param \UnitEnum|string|null $connection - * @return \Illuminate\Database\Eloquent\Builder - */ - public static function on($connection = 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(); - } - - /** - * Begin querying the model on the write connection. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public static function onWriteConnection() - { - return static::query()->useWritePdo(); - } - - /** - * Get all of the models from the database. - * - * @param array|string $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public static function all($columns = ['*']) - { - return static::query()->get( - is_array($columns) ? $columns : func_get_args() - ); - } - - /** - * Begin querying a model with eager loading. - * - * @param array|string $relations - * @return \Illuminate\Database\Eloquent\Builder - */ - public static function with($relations) - { - return static::query()->with( - is_string($relations) ? func_get_args() : $relations - ); - } - - /** - * Eager load relations on the model. - * - * @param array|string $relations - * @return $this - */ - public function load($relations) - { - $query = $this->newQueryWithoutRelationships()->with( - is_string($relations) ? func_get_args() : $relations - ); - - $query->eagerLoadRelations([$this]); - - return $this; - } - - /** - * Eager load relationships on the polymorphic relation of a model. - * - * @param string $relation - * @param array $relations - * @return $this - */ - public function loadMorph($relation, $relations) - { - if (! $this->{$relation}) { - return $this; - } - - $className = get_class($this->{$relation}); - - $this->{$relation}->load($relations[$className] ?? []); - - return $this; - } - - /** - * Eager load relations on the model if they are not already eager loaded. - * - * @param array|string $relations - * @return $this - */ - public function loadMissing($relations) - { - $relations = is_string($relations) ? func_get_args() : $relations; - - $this->newCollection([$this])->loadMissing($relations); - - return $this; - } - - /** - * Eager load relation's column aggregations on the model. - * - * @param array|string $relations - * @param string $column - * @param string|null $function - * @return $this - */ - public function loadAggregate($relations, $column, $function = null) - { - $this->newCollection([$this])->loadAggregate($relations, $column, $function); - - return $this; - } - - /** - * Eager load relation counts on the model. - * - * @param array|string $relations - * @return $this - */ - public function loadCount($relations) - { - $relations = is_string($relations) ? func_get_args() : $relations; - - return $this->loadAggregate($relations, '*', 'count'); - } - - /** - * Eager load relation max column values on the model. - * - * @param array|string $relations - * @param string $column - * @return $this - */ - public function loadMax($relations, $column) - { - return $this->loadAggregate($relations, $column, 'max'); - } - - /** - * Eager load relation min column values on the model. - * - * @param array|string $relations - * @param string $column - * @return $this - */ - public function loadMin($relations, $column) - { - return $this->loadAggregate($relations, $column, 'min'); - } - - /** - * Eager load relation's column summations on the model. - * - * @param array|string $relations - * @param string $column - * @return $this - */ - public function loadSum($relations, $column) - { - return $this->loadAggregate($relations, $column, 'sum'); - } - - /** - * Eager load relation average column values on the model. - * - * @param array|string $relations - * @param string $column - * @return $this - */ - public function loadAvg($relations, $column) - { - return $this->loadAggregate($relations, $column, 'avg'); - } + $model->setConnection( + $this->getConnectionName() + ); - /** - * Eager load related model existence values on the model. - * - * @param array|string $relations - * @return $this - */ - public function loadExists($relations) - { - return $this->loadAggregate($relations, '*', 'exists'); + return $model; } /** - * Eager load relationship column aggregation on the polymorphic relation of a model. + * Create a new model instance that is existing. * - * @param string $relation - * @param array $relations - * @param string $column - * @param string|null $function - * @return $this + * @param array $attributes + * @param string|null $connection + * @return static */ - public function loadMorphAggregate($relation, $relations, $column, $function = null) + public function newFromBuilder($attributes = [], $connection = null) { - if (! $this->{$relation}) { - return $this; - } + $model = $this->newInstance([], true); - $className = get_class($this->{$relation}); + $model->setRawAttributes((array) $attributes, true); - $this->{$relation}->loadAggregate($relations[$className] ?? [], $column, $function); + $model->setConnection($connection ?: $this->getConnectionName()); - return $this; + return $model; } /** - * Eager load relationship counts on the polymorphic relation of a model. + * Begin querying the model on a given connection. * - * @param string $relation - * @param array $relations - * @return $this + * @param string|null $connection + * @return \Illuminate\Database\Eloquent\Builder */ - public function loadMorphCount($relation, $relations) + public static function on($connection = null) { - return $this->loadMorphAggregate($relation, $relations, '*', 'count'); + // 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(); } /** - * Eager load relationship max column values on the polymorphic relation of a model. + * Begin querying the model on the write connection. * - * @param string $relation - * @param array $relations - * @param string $column - * @return $this + * @return \Illuminate\Database\Query\Builder */ - public function loadMorphMax($relation, $relations, $column) + public static function onWriteConnection() { - return $this->loadMorphAggregate($relation, $relations, $column, 'max'); + $instance = new static; + + return $instance->newQuery()->useWritePdo(); } /** - * Eager load relationship min column values on the polymorphic relation of a model. + * Get all of the models from the database. * - * @param string $relation - * @param array $relations - * @param string $column - * @return $this + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Collection|static[] */ - public function loadMorphMin($relation, $relations, $column) + public static function all($columns = ['*']) { - return $this->loadMorphAggregate($relation, $relations, $column, 'min'); + return (new static)->newQuery()->get( + is_array($columns) ? $columns : func_get_args() + ); } /** - * Eager load relationship column summations on the polymorphic relation of a model. + * Begin querying a model with eager loading. * - * @param string $relation - * @param array $relations - * @param string $column - * @return $this + * @param array|string $relations + * @return \Illuminate\Database\Eloquent\Builder|static */ - public function loadMorphSum($relation, $relations, $column) + public static function with($relations) { - return $this->loadMorphAggregate($relation, $relations, $column, 'sum'); + return (new static)->newQuery()->with( + is_string($relations) ? func_get_args() : $relations + ); } /** - * Eager load relationship average column values on the polymorphic relation of a model. + * Eager load relations on the model. * - * @param string $relation - * @param array $relations - * @param string $column + * @param array|string $relations * @return $this */ - public function loadMorphAvg($relation, $relations, $column) + public function load($relations) { - return $this->loadMorphAggregate($relation, $relations, $column, 'avg'); + $query = $this->newQuery()->with( + is_string($relations) ? func_get_args() : $relations + ); + + $query->eagerLoadRelations([$this]); + + return $this; } /** * Increment a column's value by a given amount. * * @param string $column - * @param float|int $amount + * @param int $amount * @param array $extra * @return int */ @@ -1084,7 +384,7 @@ protected function increment($column, $amount = 1, array $extra = []) * Decrement a column's value by a given amount. * * @param string $column - * @param float|int $amount + * @param int $amount * @param array $extra * @return int */ @@ -1097,118 +397,58 @@ protected function decrement($column, $amount = 1, array $extra = []) * Run the increment or decrement method on the model. * * @param string $column - * @param float|int $amount + * @param int $amount * @param array $extra * @param string $method * @return int */ protected function incrementOrDecrement($column, $amount, $extra, $method) { - if (! $this->exists) { - return $this->newQueryWithoutRelationships()->{$method}($column, $amount, $extra); - } + $query = $this->newQuery(); - $this->{$column} = $this->isClassDeviable($column) - ? $this->deviateClassCastableAttribute($method, $column, $amount) - : $this->{$column} + ($method === 'increment' ? $amount : $amount * -1); - - $this->forceFill($extra); - - if ($this->fireModelEvent('updating') === false) { - return false; - } - - if ($this->isClassDeviable($column)) { - $amount = (clone $this)->setAttribute($column, $amount)->getAttributeFromArray($column); + if (! $this->exists) { + return $query->{$method}($column, $amount, $extra); } - return tap($this->setKeysForSaveQuery($this->newQueryWithoutScopes())->{$method}($column, $amount, $extra), function () use ($column) { - $this->syncChanges(); + $this->incrementOrDecrementAttributeValue($column, $amount, $extra, $method); - $this->fireModelEvent('updated', false); - - $this->syncOriginalAttribute($column); - }); + return $query->where( + $this->getKeyName(), $this->getKey() + )->{$method}($column, $amount, $extra); } /** - * Update the model in the database. + * Increment the underlying attribute value and sync with original. * - * @param array $attributes - * @param array $options - * @return bool + * @param string $column + * @param int $amount + * @param array $extra + * @param string $method + * @return void */ - public function update(array $attributes = [], array $options = []) + protected function incrementOrDecrementAttributeValue($column, $amount, $extra, $method) { - if (! $this->exists) { - return false; - } - - return $this->fill($attributes)->save($options); - } + $this->{$column} = $this->{$column} + ($method == 'increment' ? $amount : $amount * -1); - /** - * Update the model in the database within a transaction. - * - * @param array $attributes - * @param array $options - * @return bool - * - * @throws \Throwable - */ - public function updateOrFail(array $attributes = [], array $options = []) - { - if (! $this->exists) { - return false; - } + $this->forceFill($extra); - return $this->fill($attributes)->saveOrFail($options); + $this->syncOriginalAttribute($column); } /** - * Update the model in the database without raising any events. + * Update the model in the database. * - * @param array $attributes - * @param array $options + * @param array $attributes + * @param array $options * @return bool */ - public function updateQuietly(array $attributes = [], array $options = []) + public function update(array $attributes = [], array $options = []) { if (! $this->exists) { return false; } - return $this->fill($attributes)->saveQuietly($options); - } - - /** - * Increment a column's value by a given amount without raising any events. - * - * @param string $column - * @param float|int $amount - * @param array $extra - * @return int - */ - protected function incrementQuietly($column, $amount = 1, array $extra = []) - { - return static::withoutEvents( - fn () => $this->incrementOrDecrement($column, $amount, $extra, 'increment') - ); - } - - /** - * Decrement a column's value by a given amount without raising any events. - * - * @param string $column - * @param float|int $amount - * @param array $extra - * @return int - */ - protected function decrementQuietly($column, $amount = 1, array $extra = []) - { - return static::withoutEvents( - fn () => $this->incrementOrDecrement($column, $amount, $extra, 'decrement') - ); + return $this->fill($attributes)->save($options); } /** @@ -1218,49 +458,25 @@ protected function decrementQuietly($column, $amount = 1, array $extra = []) */ public function push() { - return $this->withoutRecursion(function () { - if (! $this->save()) { - return false; - } + 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; - } + // 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; - }, true); - } - - /** - * Save the model and all of its relationships without raising any events to the parent model. - * - * @return bool - */ - public function pushQuietly() - { - return static::withoutEvents(fn () => $this->push()); - } - - /** - * Save the model to the database without raising any events. - * - * @param array $options - * @return bool - */ - public function saveQuietly(array $options = []) - { - return static::withoutEvents(fn () => $this->save($options)); + return true; } /** @@ -1271,9 +487,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 @@ -1287,7 +501,7 @@ public function save(array $options = []) // clause to only update this model. Otherwise, we'll just insert them. if ($this->exists) { $saved = $this->isDirty() ? - $this->performUpdate($query) : true; + $this->performUpdate($query) : true; } // If the model is brand new, we'll insert it into our database and set the @@ -1295,11 +509,6 @@ 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 @@ -1313,7 +522,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,7 +531,9 @@ 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); + }); } /** @@ -1335,17 +546,17 @@ 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) @@ -1367,49 +578,24 @@ protected function performUpdate(Builder $query) // 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(); + $dirty = $this->getDirty(); if (count($dirty) > 0) { $this->setKeysForSaveQuery($query)->update($dirty); - $this->syncChanges(); - $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 + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder */ - protected function setKeysForSaveQuery($query) + protected function setKeysForSaveQuery(Builder $query) { $query->where($this->getKeyName(), '=', $this->getKeyForSaveQuery()); @@ -1423,21 +609,19 @@ protected function setKeysForSaveQuery($query) */ protected function getKeyForSaveQuery() { - return $this->original[$this->getKeyName()] ?? $this->getKey(); + return isset($this->original[$this->getKeyName()]) + ? $this->original[$this->getKeyName()] + : $this->getAttribute($this->getKeyName()); } /** * 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; } @@ -1452,7 +636,7 @@ protected function performInsert(Builder $query) // 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); @@ -1484,8 +668,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) @@ -1498,32 +682,23 @@ protected function insertAndSetId(Builder $query, $attributes) /** * Destroy the models for the given IDs. * - * @param \Illuminate\Support\Collection|array|int|string $ids + * @param array|int $ids * @return int */ public static function destroy($ids) { - if ($ids instanceof EloquentCollection) { - $ids = $ids->modelKeys(); - } - - if ($ids instanceof BaseCollection) { - $ids = $ids->all(); - } + // 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(); - 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->whereIn($key, $ids)->get() as $model) { if ($model->delete()) { $count++; @@ -1538,14 +713,12 @@ public static function destroy($ids) * * @return bool|null * - * @throws \LogicException + * @throws \Exception */ public function delete() { - $this->mergeAttributesFromCachedCasts(); - if (is_null($this->getKeyName())) { - throw new LogicException('No primary key defined on model.'); + throw new Exception('No primary key defined on model.'); } // If the model doesn't exist, there is nothing to delete so we'll just return @@ -1574,36 +747,10 @@ public function delete() return true; } - /** - * Delete the model from the database without raising any events. - * - * @return bool - */ - public function deleteQuietly() - { - return static::withoutEvents(fn () => $this->delete()); - } - - /** - * Delete the model from the database within a transaction. - * - * @return bool|null - * - * @throws \Throwable - */ - public function deleteOrFail() - { - if (! $this->exists) { - return false; - } - - return $this->getConnection()->transaction(fn () => $this->delete()); - } - /** * Force a hard delete on a soft deleted model. * - * This method protects developers from running forceDelete when the trait is missing. + * This method protects developers from running forceDelete when trait is missing. * * @return bool|null */ @@ -1612,19 +759,6 @@ public function forceDelete() return $this->delete(); } - /** - * Force a hard destroy on a soft deleted model. - * - * This method protects developers from running forceDestroy when the trait is missing. - * - * @param \Illuminate\Support\Collection|array|int|string $ids - * @return bool|null - */ - public static function forceDestroy($ids) - { - return static::destroy($ids); - } - /** * Perform the actual delete query on this model instance. * @@ -1632,7 +766,7 @@ public static function forceDestroy($ids) */ protected function performDeleteOnModel() { - $this->setKeysForSaveQuery($this->newModelQuery())->delete(); + $this->setKeysForSaveQuery($this->newQueryWithoutScopes())->delete(); $this->exists = false; } @@ -1640,7 +774,7 @@ protected function performDeleteOnModel() /** * Begin querying the model. * - * @return \Illuminate\Database\Eloquent\Builder + * @return \Illuminate\Database\Eloquent\Builder */ public static function query() { @@ -1650,43 +784,12 @@ public static function query() /** * Get a new query builder for the model's table. * - * @return \Illuminate\Database\Eloquent\Builder + * @return \Illuminate\Database\Eloquent\Builder */ public function newQuery() { - return $this->registerGlobalScopes($this->newQueryWithoutScopes()); - } - - /** - * Get a new query builder that doesn't have any global scopes or eager loading. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function newModelQuery() - { - return $this->newEloquentBuilder( - $this->newBaseQueryBuilder() - )->setModel($this); - } - - /** - * Get a new query builder with no relationships loaded. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function newQueryWithoutRelationships() - { - return $this->registerGlobalScopes($this->newModelQuery()); - } + $builder = $this->newQueryWithoutScopes(); - /** - * Register the global scopes for this builder instance. - * - * @param \Illuminate\Database\Eloquent\Builder $builder - * @return \Illuminate\Database\Eloquent\Builder - */ - public function registerGlobalScopes($builder) - { foreach ($this->getGlobalScopes() as $identifier => $scope) { $builder->withGlobalScope($identifier, $scope); } @@ -1697,134 +800,81 @@ public function registerGlobalScopes($builder) /** * Get a new query builder that doesn't have any global scopes. * - * @return \Illuminate\Database\Eloquent\Builder + * @return \Illuminate\Database\Eloquent\Builder|static */ public function newQueryWithoutScopes() { - return $this->newModelQuery() - ->with($this->with) - ->withCount($this->withCount); + $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); } /** * Get a new query instance without a given scope. * * @param \Illuminate\Database\Eloquent\Scope|string $scope - * @return \Illuminate\Database\Eloquent\Builder + * @return \Illuminate\Database\Eloquent\Builder */ public function newQueryWithoutScope($scope) { - return $this->newQuery()->withoutGlobalScope($scope); - } + $builder = $this->newQuery(); - /** - * 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) - { - return $this->newQueryWithoutScopes()->whereKey($ids); + return $builder->withoutGlobalScope($scope); } /** * Create a new Eloquent query builder for the model. * * @param \Illuminate\Database\Query\Builder $query - * @return \Illuminate\Database\Eloquent\Builder<*> + * @return \Illuminate\Database\Eloquent\Builder|static */ public function newEloquentBuilder($query) { - $builderClass = $this->resolveCustomBuilderClass(); - - if ($builderClass && is_subclass_of($builderClass, Builder::class)) { - return new $builderClass($query); - } - - return new static::$builder($query); - } - - /** - * Resolve the custom Eloquent builder class from the model attributes. - * - * @return class-string<\Illuminate\Database\Eloquent\Builder>|false - */ - protected function resolveCustomBuilderClass() - { - $attributes = (new ReflectionClass($this)) - ->getAttributes(UseEloquentBuilder::class); - - return ! empty($attributes) - ? $attributes[0]->newInstance()->builderClass - : false; - } - - /** - * Get a new query builder instance for the connection. - * - * @return \Illuminate\Database\Query\Builder - */ - protected function newBaseQueryBuilder() - { - return $this->getConnection()->query(); - } - - /** - * Create a new pivot model instance. - * - * @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 - */ - public function newPivot(self $parent, array $attributes, $table, $exists, $using = null) - { - return $using ? $using::fromRawAttributes($parent, $attributes, $table, $exists) - : Pivot::fromAttributes($parent, $attributes, $table, $exists); + return new Builder($query); } /** - * Determine if the model has a given scope. + * Get a new query builder instance for the connection. * - * @param string $scope - * @return bool + * @return \Illuminate\Database\Query\Builder */ - public function hasNamedScope($scope) + protected function newBaseQueryBuilder() { - return method_exists($this, 'scope'.ucfirst($scope)) || - static::isScopeMethodWithAttribute($scope); + $connection = $this->getConnection(); + + return new QueryBuilder( + $connection, $connection->getQueryGrammar(), $connection->getPostProcessor() + ); } /** - * Apply the given named scope if possible. + * Create a new Eloquent Collection instance. * - * @param string $scope - * @param array $parameters - * @return mixed + * @param array $models + * @return \Illuminate\Database\Eloquent\Collection */ - public function callNamedScope($scope, array $parameters = []) + public function newCollection(array $models = []) { - if ($this->isScopeMethodWithAttribute($scope)) { - return $this->{$scope}(...$parameters); - } - - return $this->{'scope'.ucfirst($scope)}(...$parameters); + return new Collection($models); } /** - * Determine if the given method has a scope attribute. + * Create a new pivot model instance. * - * @param string $method - * @return bool + * @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 */ - protected static function isScopeMethodWithAttribute(string $method) + public function newPivot(Model $parent, array $attributes, $table, $exists, $using = null) { - return method_exists(static::class, $method) && - (new ReflectionMethod(static::class, $method)) - ->getAttributes(LocalScope::class) !== []; + return $using ? $using::fromRawAttributes($parent, $attributes, $table, $exists) + : new Pivot($parent, $attributes, $table, $exists); } /** @@ -1834,10 +884,7 @@ protected static function isScopeMethodWithAttribute(string $method) */ public function toArray() { - return $this->withoutRecursion( - fn () => array_merge($this->attributesToArray(), $this->relationsToArray()), - fn () => $this->attributesToArray(), - ); + return array_merge($this->attributesToArray(), $this->relationsToArray()); } /** @@ -1850,34 +897,21 @@ public function toArray() */ public function toJson($options = 0) { - try { - $json = json_encode($this->jsonSerialize(), $options | JSON_THROW_ON_ERROR); - } catch (JsonException $e) { - throw JsonEncodingException::forModel($this, $e->getMessage()); + $json = json_encode($this->jsonSerialize(), $options); + + if (JSON_ERROR_NONE !== json_last_error()) { + throw JsonEncodingException::forModel($this, json_last_error_msg()); } return $json; } - /** - * Convert the model instance to pretty print formatted JSON. - * - * @param int $options - * @return string - * - * @throws \Illuminate\Database\Eloquent\JsonEncodingException - */ - public function toPrettyJson(int $options = 0) - { - return $this->toJson(JSON_PRETTY_PRINT | $options); - } - /** * Convert the object into something JSON serializable. * - * @return mixed + * @return array */ - public function jsonSerialize(): mixed + public function jsonSerialize() { return $this->toArray(); } @@ -1894,80 +928,37 @@ public function fresh($with = []) return; } - return $this->setKeysForSelectQuery($this->newQueryWithoutScopes()) - ->useWritePdo() - ->with(is_string($with) ? func_get_args() : $with) - ->first(); - } - - /** - * Reload the current model instance with fresh attributes from the database. - * - * @return $this - */ - public function refresh() - { - if (! $this->exists) { - return $this; - } - - $this->setRawAttributes( - $this->setKeysForSelectQuery($this->newQueryWithoutScopes()) - ->useWritePdo() - ->firstOrFail() - ->attributes - ); - - $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()); - - $this->syncOriginal(); - - return $this; + return static::newQueryWithoutScopes() + ->with(is_string($with) ? func_get_args() : $with) + ->where($this->getKeyName(), $this->getKey()) + ->first(); } /** * Clone the model into a new, non-existing instance. * * @param array|null $except - * @return static + * @return \Illuminate\Database\Eloquent\Model */ - public function replicate(?array $except = null) + public function replicate(array $except = null) { - $defaults = array_values(array_filter([ + $defaults = [ $this->getKeyName(), $this->getCreatedAtColumn(), $this->getUpdatedAtColumn(), - ...$this->uniqueIds(), - 'laravel_through_key', - ])); + ]; $attributes = Arr::except( - $this->getAttributes(), $except ? array_unique(array_merge($except, $defaults)) : $defaults + $this->attributes, $except ? array_unique(array_merge($except, $defaults)) : $defaults ); return tap(new static, function ($instance) use ($attributes) { $instance->setRawAttributes($attributes); $instance->setRelations($this->relations); - - $instance->fireModelEvent('replicating', false); }); } - /** - * Clone the model into a new, non-existing instance without raising any events. - * - * @param array|null $except - * @return static - */ - public function replicateQuietly(?array $except = null) - { - return static::withoutEvents(fn () => $this->replicate($except)); - } - /** * Determine if two models have the same ID and belong to the same table. * @@ -1977,20 +968,9 @@ public function replicateQuietly(?array $except = null) public function is($model) { return ! is_null($model) && - $this->getKey() === $model->getKey() && - $this->getTable() === $model->getTable() && - $this->getConnectionName() === $model->getConnectionName(); - } - - /** - * Determine if two models are not the same. - * - * @param \Illuminate\Database\Eloquent\Model|null $model - * @return bool - */ - public function isNot($model) - { - return ! $this->is($model); + $this->getKey() === $model->getKey() && + $this->getTable() === $model->getTable() && + $this->getConnectionName() === $model->getConnectionName(); } /** @@ -2006,17 +986,17 @@ public function getConnection() /** * Get the current connection name for the model. * - * @return string|null + * @return string */ public function getConnectionName() { - return enum_value($this->connection); + return $this->connection; } /** * Set the connection associated with the model. * - * @param \UnitEnum|string|null $name + * @param string $name * @return $this */ public function setConnection($name) @@ -2029,7 +1009,7 @@ public function setConnection($name) /** * Resolve a connection instance. * - * @param \UnitEnum|string|null $connection + * @param string|null $connection * @return \Illuminate\Database\Connection */ public static function resolveConnection($connection = null) @@ -2040,7 +1020,7 @@ public static function resolveConnection($connection = null) /** * Get the connection resolver instance. * - * @return \Illuminate\Database\ConnectionResolverInterface|null + * @return \Illuminate\Database\ConnectionResolverInterface */ public static function getConnectionResolver() { @@ -2075,7 +1055,11 @@ public static function unsetConnectionResolver() */ public function getTable() { - return $this->table ?? Str::snake(Str::pluralStudly(class_basename($this))); + if (! isset($this->table)) { + return str_replace('\\', '', Str::snake(Str::plural(class_basename($this)))); + } + + return $this->table; } /** @@ -2121,7 +1105,7 @@ public function setKeyName($key) */ public function getQualifiedKeyName() { - return $this->qualifyColumn($this->getKeyName()); + return $this->getTable().'.'.$this->getKeyName(); } /** @@ -2190,50 +1174,6 @@ public function getQueueableId() return $this->getKey(); } - /** - * Get the queueable relationships for the entity. - * - * @return array - */ - public function getQueueableRelations() - { - return $this->withoutRecursion(function () { - $relations = []; - - foreach ($this->getRelations() as $key => $relation) { - if (! method_exists($this, $key)) { - continue; - } - - $relations[] = $key; - - if ($relation instanceof QueueableCollection) { - foreach ($relation->getQueueableRelations() as $collectionValue) { - $relations[] = $key.'.'.$collectionValue; - } - } - - if ($relation instanceof QueueableEntity) { - foreach ($relation->getQueueableRelations() as $entityValue) { - $relations[] = $key.'.'.$entityValue; - } - } - } - - return array_unique($relations); - }, []); - } - - /** - * Get the queueable connection for the entity. - * - * @return string|null - */ - public function getQueueableConnection() - { - return $this->getConnectionName(); - } - /** * Get the value of the model's route key. * @@ -2254,104 +1194,6 @@ public function getRouteKeyName() return $this->getKeyName(); } - /** - * Retrieve the model for a bound value. - * - * @param mixed $value - * @param string|null $field - * @return \Illuminate\Database\Eloquent\Model|null - */ - public function resolveRouteBinding($value, $field = null) - { - return $this->resolveRouteBindingQuery($this, $value, $field)->first(); - } - - /** - * Retrieve the model for a bound value. - * - * @param mixed $value - * @param string|null $field - * @return \Illuminate\Database\Eloquent\Model|null - */ - public function resolveSoftDeletableRouteBinding($value, $field = null) - { - return $this->resolveRouteBindingQuery($this, $value, $field)->withTrashed()->first(); - } - - /** - * Retrieve the child model for a bound value. - * - * @param string $childType - * @param mixed $value - * @param string|null $field - * @return \Illuminate\Database\Eloquent\Model|null - */ - public function resolveChildRouteBinding($childType, $value, $field) - { - return $this->resolveChildRouteBindingQuery($childType, $value, $field)->first(); - } - - /** - * Retrieve the child model for a bound value. - * - * @param string $childType - * @param mixed $value - * @param string|null $field - * @return \Illuminate\Database\Eloquent\Model|null - */ - public function resolveSoftDeletableChildRouteBinding($childType, $value, $field) - { - return $this->resolveChildRouteBindingQuery($childType, $value, $field)->withTrashed()->first(); - } - - /** - * Retrieve the child model query for a bound value. - * - * @param string $childType - * @param mixed $value - * @param string|null $field - * @return \Illuminate\Database\Eloquent\Relations\Relation<\Illuminate\Database\Eloquent\Model, $this, *> - */ - protected function resolveChildRouteBindingQuery($childType, $value, $field) - { - $relationship = $this->{$this->childRouteBindingRelationshipName($childType)}(); - - $field = $field ?: $relationship->getRelated()->getRouteKeyName(); - - 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); - } - - /** - * Retrieve the child route model binding relationship name for the given child type. - * - * @param string $childType - * @return string - */ - protected function childRouteBindingRelationshipName($childType) - { - return Str::plural(Str::camel($childType)); - } - - /** - * Retrieve the model for a bound value. - * - * @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 - */ - public function resolveRouteBindingQuery($query, $value, $field = null) - { - return $query->where($field ?? $this->getRouteKeyName(), $value); - } - /** * Get the default foreign key name for the model. * @@ -2359,7 +1201,7 @@ public function resolveRouteBindingQuery($query, $value, $field = null) */ public function getForeignKey() { - return Str::snake(class_basename($this)).'_'.$this->getKeyName(); + return Str::snake(class_basename($this)).'_'.$this->primaryKey; } /** @@ -2385,129 +1227,6 @@ public function setPerPage($perPage) return $this; } - /** - * Determine if the model is soft deletable. - */ - public static function isSoftDeletable(): bool - { - return static::$isSoftDeletable[static::class] ??= in_array(SoftDeletes::class, class_uses_recursive(static::class)); - } - - /** - * Determine if the model is prunable. - */ - protected function isPrunable(): bool - { - return self::$isPrunable[static::class] ??= in_array(Prunable::class, class_uses_recursive(static::class)) || static::isMassPrunable(); - } - - /** - * Determine if the model is mass prunable. - */ - protected function isMassPrunable(): bool - { - return self::$isMassPrunable[static::class] ??= in_array(MassPrunable::class, class_uses_recursive(static::class)); - } - - /** - * Determine if lazy loading is disabled. - * - * @return bool - */ - public static function preventsLazyLoading() - { - return static::$modelsShouldPreventLazyLoading; - } - - /** - * Determine if relationships are being automatically eager loaded when accessed. - * - * @return bool - */ - public static function isAutomaticallyEagerLoadingRelationships() - { - return static::$modelsShouldAutomaticallyEagerLoadRelationships; - } - - /** - * Determine if discarding guarded attribute fills is disabled. - * - * @return bool - */ - public static function preventsSilentlyDiscardingAttributes() - { - return static::$modelsShouldPreventSilentlyDiscardingAttributes; - } - - /** - * Determine if accessing missing attributes is disabled. - * - * @return bool - */ - public static function preventsAccessingMissingAttributes() - { - return static::$modelsShouldPreventAccessingMissingAttributes; - } - - /** - * Get the broadcast channel route definition that is associated with the given entity. - * - * @return string - */ - public function broadcastChannelRoute() - { - return str_replace('\\', '.', get_class($this)).'.{'.Str::camel(class_basename($this)).'}'; - } - - /** - * Get the broadcast channel name that is associated with the given entity. - * - * @return string - */ - public function broadcastChannel() - { - return str_replace('\\', '.', get_class($this)).'.'.$this->getKey(); - } - - /** - * Resolve a class attribute value from the model. - * - * @template TAttribute of object - * - * @param class-string $attributeClass - * @param string|null $property - * @param string|null $class - * @return mixed - */ - protected static function resolveClassAttribute(string $attributeClass, ?string $property = null, ?string $class = null) - { - $class = $class ?? static::class; - - $cacheKey = $class.'@'.$attributeClass; - - if (array_key_exists($cacheKey, static::$classAttributes)) { - return static::$classAttributes[$cacheKey]; - } - - try { - $reflection = new ReflectionClass($class); - - do { - $attributes = $reflection->getAttributes($attributeClass); - - if (count($attributes) > 0) { - $instance = $attributes[0]->newInstance(); - - return static::$classAttributes[$cacheKey] = $property ? $instance->{$property} : $instance; - } - } while ($reflection = $reflection->getParentClass()); - } catch (Exception) { - // - } - - return static::$classAttributes[$cacheKey] = null; - } - /** * Dynamically retrieve attributes on the model. * @@ -2537,17 +1256,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 ! is_null($this->getAttribute($offset)); } /** @@ -2556,7 +1267,7 @@ public function offsetExists($offset): bool * @param mixed $offset * @return mixed */ - public function offsetGet($offset): mixed + public function offsetGet($offset) { return $this->getAttribute($offset); } @@ -2568,7 +1279,7 @@ 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); } @@ -2579,14 +1290,9 @@ 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->attributes[$offset], $this->relations[$offset]); } /** @@ -2620,24 +1326,15 @@ public function __unset($key) */ public function __call($method, $parameters) { - if (in_array($method, ['increment', 'decrement', 'incrementQuietly', 'decrementQuietly'])) { + if (in_array($method, ['increment', 'decrement'])) { return $this->$method(...$parameters); } - if ($resolver = $this->relationResolver(static::class, $method)) { - return $resolver($this); - } - - if (Str::startsWith($method, 'through') && - method_exists($this, $relationMethod = (new SupportStringable($method))->after('through')->lcfirst()->toString())) { - return $this->through($relationMethod); - } - - return $this->forwardCallTo($this->newQuery(), $method, $parameters); + return $this->newQuery()->$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,10 +1342,6 @@ public function __call($method, $parameters) */ public static function __callStatic($method, $parameters) { - if (static::isScopeMethodWithAttribute($method)) { - return static::query()->$method(...$parameters); - } - return (new static)->$method(...$parameters); } @@ -2659,49 +1352,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 +1363,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..c3db8242fd 100755 --- a/Eloquent/ModelNotFoundException.php +++ b/Eloquent/ModelNotFoundException.php @@ -2,33 +2,30 @@ namespace Illuminate\Database\Eloquent; -use Illuminate\Database\RecordsNotFoundException; +use RuntimeException; use Illuminate\Support\Arr; -/** - * @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. * - * @var array + * @var int|array */ protected $ids; /** * Set the affected Eloquent model and instance ids. * - * @param class-string $model - * @param array|int|string $ids + * @param string $model + * @param int|array $ids * @return $this */ public function setModel($model, $ids = []) @@ -50,7 +47,7 @@ public function setModel($model, $ids = []) /** * Get the affected Eloquent model. * - * @return class-string + * @return string */ public function getModel() { @@ -60,7 +57,7 @@ public function getModel() /** * Get the affected Eloquent model IDs. * - * @return array + * @return int|array */ public function getIds() { 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 index 73257bb101..5026ac9c89 100755 --- a/Eloquent/RelationNotFoundException.php +++ b/Eloquent/RelationNotFoundException.php @@ -6,41 +6,17 @@ class RelationNotFoundException extends RuntimeException { - /** - * The name of the affected Eloquent model. - * - * @var string - */ - public $model; - - /** - * The name of the relation. - * - * @var string - */ - public $relation; - /** * Create a new exception instance. * - * @param object $model + * @param mixed $model * @param string $relation - * @param string|null $type * @return static */ - public static function make($model, $relation, $type = null) + public static function make($model, $relation) { $class = get_class($model); - $instance = new static( - is_null($type) - ? "Call to undefined relationship [{$relation}] on model [{$class}]." - : "Call to undefined relationship [{$relation}] on model [{$class}] of type [{$type}].", - ); - - $instance->model = $class; - $instance->relation = $relation; - - return $instance; + return new static("Call to undefined relationship [{$relation}] on model [{$class}]."); } } diff --git a/Eloquent/Relations/BelongsTo.php b/Eloquent/Relations/BelongsTo.php index a550eac730..e6dce103ec 100755 --- a/Eloquent/Relations/BelongsTo.php +++ b/Eloquent/Relations/BelongsTo.php @@ -2,31 +2,14 @@ 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\Eloquent\Collection; + class BelongsTo extends Relation { - use ComparesRelatedModels, - InteractsWithDictionary, - SupportsDefaultModels; - /** * The child model instance of the relation. - * - * @var TDeclaringModel */ protected $child; @@ -49,21 +32,29 @@ class BelongsTo extends Relation * * @var string */ - protected $relationName; + protected $relation; + + /** + * The count of self joins. + * + * @var int + */ + protected static $selfJoinCount = 0; /** * 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 $child * @param string $foreignKey * @param string $ownerKey - * @param string $relationName + * @param string $relation + * @return void */ - public function __construct(Builder $query, Model $child, $foreignKey, $ownerKey, $relationName) + public function __construct(Builder $query, Model $child, $foreignKey, $ownerKey, $relation) { $this->ownerKey = $ownerKey; - $this->relationName = $relationName; + $this->relation = $relation; $this->foreignKey = $foreignKey; // In the underlying base relationship class, this variable is referred to as @@ -74,14 +65,14 @@ public function __construct(Builder $query, Model $child, $foreignKey, $ownerKey parent::__construct($query, $child); } - /** @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 +86,32 @@ 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->ownerKey, '=', $this->child->{$this->foreignKey}); } } - /** @inheritDoc */ + /** + * 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->ownerKey; - $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,59 +122,90 @@ 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; } } + // 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->relationHasIncrementingId() ? 0 : null]; + } + sort($keys); 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; + + $owner = $this->ownerKey; + // 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($owner)] = $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}]); } } return $models; } + /** + * Update the parent model on the relationship. + * + * @param array $attributes + * @return mixed + */ + public function update(array $attributes) + { + return $this->getResults()->fill($attributes)->save(); + } + /** * Associate the model instance to the given parent. * - * @param TRelatedModel|int|string|null $model - * @return TDeclaringModel + * @param \Illuminate\Database\Eloquent\Model|int|string $model + * @return \Illuminate\Database\Eloquent\Model */ public function associate($model) { @@ -189,9 +214,7 @@ public function associate($model) $this->child->setAttribute($this->foreignKey, $ownerKey); if ($model instanceof Model) { - $this->child->setRelation($this->relationName, $model); - } else { - $this->child->unsetRelation($this->relationName); + $this->child->setRelation($this->relation, $model); } return $this->child; @@ -200,38 +223,23 @@ public function associate($model) /** * 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(); + return $this->child->setRelation($this->relation, null); } /** - * Touch all of the related models for the relationship. + * Add the constraints for a relationship query. * - * @return void + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parentQuery + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder */ - 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) { @@ -239,17 +247,17 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, } return $query->select($columns)->whereColumn( - $this->getQualifiedForeignKeyName(), '=', $query->qualifyColumn($this->ownerKey) + $this->getQualifiedForeignKey(), '=', $query->getModel()->getTable().'.'.$this->ownerKey ); } /** * 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 + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parentQuery + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder */ public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*']) { @@ -260,40 +268,29 @@ public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $query->getModel()->setTable($hash); return $query->whereColumn( - $hash.'.'.$this->ownerKey, '=', $this->getQualifiedForeignKeyName() + $hash.'.'.$query->getModel()->getKeyName(), '=', $this->getQualifiedForeignKey() ); } /** - * Determine if the related model has an auto-incrementing ID. + * Get a relationship join table hash. * - * @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 + * @return string */ - protected function newRelatedInstanceFor(Model $parent) + public function getRelationCountHash() { - return $this->related->newInstance(); + return 'laravel_reserved_'.static::$selfJoinCount++; } /** - * Get the child of the relationship. + * Determine if the related model has an auto-incrementing ID. * - * @return TDeclaringModel + * @return bool */ - public function getChild() + protected function relationHasIncrementingId() { - return $this->child; + return $this->related->getIncrementing() && + $this->related->getKeyType() === 'int'; } /** @@ -301,29 +298,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->child->getTable().'.'.$this->foreignKey; } /** @@ -331,43 +318,19 @@ public function getParentKey() * * @return string */ - public function getOwnerKeyName() + public function getOwnerKey() { return $this->ownerKey; } /** - * Get the fully-qualified associated key of the relationship. + * 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->related->getTable().'.'.$this->ownerKey; } /** @@ -375,8 +338,8 @@ protected function getForeignKeyFrom(Model $model) * * @return string */ - public function getRelationName() + public function getRelation() { - return $this->relationName; + return $this->relation; } } diff --git a/Eloquent/Relations/BelongsToMany.php b/Eloquent/Relations/BelongsToMany.php index 2a7e011ddf..64cf0dd212 100755 --- a/Eloquent/Relations/BelongsToMany.php +++ b/Eloquent/Relations/BelongsToMany.php @@ -2,35 +2,16 @@ 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; + use Concerns\InteractsWithPivotTable; /** * The intermediate table for the relation. @@ -77,7 +58,7 @@ class BelongsToMany extends Relation /** * The pivot table columns to retrieve. * - * @var array + * @var array */ protected $pivotColumns = []; @@ -95,112 +76,60 @@ class BelongsToMany extends Relation */ protected $pivotWhereIns = []; - /** - * Any pivot table restrictions for whereNull clauses. - * - * @var array - */ - protected $pivotWhereNulls = []; - - /** - * The default values for the pivot columns. - * - * @var array - */ - protected $pivotValues = []; - - /** - * Indicates if timestamps are available on the pivot table. - * - * @var bool - */ - public $withTimestamps = false; - /** * The custom pivot table column for the created_at timestamp. * - * @var string|null + * @var string */ protected $pivotCreatedAt; /** * The custom pivot table column for the updated_at timestamp. * - * @var string|null + * @var string */ protected $pivotUpdatedAt; /** * The class name of the custom pivot model to use for the relationship. * - * @var class-string + * @var string */ protected $using; /** - * The name of the accessor to use for the "pivot" relationship. + * The count of self joins. * - * @var TAccessor + * @var int */ - protected $accessor = 'pivot'; + protected static $selfJoinCount = 0; /** * Create a new belongs to many relationship instance. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param TDeclaringModel $parent - * @param string|class-string $table - * @param string $foreignPivotKey - * @param string $relatedPivotKey - * @param string $parentKey + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Model $parent + * @param string $table + * @param string $foreignKey * @param string $relatedKey - * @param string|null $relationName - */ - public function __construct( - Builder $query, - Model $parent, - $table, - $foreignPivotKey, - $relatedPivotKey, - $parentKey, - $relatedKey, - $relationName = null, - ) { + * @param string $parentKey + * @param string $localKey + * @param string $relationName + * @return void + */ + public function __construct(Builder $query, Model $parent, $table, $foreignPivotKey, + $relatedPivotKey, $parentKey, $relatedKey, $relationName = null) + { + $this->table = $table; $this->parentKey = $parentKey; $this->relatedKey = $relatedKey; $this->relationName = $relationName; $this->relatedPivotKey = $relatedPivotKey; $this->foreignPivotKey = $foreignPivotKey; - $this->table = $this->resolveTableName($table); parent::__construct($query, $parent); } - /** - * Attempt to resolve the intermediate table name from the given string. - * - * @param string $table - * @return string - */ - protected function resolveTableName($table) - { - if (! str_contains($table, '\\') || ! class_exists($table)) { - return $table; - } - - $model = new $table; - - if (! $model instanceof Model) { - return $table; - } - - if (in_array(AsPivot::class, class_uses_recursive($model))) { - $this->using($table); - } - - return $model->getTable(); - } - /** * Set the base constraints on the relation query. * @@ -218,7 +147,7 @@ public function addConstraints() /** * Set the join clause for the relation query. * - * @param \Illuminate\Database\Eloquent\Builder|null $query + * @param \Illuminate\Database\Eloquent\Builder|null $query * @return $this */ protected function performJoin($query = null) @@ -228,12 +157,11 @@ protected function performJoin($query = null) // 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() - ); + $baseTable = $this->related->getTable(); + + $key = $baseTable.'.'.$this->relatedKey; + + $query->join($this->table, $key, '=', $this->getQualifiedRelatedPivotKeyName()); return $this; } @@ -252,19 +180,24 @@ protected function addWhereConstraints() return $this; } - /** @inheritDoc */ + /** + * Set the constraints for an eager load of the relation. + * + * @param array $models + * @return void + */ public function addEagerConstraints(array $models) { - $whereIn = $this->whereInMethod($this->parent, $this->parentKey); - - $this->whereInEager( - $whereIn, - $this->getQualifiedForeignPivotKeyName(), - $this->getKeys($models, $this->parentKey) - ); + $this->query->whereIn($this->getQualifiedForeignPivotKeyName(), $this->getKeys($models)); } - /** @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) { @@ -274,18 +207,23 @@ 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); // 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. + // the parent models. Then we will return the hydrated models back out. foreach ($models as $model) { - $key = $this->getDictionaryKey($model->{$this->parentKey}); - - if (isset($dictionary[$key])) { + if (isset($dictionary[$key = $model->getKey()])) { $model->setRelation( $relation, $this->related->newCollection($dictionary[$key]) ); @@ -298,50 +236,28 @@ public function match(array $models, EloquentCollection $results, $relation) /** * 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) { - // 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. + // 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 = []; - $isAssociative = Arr::isAssoc($results->all()); - - foreach ($results as $key => $result) { - $value = $this->getDictionaryKey($result->{$this->accessor}->{$this->foreignPivotKey}); - - if ($isAssociative) { - $dictionary[$value][$key] = $result; - } else { - $dictionary[$value][] = $result; - } + foreach ($results as $result) { + $dictionary[$result->pivot->{$this->foreignPivotKey}][] = $result; } return $dictionary; } - /** - * Get the class being used for pivot models. - * - * @return class-string - */ - public function getPivotClass() - { - return $this->using ?? Pivot::class; - } - /** * Specify the custom pivot model to use for the relationship. * - * @template TNewPivotModel of \Illuminate\Database\Eloquent\Relations\Pivot - * - * @param class-string $class - * @return $this - * - * @phpstan-this-out static + * @param string $class + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function using($class) { @@ -350,155 +266,57 @@ public function using($class) return $this; } - /** - * Specify the custom pivot accessor to use for the relationship. - * - * @template TNewAccessor of string - * - * @param TNewAccessor $accessor - * @return $this - * - * @phpstan-this-out static - */ - public function as($accessor) - { - $this->accessor = $accessor; - - return $this; - } - /** * Set a where clause for a pivot table column. * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value + * @param string $column + * @param string $operator + * @param mixed $value * @param string $boolean - * @return $this + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function wherePivot($column, $operator = null, $value = null, $boolean = 'and') { $this->pivotWheres[] = func_get_args(); - return $this->where($this->qualifyPivotColumn($column), $operator, $value, $boolean); - } - - /** - * Set a "where between" clause for a pivot table column. - * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param array $values - * @param string $boolean - * @param bool $not - * @return $this - */ - public function wherePivotBetween($column, array $values, $boolean = 'and', $not = false) - { - return $this->whereBetween($this->qualifyPivotColumn($column), $values, $boolean, $not); - } - - /** - * Set a "or where between" clause for a pivot table column. - * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param array $values - * @return $this - */ - public function orWherePivotBetween($column, array $values) - { - return $this->wherePivotBetween($column, $values, 'or'); - } - - /** - * Set a "where pivot not between" clause for a pivot table column. - * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param array $values - * @param string $boolean - * @return $this - */ - public function wherePivotNotBetween($column, array $values, $boolean = 'and') - { - return $this->wherePivotBetween($column, $values, $boolean, true); - } - - /** - * Set a "or where not between" clause for a pivot table column. - * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param array $values - * @return $this - */ - public function orWherePivotNotBetween($column, array $values) - { - return $this->wherePivotBetween($column, $values, 'or', true); + return $this->where($this->table.'.'.$column, $operator, $value, $boolean); } /** * Set a "where in" clause for a pivot table column. * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $values + * @param string $column + * @param mixed $values * @param string $boolean - * @param bool $not - * @return $this + * @param bool $not + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function wherePivotIn($column, $values, $boolean = 'and', $not = false) { $this->pivotWhereIns[] = func_get_args(); - return $this->whereIn($this->qualifyPivotColumn($column), $values, $boolean, $not); + return $this->whereIn($this->table.'.'.$column, $values, $boolean, $not); } /** * Set an "or where" clause for a pivot table column. * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @return $this + * @param string $column + * @param string $operator + * @param mixed $value + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function orWherePivot($column, $operator = null, $value = null) { return $this->wherePivot($column, $operator, $value, 'or'); } - /** - * Set a where clause for a pivot table column. - * - * In addition, new pivot records will receive this value. - * - * @param string|\Illuminate\Contracts\Database\Query\Expression|array $column - * @param mixed $value - * @return $this - * - * @throws \InvalidArgumentException - */ - public function withPivotValue($column, $value = null) - { - if (is_array($column)) { - foreach ($column as $name => $value) { - $this->withPivotValue($name, $value); - } - - return $this; - } - - if (is_null($value)) { - throw new InvalidArgumentException('The provided value may not be null.'); - } - - $this->pivotValues[] = compact('column', 'value'); - - return $this->wherePivot($column, '=', $value); - } - /** * Set an "or where in" clause for a pivot table column. * * @param string $column - * @param mixed $values - * @return $this + * @param mixed $values + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function orWherePivotIn($column, $values) { @@ -506,113 +324,11 @@ public function orWherePivotIn($column, $values) } /** - * Set a "where not in" clause for a pivot table column. - * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $values - * @param string $boolean - * @return $this - */ - public function wherePivotNotIn($column, $values, $boolean = 'and') - { - return $this->wherePivotIn($column, $values, $boolean, true); - } - - /** - * Set an "or where not in" clause for a pivot table column. - * - * @param string $column - * @param mixed $values - * @return $this - */ - public function orWherePivotNotIn($column, $values) - { - return $this->wherePivotNotIn($column, $values, 'or'); - } - - /** - * Set a "where null" clause for a pivot table column. - * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param string $boolean - * @param bool $not - * @return $this - */ - public function wherePivotNull($column, $boolean = 'and', $not = false) - { - $this->pivotWhereNulls[] = func_get_args(); - - return $this->whereNull($this->qualifyPivotColumn($column), $boolean, $not); - } - - /** - * Set a "where not null" clause for a pivot table column. - * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param string $boolean - * @return $this - */ - public function wherePivotNotNull($column, $boolean = 'and') - { - return $this->wherePivotNull($column, $boolean, true); - } - - /** - * Set a "or where null" clause for a pivot table column. - * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param bool $not - * @return $this - */ - public function orWherePivotNull($column, $not = false) - { - return $this->wherePivotNull($column, 'or', $not); - } - - /** - * Set a "or where not null" clause for a pivot table column. - * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @return $this - */ - public function orWherePivotNotNull($column) - { - return $this->orWherePivotNull($column, true); - } - - /** - * Add an "order by" clause for a pivot table column. - * - * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param string $direction - * @return $this - */ - public function orderByPivot($column, $direction = 'asc') - { - return $this->orderBy($this->qualifyPivotColumn($column), $direction); - } - - /** - * 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'); - } - - /** - * Find a related model by its primary key or return a new instance of the related model. + * Find a related model by its primary key or return 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} - * ) + * @return \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model */ public function findOrNew($id, $columns = ['*']) { @@ -627,90 +343,54 @@ public function findOrNew($id, $columns = ['*']) * Get the first related model record matching the attributes or instantiate it. * * @param array $attributes - * @param array $values - * @return TRelatedModel&object{pivot: TPivotModel} + * @return \Illuminate\Database\Eloquent\Model */ - public function firstOrNew(array $attributes = [], array $values = []) + public function firstOrNew(array $attributes) { - if (is_null($instance = $this->related->where($attributes)->first())) { - $instance = $this->related->newInstance(array_merge($attributes, $values)); + if (is_null($instance = $this->where($attributes)->first())) { + $instance = $this->related->newInstance($attributes); } return $instance; } /** - * Get the first record matching the attributes. If the record is not found, create it. + * Get the first related record matching the attributes or create it. * * @param array $attributes - * @param (\Closure(): array)|array $values * @param array $joining - * @param bool $touch - * @return TRelatedModel&object{pivot: TPivotModel} + * @param bool $touch + * @return \Illuminate\Database\Eloquent\Model */ - public function firstOrCreate(array $attributes = [], Closure|array $values = [], array $joining = [], $touch = true) + public function firstOrCreate(array $attributes, 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... - } - } + if (is_null($instance = $this->where($attributes)->first())) { + $instance = $this->create($attributes, $joining, $touch); } 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 - * @param array $joining - * @param bool $touch - * @return TRelatedModel&object{pivot: TPivotModel} - * - * @throws \Illuminate\Database\UniqueConstraintViolationException - */ - public function createOrFirst(array $attributes = [], Closure|array $values = [], array $joining = [], $touch = true) - { - try { - return $this->getQuery()->withSavepointIfNeeded(fn () => $this->create(array_merge($attributes, value($values)), $joining, $touch)); - } catch (UniqueConstraintViolationException $e) { - // ... - } - - try { - return tap($this->related->where($attributes)->first() ?? throw $e, function ($instance) use ($joining, $touch) { - $this->getQuery()->withSavepointIfNeeded(fn () => $this->attach($instance, $joining, $touch)); - }); - } catch (UniqueConstraintViolationException $e) { - return (clone $this)->useWritePdo()->where($attributes)->first() ?? throw $e; - } - } - /** * Create or update a related record matching the attributes, and fill it with values. * * @param array $attributes * @param array $values * @param array $joining - * @param bool $touch - * @return TRelatedModel&object{pivot: TPivotModel} + * @param bool $touch + * @return \Illuminate\Database\Eloquent\Model */ public function updateOrCreate(array $attributes, array $values = [], array $joining = [], $touch = true) { - return tap($this->firstOrCreate($attributes, $values, $joining, $touch), function ($instance) use ($values) { - if (! $instance->wasRecentlyCreated) { - $instance->fill($values); + if (is_null($instance = $this->where($attributes)->first())) { + return $this->create($values, $joining, $touch); + } - $instance->save(['touch' => false]); - } - }); + $instance->fill($values); + + $instance->save(['touch' => false]); + + return $instance; } /** @@ -718,57 +398,26 @@ 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)) { - return $this->findMany($id, $columns); - } - - return $this->where( - $this->getRelated()->getQualifiedKeyName(), '=', $this->parseId($id) + return is_array($id) ? $this->findMany($id, $columns) : $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&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); - } - /** * 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) + return empty($ids) ? $this->getRelated()->newCollection() : $this->whereIn( + $this->getRelated()->getQualifiedKeyName(), $ids )->get($columns); } @@ -777,91 +426,34 @@ 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))) { - 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&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))) { + 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(); + throw (new ModelNotFoundException)->setModel(get_class($this->related)); } /** * Execute the query and get the first result. * - * @param array $columns - * @return (TRelatedModel&object{pivot: TPivotModel})|null + * @param array $columns + * @return mixed */ public function first($columns = ['*']) { - $results = $this->limit(1)->get($columns); + $results = $this->take(1)->get($columns); return count($results) > 0 ? $results->first() : null; } @@ -870,9 +462,9 @@ public function first($columns = ['*']) * Execute the query and get the first result or throw an exception. * * @param array $columns - * @return TRelatedModel&object{pivot: TPivotModel} + * @return \Illuminate\Database\Eloquent\Model|static * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException */ public function firstOrFail($columns = ['*']) { @@ -884,46 +476,29 @@ public function firstOrFail($columns = ['*']) } /** - * Execute the query and get the first result or call a callback. - * - * @template TValue + * Get the results of the relationship. * - * @param (\Closure(): TValue)|list $columns - * @param (\Closure(): TValue)|null $callback - * @return (TRelatedModel&object{pivot: TPivotModel})|TValue + * @return mixed */ - public function firstOr($columns = ['*'], ?Closure $callback = null) - { - if ($columns instanceof Closure) { - $callback = $columns; - - $columns = ['*']; - } - - if (! is_null($model = $this->first($columns))) { - return $model; - } - - return $callback(); - } - - /** @inheritDoc */ public function getResults() { - return ! is_null($this->parent->{$this->parentKey}) - ? $this->get() - : $this->related->newCollection(); + return $this->get(); } - /** @inheritDoc */ + /** + * Execute the query as a "select" statement. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ public function get($columns = ['*']) { // First we'll add the proper select columns onto the query so it is run with - // the proper columns. Then, we will get the results and hydrate our pivot + // 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. - $builder = $this->query->applyScopes(); + $columns = $this->query->getQuery()->columns ? [] : $columns; - $columns = $builder->getQuery()->columns ? [] : $columns; + $builder = $this->query->applyScopes(); $models = $builder->addSelect( $this->shouldSelect($columns) @@ -938,21 +513,19 @@ public function get($columns = ['*']) $models = $builder->eagerLoadRelations($models); } - return $this->query->applyAfterQueryCallbacks( - $this->related->newCollection($models) - ); + return $this->related->newCollection($models); } /** * Get the select columns for the relation query. * * @param array $columns - * @return array + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ protected function shouldSelect(array $columns = ['*']) { if ($columns == ['*']) { - $columns = [$this->related->qualifyColumn('*')]; + $columns = [$this->related->getTable().'.*']; } return array_merge($columns, $this->aliasedPivotColumns()); @@ -961,30 +534,27 @@ protected function shouldSelect(array $columns = ['*']) /** * Get the pivot columns for the relation. * - * "pivot_" is prefixed at each column for easy removal later. + * "pivot_" is prefixed ot 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(); + $defaults = [$this->foreignPivotKey, $this->relatedPivotKey]; + + return collect(array_merge($defaults, $this->pivotColumns))->map(function ($column) { + return $this->table.'.'.$column.' as pivot_'.$column; + })->unique()->all(); } /** * Get a paginator for the "select" statement. * - * @param int|null $perPage + * @param int $perPage * @param array $columns * @param string $pageName * @param int|null $page - * @return \Illuminate\Pagination\LengthAwarePaginator + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator */ public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) { @@ -998,11 +568,11 @@ public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', /** * Paginate the given query into a simple paginator. * - * @param int|null $perPage + * @param int $perPage * @param array $columns * @param string $pageName * @param int|null $page - * @return \Illuminate\Contracts\Pagination\Paginator + * @return \Illuminate\Contracts\Pagination\Paginator */ public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) { @@ -1013,24 +583,6 @@ public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'p }); } - /** - * 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. * @@ -1040,193 +592,19 @@ public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = */ 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(); + $this->query->addSelect($this->shouldSelect()); - return $this->prepareQueryBuilder()->orderedChunkById($count, function ($results, $page) use ($callback) { + return $this->query->chunk($count, function ($results) use ($callback) { $this->hydratePivotRelation($results->all()); - return $callback($results, $page); - }, $column, $alias, $descending); - } - - /** - * Execute a callback over each item while chunking. - * - * @param callable $callback - * @param int $count - * @return bool - */ - public function each(callable $callback, $count = 1000) - { - return $this->chunk($count, function ($results) use ($callback) { - foreach ($results as $key => $value) { - if ($callback($value, $key) === false) { - return false; - } - } - }); - } - - /** - * Query lazily, by chunks of the given size. - * - * @param int $chunkSize - * @return \Illuminate\Support\LazyCollection - */ - public function lazy($chunkSize = 1000) - { - return $this->prepareQueryBuilder()->lazy($chunkSize)->map(function ($model) { - $this->hydratePivotRelation([$model]); - - return $model; - }); - } - - /** - * Query lazily, by chunking the results of a query by comparing IDs. - * - * @param int $chunkSize - * @param string|null $column - * @param string|null $alias - * @return \Illuminate\Support\LazyCollection - */ - public function lazyById($chunkSize = 1000, $column = null, $alias = null) - { - $column ??= $this->getRelated()->qualifyColumn( - $this->getRelatedKeyName() - ); - - $alias ??= $this->getRelatedKeyName(); - - return $this->prepareQueryBuilder()->lazyById($chunkSize, $column, $alias)->map(function ($model) { - $this->hydratePivotRelation([$model]); - - return $model; - }); - } - - /** - * Query lazily, by chunking the results of a query by comparing IDs in descending order. - * - * @param int $chunkSize - * @param string|null $column - * @param string|null $alias - * @return \Illuminate\Support\LazyCollection - */ - public function lazyByIdDesc($chunkSize = 1000, $column = null, $alias = null) - { - $column ??= $this->getRelated()->qualifyColumn( - $this->getRelatedKeyName() - ); - - $alias ??= $this->getRelatedKeyName(); - - return $this->prepareQueryBuilder()->lazyByIdDesc($chunkSize, $column, $alias)->map(function ($model) { - $this->hydratePivotRelation([$model]); - - return $model; - }); - } - - /** - * Get a lazy collection for the given query. - * - * @return \Illuminate\Support\LazyCollection - */ - public function cursor() - { - return $this->prepareQueryBuilder()->cursor()->map(function ($model) { - $this->hydratePivotRelation([$model]); - - return $model; + return $callback($results); }); } - /** - * Prepare the query builder for query execution. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - protected function prepareQueryBuilder() - { - return $this->query->addSelect($this->shouldSelect()); - } - /** * Hydrate the pivot table relationship on the models. * - * @param array $models + * @param array $models * @return void */ protected function hydratePivotRelation(array $models) @@ -1235,7 +613,7 @@ protected function hydratePivotRelation(array $models) // 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( + $model->setRelation('pivot', $this->newExistingPivot( $this->migratePivotAttributes($model) )); } @@ -1244,7 +622,7 @@ protected function hydratePivotRelation(array $models) /** * Get the pivot attributes from a model. * - * @param TRelatedModel $model + * @param \Illuminate\Database\Eloquent\Model $model * @return array */ protected function migratePivotAttributes(Model $model) @@ -1255,7 +633,7 @@ protected function migratePivotAttributes(Model $model) // 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_')) { + if (strpos($key, 'pivot_') === 0) { $values[substr($key, 6)] = $value; unset($model->$key); @@ -1298,7 +676,7 @@ protected function touchingParent() */ protected function guessInverseRelation() { - return Str::camel(Str::pluralStudly(class_basename($this->getParent()))); + return Str::camel(Str::plural(class_basename($this->getParent()))); } /** @@ -1310,9 +688,7 @@ protected function guessInverseRelation() */ public function touch() { - if ($this->related->isIgnoringTouch()) { - return; - } + $key = $this->getRelated()->getKeyName(); $columns = [ $this->related->getUpdatedAtColumn() => $this->related->freshTimestampString(), @@ -1322,65 +698,52 @@ public function touch() // 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); + $this->getRelated()->newQuery()->whereIn($key, $ids)->update($columns); } } /** * Get all of the IDs for the related models. * - * @return \Illuminate\Support\Collection + * @return \Illuminate\Support\Collection */ public function allRelatedIds() { - return $this->newPivotQuery()->pluck($this->relatedPivotKey); + $related = $this->getRelated(); + + return $this->getQuery()->select( + $related->getQualifiedKeyName() + )->pluck($related->getKeyName()); } /** * Save a new model and attach it to the parent model. * - * @param TRelatedModel $model + * @param \Illuminate\Database\Eloquent\Model $model * @param array $pivotAttributes - * @param bool $touch - * @return TRelatedModel&object{pivot: TPivotModel} + * @param bool $touch + * @return \Illuminate\Database\Eloquent\Model */ public function save(Model $model, array $pivotAttributes = [], $touch = true) { $model->save(['touch' => false]); - $this->attach($model, $pivotAttributes, $touch); + $this->attach($model->getKey(), $pivotAttributes, $touch); return $model; } - /** - * Save a new model without raising any events and attach it to the parent model. - * - * @param TRelatedModel $model - * @param array $pivotAttributes - * @param bool $touch - * @return TRelatedModel&object{pivot: TPivotModel} - */ - public function saveQuietly(Model $model, array $pivotAttributes = [], $touch = true) - { - return Model::withoutEvents(function () use ($model, $pivotAttributes, $touch) { - return $this->save($model, $pivotAttributes, $touch); - }); - } - /** * Save an array of new models and attach them to the parent model. * - * @template TContainer of \Illuminate\Support\Collection|array - * - * @param TContainer $models + * @param \Illuminate\Support\Collection|array $models * @param array $pivotAttributes - * @return TContainer + * @return array */ public function saveMany($models, array $pivotAttributes = []) { foreach ($models as $key => $model) { - $this->save($model, (array) ($pivotAttributes[$key] ?? []), false); + $this->save($model, (array) Arr::get($pivotAttributes, $key), false); } $this->touchIfTouching(); @@ -1388,34 +751,16 @@ public function saveMany($models, array $pivotAttributes = []) return $models; } - /** - * Save an array of new models without raising any events and attach them to the parent model. - * - * @template TContainer of \Illuminate\Support\Collection|array - * - * @param TContainer $models - * @param array $pivotAttributes - * @return TContainer - */ - public function saveManyQuietly($models, array $pivotAttributes = []) - { - return Model::withoutEvents(function () use ($models, $pivotAttributes) { - return $this->saveMany($models, $pivotAttributes); - }); - } - /** * Create a new instance of the related model. * * @param array $attributes * @param array $joining - * @param bool $touch - * @return TRelatedModel&object{pivot: TPivotModel} + * @param bool $touch + * @return \Illuminate\Database\Eloquent\Model */ - public function create(array $attributes = [], array $joining = [], $touch = true) + public function create(array $attributes, array $joining = [], $touch = true) { - $attributes = array_merge($this->getQuery()->pendingAttributes, $attributes); - $instance = $this->related->newInstance($attributes); // Once we save the related model, we need to attach it to the base model via @@ -1423,7 +768,7 @@ public function create(array $attributes = [], array $joining = [], $touch = tru // accomplish this which will insert the record and any more attributes. $instance->save(['touch' => false]); - $this->attach($instance, $joining, $touch); + $this->attach($instance->getKey(), $joining, $touch); return $instance; } @@ -1431,16 +776,16 @@ public function create(array $attributes = [], array $joining = [], $touch = tru /** * Create an array of new instances of the related models. * - * @param iterable $records + * @param array $records * @param array $joinings - * @return array + * @return array */ - public function createMany(iterable $records, array $joinings = []) + public function createMany(array $records, array $joinings = []) { $instances = []; foreach ($records as $key => $record) { - $instances[] = $this->create($record, (array) ($joinings[$key] ?? []), false); + $instances[] = $this->create($record, (array) Arr::get($joinings, $key), false); } $this->touchIfTouching(); @@ -1448,7 +793,14 @@ public function createMany(iterable $records, array $joinings = []) return $instances; } - /** @inheritDoc */ + /** + * Add the constraints for a relationship query. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parentQuery + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder + */ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) { if ($parentQuery->getQuery()->from == $query->getQuery()->from) { @@ -1463,10 +815,10 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, /** * 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 + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parentQuery + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder */ public function getRelationExistenceQueryForSelfJoin(Builder $query, Builder $parentQuery, $columns = ['*']) { @@ -1482,71 +834,38 @@ public function getRelationExistenceQueryForSelfJoin(Builder $query, Builder $pa } /** - * 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. + * Get the key for comparing against the parent key in "has" query. * - * @param int $value - * @return $this + * @return string */ - public function limit($value) + public function getExistenceCompareKey() { - if ($this->parent->exists) { - $this->query->limit($value); - } else { - $column = $this->getExistenceCompareKey(); - - $grammar = $this->query->getQuery()->getGrammar(); - - if ($grammar instanceof MySqlGrammar && $grammar->useLegacyGroupLimit($this->query->getQuery())) { - $column = 'pivot_'.last(explode('.', $column)); - } - - $this->query->groupLimit($value, $column); - } - - return $this; + return $this->getQualifiedForeignPivotKeyName(); } /** - * Get the key for comparing against the parent key in "has" query. + * Get a relationship join table hash. * * @return string */ - public function getExistenceCompareKey() + public function getRelationCountHash() { - return $this->getQualifiedForeignPivotKeyName(); + return 'laravel_reserved_'.static::$selfJoinCount++; } /** * Specify that the pivot table has creation and update timestamps. * - * @param string|null|false $createdAt - * @param string|null|false $updatedAt - * @return $this + * @param mixed $createdAt + * @param mixed $updatedAt + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function withTimestamps($createdAt = null, $updatedAt = null) { - $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->pivotCreatedAt = $createdAt; + $this->pivotUpdatedAt = $updatedAt; - $this->withTimestamps = ! empty($pivots); - - return $this->withTimestamps ? $this->withPivot($pivots) : $this; + return $this->withPivot($this->createdAt(), $this->updatedAt()); } /** @@ -1556,7 +875,7 @@ public function withTimestamps($createdAt = null, $updatedAt = null) */ public function createdAt() { - return $this->pivotCreatedAt ?? $this->parent->getCreatedAtColumn() ?? Model::CREATED_AT; + return $this->pivotCreatedAt ?: $this->parent->getCreatedAtColumn(); } /** @@ -1566,87 +885,27 @@ public function createdAt() */ public function updatedAt() { - return $this->pivotUpdatedAt ?? $this->parent->getUpdatedAtColumn() ?? Model::UPDATED_AT; - } - - /** - * Get the foreign key for the relation. - * - * @return string - */ - public function getForeignPivotKeyName() - { - return $this->foreignPivotKey; + return $this->pivotUpdatedAt ?: $this->parent->getUpdatedAtColumn(); } /** - * Get the fully-qualified foreign key for the relation. + * Get the fully qualified foreign key for the relation. * * @return string */ public function getQualifiedForeignPivotKeyName() { - return $this->qualifyPivotColumn($this->foreignPivotKey); + return $this->table.'.'.$this->foreignPivotKey; } /** - * Get the "related key" for the relation. - * - * @return string - */ - public function getRelatedPivotKeyName() - { - return $this->relatedPivotKey; - } - - /** - * Get the fully-qualified "related key" for the relation. + * Get the fully qualified "related key" for the relation. * * @return string */ public function getQualifiedRelatedPivotKeyName() { - return $this->qualifyPivotColumn($this->relatedPivotKey); - } - - /** - * Get the parent key for the relationship. - * - * @return string - */ - public function getParentKeyName() - { - return $this->parentKey; - } - - /** - * Get the fully-qualified parent key name for the relation. - * - * @return string - */ - public function getQualifiedParentKeyName() - { - return $this->parent->qualifyColumn($this->parentKey); - } - - /** - * Get the related key for the relationship. - * - * @return string - */ - public function getRelatedKeyName() - { - return $this->relatedKey; - } - - /** - * Get the fully-qualified related key name for the relation. - * - * @return string - */ - public function getQualifiedRelatedKeyName() - { - return $this->related->qualifyColumn($this->relatedKey); + return $this->table.'.'.$this->relatedPivotKey; } /** @@ -1668,41 +927,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 index dd324d09a6..af7a1b161c 100644 --- a/Eloquent/Relations/Concerns/InteractsWithPivotTable.php +++ b/Eloquent/Relations/Concerns/InteractsWithPivotTable.php @@ -2,10 +2,8 @@ namespace Illuminate\Database\Eloquent\Relations\Concerns; -use BackedEnum; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\Pivot; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Collection as BaseCollection; trait InteractsWithPivotTable @@ -16,7 +14,7 @@ trait InteractsWithPivotTable * Each existing model is detached, and non existing ones are attached. * * @param mixed $ids - * @param bool $touch + * @param bool $touch * @return array */ public function toggle($ids, $touch = true) @@ -25,7 +23,7 @@ public function toggle($ids, $touch = true) 'attached' => [], 'detached' => [], ]; - $records = $this->formatRecordsList($this->parseIds($ids)); + $records = $this->formatRecordsList((array) $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 @@ -66,8 +64,8 @@ public function toggle($ids, $touch = true) /** * 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} + * @param \Illuminate\Database\Eloquent\Collection|array $ids + * @return array */ public function syncWithoutDetaching($ids) { @@ -77,9 +75,9 @@ public function syncWithoutDetaching($ids) /** * 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} + * @param \Illuminate\Database\Eloquent\Collection|\Illuminate\Support\Collection|array $ids + * @param bool $detaching + * @return array */ public function sync($ids, $detaching = true) { @@ -87,29 +85,24 @@ public function sync($ids, $detaching = true) '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(); + $current = $this->newPivotQuery()->pluck( + $this->relatedPivotKey + )->all(); + + $detach = array_diff($current, array_keys( + $records = $this->formatRecordsList((array) $this->parseIds($ids)) + )); // 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 ($detaching && count($detach) > 0) { + $this->detach($detach); - if (count($detach) > 0) { - $this->detach($detach, false); - - $changes['detached'] = $this->castKeys($detach); - } + $changes['detached'] = $this->castKeys($detach); } // Now we are finally ready to attach the new records. Note that we'll disable @@ -123,29 +116,13 @@ public function sync($ids, $detaching = true) // 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'])) { + count($changes['updated'])) { $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. * @@ -154,13 +131,9 @@ public function syncWithPivotValues($ids, array $values, bool $detaching = true) */ protected function formatRecordsList(array $records) { - return (new BaseCollection($records))->mapWithKeys(function ($attributes, $id) { + return collect($records)->mapWithKeys(function ($attributes, $id) { if (! is_array($attributes)) { - [$id, $attributes] = [$attributes, []]; - } - - if ($id instanceof BackedEnum) { - $id = $id->value; + list($id, $attributes) = [$attributes, []]; } return [$id => $attributes]; @@ -172,7 +145,7 @@ protected function formatRecordsList(array $records) * * @param array $records * @param array $current - * @param bool $touch + * @param bool $touch * @return array */ protected function attachNew(array $records, array $current, $touch = true) @@ -206,22 +179,16 @@ protected function attachNew(array $records, array $current, $touch = true) * * @param mixed $id * @param array $attributes - * @param bool $touch + * @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())) { + if (in_array($this->updatedAt(), $this->pivotColumns)) { $attributes = $this->addTimestampsToAttachment($attributes, true); } - $updated = $this->newPivotStatementForId($id)->update( - $this->castAttributes($attributes) - ); + $updated = $this->newPivotStatementForId($id)->update($attributes); if ($touch) { $this->touchIfTouching(); @@ -230,75 +197,28 @@ public function updateExistingPivot($id, array $attributes, $touch = true) 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 mixed $id * @param array $attributes - * @param bool $touch + * @param bool $touch * @return void */ - public function attach($ids, array $attributes = [], $touch = true) + public function attach($id, 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 - )); - } + // 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( + (array) $this->parseIds($id), $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. * @@ -328,18 +248,18 @@ protected function formatAttachRecords($ids, array $attributes) /** * Create a full attachment record payload. * - * @param int $key + * @param int $key * @param mixed $value * @param array $attributes - * @param bool $hasTimestamps + * @param bool $hasTimestamps * @return array */ protected function formatAttachRecord($key, $value, $attributes, $hasTimestamps) { - [$id, $attributes] = $this->extractAttachIdAndAttributes($key, $value, $attributes); + list($id, $attributes) = $this->extractAttachIdAndAttributes($key, $value, $attributes); return array_merge( - $this->baseAttachRecord($id, $hasTimestamps), $this->castAttributes($attributes) + $this->baseAttachRecord($id, $hasTimestamps), $attributes ); } @@ -354,14 +274,14 @@ protected function formatAttachRecord($key, $value, $attributes, $hasTimestamps) protected function extractAttachIdAndAttributes($key, $value, array $attributes) { return is_array($value) - ? [$key, array_merge($value, $attributes)] - : [$value, $attributes]; + ? [$key, array_merge($value, $attributes)] + : [$value, $attributes]; } /** * Create a new pivot attachment record. * - * @param int $id + * @param int $id * @param bool $timed * @return array */ @@ -369,7 +289,7 @@ protected function baseAttachRecord($id, $timed) { $record[$this->relatedPivotKey] = $id; - $record[$this->foreignPivotKey] = $this->parent->{$this->parentKey}; + $record[$this->foreignPivotKey] = $this->parent->getKey(); // If the record needs to have creation and update timestamps, we will make // them by calling the parent model's "freshTimestamp" method which will @@ -378,10 +298,6 @@ protected function baseAttachRecord($id, $timed) $record = $this->addTimestampsToAttachment($record); } - foreach ($this->pivotValues as $value) { - $record[$value['column']] = $value['value']; - } - return $record; } @@ -389,19 +305,13 @@ protected function baseAttachRecord($id, $timed) * Set the creation and update timestamps on an attach record. * * @param array $record - * @param bool $exists + * @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; } @@ -419,7 +329,7 @@ protected function addTimestampsToAttachment(array $record, $exists = false) * @param string $column * @return bool */ - public function hasPivotColumn($column) + protected function hasPivotColumn($column) { return in_array($column, $this->pivotColumns); } @@ -433,30 +343,24 @@ public function hasPivotColumn($column) */ 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); + $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 = $this->parseIds($ids))) { + if (count($ids) === 0) { + return 0; } - // 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(); + $query->whereIn($this->relatedPivotKey, (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(); } @@ -464,77 +368,20 @@ public function detach($ids = null, $touch = true) 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 + * @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); + return $pivot->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey); } /** @@ -566,7 +413,7 @@ public function newPivotStatement() */ public function newPivotStatementForId($id) { - return $this->newPivotQuery()->whereIn($this->getQualifiedRelatedPivotKeyName(), $this->parseIds($id)); + return $this->newPivotQuery()->where($this->relatedPivotKey, $id); } /** @@ -574,29 +421,25 @@ public function newPivotStatementForId($id) * * @return \Illuminate\Database\Query\Builder */ - public function newPivotQuery() + protected function newPivotQuery() { $query = $this->newPivotStatement(); foreach ($this->pivotWheres as $arguments) { - $query->where(...$arguments); + call_user_func_array([$query, 'where'], $arguments); } foreach ($this->pivotWhereIns as $arguments) { - $query->whereIn(...$arguments); - } - - foreach ($this->pivotWhereNulls as $arguments) { - $query->whereNull(...$arguments); + call_user_func_array([$query, 'whereIn'], $arguments); } - return $query->where($this->getQualifiedForeignPivotKeyName(), $this->parent->{$this->parentKey}); + return $query->where($this->foreignPivotKey, $this->parent->getKey()); } /** * Set the columns on the pivot table to retrieve. * - * @param mixed $columns + * @param array|mixed $columns * @return $this */ public function withPivot($columns) @@ -617,31 +460,18 @@ public function withPivot($columns) protected function parseIds($value) { if ($value instanceof Model) { - return [$value->{$this->relatedKey}]; + return $value->getKey(); } - if ($value instanceof EloquentCollection) { - return $value->pluck($this->relatedKey)->all(); + if ($value instanceof Collection) { + return $value->modelKeys(); } - if ($value instanceof BaseCollection || is_array($value)) { - return (new BaseCollection($value)) - ->map(fn ($item) => $item instanceof Model ? $item->{$this->relatedKey} : $item) - ->all(); + if ($value instanceof BaseCollection) { + return $value->toArray(); } - 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; + return $value; } /** @@ -652,52 +482,19 @@ protected function parseId($value) */ protected function castKeys(array $keys) { - return array_map(function ($v) { + return (array) array_map(function ($v) { return $this->castKey($v); }, $keys); } /** - * Cast the given key to convert to primary key type. + * Cast the given key to an integer if it is numeric. * * @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, - }; + return is_numeric($key) ? (int) $key : (string) $key; } } 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..3fb7a7cc25 100644 --- a/Eloquent/Relations/HasManyThrough.php +++ b/Eloquent/Relations/HasManyThrough.php @@ -2,40 +2,138 @@ 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\Eloquent\Collection; +use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Database\Eloquent\ModelNotFoundException; + +class HasManyThrough extends Relation { - use InteractsWithDictionary; + /** + * The "through" parent model instance. + * + * @var \Illuminate\Database\Eloquent\Model + */ + protected $throughParent; /** - * Convert the relationship to a "has one through" relationship. + * The far parent model instance. * - * @return \Illuminate\Database\Eloquent\Relations\HasOneThrough + * @var \Illuminate\Database\Eloquent\Model */ - public function one() + 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; + + /** + * 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 $throughParent + * @param string $firstKey + * @param string $secondKey + * @param string $localKey + * @return void + */ + public function __construct(Builder $query, Model $farParent, Model $throughParent, $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; + $this->throughParent = $throughParent; + + parent::__construct($query, $throughParent); + } + + /** + * Set the base constraints on the relation query. + * + * @return void + */ + public function addConstraints() + { + $localValue = $this->farParent[$this->localKey]; + + $this->performJoin(); + + if (static::$constraints) { + $this->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 = $query ?: $this->query; + + $farKey = $this->getQualifiedFarKeyName(); + + $query->join($this->throughParent->getTable(), $this->getQualifiedParentKeyName(), '=', $farKey); + + if ($this->throughParentSoftDeletes()) { + $query->whereNull($this->throughParent->getQualifiedDeletedAtColumn()); + } + } + + /** + * Determine whether "through" parent of the relation uses Soft Deletes. + * + * @return bool + */ + public function throughParentSoftDeletes() + { + return in_array(SoftDeletes::class, class_uses_recursive( + get_class($this->throughParent) )); } - /** @inheritDoc */ + /** + * Set the constraints for an eager load of the relation. + * + * @param array $models + * @return void + */ + public function addEagerConstraints(array $models) + { + $this->query->whereIn( + $this->getQualifiedFirstKeyName(), $this->getKeys($models, $this->localKey) + ); + } + + /** + * 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 +143,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,9 +159,7 @@ 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)); - - if ($key !== null && isset($dictionary[$key])) { + if (isset($dictionary[$key = $model->getKey()])) { $model->setRelation( $relation, $this->related->newCollection($dictionary[$key]) ); @@ -66,11 +169,287 @@ public function match(array $models, EloquentCollection $results, $relation) 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 = []; + + // 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->{$this->firstKey}][] = $result; + } + + return $dictionary; + } + + /** + * 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); + } + + return $instance; + } + + /** + * 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)->save(); + + return $instance; + } + + /** + * 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)->setModel(get_class($this->related)); + } + + /** + * 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); + } + + return $this->where( + $this->getRelated()->getQualifiedKeyName(), '=', $id + )->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(); + } + + 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 \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->related)); + } + + /** + * 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 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; + + $builder = $this->query->applyScopes(); + + $models = $builder->addSelect( + $this->shouldSelect($columns) + )->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); + } + + /** + * 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->shouldSelect($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 + * @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); + } + + /** + * Set the select clause for the relation query. + * + * @param array $columns + * @return array + */ + protected function shouldSelect(array $columns = ['*']) + { + if ($columns == ['*']) { + $columns = [$this->related->getTable().'.*']; + } + + return array_merge($columns, [$this->getQualifiedFirstKeyName()]); + } + + /** + * Add the constraints for a relationship query. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parentQuery + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder + */ + public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) + { + $this->performJoin($query); + + return $query->select($columns)->whereColumn( + $this->getExistenceCompareKey(), '=', $this->getQualifiedFirstKeyName() + ); + } + + /** + * Get the key for comparing against the parent key in "has" query. + * + * @return string + */ + public function getExistenceCompareKey() + { + return $this->farParent->getQualifiedKeyName(); + } + + /** + * Get the qualified foreign key on the related model. + * + * @return string + */ + public function getQualifiedFarKeyName() + { + return $this->getQualifiedForeignKeyName(); + } + + /** + * Get the qualified foreign key on the related model. + * + * @return string + */ + public function getQualifiedForeignKeyName() + { + return $this->related->getTable().'.'.$this->secondKey; + } + + /** + * Get the qualified foreign key on the "through" model. + * + * @return string + */ + public function getQualifiedFirstKeyName() + { + return $this->throughParent->getTable().'.'.$this->firstKey; } } diff --git a/Eloquent/Relations/HasOne.php b/Eloquent/Relations/HasOne.php index 911d4e26c7..a118f296be 100755 --- a/Eloquent/Relations/HasOne.php +++ b/Eloquent/Relations/HasOne.php @@ -2,36 +2,37 @@ 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; + /** + * Indicates if a default model instance should be used. + * + * Alternatively, may be a Closure or array. + * + * @var \Closure|array|bool + */ + protected $withDefault; - /** @inheritDoc */ + /** + * Get the results of the relationship. + * + * @return mixed + */ public function getResults() { - if (is_null($this->getParentKey())) { - return $this->getDefaultFor($this->parent); - } - return $this->query->first() ?: $this->getDefaultFor($this->parent); } - /** @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) { @@ -41,78 +42,56 @@ public function initRelation(array $models, $relation) 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 default value for this relation. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param string|null $column - * @param string|null $aggregate - * @return void + * @param \Illuminate\Database\Eloquent\Model $model + * @return \Illuminate\Database\Eloquent\Model|null */ - public function addOneOfManySubQueryConstraints(Builder $query, $column = null, $aggregate = null) + protected function getDefaultFor(Model $model) { - $query->addSelect($this->foreignKey); - } + if (! $this->withDefault) { + return; + } - /** - * Get the columns that should be selected by the one of many subquery. - * - * @return array|string - */ - public function getOneOfManySubQuerySelectColumns() - { - return $this->foreignKey; - } + $instance = $this->related->newInstance()->setAttribute( + $this->getForeignKeyName(), $model->getAttribute($this->localKey) + ); - /** - * Add join query constraints for one of many relationships. - * - * @param \Illuminate\Database\Query\JoinClause $join - * @return void - */ - public function addOneOfManyJoinSubQueryConstraints(JoinClause $join) - { - $join->on($this->qualifySubSelectColumn($this->foreignKey), '=', $this->qualifyRelatedColumn($this->foreignKey)); + if (is_callable($this->withDefault)) { + return call_user_func($this->withDefault, $instance) ?: $instance; + } + + if (is_array($this->withDefault)) { + $instance->forceFill($this->withDefault); + } + + return $instance; } /** - * Make a new related instance for the given model. + * Match the eagerly loaded results to their parents. * - * @param TDeclaringModel $parent - * @return TRelatedModel + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results + * @param string $relation + * @return array */ - public function newRelatedInstanceFor(Model $parent) + public function match(array $models, Collection $results, $relation) { - return tap($this->related->newInstance(), function ($instance) use ($parent) { - $instance->setAttribute($this->getForeignKeyName(), $parent->{$this->localKey}); - $this->applyInverseRelationToModel($instance, $parent); - }); + return $this->matchOne($models, $results, $relation); } /** - * Get the value of the model's foreign key. + * Return a new model instance in case the relationship does not exist. * - * @param TRelatedModel $model - * @return int|string + * @param \Closure|array|bool $callback + * @return $this */ - protected function getRelatedKeyFrom(Model $model) + public function withDefault($callback = true) { - return $model->getAttribute($this->getForeignKeyName()); + $this->withDefault = $callback; + + return $this; } } diff --git a/Eloquent/Relations/HasOneOrMany.php b/Eloquent/Relations/HasOneOrMany.php index ba32c0e3b3..d115e8d35a 100755 --- a/Eloquent/Relations/HasOneOrMany.php +++ b/Eloquent/Relations/HasOneOrMany.php @@ -2,26 +2,12 @@ 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\Eloquent\Collection; + abstract class HasOneOrMany extends Relation { - use InteractsWithDictionary, SupportsInverseRelations; - /** * The foreign key of the parent model. * @@ -36,13 +22,21 @@ abstract class HasOneOrMany extends Relation */ protected $localKey; + /** + * The count of self joins. + * + * @var int + */ + protected static $selfJoinCount = 0; + /** * 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) { @@ -52,37 +46,6 @@ public function __construct(Builder $query, Model $parent, $foreignKey, $localKe parent::__construct($query, $parent); } - /** - * Create and return an un-saved instance of the related model. - * - * @param array $attributes - * @return TRelatedModel - */ - public function make(array $attributes = []) - { - return tap($this->related->newInstance($attributes), function ($instance) { - $this->setForeignAttributesForCreate($instance); - $this->applyInverseRelationToModel($instance); - }); - } - - /** - * Create and return an un-saved instance of the related models. - * - * @param iterable $records - * @return \Illuminate\Database\Eloquent\Collection - */ - public function makeMany($records) - { - $instances = $this->related->newCollection(); - - foreach ($records as $record) { - $instances->push($this->make($record)); - } - - return $instances; - } - /** * Set the base constraints on the relation query. * @@ -91,36 +54,34 @@ public function makeMany($records) public function addConstraints() { if (static::$constraints) { - $query = $this->getRelationQuery(); - - $query->where($this->foreignKey, '=', $this->getParentKey()); + $this->query->where($this->foreignKey, '=', $this->getParentKey()); - $query->whereNotNull($this->foreignKey); + $this->query->whereNotNull($this->foreignKey); } } - /** @inheritDoc */ + /** + * 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 +89,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 +102,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 +116,10 @@ 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); - - $model->setRelation($relation, $related); - - // Apply the inverse relation if we have one... - $type === 'one' - ? $this->applyInverseRelationToModel($related, $model) - : $this->applyInverseRelationToCollection($related, $model); + if (isset($dictionary[$key = $model->getAttribute($this->localKey)])) { + $model->setRelation( + $relation, $this->getRelationValue($dictionary, $key, $type) + ); } } @@ -175,7 +129,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,49 +138,44 @@ 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->getForeignKeyName(); - 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. + * Find a model by its primary key or return 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) + * @return \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model */ public function findOrNew($id, $columns = ['*']) { if (is_null($instance = $this->find($id, $columns))) { $instance = $this->related->newInstance(); - $this->setForeignAttributesForCreate($instance); + $instance->setAttribute($this->getForeignKeyName(), $this->getParentKey()); } return $instance; @@ -236,122 +185,68 @@ public function findOrNew($id, $columns = ['*']) * Get the first related model record matching the attributes or instantiate it. * * @param array $attributes - * @param array $values - * @return TRelatedModel + * @return \Illuminate\Database\Eloquent\Model */ - public function firstOrNew(array $attributes = [], array $values = []) + public function firstOrNew(array $attributes) { if (is_null($instance = $this->where($attributes)->first())) { - $instance = $this->related->newInstance(array_merge($attributes, $values)); + $instance = $this->related->newInstance($attributes); - $this->setForeignAttributesForCreate($instance); + $instance->setAttribute($this->getForeignKeyName(), $this->getParentKey()); } return $instance; } /** - * Get the first record matching the attributes. If the record is not found, create it. + * Get the first related record matching the attributes or create it. * * @param array $attributes - * @param (\Closure(): array)|array $values - * @return TRelatedModel + * @return \Illuminate\Database\Eloquent\Model */ - public function firstOrCreate(array $attributes = [], Closure|array $values = []) + public function firstOrCreate(array $attributes) { - if (is_null($instance = (clone $this)->where($attributes)->first())) { - $instance = $this->createOrFirst($attributes, $values); + if (is_null($instance = $this->where($attributes)->first())) { + $instance = $this->create($attributes); } 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 + * @return \Illuminate\Database\Eloquent\Model */ 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]; - } + return tap($this->firstOrNew($attributes), function ($instance) use ($values) { + $instance->fill($values); - foreach ($values as $key => $value) { - $values[$key][$this->getForeignKeyName()] = $this->getParentKey(); - } - - return $this->getQuery()->upsert($values, $uniqueBy, $update); + $instance->save(); + }); } /** * 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|false */ public function save(Model $model) { - $this->setForeignAttributesForCreate($model); + $model->setAttribute($this->getForeignKeyName(), $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) { @@ -362,78 +257,28 @@ public function saveMany($models) return $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. * * @param array $attributes - * @return TRelatedModel + * @return \Illuminate\Database\Eloquent\Model */ - public function create(array $attributes = []) + public function create(array $attributes) { return tap($this->related->newInstance($attributes), function ($instance) { - $this->setForeignAttributesForCreate($instance); + $instance->setAttribute($this->getForeignKeyName(), $this->getParentKey()); $instance->save(); - - $this->applyInverseRelationToModel($instance); }); } - /** - * Create a new instance of the related model without raising any events to the parent model. - * - * @param array $attributes - * @return TRelatedModel - */ - public function createQuietly(array $attributes = []) - { - return Model::withoutEvents(fn () => $this->create($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(); - - return $this->applyInverseRelationToModel($this->related->forceCreate($attributes)); - } - - /** - * Create a new instance of the related model with mass assignment without raising model events. - * - * @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 + * @param array $records + * @return \Illuminate\Database\Eloquent\Collection */ - public function createMany(iterable $records) + public function createMany(array $records) { $instances = $this->related->newCollection(); @@ -445,66 +290,28 @@ public function createMany(iterable $records) } /** - * Create a Collection of new instances of the related model without raising any events to the parent model. - * - * @param iterable $records - * @return \Illuminate\Database\Eloquent\Collection - */ - public function createManyQuietly(iterable $records) - { - return Model::withoutEvents(fn () => $this->createMany($records)); - } - - /** - * Create a Collection of new instances of the related model, allowing mass-assignment. + * Perform an update on all the related models. * - * @param iterable $records - * @return \Illuminate\Database\Eloquent\Collection + * @param array $attributes + * @return int */ - public function forceCreateMany(iterable $records) + public function update(array $attributes) { - $instances = $this->related->newCollection(); - - foreach ($records as $record) { - $instances->push($this->forceCreate($record)); + if ($this->related->usesTimestamps()) { + $attributes[$this->relatedUpdatedAt()] = $this->related->freshTimestampString(); } - return $instances; + return $this->query->update($attributes); } /** - * Create a Collection of new instances of the related model, allowing mass-assignment and without raising any events to the parent model. + * 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 $parentQuery + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder */ - public function forceCreateManyQuietly(iterable $records) - { - return Model::withoutEvents(fn () => $this->forceCreateMany($records)); - } - - /** - * Set the foreign ID for creating a related model. - * - * @param TRelatedModel $model - * @return void - */ - protected function setForeignAttributesForCreate(Model $model) - { - $model->setAttribute($this->getForeignKeyName(), $this->getParentKey()); - - foreach ($this->getQuery()->pendingAttributes as $key => $value) { - $attributes ??= $model->getAttributes(); - - if (! array_key_exists($key, $attributes)) { - $model->setAttribute($key, $value); - } - } - - $this->applyInverseRelationToModel($model); - } - - /** @inheritDoc */ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) { if ($query->getQuery()->from == $parentQuery->getQuery()->from) { @@ -517,10 +324,10 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, /** * 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 + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parentQuery + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder */ public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*']) { @@ -534,31 +341,13 @@ public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder } /** - * Alias to set the "limit" value of the query. + * Get a relationship join table hash. * - * @param int $value - * @return $this + * @return string */ - public function take($value) + public function getRelationCountHash() { - return $this->limit($value); - } - - /** - * Set the "limit" value of the query. - * - * @param int $value - * @return $this - */ - public function limit($value) - { - if ($this->parent->exists) { - $this->query->limit($value); - } else { - $this->query->groupLimit($value, $this->getExistenceCompareKey()); - } - - return $this; + return 'laravel_reserved_'.static::$selfJoinCount++; } /** @@ -582,13 +371,13 @@ public function getParentKey() } /** - * Get the fully-qualified parent key name. + * Get the fully qualified parent key name. * * @return string */ public function getQualifiedParentKeyName() { - return $this->parent->qualifyColumn($this->localKey); + return $this->parent->getTable().'.'.$this->localKey; } /** @@ -600,7 +389,7 @@ public function getForeignKeyName() { $segments = explode('.', $this->getQualifiedForeignKeyName()); - return array_last($segments); + return $segments[count($segments) - 1]; } /** @@ -612,14 +401,4 @@ public function getQualifiedForeignKeyName() { return $this->foreignKey; } - - /** - * Get the local key for the relationship. - * - * @return string - */ - public function getLocalKeyName() - { - return $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..361f2aef49 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,83 +48,155 @@ 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 */ + /** + * 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. + * Find a related 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 forceCreate(array $attributes = []) + public function findOrNew($id, $columns = ['*']) { - $attributes[$this->getForeignKeyName()] = $this->getParentKey(); - $attributes[$this->getMorphType()] = $this->morphClass; + if (is_null($instance = $this->find($id, $columns))) { + $instance = $this->related->newInstance(); + + // 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); + } - return $this->applyInverseRelationToModel($this->related->forceCreate($attributes)); + return $instance; } /** - * Set the foreign ID and type for creating a related model. + * Get the first related model record matching the attributes or instantiate it. * - * @param TRelatedModel $model - * @return void + * @param array $attributes + * @return \Illuminate\Database\Eloquent\Model */ - protected function setForeignAttributesForCreate(Model $model) + public function firstOrNew(array $attributes) { - $model->{$this->getForeignKeyName()} = $this->getParentKey(); + if (is_null($instance = $this->where($attributes)->first())) { + $instance = $this->related->newInstance($attributes); - $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 record matching the attributes or create it. + * + * @param array $attributes + * @return \Illuminate\Database\Eloquent\Model + */ + public function firstOrCreate(array $attributes) + { + if (is_null($instance = $this->where($attributes)->first())) { + $instance = $this->create($attributes); } - $this->applyInverseRelationToModel($model); + return $instance; } /** - * Insert new records or update the existing ones. + * Create or update a related record matching the attributes, and fill it with values. * + * @param array $attributes * @param array $values - * @param array|string $uniqueBy - * @param array|null $update - * @return int + * @return \Illuminate\Database\Eloquent\Model */ - public function upsert(array $values, $uniqueBy, $update = null) + public function updateOrCreate(array $attributes, array $values = []) { - if (! empty($values) && ! is_array(array_first($values))) { - $values = [$values]; - } + return tap($this->firstOrNew($attributes), function ($instance) use ($values) { + $instance->fill($values); - foreach ($values as $key => $value) { - $values[$key][$this->getMorphType()] = $this->getMorphClass(); - } + $instance->save(); + }); + } + + /** + * Attach a model instance to the parent model. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return \Illuminate\Database\Eloquent\Model + */ + public function save(Model $model) + { + $model->setAttribute($this->getMorphType(), $this->morphClass); + + return parent::save($model); + } + + /** + * 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; + } + + /** + * Set the foreign ID and type for creating a related model. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return void + */ + protected function setForeignAttributesForCreate(Model $model) + { + $model->{$this->getForeignKeyName()} = $this->getParentKey(); - return parent::upsert($values, $uniqueBy, $update); + $model->{$this->getMorphType()} = $this->morphClass; } - /** @inheritDoc */ + /** + * Get the relationship query. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parentQuery + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder + */ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) { return parent::getRelationExistenceQuery($query, $parentQuery, $columns)->where( - $query->qualifyColumn($this->getMorphType()), $this->morphClass + $this->morphType, $this->morphClass ); } @@ -158,23 +223,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..93a1a17064 100644 --- a/Eloquent/Relations/MorphTo.php +++ b/Eloquent/Relations/MorphTo.php @@ -3,22 +3,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 +16,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; @@ -54,36 +37,16 @@ class MorphTo extends BelongsTo */ 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. - * - * @var array - */ - protected $morphableConstraints = []; - /** * 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 $ownerKey * @param string $type * @param string $relation + * @return void */ public function __construct(Builder $query, Model $parent, $foreignKey, $ownerKey, $type, $relation) { @@ -92,43 +55,48 @@ public function __construct(Builder $query, Model $parent, $foreignKey, $ownerKe parent::__construct($query, $parent, $foreignKey, $ownerKey, $relation); } - /** @inheritDoc */ - #[\Override] + /** + * 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. + * + * @return mixed + */ + public function getResults() + { + return $this->ownerKey ? $this->query->first() : null; + } + /** * Get the results of the relationship. * * Called via eager load method of Eloquent query builder. * - * @return \Illuminate\Database\Eloquent\Collection + * @return mixed */ public function getEager() { @@ -143,32 +111,18 @@ public function getEager() * Get all of the relation results for a type. * * @param string $type - * @return \Illuminate\Database\Eloquent\Collection + * @return \Illuminate\Database\Eloquent\Collection */ protected function getResultsByType($type) { $instance = $this->createModelByType($type); - $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); - } + ->mergeConstraintsFrom($this->getQuery()) + ->with($this->getQuery()->getEagerLoads()); - $whereIn = $this->whereInMethod($instance, $ownerKey); - - return $query->{$whereIn}( - $instance->qualifyColumn($ownerKey), $this->gatherKeysByType($type, $instance->getKeyType()) + return $query->whereIn( + $instance->getTable().'.'.$instance->getKeyName(), $this->gatherKeysByType($type) )->get(); } @@ -176,38 +130,37 @@ protected function getResultsByType($type) * Gather all of the foreign keys for a given type. * * @param string $type - * @param string $keyType * @return array */ - protected function gatherKeysByType($type, $keyType) + protected function gatherKeysByType($type) { - return $keyType !== 'string' - ? array_keys($this->dictionary[$type]) - : array_map(function ($modelId) { - return (string) $modelId; - }, array_filter(array_keys($this->dictionary[$type]))); + return collect($this->dictionary[$type])->map(function ($models) { + return head($models)->{$this->foreignKey}; + })->values()->unique()->all(); } /** * Create a new model instance by type. * * @param string $type - * @return TRelatedModel + * @return \Illuminate\Database\Eloquent\Model */ public function createModelByType($type) { $class = Model::getActualClassNameForMorph($type); - return tap(new $class, function ($instance) { - if (! $instance->getConnectionName()) { - $instance->setConnection($this->getConnection()->getName()); - } - }); + return new $class; } - /** @inheritDoc */ - #[\Override] - 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 $models; } @@ -216,17 +169,15 @@ public function match(array $models, EloquentCollection $results, $relation) * 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); } } } @@ -235,58 +186,30 @@ protected function matchToMorphParents($type, EloquentCollection $results) /** * Associate the model instance to the given parent. * - * @param TRelatedModel|null $model - * @return TDeclaringModel + * @param \Illuminate\Database\Eloquent\Model $model + * @return \Illuminate\Database\Eloquent\Model */ - #[\Override] public function associate($model) { - if ($model instanceof Model) { - $foreignKey = $this->ownerKey && $model->{$this->ownerKey} - ? $this->ownerKey - : $model->getKeyName(); - } + $this->parent->setAttribute($this->foreignKey, $model->getKey()); - $this->parent->setAttribute( - $this->foreignKey, $model instanceof Model ? $model->{$foreignKey} : null - ); + $this->parent->setAttribute($this->morphType, $model->getMorphClass()); - $this->parent->setAttribute( - $this->morphType, $model instanceof Model ? $model->getMorphClass() : null - ); - - return $this->parent->setRelation($this->relationName, $model); + return $this->parent->setRelation($this->relation, $model); } /** * Dissociate previously associated model from the given parent. * - * @return TDeclaringModel + * @return \Illuminate\Database\Eloquent\Model */ - #[\Override] public function dissociate() { $this->parent->setAttribute($this->foreignKey, null); $this->parent->setAttribute($this->morphType, null); - return $this->parent->setRelation($this->relationName, null); - } - - /** @inheritDoc */ - #[\Override] - public function touch() - { - if (! is_null($this->getParentKey())) { - parent::touch(); - } - } - - /** @inheritDoc */ - #[\Override] - protected function newRelatedInstanceFor(Model $parent) - { - return $parent->{$this->getRelationName()}()->getRelated()->newInstance(); + return $this->parent->setRelation($this->relation, null); } /** @@ -309,107 +232,11 @@ public function getDictionary() return $this->dictionary; } - /** - * 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. - * - * @return $this - */ - public function withTrashed() - { - $callback = fn ($query) => $query->hasMacro('withTrashed') ? $query->withTrashed() : $query; - - $this->macroBuffer[] = [ - 'method' => 'when', - 'parameters' => [true, $callback], - ]; - - return $this->when(true, $callback); - } - - /** - * 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); - } - /** * Replay stored macro calls on the actual related instance. * - * @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) { @@ -420,40 +247,23 @@ protected function replayMacros(Builder $query) 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 + * @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; + return parent::__call($method, $parameters); } // 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) { + catch (BadMethodCallException $e) { $this->macroBuffer[] = compact('method', 'parameters'); return $this; diff --git a/Eloquent/Relations/MorphToMany.php b/Eloquent/Relations/MorphToMany.php index dd0680c980..9d14c4471d 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,29 +34,21 @@ 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 $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, $foreignPivotKey, + $relatedPivotKey, $parentKey, $relatedKey, $relationName = null, $inverse = false) + { $this->inverse = $inverse; $this->morphType = $name.'_type'; $this->morphClass = $inverse ? $query->getModel()->getMorphClass() : $parent->getMorphClass(); @@ -85,23 +68,28 @@ protected function addWhereConstraints() { parent::addWhereConstraints(); - $this->query->where($this->qualifyPivotColumn($this->morphType), $this->morphClass); + $this->query->where($this->table.'.'.$this->morphType, $this->morphClass); return $this; } - /** @inheritDoc */ + /** + * Set the constraints for an eager load of the relation. + * + * @param array $models + * @return void + */ public function addEagerConstraints(array $models) { parent::addEagerConstraints($models); - $this->query->where($this->qualifyPivotColumn($this->morphType), $this->morphClass); + $this->query->where($this->table.'.'.$this->morphType, $this->morphClass); } /** * Create a new pivot attachment record. * - * @param int $id + * @param int $id * @param bool $timed * @return array */ @@ -112,28 +100,19 @@ protected function baseAttachRecord($id, $timed) ); } - /** @inheritDoc */ - public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) - { - return parent::getRelationExistenceQuery($query, $parentQuery, $columns)->where( - $this->qualifyPivotColumn($this->morphType), $this->morphClass - ); - } - /** - * Get the pivot models that are currently attached, filtered by related model keys. + * Add the constraints for a relationship count query. * - * @param mixed $ids - * @return \Illuminate\Support\Collection + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parentQuery + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder */ - protected function getCurrentlyAttachedPivotsForIds($ids = null) + public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) { - return parent::getCurrentlyAttachedPivotsForIds($ids)->map(function ($record) { - return $record instanceof MorphPivot - ? $record->setMorphType($this->morphType) - ->setMorphClass($this->morphClass) - : $record; - }); + return parent::getRelationExistenceQuery($query, $parentQuery, $columns)->where( + $this->table.'.'.$this->morphType, $this->morphClass + ); } /** @@ -141,7 +120,7 @@ protected function getCurrentlyAttachedPivotsForIds($ids = null) * * @return \Illuminate\Database\Query\Builder */ - public function newPivotQuery() + protected function newPivotQuery() { return parent::newPivotQuery()->where($this->morphType, $this->morphClass); } @@ -150,47 +129,23 @@ public function newPivotQuery() * 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; - $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 = $using ? $using::fromRawAttributes($this->parent, $attributes, $this->table, $exists) + : new MorphPivot($this->parent, $attributes, $this->table, $exists); $pivot->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey) - ->setRelatedModel($this->related) - ->setMorphType($this->morphType) - ->setMorphClass($this->morphClass); + ->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 +156,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..5b817c423d 100755 --- a/Eloquent/Relations/Pivot.php +++ b/Eloquent/Relations/Pivot.php @@ -3,23 +3,196 @@ 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 + */ + public $pivotParent; + + /** + * The name of the foreign key column. + * + * @var string + */ + protected $foreignKey; /** - * Indicates if the IDs are auto-incrementing. + * The name of the "other key" column. * - * @var bool + * @var string */ - public $incrementing = false; + protected $relatedKey; /** * 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->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. + $this->pivotParent = $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->relatedKey, $this->getAttribute($this->relatedKey)); + } + + /** + * 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() + { + return $this->newQuery()->where([ + $this->foreignKey => $this->getAttribute($this->foreignKey), + $this->relatedKey => $this->getAttribute($this->relatedKey), + ]); + } + + /** + * 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; + } + + /** + * 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->pivotParent->getCreatedAtColumn(); + } + + /** + * Get the name of the "updated at" column. + * + * @return string + */ + public function getUpdatedAtColumn() + { + return $this->pivotParent->getUpdatedAtColumn(); + } } diff --git a/Eloquent/Relations/Relation.php b/Eloquent/Relations/Relation.php index 5f0ced5081..acbfbede1a 100755 --- a/Eloquent/Relations/Relation.php +++ b/Eloquent/Relations/Relation.php @@ -3,58 +3,40 @@ 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\Query\Expression; -use Illuminate\Support\Collection as BaseCollection; -use Illuminate\Support\Traits\ForwardsCalls; use Illuminate\Support\Traits\Macroable; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Query\Expression; +use Illuminate\Database\Eloquent\Collection; -/** - * @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 +abstract class Relation { - use ForwardsCalls, Macroable { - Macroable::__call as macroCall; + use Macroable { + __call as macroCall; } /** * 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 +45,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. + * An array to map class names to their morph names in database. * - * @var bool + * @var array */ - protected static $requireMorphMap = false; - - /** - * The count of self joins. - * - * @var int - */ - 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) { @@ -101,10 +70,8 @@ public function __construct(Builder $query, Model $parent) /** * Run a callback with constraints disabled on the relation. * - * @template TReturn of mixed - * - * @param Closure(): TReturn $callback - * @return TReturn + * @param \Closure $callback + * @return mixed */ public static function noConstraints(Closure $callback) { @@ -116,7 +83,7 @@ public static function noConstraints(Closure $callback) // 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(); + return call_user_func($callback); } finally { static::$constraints = $previous; } @@ -132,7 +99,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 +107,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 +147,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,21 +160,21 @@ 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 $parentQuery + * @return \Illuminate\Database\Eloquent\Builder */ public function getRelationExistenceCountQuery(Builder $query, Builder $parentQuery) { return $this->getRelationExistenceQuery( $query, $parentQuery, new Expression('count(*)') - )->setBindings([], 'select'); + ); } /** @@ -258,10 +182,10 @@ public function getRelationExistenceCountQuery(Builder $query, Builder $parentQu * * 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 $parentQuery + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder */ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) { @@ -270,45 +194,24 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, ); } - /** - * Get a relationship join table hash. - * - * @param bool $incrementJoinCount - * @return string - */ - public function getRelationCountHash($incrementJoinCount = true) - { - return 'laravel_reserved_'.($incrementJoinCount ? static::$selfJoinCount++ : static::$selfJoinCount); - } - /** * 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 collect($models)->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; + })->values()->unique()->sort()->all(); } /** * Get the underlying query for the relation. * - * @return \Illuminate\Database\Eloquent\Builder + * @return \Illuminate\Database\Eloquent\Builder */ public function getQuery() { @@ -325,20 +228,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 +239,7 @@ public function getParent() } /** - * Get the fully-qualified parent key name. + * Get the fully qualified parent key name. * * @return string */ @@ -358,7 +251,7 @@ public function getQualifiedParentKeyName() /** * Get the related model of the relation. * - * @return TRelatedModel + * @return \Illuminate\Database\Eloquent\Model */ public function getRelated() { @@ -395,89 +288,20 @@ public function relatedUpdatedAt() return $this->related->getUpdatedAtColumn(); } - /** - * 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. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @param string $key - * @return string - */ - protected function whereInMethod(Model $model, $key) - { - 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); - } - /** * 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; + ? array_merge(static::$morphMap, $map) : $map; } return static::$morphMap; @@ -486,12 +310,12 @@ 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; } @@ -500,33 +324,11 @@ protected static function buildMorphMapFromModels(?array $models = null) }, $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; - } - /** * Handle dynamic method calls to the relationship. * * @param string $method - * @param array $parameters + * @param array $parameters * @return mixed */ public function __call($method, $parameters) @@ -535,7 +337,13 @@ public function __call($method, $parameters) return $this->macroCall($method, $parameters); } - return $this->forwardDecoratedCallTo($this->query, $method, $parameters); + $result = call_user_func_array([$this->query, $method], $parameters); + + if ($result === $this->query) { + return $this; + } + + 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..fdca75ce11 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(); - } - - $ids = is_array($ids) ? $ids : func_get_args(); - - if (count($ids) === 0) { - return 0; - } + $deleted = $this->delete(); - // 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(); + $this->forceDeleting = false; - $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(); + $query = $this->newQueryWithoutScopes()->where($this->getKeyName(), $this->getKey()); - $columns = [$this->getDeletedAtColumn() => $this->fromDateTime($time)]; + $this->{$this->getDeletedAtColumn()} = $time = $this->freshTimestamp(); - $this->{$this->getDeletedAtColumn()} = $time; - - if ($this->usesTimestamps() && ! is_null($this->getUpdatedAtColumn())) { - $this->{$this->getUpdatedAtColumn()} = $time; - - $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,20 +104,9 @@ public function trashed() } /** - * Register a "softDeleted" model event callback with the dispatcher. - * - * @param \Illuminate\Events\QueuedClosure|callable|class-string $callback - * @return void - */ - public static function softDeleted($callback) - { - static::registerModelEvent('trashed', $callback); - } - - /** - * Register a "restoring" 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 restoring($callback) @@ -228,9 +115,9 @@ public static function restoring($callback) } /** - * Register a "restored" model event callback with the dispatcher. + * Register a restored model event with the dispatcher. * - * @param \Illuminate\Events\QueuedClosure|callable|class-string $callback + * @param \Closure|string $callback * @return void */ public static function restored($callback) @@ -238,28 +125,6 @@ public static function restored($callback) static::registerModelEvent('restored', $callback); } - /** - * 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); - } - - /** - * Register a "forceDeleted" model event callback with the dispatcher. - * - * @param \Illuminate\Events\QueuedClosure|callable|class-string $callback - * @return void - */ - public static function forceDeleted($callback) - { - static::registerModelEvent('forceDeleted', $callback); - } - /** * Determine if the model is currently force deleting. * @@ -277,16 +142,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..cf5ded1e84 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 = ['Restore', 'WithTrashed', 'WithoutTrashed', '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,12 +47,12 @@ 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(); } @@ -64,7 +62,7 @@ protected function getDeletedAtColumn(Builder $builder) /** * Add the restore extension to the builder. * - * @param \Illuminate\Database\Eloquent\Builder<*> $builder + * @param \Illuminate\Database\Eloquent\Builder $builder * @return void */ protected function addRestore(Builder $builder) @@ -76,53 +74,15 @@ protected function addRestore(Builder $builder) }); } - /** - * 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. - * - * @param \Illuminate\Database\Eloquent\Builder<*> $builder - * @return void - */ - protected function addCreateOrRestore(Builder $builder) - { - $builder->macro('createOrRestore', function (Builder $builder, array $attributes = [], array $values = []) { - $builder->withTrashed(); - - return tap($builder->createOrFirst($attributes, $values), function ($instance) { - $instance->restore(); - }); - }); - } - /** * 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); }); } @@ -130,7 +90,7 @@ protected function addWithTrashed(Builder $builder) /** * Add the without-trashed extension to the builder. * - * @param \Illuminate\Database\Eloquent\Builder<*> $builder + * @param \Illuminate\Database\Eloquent\Builder $builder * @return void */ protected function addWithoutTrashed(Builder $builder) @@ -149,7 +109,7 @@ protected function addWithoutTrashed(Builder $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 index 43f02a0e26..2f603235da 100644 --- a/Events/StatementPrepared.php +++ b/Events/StatementPrepared.php @@ -4,15 +4,30 @@ class StatementPrepared { + /** + * The database connection instance. + * + * @var \Illuminate\Database\Connection + */ + public $connection; + + /** + * The PDO statement. + * + * @var \PDOStatement + */ + public $statement; + /** * Create a new event instance. * - * @param \Illuminate\Database\Connection $connection The database connection instance. - * @param \PDOStatement $statement The PDO statement. + * @param \Illuminate\Database\Connection $connection + * @param \PDOStatement $statement + * @return void */ - public function __construct( - public $connection, - public $statement, - ) { + public function __construct($connection, $statement) + { + $this->statement = $statement; + $this->connection = $connection; } } diff --git a/Events/TransactionCommitting.php b/Events/TransactionCommitting.php deleted file mode 100644 index 9b8179d32d..0000000000 --- a/Events/TransactionCommitting.php +++ /dev/null @@ -1,8 +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); + if (! $this->isExpression($table)) { + return $this->wrap($this->tablePrefix.$table, true); } - $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->getValue($table); } /** * 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) { + return $this->wrapAliasedValue($value, $prefixAlias); } return $this->wrapSegments(explode('.', $value)); @@ -108,43 +66,37 @@ public function wrap($value) * Wrap a value that has an alias. * * @param string $value + * @param bool $prefixAlias * @return string */ - protected function wrapAliasedValue($value) + protected function wrapAliasedValue($value, $prefixAlias = false) { $segments = preg_split('/\s+as\s+/i', $value); - return $this->wrap($segments[0]).' as '.$this->wrapValue($segments[1]); - } - - /** - * 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(); + // If we are wrapping a table we need to prefix the alias with the table prefix + // as well in order to generate proper syntax. If this is a column of course + // no prefix is necessary. The condition will be true when from wrapTable. + if ($prefixAlias) { + $segments[1] = $this->tablePrefix.$segments[1]; + } - return $this->wrapTable($segments[0], $prefix).' as '.$this->wrapValue($prefix.$segments[1]); + return $this->wrap( + $segments[0]).' as '.$this->wrapValue($segments[1] + ); } /** * Wrap the given value segments. * - * @param list $segments + * @param array $segments * @return string */ protected function wrapSegments($segments) { - return (new Collection($segments))->map(function ($segment, $key) use ($segments) { + return collect($segments)->map(function ($segment, $key) use ($segments) { return $key == 0 && count($segments) > 1 - ? $this->wrapTable($segment) - : $this->wrapValue($segment); + ? $this->wrapTable($segment) + : $this->wrapValue($segment); })->implode('.'); } @@ -163,56 +115,32 @@ protected function wrapValue($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, '->'); - } - /** * 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) @@ -220,33 +148,6 @@ public function parameter($value) return $this->isExpression($value) ? $this->getValue($value) : '?'; } - /** - * 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 "'$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) - { - return $this->connection->escape($value, $binary); - } - /** * Determine if the given value is a raw expression. * @@ -259,18 +160,14 @@ public function isExpression($value) } /** - * Transforms expressions to their scalar types. + * Get the value of a raw expression. * - * @param \Illuminate\Contracts\Database\Query\Expression|string|int|float $expression - * @return string|int|float + * @param \Illuminate\Database\Query\Expression $expression + * @return string */ public function getValue($expression) { - if ($this->isExpression($expression)) { - return $this->getValue($expression->getValue($this)); - } - - return $expression; + return $expression->getValue(); } /** @@ -286,26 +183,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..a8b92ab6e4 100755 --- a/MigrationServiceProvider.php +++ b/MigrationServiceProvider.php @@ -2,38 +2,19 @@ namespace Illuminate\Database; -use Illuminate\Contracts\Events\Dispatcher; -use Illuminate\Contracts\Support\DeferrableProvider; -use Illuminate\Database\Console\Migrations\FreshCommand; -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\Migrations\DatabaseMigrationRepository; -use Illuminate\Database\Migrations\MigrationCreator; -use Illuminate\Database\Migrations\Migrator; use Illuminate\Support\ServiceProvider; +use Illuminate\Database\Migrations\Migrator; +use Illuminate\Database\Migrations\MigrationCreator; +use Illuminate\Database\Migrations\DatabaseMigrationRepository; -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. @@ -47,8 +28,6 @@ public function register() $this->registerMigrator(); $this->registerCreator(); - - $this->registerCommands($this->commands); } /** @@ -59,9 +38,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 +57,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 +69,7 @@ protected function registerMigrator() protected function registerCreator() { $this->app->singleton('migration.creator', function ($app) { - return new MigrationCreator($app['files'], $app->basePath('stubs')); - }); - } - - /** - * Register the given commands. - * - * @param array $commands - * @return void - */ - protected function registerCommands(array $commands) - { - foreach (array_keys($commands) as $command) { - $this->{"register{$command}Command"}(); - } - - $this->commands(array_values($commands)); - } - - /** - * Register the command. - * - * @return void - */ - protected function registerMigrateCommand() - { - $this->app->singleton(MigrateCommand::class, function ($app) { - return new MigrateCommand($app['migrator'], $app[Dispatcher::class]); - }); - } - - /** - * Register the command. - * - * @return void - */ - protected function registerMigrateFreshCommand() - { - $this->app->singleton(FreshCommand::class, function ($app) { - return new FreshCommand($app['migrator']); - }); - } - - /** - * Register the command. - * - * @return void - */ - protected function registerMigrateInstallCommand() - { - $this->app->singleton(InstallCommand::class, function ($app) { - return new InstallCommand($app['migration.repository']); - }); - } - - /** - * Register the command. - * - * @return void - */ - protected function registerMigrateMakeCommand() - { - $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); - }); - } - - /** - * Register the command. - * - * @return void - */ - protected function registerMigrateRefreshCommand() - { - $this->app->singleton(RefreshCommand::class); - } - - /** - * Register the command. - * - * @return void - */ - protected function registerMigrateResetCommand() - { - $this->app->singleton(ResetCommand::class, function ($app) { - return new ResetCommand($app['migrator']); - }); - } - - /** - * Register the command. - * - * @return void - */ - protected function registerMigrateRollbackCommand() - { - $this->app->singleton(RollbackCommand::class, function ($app) { - return new RollbackCommand($app['migrator']); - }); - } - - /** - * Register the command. - * - * @return void - */ - protected function registerMigrateStatusCommand() - { - $this->app->singleton(StatusCommand::class, function ($app) { - return new StatusCommand($app['migrator']); + return new MigrationCreator($app['files']); }); } @@ -221,8 +80,8 @@ protected function registerMigrateStatusCommand() */ public function provides() { - return array_merge([ - 'migrator', 'migration.repository', 'migration.creator', Migrator::class, - ], array_values($this->commands)); + return [ + 'migrator', 'migration.repository', 'migration.creator', + ]; } } diff --git a/Migrations/DatabaseMigrationRepository.php b/Migrations/DatabaseMigrationRepository.php index 45c4389d27..b0e4a28927 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,35 @@ 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(); + ->orderBy('batch', 'asc') + ->orderBy('migration', 'asc') + ->pluck('migration')->all(); } /** - * Get the list of migrations. + * Get list of migrations. * * @param int $steps - * @return array{id: int, migration: string, batch: int}[] + * @return array */ 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(); + return $query->orderBy('migration', 'desc')->take($steps)->get()->all(); } /** * Get the last migration batch. * - * @return array{id: int, migration: string, batch: int}[] + * @return array */ public function getLast() { @@ -96,24 +78,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 +95,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) @@ -185,18 +154,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 +161,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..29bae7add9 100755 --- a/Migrations/MigrationCreator.php +++ b/Migrations/MigrationCreator.php @@ -3,9 +3,9 @@ namespace Illuminate\Database\Migrations; use Closure; -use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Str; use InvalidArgumentException; +use Illuminate\Filesystem\Filesystem; class MigrationCreator { @@ -16,17 +16,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 +27,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,33 +39,29 @@ 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); + $this->ensureMigrationDoesntAlreadyExist($name); // 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($stub, $table) + $path = $this->getPath($name, $path), + $this->populateStub($name, $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; } @@ -82,69 +70,57 @@ public function create($name, $path, $table = null, $create = false) * 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) + protected function ensureMigrationDoesntAlreadyExist($name) { - 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."); + throw new InvalidArgumentException("A {$className} migration 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->stubPath().'/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->stubPath()."/{$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,7 +130,7 @@ 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) { @@ -176,21 +152,19 @@ protected function getPath($name, $path) /** * 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) diff --git a/Migrations/MigrationRepositoryInterface.php b/Migrations/MigrationRepositoryInterface.php index 49cc08e21a..60bc921f89 100755 --- a/Migrations/MigrationRepositoryInterface.php +++ b/Migrations/MigrationRepositoryInterface.php @@ -5,47 +5,32 @@ 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. + * Get list of migrations. * * @param int $steps - * @return array{id: int, migration: string, batch: int}[] + * @return array */ 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 +38,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 +64,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. + * The notes for the current operation. * - * @var \Symfony\Component\Console\Output\OutputInterface + * @var array */ - protected $output; + protected $notes = []; /** - * The pending migrations to skip. + * The paths to all of the migration files. * - * @var list + * @var array */ - protected static $withoutMigrations = []; + protected $paths = []; /** * Create a new migrator instance. @@ -100,16 +58,13 @@ 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 - */ - public function __construct( - MigrationRepositoryInterface $repository, - Resolver $resolver, - Filesystem $files, - ?Dispatcher $dispatcher = null, - ) { + * @return void + */ + public function __construct(MigrationRepositoryInterface $repository, + Resolver $resolver, + Filesystem $files) + { $this->files = $files; - $this->events = $dispatcher; $this->resolver = $resolver; $this->repository = $repository; } @@ -117,12 +72,14 @@ public function __construct( /** * Run the pending migrations at a given path. * - * @param string[]|string $paths - * @param array $options - * @return string[] + * @param array|string $paths + * @param array $options + * @return array */ public function run($paths = [], array $options = []) { + $this->notes = []; + // 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. @@ -143,39 +100,23 @@ public function run($paths = [], array $options = []) /** * Get the migration files that have not yet run. * - * @param string[] $files - * @param string[] $ran - * @return string[] + * @param array $files + * @param array $ran + * @return array */ 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(); - } - - /** - * Get list of pending migrations to skip. - * - * @return list - */ - protected function migrationsToSkip() - { - return (new Collection(self::$withoutMigrations)) - ->map($this->getMigrationName(...)) - ->all(); + return Collection::make($files) + ->reject(function ($file) use ($ran) { + return in_array($this->getMigrationName($file), $ran); + })->values()->all(); } /** * 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 = []) @@ -183,10 +124,8 @@ public function runPending(array $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; } @@ -196,13 +135,9 @@ public function runPending(array $migrations, array $options = []) // 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 @@ -214,18 +149,14 @@ public function runPending(array $migrations, array $options = []) $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,85 +164,73 @@ 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( + $name = $this->getMigrationName($file) + ); if ($pretend) { return $this->pretendToRun($migration, 'up'); } - $shouldRunMigration = $migration instanceof Migration - ? $migration->shouldRun() - : true; + $this->note("Migrating: {$name}"); - if (! $shouldRunMigration) { - $this->fireMigrationEvent(new MigrationSkipped($name)); + $this->runMigration($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($name, $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: {$name}"); } /** * Rollback the last migration operation. * - * @param string[]|string $paths - * @param array $options - * @return string[] + * @param array|string $paths + * @param array $options + * @return array */ public function rollback($paths = [], array $options = []) { + $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')); - - $this->write(Info::class, 'Nothing to rollback.'); + $this->note('Nothing to rollback.'); return []; + } else { + return $this->rollbackMigrations($migrations, $paths, $options); } - - 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}[] + * @param array $options + * @return array */ protected function getMigrationsForRollback(array $options) { - if (($steps = $options['step'] ?? 0) > 0) { + if (($steps = Arr::get($options, 'step', 0)) > 0) { return $this->repository->getMigrations($steps); + } else { + return $this->repository->getLast(); } - - if (($batch = $options['batch'] ?? 0) > 0) { - return $this->repository->getMigrationsByBatch($batch); - } - - return $this->repository->getLast(); } /** * Rollback the given migrations. * * @param array $migrations - * @param string[]|string $paths - * @param array $options - * @return string[] + * @param array|string $paths + * @param array $options + * @return array */ protected function rollbackMigrations(array $migrations, $paths, array $options) { @@ -319,65 +238,53 @@ protected function rollbackMigrations(array $migrations, $paths, array $options) $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; - } - - $rolledBack[] = $file; + $rolledBack[] = $files[$migration->migration]; $this->runDown( - $file, $migration, - $options['pretend'] ?? false + $files[$migration->migration], + $migration, Arr::get($options, 'pretend', false) ); } - $this->fireMigrationEvent(new MigrationsEnded('down', $options)); - return $rolledBack; } /** * Rolls all of the currently applied migrations back. * - * @param string[]|string $paths + * @param array|string $paths * @param bool $pretend * @return array */ public function reset($paths = [], $pretend = false) { + $this->notes = []; + // 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. $migrations = array_reverse($this->repository->getRan()); if (count($migrations) === 0) { - $this->write(Info::class, 'Nothing to rollback.'); + $this->note('Nothing to rollback.'); return []; + } else { + return $this->resetMigrations($migrations, $paths, $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 array $migrations + * @param array $paths * @param bool $pretend * @return array */ @@ -386,7 +293,9 @@ protected function resetMigrations(array $migrations, array $paths, $pretend = f // 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(); + $migrations = collect($migrations)->map(function ($m) { + return (object) ['migration' => $m]; + })->all(); return $this->rollbackMigrations( $migrations, $paths, compact('pretend') @@ -398,7 +307,7 @@ protected function resetMigrations(array $migrations, array $paths, $pretend = f * * @param string $file * @param object $migration - * @param bool $pretend + * @param bool $pretend * @return void */ protected function runDown($file, $migration, $pretend) @@ -406,20 +315,24 @@ protected function runDown($file, $migration, $pretend) // 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); + $instance = $this->resolve( + $name = $this->getMigrationName($file) + ); - $name = $this->getMigrationName($file); + $this->note("Rolling back: {$name}"); if ($pretend) { return $this->pretendToRun($instance, 'down'); } - $this->write(Task::class, $name, fn () => $this->runMigration($instance, 'down')); + $this->runMigration($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: {$name}"); } /** @@ -435,20 +348,15 @@ protected function runMigration($migration, $method) $migration->getConnection() ); - $callback = function () use ($connection, $migration, $method) { + $callback = function () use ($migration, $method) { if (method_exists($migration, $method)) { - $this->fireMigrationEvent(new MigrationStarted($migration, $method)); - - $this->runMethod($connection, $migration, $method); - - $this->fireMigrationEvent(new MigrationEnded($migration, $method)); + $migration->{$method}(); } }; $this->getSchemaGrammar($connection)->supportsSchemaTransactions() - && $migration->withinTransaction - ? $connection->transaction($callback) - : $callback(); + ? $connection->transaction($callback) + : $callback(); } /** @@ -460,20 +368,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']) - ); } /** @@ -489,37 +388,16 @@ protected function getQueries($migration, $method) // 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() + $connection = $migration->getConnection() ); - return $db->pretend(function () use ($db, $migration, $method) { + return $db->pretend(function () use ($migration, $method) { if (method_exists($migration, $method)) { - $this->runMethod($db, $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,68 +406,32 @@ protected function runMethod($connection, $migration, $method) */ public function resolve($file) { - $class = $this->getMigrationClass($file); + $class = Str::studly(implode('_', array_slice(explode('_', $file), 4))); 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; - } - - $migration = static::$requiredPathCache[$path] ??= $this->files->getRequire($path); - - if (is_object($migration)) { - return method_exists($migration, '__construct') - ? $this->files->getRequire($path) - : clone $migration; - } - - 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 + * @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(); + return Collection::make($paths)->flatMap(function ($path) { + return $this->files->glob($path.'/*_*.php'); + })->filter()->sortBy(function ($file) { + return $this->getMigrationName($file); + })->values()->keyBy(function ($file) { + return $this->getMigrationName($file); + })->all(); } /** * Require in all the migration files in a given path. * - * @param string[] $files + * @param array $files * @return void */ public function requireFiles(array $files) @@ -624,56 +466,13 @@ public function path($path) /** * Get all of the custom migration paths. * - * @return string[] + * @return array */ public function paths() { return $this->paths; } - /** - * Set the pending migrations to skip. - * - * @param list $migrations - * @return void - */ - public static function withoutMigrations(array $migrations) - { - static::$withoutMigrations = $migrations; - } - - /** - * Get the default connection name. - * - * @return string - */ - public function getConnection() - { - 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); - } - } - /** * Set the default connection name. * @@ -699,26 +498,7 @@ public function setConnection($name) */ 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; + return $this->resolver->connection($connection ?: $this->connection); } /** @@ -758,26 +538,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. * @@ -789,46 +549,23 @@ public function getFilesystem() } /** - * 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. + * Raise a note event for the migrator. * - * @param string $component - * @param array|string ...$arguments + * @param string $message * @return void */ - protected function write($component, ...$arguments) + protected function note($message) { - if ($this->output && class_exists($component)) { - (new $component($this->output))->render(...$arguments); - } else { - foreach ($arguments as $argument) { - if (is_callable($argument)) { - $argument(); - } - } - } + $this->notes[] = $message; } /** - * Fire the given event for the migration. + * Get the notes for the last operation. * - * @param \Illuminate\Contracts\Database\Events\MigrationEvent $event - * @return void + * @return array */ - public function fireMigrationEvent($event) + public function getNotes() { - $this->events?->dispatch($event); + return $this->notes; } } diff --git a/Migrations/stubs/migration.stub b/Migrations/stubs/blank.stub similarity index 65% rename from Migrations/stubs/migration.stub rename to Migrations/stubs/blank.stub index 88fa2f36b0..da4ce827a2 100755 --- a/Migrations/stubs/migration.stub +++ b/Migrations/stubs/blank.stub @@ -1,24 +1,28 @@ id(); + Schema::create('DummyTable', function (Blueprint $table) { + $table->increments('id'); $table->timestamps(); }); } /** * Reverse the migrations. + * + * @return void */ - public function down(): void + public function down() { - Schema::dropIfExists('{{ table }}'); + Schema::dropIfExists('DummyTable'); } -}; +} diff --git a/Migrations/stubs/migration.update.stub b/Migrations/stubs/update.stub similarity index 52% rename from Migrations/stubs/migration.update.stub rename to Migrations/stubs/update.stub index c9ff2c4b73..1fd4f6ec09 100755 --- a/Migrations/stubs/migration.update.stub +++ b/Migrations/stubs/update.stub @@ -1,28 +1,32 @@ 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..1e7fff2f53 100755 --- a/MySqlConnection.php +++ b/MySqlConnection.php @@ -2,118 +2,15 @@ namespace Illuminate\Database; -use Exception; -use Illuminate\Database\Query\Grammars\MySqlGrammar as QueryGrammar; +use PDO; +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. - * - * @return bool - */ - public function isMaria() - { - return str_contains($this->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION), 'MariaDB'); - } - - /** - * 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; - } - /** * Get the default query grammar instance. * @@ -121,7 +18,7 @@ public function getServerVersion(): string */ protected function getDefaultQueryGrammar() { - return new QueryGrammar($this); + return $this->withTablePrefix(new QueryGrammar); } /** @@ -145,28 +42,43 @@ 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; + } + + /** + * Bind values to their parameters in the given statement. + * + * @param \PDOStatement $statement + * @param array $bindings + * @return void + */ + public function bindValues($statement, $bindings) + { + foreach ($bindings as $key => $value) { + $statement->bindValue( + is_string($key) ? $key : $key + 1, $value, + is_int($value) || is_float($value) ? PDO::PARAM_INT : PDO::PARAM_STR + ); + } } } diff --git a/PostgresConnection.php b/PostgresConnection.php index f80b5dce5d..01804a7b8a 100755 --- a/PostgresConnection.php +++ b/PostgresConnection.php @@ -2,59 +2,14 @@ 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. - * - * @param bool $value - * @return string - */ - protected function escapeBool($value) - { - return $value ? 'true' : 'false'; - } - - /** - * 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(); - } - /** * Get the default query grammar instance. * @@ -62,7 +17,7 @@ protected function isUniqueConstraintError(Exception $exception) */ protected function getDefaultQueryGrammar() { - return new QueryGrammar($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..ab8d4450d1 100755 --- a/Query/Builder.php +++ b/Query/Builder.php @@ -2,48 +2,32 @@ 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\Support\Collection; +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\Concerns\BuildsQueries; 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 BuildsQueries, Macroable { __call as macroCall; } /** * The database connection instance. * - * @var \Illuminate\Database\ConnectionInterface + * @var \Illuminate\Database\Connection */ public $connection; @@ -64,74 +48,49 @@ class Builder implements BuilderContract /** * 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, - * } + * @var array */ public $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 +99,111 @@ 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. - * - * @var array - */ - public $beforeQueryCallbacks = []; - - /** - * The callbacks that should be invoked after retrieving data from the database. - * - * @var array - */ - protected $afterQueryCallbacks = []; - /** * 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', + '=', '<', '>', '<=', '>=', '<>', '!=', + '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 = [ - '&', '|', '^', '<<', '>>', '&~', - ]; - - /** - * 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 = []; - /** * 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 +212,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,44 +241,15 @@ 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 @@ -409,117 +257,51 @@ protected function createSub($query) if ($query instanceof Closure) { $callback = $query; - $callback($query = $this->forSubQuery()); + $callback($query = $this->newQuery()); } - return $this->parseSub($query); + // Here, we will parse this query into an SQL string and an array of bindings + // so we can add it to the query builder using the selectRaw method so the + // query is included in the real SQL generated by this builder instance. + list($query, $bindings) = $this->parseSubSelect($query); + + return $this->selectRaw( + '('.$query.') as '.$this->grammar->wrap($as), $bindings + ); } /** - * Parse the subquery into SQL and bindings. + * Parse the sub-select query into SQL and bindings. * * @param mixed $query * @return array - * - * @throws \InvalidArgumentException */ - protected function parseSub($query) + protected function parseSubSelect($query) { - if ($query instanceof self || $query instanceof EloquentBuilder || $query instanceof Relation) { - $query = $this->prependDatabaseNameIfCrossDatabaseQuery($query); - + if ($query instanceof self) { return [$query->toSql(), $query->getBindings()]; } elseif (is_string($query)) { return [$query, []]; } 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; } /** * 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.'.*'); - } - - $this->selectSub($column, $as); - } else { - if (is_array($this->columns) && in_array($column, $this->columns, true)) { - continue; - } + $column = is_array($column) ? $column : func_get_args(); - $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 +309,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,80 +317,36 @@ 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 + * @param string $table * @return $this */ - public function from($table, $as = null) + public function from($table) { - 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 - * @return $this - */ - public function ignoreIndex($index) - { - $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 $first + * @param string $operator + * @param string $second * @param string $type - * @param bool $where + * @param bool $where * @return $this */ public function join($table, $first, $operator = null, $second = null, $type = 'inner', $where = false) { - $join = $this->newJoinClause($this, $type, $table); + $join = new JoinClause($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); + call_user_func($first, $join); $this->joins[] = $join; @@ -638,81 +370,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 $first * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|string $second + * @param string $second * @param string $type - * @return $this + * @return \Illuminate\Database\Query\Builder|static */ 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 $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 - */ - public function leftJoinLateral($query, string $as) - { - return $this->joinLateral($query, $as, 'left'); - } - /** * 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 +399,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 $first * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second - * @return $this + * @param string $second + * @return \Illuminate\Database\Query\Builder|static */ 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 - */ - public function leftJoinSub($query, $as, $first, $operator = null, $second = null) - { - return $this->joinSub($query, $as, $first, $operator, $second, '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 +427,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 $first * @param string $operator - * @param \Illuminate\Contracts\Database\Query\Expression|string $second - * @return $this + * @param string $second + * @return \Illuminate\Database\Query\Builder|static */ 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 - */ - public function rightJoinSub($query, $as, $first, $operator = null, $second = null) - { - return $this->joinSub($query, $as, $first, $operator, $second, '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 +453,49 @@ 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($this, 'cross', $table); return $this; } /** - * Add a subquery cross join to the query. + * Pass the query to a given callback. * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|string $query - * @param string $as - * @return $this - */ - public function crossJoinSub($query, $as) - { - [$query, $bindings] = $this->createSub($query); - - $expression = '('.$query.') as '.$this->grammar->wrapTable($as); - - $this->addBinding($bindings, 'join'); - - $this->joins[] = $this->newJoinClause($this, 'cross', new Expression($expression)); - - return $this; - } - - /** - * 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'); - } - - /** - * Add a "straight 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 $operator - * @param \Illuminate\Contracts\Database\Query\Expression|string $second - * @return $this - */ - public function straightJoinWhere($table, $first, $operator, $second) - { - 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 + * @param \Closure $callback + * @return \Illuminate\Database\Query\Builder */ - protected function newJoinLateralClause(self $parentQuery, $type, $table) + public function tap($callback) { - return new JoinLateralClause($parentQuery, $type, $table); + return $this->when(true, $callback); } /** - * Merge an array of "where" clauses and bindings. + * Merge an array of where clauses and bindings. * * @param array $wheres * @param array $bindings - * @return $this + * @return void */ public function mergeWheres($wheres, $bindings) { - $this->wheres = array_merge($this->wheres, (array) $wheres); + $this->wheres = array_merge((array) $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. + * 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|array|\Closure $column + * @param string $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 +506,28 @@ 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 + list($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)) { + // 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 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 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, '=']; + 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,46 +535,34 @@ 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)); + return $this->whereNull($column, $boolean, $operator != '='); } - $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)) { + if (Str::contains($column, '->') && is_bool($value)) { $value = new Expression($value ? 'true' : 'false'); - - if (is_string($column)) { - $type = 'JsonBoolean'; - } - } - - if ($this->isBitwiseOperator($operator)) { - $type = 'Bitwise'; } // 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. + $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 @@ -1032,12 +571,12 @@ public function where($column, $operator = null, $value = null, $boolean = 'and' */ protected function addArrayOfWheres($column, $boolean, $method = 'where') { - return $this->whereNested(function ($query) use ($column, $method, $boolean) { + return $this->whereNested(function ($query) use ($column, $method) { foreach ($column as $key => $value) { if (is_numeric($key) && is_array($value)) { - $query->{$method}(...array_values($value), boolean: $boolean); + $query->{$method}(...array_values($value)); } else { - $query->{$method}($key, '=', $value, $boolean); + $query->$method($key, '=', $value); } } }, $boolean); @@ -1053,7 +592,7 @@ protected function addArrayOfWheres($column, $boolean, $method = 'where') * * @throws \InvalidArgumentException */ - public function prepareValueAndOperator($value, $operator, $useDefault = false) + protected function prepareValueAndOperator($value, $operator, $useDefault = false) { if ($useDefault) { return [$operator, '=']; @@ -1076,7 +615,7 @@ public function prepareValueAndOperator($value, $operator, $useDefault = false) protected function invalidOperatorAndValue($operator, $value) { return is_null($value) && in_array($operator, $this->operators) && - ! in_array($operator, ['=', '<=>', '<>', '!=']); + ! in_array($operator, ['=', '<>', '!=']); } /** @@ -1087,80 +626,31 @@ protected function invalidOperatorAndValue($operator, $value) */ protected function invalidOperator($operator) { - return ! is_string($operator) || (! in_array(strtolower($operator), $this->operators, true) && - ! in_array(strtolower($operator), $this->grammar->getOperators(), true)); - } - - /** - * Determine if the operator is a bitwise operator. - * - * @param string $operator - * @return bool - */ - protected function isBitwiseOperator($operator) - { - return in_array(strtolower($operator), $this->bitwiseOperators, true) || - in_array(strtolower($operator), $this->grammar->getBitwiseOperators(), true); + return ! in_array(strtolower($operator), $this->operators, true) && + ! in_array(strtolower($operator), $this->grammar->getOperators(), true); } /** * Add an "or where" clause to the query. * - * @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @return $this + * @param \Closure|string $column + * @param string $operator + * @param mixed $value + * @return \Illuminate\Database\Query\Builder|static */ public function orWhere($column, $operator = null, $value = null) { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - return $this->where($column, $operator, $value, 'or'); } - /** - * Add a basic "where not" 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 whereNot($column, $operator = null, $value = null, $boolean = 'and') - { - 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'); - } - - /** - * Add an "or where not" clause to the query. - * - * @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column - * @param mixed $operator - * @param mixed $value - * @return $this - */ - public function orWhereNot($column, $operator = null, $value = null) - { - return $this->whereNot($column, $operator, $value, 'or'); - } - /** * Add a "where" clause comparing two columns to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string|array $first + * @param string|array $first * @param string|null $operator * @param string|null $second * @param string|null $boolean - * @return $this + * @return \Illuminate\Database\Query\Builder|static */ public function whereColumn($first, $operator = null, $second = null, $boolean = 'and') { @@ -1175,7 +665,7 @@ public function whereColumn($first, $operator = null, $second = null, $boolean = // 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, '=']; + list($second, $operator) = [$operator, '=']; } // Finally, we will add this where clause into this array of clauses that we @@ -1193,10 +683,10 @@ public function whereColumn($first, $operator = null, $second = null, $boolean = /** * Add an "or where" clause comparing two columns to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string|array $first + * @param string|array $first * @param string|null $operator * @param string|null $second - * @return $this + * @return \Illuminate\Database\Query\Builder|static */ public function orWhereColumn($first, $operator = null, $second = null) { @@ -1204,79 +694,10 @@ public function orWhereColumn($first, $operator = null, $second = null) } /** - * Add a vector similarity clause to the query, filtering by minimum similarity and ordering by similarity. - * - * @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 - */ - public function whereVectorSimilarTo($column, $vector, $minSimilarity = 0.6, $order = true) - { - if (is_string($vector)) { - $vector = Str::of($vector)->toEmbeddings(cache: true); - } - - $this->whereVectorDistanceLessThan($column, $vector, 1 - $minSimilarity); - - if ($order) { - $this->orderByVectorDistance($column, $vector); - } - - return $this; - } - - /** - * Add a vector distance "where" clause 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 $boolean - * @return $this - */ - public function whereVectorDistanceLessThan($column, $vector, $maxDistance, $boolean = 'and') - { - $this->ensureConnectionSupportsVectors(); - - if (is_string($vector)) { - $vector = Str::of($vector)->toEmbeddings(cache: true); - } - - return $this->whereRaw( - "({$this->getGrammar()->wrap($column)} <=> ?) <= ?", - [ - json_encode( - $vector instanceof Arrayable - ? $vector->toArray() - : $vector, - flags: JSON_THROW_ON_ERROR - ), - $maxDistance, - ], - $boolean - ); - } - - /** - * Add a vector distance "or where" clause 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 - * @return $this - */ - public function orWhereVectorDistanceLessThan($column, $vector, $maxDistance) - { - return $this->whereVectorDistanceLessThan($column, $vector, $maxDistance, 'or'); - } - - /** - * Add a raw "where" clause to the query. + * Add a raw where clause to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $sql - * @param mixed $bindings + * @param string $sql + * @param mixed $bindings * @param string $boolean * @return $this */ @@ -1290,106 +711,44 @@ public function whereRaw($sql, $bindings = [], $boolean = 'and') } /** - * Add a raw "or where" clause to the query. + * Add a raw or where clause to the query. * * @param string $sql - * @param mixed $bindings - * @return $this + * @param array $bindings + * @return \Illuminate\Database\Query\Builder|static */ - public function orWhereRaw($sql, $bindings = []) + public function orWhereRaw($sql, array $bindings = []) { return $this->whereRaw($sql, $bindings, 'or'); } - /** - * Add a "where like" 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 - */ - public function whereLike($column, $value, $caseSensitive = false, $boolean = 'and', $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; - } - - /** - * Add an "or where like" clause to the query. - * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $value - * @param bool $caseSensitive - * @return $this - */ - public function orWhereLike($column, $value, $caseSensitive = false) - { - return $this->whereLike($column, $value, $caseSensitive, 'or', false); - } - - /** - * Add a "where not like" clause to the query. - * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $value - * @param bool $caseSensitive - * @param string $boolean - * @return $this - */ - public function whereNotLike($column, $value, $caseSensitive = false, $boolean = 'and') - { - return $this->whereLike($column, $value, $caseSensitive, $boolean, true); - } - - /** - * Add an "or where not like" clause to the query. - * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $value - * @param bool $caseSensitive - * @return $this - */ - public function orWhereNotLike($column, $value, $caseSensitive = false) - { - return $this->whereNotLike($column, $value, $caseSensitive, 'or'); - } - /** * 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 + // look for any values that exists 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 @@ -1401,14 +760,14 @@ public function whereIn($column, $values, $boolean = 'and', $not = false) $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 + // Finally we'll add a binding for each values 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'); + foreach ($values as $value) { + if (! $value instanceof Expression) { + $this->addBinding($value, 'where'); + } + } return $this; } @@ -1416,9 +775,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 +787,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 +800,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,85 +810,63 @@ 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'; + $type = $not ? 'NotInSub' : 'InSub'; - if ($values instanceof Arrayable) { - $values = $values->toArray(); - } - - $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 an 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'; - /** - * Add a "where not in raw" clause for integer values to the query. - * - * @param string $column - * @param \Illuminate\Contracts\Support\Arrayable|array $values - * @param string $boolean - * @return $this - */ - public function whereIntegerNotInRaw($column, $values, $boolean = 'and') - { - return $this->whereIntegerInRaw($column, $values, $boolean, true); - } + $this->wheres[] = compact('type', 'column', 'query', 'boolean'); - /** - * Add an "or where not in raw" clause for integer values to the query. - * - * @param string $column - * @param \Illuminate\Contracts\Support\Arrayable|array $values - * @return $this - */ - public function orWhereIntegerNotInRaw($column, $values) - { - return $this->whereIntegerNotInRaw($column, $values, 'or'); + $this->addBinding($query->getBindings(), 'where'); + + return $this; } /** * Add a "where null" clause to the query. * - * @param string|array|\Illuminate\Contracts\Database\Query\Expression $columns + * @param string $column * @param string $boolean - * @param bool $not + * @param bool $not * @return $this */ - public function whereNull($columns, $boolean = 'and', $not = false) + public function whereNull($column, $boolean = 'and', $not = false) { $type = $not ? 'NotNull' : 'Null'; - foreach (Arr::wrap($columns) as $column) { - $this->wheres[] = compact('type', 'column', 'boolean'); - } + $this->wheres[] = compact('type', 'column', 'boolean'); return $this; } @@ -1537,8 +874,8 @@ public function whereNull($columns, $boolean = 'and', $not = false) /** * Add an "or where 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) { @@ -1548,199 +885,77 @@ public function orWhereNull($column) /** * Add a "where not null" clause to the query. * - * @param string|array|\Illuminate\Contracts\Database\Query\Expression $columns + * @param string $column * @param string $boolean - * @return $this + * @return \Illuminate\Database\Query\Builder|static */ - public function whereNotNull($columns, $boolean = 'and') + public function whereNotNull($column, $boolean = 'and') { - return $this->whereNull($columns, $boolean, true); + return $this->whereNull($column, $boolean, true); } /** - * Add a "where between" statement to the query. + * Add a where between statement to the query. * - * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|\Illuminate\Contracts\Database\Query\Expression|string $column + * @param string $column + * @param array $values * @param string $boolean * @param bool $not * @return $this */ - public function whereBetween($column, iterable $values, $boolean = 'and', $not = false) + public function whereBetween($column, array $values, $boolean = 'and', $not = false) { $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->wheres[] = compact('column', 'type', 'boolean', 'not'); - $this->addBinding(array_slice($this->cleanBindings(Arr::flatten($values)), 0, 2), 'where'); + $this->addBinding($values, 'where'); return $this; } /** - * Add a "where between" statement using columns to the query. + * Add an or where between statement to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $boolean - * @param bool $not - * @return $this + * @param string $column + * @param array $values + * @return \Illuminate\Database\Query\Builder|static */ - public function whereBetweenColumns($column, array $values, $boolean = 'and', $not = false) + public function orWhereBetween($column, array $values) { - $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->whereBetween($column, $values, 'or'); } /** - * Add an "or where between" statement to the query. + * Add a where not between 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 array $values + * @param string $boolean + * @return \Illuminate\Database\Query\Builder|static */ - public function orWhereBetween($column, iterable $values) + public function whereNotBetween($column, array $values, $boolean = 'and') { - return $this->whereBetween($column, $values, 'or'); + return $this->whereBetween($column, $values, $boolean, true); } /** - * Add an "or where between" statement using columns to the query. + * Add an or where not between statement to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @return $this - */ - public function orWhereBetweenColumns($column, array $values) - { - return $this->whereBetweenColumns($column, $values, 'or'); - } - - /** - * Add a "where not between" statement to the query. - * - * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|\Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $boolean - * @return $this - */ - public function whereNotBetween($column, iterable $values, $boolean = 'and') - { - return $this->whereBetween($column, $values, $boolean, true); - } - - /** - * Add a "where not between" statement using columns to the query. - * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $boolean - * @return $this - */ - public function whereNotBetweenColumns($column, array $values, $boolean = 'and') - { - return $this->whereBetweenColumns($column, $values, $boolean, true); - } - - /** - * Add an "or where not between" 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 array $values + * @return \Illuminate\Database\Query\Builder|static */ - public function orWhereNotBetween($column, iterable $values) + public function orWhereNotBetween($column, array $values) { return $this->whereNotBetween($column, $values, 'or'); } - /** - * Add an "or where not between" statement using columns to the query. - * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @return $this - */ - public function orWhereNotBetweenColumns($column, array $values) - { - return $this->whereNotBetweenColumns($column, $values, 'or'); - } - - /** - * Add a "where 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 - * @param string $boolean - * @param bool $not - * @return $this - */ - public function whereValueBetween($value, array $columns, $boolean = 'and', $not = false) - { - $type = 'valueBetween'; - - $this->wheres[] = compact('type', 'value', 'columns', 'boolean', 'not'); - - $this->addBinding($value, 'where'); - - return $this; - } - - /** - * Add an "or where 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 orWhereValueBetween($value, array $columns) - { - return $this->whereValueBetween($value, $columns, 'or'); - } - - /** - * Add a "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 - * @param string $boolean - * @return $this - */ - public function whereValueNotBetween($value, array $columns, $boolean = 'and') - { - return $this->whereValueBetween($value, $columns, $boolean, true); - } - - /** - * 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'); - } - /** * Add an "or where not null" clause to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @return $this + * @param string $column + * @return \Illuminate\Database\Query\Builder|static */ public function orWhereNotNull($column) { @@ -1750,258 +965,122 @@ public function orWhereNotNull($column) /** * Add a "where date" statement to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param \DateTimeInterface|string|null $operator - * @param \DateTimeInterface|string|null $value + * @param string $column + * @param string $operator + * @param mixed $value * @param string $boolean - * @return $this + * @return \Illuminate\Database\Query\Builder|static */ public function whereDate($column, $operator, $value = null, $boolean = 'and') { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 + list($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('Y-m-d'); - } - return $this->addDateBasedWhere('Date', $column, $operator, $value, $boolean); } /** * Add an "or where date" statement 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 + * @param string $operator + * @param string $value + * @return \Illuminate\Database\Query\Builder|static */ - public function orWhereDate($column, $operator, $value = null) + public function orWhereDate($column, $operator, $value) { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - return $this->whereDate($column, $operator, $value, 'or'); } /** * Add a "where time" statement 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 + * @param string $operator + * @param int $value + * @param string $boolean + * @return \Illuminate\Database\Query\Builder|static */ - public function whereTime($column, $operator, $value = null, $boolean = 'and') + public function whereTime($column, $operator, $value, $boolean = 'and') { - [$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); } /** * Add an "or where time" statement 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 + * @param string $operator + * @param int $value + * @return \Illuminate\Database\Query\Builder|static */ - public function orWhereTime($column, $operator, $value = null) + public function orWhereTime($column, $operator, $value) { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - return $this->whereTime($column, $operator, $value, 'or'); } /** * Add a "where day" statement to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param \DateTimeInterface|string|int|null $operator - * @param \DateTimeInterface|string|int|null $value + * @param string $column + * @param string $operator + * @param mixed $value * @param string $boolean - * @return $this + * @return \Illuminate\Database\Query\Builder|static */ public function whereDay($column, $operator, $value = null, $boolean = 'and') { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 + list($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'); - } - - if (! $value instanceof ExpressionContract) { - $value = sprintf('%02d', $value); - } - return $this->addDateBasedWhere('Day', $column, $operator, $value, $boolean); } - /** - * Add an "or where day" 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 - */ - public function orWhereDay($column, $operator, $value = null) - { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - - return $this->whereDay($column, $operator, $value, 'or'); - } - /** * Add a "where month" statement to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param \DateTimeInterface|string|int|null $operator - * @param \DateTimeInterface|string|int|null $value + * @param string $column + * @param string $operator + * @param mixed $value * @param string $boolean - * @return $this + * @return \Illuminate\Database\Query\Builder|static */ public function whereMonth($column, $operator, $value = null, $boolean = 'and') { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 + list($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'); - } - - if (! $value instanceof ExpressionContract) { - $value = sprintf('%02d', $value); - } - return $this->addDateBasedWhere('Month', $column, $operator, $value, $boolean); } - /** - * Add an "or where month" 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 - */ - public function orWhereMonth($column, $operator, $value = null) - { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - - return $this->whereMonth($column, $operator, $value, 'or'); - } - /** * Add a "where year" statement to the query. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param \DateTimeInterface|string|int|null $operator - * @param \DateTimeInterface|string|int|null $value + * @param string $column + * @param string $operator + * @param mixed $value * @param string $boolean - * @return $this + * @return \Illuminate\Database\Query\Builder|static */ public function whereYear($column, $operator, $value = null, $boolean = 'and') { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 + list($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('Y'); - } - return $this->addDateBasedWhere('Year', $column, $operator, $value, $boolean); } - /** - * Add an "or where year" 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 - */ - public function orWhereYear($column, $operator, $value = null) - { - [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 - ); - - return $this->whereYear($column, $operator, $value, 'or'); - } - /** * Add a date based (year, month, day, time) statement to the query. * * @param string $type - * @param \Illuminate\Contracts\Database\Query\Expression|string $column + * @param string $column * @param string $operator - * @param mixed $value + * @param int $value * @param string $boolean * @return $this */ @@ -2009,22 +1088,21 @@ protected function addDateBasedWhere($type, $column, $operator, $value, $boolean { $this->wheres[] = compact('column', 'type', 'boolean', 'operator', 'value'); - if (! $value instanceof ExpressionContract) { - $this->addBinding($value, 'where'); - } + $this->addBinding($value, 'where'); return $this; } /** - * Add a nested "where" statement to the query. + * Add a nested where statement to the query. * - * @param string $boolean - * @return $this + * @param \Closure $callback + * @param string $boolean + * @return \Illuminate\Database\Query\Builder|static */ public function whereNested(Closure $callback, $boolean = 'and') { - $callback($query = $this->forNestedWhere()); + call_user_func($callback, $query = $this->forNestedWhere()); return $this->addNestedWhereQuery($query, $boolean); } @@ -2042,7 +1120,7 @@ public function forNestedWhere() /** * Add another query builder as a nested where to the query builder. * - * @param \Illuminate\Database\Query\Builder $query + * @param \Illuminate\Database\Query\Builder|static $query * @param string $boolean * @return $this */ @@ -2053,7 +1131,7 @@ public function addNestedWhereQuery($query, $boolean = 'and') $this->wheres[] = compact('type', 'query', 'boolean'); - $this->addBinding($query->getRawBindings()['where'], 'where'); + $this->addBinding($query->getBindings(), 'where'); } return $this; @@ -2062,24 +1140,20 @@ public function addNestedWhereQuery($query, $boolean = 'and') /** * Add a full sub-select to the query. * - * @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 + * @param string $column + * @param string $operator + * @param \Closure $callback + * @param string $boolean * @return $this */ - protected function whereSub($column, $operator, $callback, $boolean) + protected function whereSub($column, $operator, Closure $callback, $boolean) { $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; - } + // 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->newQuery()); $this->wheres[] = compact( 'type', 'column', 'operator', 'query', 'boolean' @@ -2091,76 +1165,73 @@ protected function whereSub($column, $operator, $callback, $boolean) } /** - * Add an "exists" clause to the query. + * Add an exists clause to the query. * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*> $callback - * @param string $boolean - * @param bool $not + * @param \Closure $callback + * @param string $boolean + * @param bool $not * @return $this */ - public function whereExists($callback, $boolean = 'and', $not = false) + public function whereExists(Closure $callback, $boolean = 'and', $not = false) { - if ($callback instanceof Closure) { - $query = $this->forSubQuery(); + $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. - $callback($query); - } else { - $query = $callback instanceof EloquentBuilder ? $callback->toBase() : $callback; - } + // 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 an "or where exists" clause to the query. + * Add an or exists clause to the query. * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*> $callback - * @param bool $not - * @return $this + * @param \Closure $callback + * @param bool $not + * @return \Illuminate\Database\Query\Builder|static */ - public function orWhereExists($callback, $not = false) + public function orWhereExists(Closure $callback, $not = false) { return $this->whereExists($callback, 'or', $not); } /** - * Add a "where not exists" clause to the query. + * Add a where not exists clause to the query. * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*> $callback - * @param string $boolean - * @return $this + * @param \Closure $callback + * @param string $boolean + * @return \Illuminate\Database\Query\Builder|static */ - public function whereNotExists($callback, $boolean = 'and') + public function whereNotExists(Closure $callback, $boolean = 'and') { return $this->whereExists($callback, $boolean, true); } /** - * Add an "or where not exists" clause to the query. + * Add a where not exists clause to the query. * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*> $callback - * @return $this + * @param \Closure $callback + * @return \Illuminate\Database\Query\Builder|static */ - public function orWhereNotExists($callback) + public function orWhereNotExists(Closure $callback) { return $this->orWhereExists($callback, true); } /** - * Add an "exists" clause to the query. + * Add an exists clause to the query. * + * @param \Illuminate\Database\Query\Builder $query * @param string $boolean * @param bool $not * @return $this */ - public function addWhereExistsQuery(self $query, $boolean = 'and', $not = false) + public function addWhereExistsQuery(Builder $query, $boolean = 'and', $not = false) { $type = $not ? 'NotExists' : 'Exists'; - $this->wheres[] = compact('type', 'query', 'boolean'); + $this->wheres[] = compact('type', 'operator', 'query', 'boolean'); $this->addBinding($query->getBindings(), 'where'); @@ -2168,294 +1239,36 @@ public function addWhereExistsQuery(self $query, $boolean = 'and', $not = false) } /** - * Adds a where condition using row values. + * Handles dynamic "where" clauses to the query. * - * @param array $columns - * @param string $operator - * @param array $values - * @param string $boolean + * @param string $method + * @param string $parameters * @return $this - * - * @throws \InvalidArgumentException */ - public function whereRowValues($columns, $operator, $values, $boolean = 'and') + public function dynamicWhere($method, $parameters) { - if (count($columns) !== count($values)) { - throw new InvalidArgumentException('The number of columns must match the number of values'); - } - - $type = 'RowValues'; - - $this->wheres[] = compact('type', 'columns', 'operator', 'values', 'boolean'); - - $this->addBinding($this->cleanBindings($values)); - - return $this; - } + $finder = substr($method, 5); - /** - * Adds an or where condition using row values. - * - * @param array $columns - * @param string $operator - * @param array $values - * @return $this - */ - public function orWhereRowValues($columns, $operator, $values) - { - return $this->whereRowValues($columns, $operator, $values, 'or'); - } + $segments = preg_split( + '/(And|Or)(?=[A-Z])/', $finder, -1, PREG_SPLIT_DELIM_CAPTURE + ); - /** - * Add a "where JSON contains" clause to the query. - * - * @param string $column - * @param mixed $value - * @param string $boolean - * @param bool $not - * @return $this - */ - public function whereJsonContains($column, $value, $boolean = 'and', $not = false) - { - $type = 'JsonContains'; + // 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'; - $this->wheres[] = compact('type', 'column', 'value', 'boolean', 'not'); + $index = 0; - if (! $value instanceof ExpressionContract) { - $this->addBinding($this->grammar->prepareBindingForJsonContains($value)); - } + 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); - return $this; - } - - /** - * Add an "or where JSON contains" clause to the query. - * - * @param string $column - * @param mixed $value - * @return $this - */ - public function orWhereJsonContains($column, $value) - { - return $this->whereJsonContains($column, $value, 'or'); - } - - /** - * Add a "where JSON not contains" clause to the query. - * - * @param string $column - * @param mixed $value - * @param string $boolean - * @return $this - */ - public function whereJsonDoesntContain($column, $value, $boolean = 'and') - { - return $this->whereJsonContains($column, $value, $boolean, true); - } - - /** - * Add an "or where JSON not contains" clause to the query. - * - * @param string $column - * @param mixed $value - * @return $this - */ - public function orWhereJsonDoesntContain($column, $value) - { - return $this->whereJsonDoesntContain($column, $value, 'or'); - } - - /** - * Add a "where JSON overlaps" clause to the query. - * - * @param string $column - * @param mixed $value - * @param string $boolean - * @param bool $not - * @return $this - */ - public function whereJsonOverlaps($column, $value, $boolean = 'and', $not = false) - { - $type = 'JsonOverlaps'; - - $this->wheres[] = compact('type', 'column', 'value', 'boolean', 'not'); - - if (! $value instanceof ExpressionContract) { - $this->addBinding($this->grammar->prepareBindingForJsonContains($value)); - } - - return $this; - } - - /** - * Add an "or where JSON overlaps" clause to the query. - * - * @param string $column - * @param mixed $value - * @return $this - */ - public function orWhereJsonOverlaps($column, $value) - { - return $this->whereJsonOverlaps($column, $value, 'or'); - } - - /** - * Add a "where JSON not overlap" clause to the query. - * - * @param string $column - * @param mixed $value - * @param string $boolean - * @return $this - */ - public function whereJsonDoesntOverlap($column, $value, $boolean = 'and') - { - return $this->whereJsonOverlaps($column, $value, $boolean, true); - } - - /** - * Add an "or where JSON not overlap" clause to the query. - * - * @param string $column - * @param mixed $value - * @return $this - */ - public function orWhereJsonDoesntOverlap($column, $value) - { - return $this->whereJsonDoesntOverlap($column, $value, 'or'); - } - - /** - * 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++; - } + $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 @@ -2469,433 +1282,96 @@ public function dynamicWhere($method, $parameters) } /** - * 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. + * Add a single dynamic where clause statement to the query. * - * @param string $column - * @return $this + * @param string $segment + * @param string $connector + * @param array $parameters + * @param int $index + * @return void */ - public function orHavingNull($column) + protected function addDynamic($segment, $connector, $parameters, $index) { - return $this->havingNull($column, '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); - /** - * 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); + $this->where(Str::snake($segment), '=', $parameters[$index], $bool); } /** - * Add an "or having not null" clause to the query. + * Add a "group by" clause to the query. * - * @param string $column + * @param array ...$groups * @return $this */ - public function orHavingNotNull($column) + public function groupBy(...$groups) { - return $this->havingNotNull($column, 'or'); + foreach ($groups as $group) { + $this->groups = array_merge( + (array) $this->groups, + Arr::wrap($group) + ); + } + + return $this; } /** - * Add a "having between" clause to the query. + * Add a "having" clause to the query. * * @param string $column + * @param string $operator + * @param string $value * @param string $boolean - * @param bool $not * @return $this */ - public function havingBetween($column, iterable $values, $boolean = 'and', $not = false) + public function having($column, $operator = null, $value = null, $boolean = 'and') { - $type = 'between'; + $type = 'Basic'; + + // 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. + list($value, $operator) = $this->prepareValueAndOperator( + $value, $operator, func_num_args() == 2 + ); - if ($values instanceof DatePeriod) { - $values = $this->resolveDatePeriodBounds($values); + // 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)) { + list($value, $operator) = [$operator, '=']; } - $this->havings[] = compact('type', 'column', 'values', 'boolean', 'not'); + $this->havings[] = compact('type', 'column', 'operator', 'value', 'boolean'); - $this->addBinding(array_slice($this->cleanBindings(Arr::flatten($values)), 0, 2), 'having'); + if (! $value instanceof Expression) { + $this->addBinding($value, '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. + * Add a "or having" 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} + * @param string $operator + * @param string $value + * @return \Illuminate\Database\Query\Builder|static */ - protected function resolveDatePeriodBounds(DatePeriod $period) + public function orHaving($column, $operator = null, $value = null) { - [$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]; + return $this->having($column, $operator, $value, 'or'); } /** - * Add a raw "having" clause to the query. + * Add a raw having clause to the query. * * @param string $sql + * @param array $bindings * @param string $boolean * @return $this */ @@ -2911,10 +1387,11 @@ public function havingRaw($sql, array $bindings = [], $boolean = 'and') } /** - * Add a raw "or having" clause to the query. + * Add a raw or having clause to the query. * * @param string $sql - * @return $this + * @param array $bindings + * @return \Illuminate\Database\Query\Builder|static */ public function orHavingRaw($sql, array $bindings = []) { @@ -2924,31 +1401,15 @@ public function orHavingRaw($sql, array $bindings = []) /** * 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 $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, + 'direction' => strtolower($direction) == 'asc' ? 'asc' : 'desc', ]; return $this; @@ -2957,7 +1418,7 @@ public function orderBy($column, $direction = 'asc') /** * 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 + * @param string $column * @return $this */ public function orderByDesc($column) @@ -2968,8 +1429,8 @@ public function orderByDesc($column) /** * 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 + * @param string $column + * @return \Illuminate\Database\Query\Builder|static */ public function latest($column = 'created_at') { @@ -2979,51 +1440,18 @@ public function latest($column = 'created_at') /** * 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 + * @param string $column + * @return \Illuminate\Database\Query\Builder|static */ 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 + * @param string $seed * @return $this */ public function inRandomOrder($seed = '') @@ -3044,7 +1472,7 @@ public function orderByRaw($sql, $bindings = []) $this->{$this->unions ? 'unionOrders' : 'orders'}[] = compact('type', 'sql'); - $this->addBinding($bindings, $this->unions ? 'unionOrder' : 'order'); + $this->addBinding($bindings, 'order'); return $this; } @@ -3053,7 +1481,7 @@ public function orderByRaw($sql, $bindings = []) * Alias to set the "offset" value of the query. * * @param int $value - * @return $this + * @return \Illuminate\Database\Query\Builder|static */ public function skip($value) { @@ -3070,7 +1498,7 @@ public function offset($value) { $property = $this->unions ? 'unionOffset' : 'offset'; - $this->$property = max(0, (int) $value); + $this->$property = max(0, $value); return $this; } @@ -3079,7 +1507,7 @@ public function offset($value) * Alias to set the "limit" value of the query. * * @param int $value - * @return $this + * @return \Illuminate\Database\Query\Builder|static */ public function take($value) { @@ -3097,23 +1525,7 @@ 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'); + $this->$property = $value; } return $this; @@ -3124,114 +1536,55 @@ public function groupLimit($value, $column) * * @param int $page * @param int $perPage - * @return $this + * @return \Illuminate\Database\Query\Builder|static */ 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); + return $this->skip(($page - 1) * $perPage)->take($perPage); } /** * Constrain the query to the next "page" of results after a given ID. * * @param int $perPage - * @param int|null $lastId + * @param int $lastId * @param string $column - * @return $this + * @return \Illuminate\Database\Query\Builder|static */ 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'); + return $this->where($column, '>', $lastId) + ->orderBy($column, 'asc') + ->take($perPage); } /** - * Get an array with all orders with a given column removed. + * Get an array orders with all orders for an 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(); + return Collection::make($this->orders) + ->reject(function ($order) use ($column) { + return $order['column'] === $column; + })->values()->all(); } /** - * Add a "union" statement to the query. + * Add a union statement to the query. * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*> $query + * @param \Illuminate\Database\Query\Builder|\Closure $query * @param bool $all - * @return $this + * @return \Illuminate\Database\Query\Builder|static */ public function union($query, $all = false) { if ($query instanceof Closure) { - $query($query = $this->newQuery()); + call_user_func($query, $query = $this->newQuery()); } $this->unions[] = compact('query', 'all'); @@ -3242,10 +1595,10 @@ public function union($query, $all = false) } /** - * Add a "union all" statement to the query. + * Add a union all statement to the query. * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*> $query - * @return $this + * @param \Illuminate\Database\Query\Builder|\Closure $query + * @return \Illuminate\Database\Query\Builder|static */ public function unionAll($query) { @@ -3272,216 +1625,77 @@ public function lock($value = true) /** * Lock the selected rows in the table for updating. * - * @return $this + * @return \Illuminate\Database\Query\Builder */ 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); + return $this->lock(true); } /** - * Execute a query for a single record by ID or call a callback. - * - * @template TValue + * Share lock the selected rows in the table. * - * @param mixed $id - * @param (\Closure(): TValue)|string|\Illuminate\Contracts\Database\Query\Expression|array $columns - * @param (\Closure(): TValue)|null $callback - * @return \stdClass|TValue + * @return \Illuminate\Database\Query\Builder */ - public function findOr($id, $columns = ['*'], ?Closure $callback = null) + public function sharedLock() { - if ($columns instanceof Closure) { - $callback = $columns; - - $columns = ['*']; - } - - if (! is_null($data = $this->find($id, $columns))) { - return $data; - } - - return $callback(); + return $this->lock(false); } /** - * Get a single column's value from the first result of a query. + * Get the SQL representation of the query. * - * @param string $column - * @return mixed + * @return string */ - public function value($column) + public function toSql() { - $result = (array) $this->first([$column]); - - return count($result) > 0 ? array_first($result) : null; + return $this->grammar->compileSelect($this); } /** - * Get a single expression value from the first result of a query. + * Execute a query for a single record by ID. * - * @return mixed + * @param int $id + * @param array $columns + * @return mixed|static */ - public function rawValue(string $expression, array $bindings = []) + public function find($id, $columns = ['*']) { - $result = (array) $this->selectRaw($expression, $bindings)->first(); - - return count($result) > 0 ? array_first($result) : null; + return $this->where('id', '=', $id)->first($columns); } /** - * Get a single column's value from the first result of a query if it's the sole matching record. + * Get a single column's value from the first result of a query. * * @param string $column * @return mixed - * - * @throws \Illuminate\Database\RecordsNotFoundException - * @throws \Illuminate\Database\MultipleRecordsFoundException */ - public function soleValue($column) + public function value($column) { - $result = (array) $this->sole([$column]); + $result = (array) $this->first([$column]); - return array_first($result); + return count($result) > 0 ? reset($result) : null; } /** * Execute the query as a "select" statement. * - * @param string|\Illuminate\Contracts\Database\Query\Expression|array $columns - * @return \Illuminate\Support\Collection + * @param array $columns + * @return \Illuminate\Support\Collection */ public function get($columns = ['*']) { $original = $this->columns; - $this->columns ??= Arr::wrap($columns); + if (is_null($original)) { + $this->columns = $columns; + } - $items = new Collection($this->processor->processSelect($this, $this->runSelect())); + $results = $this->processor->processSelect($this, $this->runSelect()); $this->columns = $original; - return $this->applyAfterQueryCallbacks( - isset($this->groupLimit) ? $this->withoutGroupLimitKeys($items) : $items - ); + return collect($results); } /** @@ -3492,57 +1706,28 @@ public function get($columns = ['*']) protected function runSelect() { return $this->connection->select( - $this->toSql(), $this->getBindings(), ! $this->useWritePdo, $this->fetchUsing + $this->toSql(), $this->getBindings(), ! $this->useWritePdo ); } - /** - * 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 int $perPage + * @param array $columns * @param string $pageName * @param int|null $page - * @param \Closure|int|null $total - * @return \Illuminate\Pagination\LengthAwarePaginator + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator */ - public function paginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null, $total = null) + public function paginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null) { $page = $page ?: Paginator::resolveCurrentPage($pageName); - $total = value($total) ?? $this->getCountForPagination(); - - $perPage = value($perPage, $total); + $total = $this->getCountForPagination($columns); - $results = $total ? $this->forPage($page, $perPage)->get($columns) : new Collection; + $results = $total ? $this->forPage($page, $perPage)->get($columns) : collect(); - return $this->paginator($results, $total, $perPage, $page, [ + return new LengthAwarePaginator($results, $total, $perPage, $page, [ 'path' => Paginator::resolveCurrentPath(), 'pageName' => $pageName, ]); @@ -3554,7 +1739,7 @@ public function paginate($perPage = 15, $columns = ['*'], $pageName = 'page', $p * This is more efficient on larger data-sets, etc. * * @param int $perPage - * @param string|\Illuminate\Contracts\Database\Query\Expression|array $columns + * @param array $columns * @param string $pageName * @param int|null $page * @return \Illuminate\Contracts\Pagination\Paginator @@ -3563,69 +1748,19 @@ public function simplePaginate($perPage = 15, $columns = ['*'], $pageName = 'pag { $page = $page ?: Paginator::resolveCurrentPage($pageName); - $this->offset(($page - 1) * $perPage)->limit($perPage + 1); + $this->skip(($page - 1) * $perPage)->take($perPage + 1); - return $this->simplePaginator($this->get($columns), $perPage, $page, [ + return new Paginator($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> + * @param array $columns + * @return int */ public function getCountForPagination($columns = ['*']) { @@ -3634,75 +1769,49 @@ public function getCountForPagination($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])) { + if (isset($this->groups)) { + return count($results); + } elseif (! isset($results[0])) { return 0; } elseif (is_object($results[0])) { return (int) $results[0]->aggregate; + } else { + return (int) array_change_key_case((array) $results[0])['aggregate']; } - - return (int) array_change_key_case((array) $results[0])['aggregate']; } /** * Run a pagination count query. * - * @param array $columns - * @return array + * @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']); + return $this->cloneWithout(['columns', 'orders', 'limit', 'offset']) + ->cloneWithoutBindings(['select', 'order']) + ->setAggregate('count', $this->withoutSelectAliases($columns)) + ->get()->all(); } /** * Remove the column aliases since they will break count queries. * - * @param array $columns - * @return array + * @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; + return is_string($column) && ($aliasPosition = strpos(strtolower($column), ' as ')) !== false + ? substr($column, 0, $aliasPosition) : $column; }, $columns); } /** - * Get a lazy collection for the given query. + * Get a generator for the given query. * - * @return \Illuminate\Support\LazyCollection + * @return \Generator */ public function cursor() { @@ -3710,138 +1819,96 @@ public function cursor() $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)); + return $this->connection->cursor( + $this->toSql(), $this->getBindings(), ! $this->useWritePdo + ); } /** - * Throw an exception if the query doesn't have an orderBy clause. + * Chunk the results of a query by comparing numeric IDs. * - * @return void - * - * @throws \RuntimeException + * @param int $count + * @param callable $callback + * @param string $column + * @param string $alias + * @return bool */ - protected function enforceOrderBy() + public function chunkById($count, callable $callback, $column = 'id', $alias = null) { - if (empty($this->orders) && empty($this->unionOrders)) { - throw new RuntimeException('You must specify an orderBy clause when using this function.'); - } - } + $alias = $alias ?: $column; - /** - * 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; + $lastId = 0; - // 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]; + do { + $clone = clone $this; - $queryResult = $this->processor->processSelect($this, $this->runSelect()); + // 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. + $results = $clone->forPageAfterId($count, $lastId, $column)->get(); - $this->columns = $original; + $countResults = $results->count(); - if (empty($queryResult)) { - return new Collection; - } + if ($countResults == 0) { + break; + } - // 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); + // 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) === false) { + return false; + } - $key = $this->stripTableForPluck($key); + $lastId = $results->last()->{$alias}; + } while ($countResults == $count); - return $this->applyAfterQueryCallbacks( - is_array($queryResult[0]) - ? $this->pluckFromArrayColumn($queryResult, $column, $key) - : $this->pluckFromObjectColumn($queryResult, $column, $key) - ); + return true; } /** - * Strip off the table name or alias from a column identifier. + * Throw an exception if the query doesn't have an orderBy clause. * - * @param string $column - * @return string|null + * @return void + * + * @throws \RuntimeException */ - protected function stripTableForPluck($column) + protected function enforceOrderBy() { - if (is_null($column)) { - return $column; + if (empty($this->orders) && empty($this->unionOrders)) { + throw new RuntimeException('You must specify an orderBy clause when using this function.'); } - - $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. + * Get an array with the values of a given column. * - * @param array $queryResult * @param string $column - * @param string $key + * @param string|null $key * @return \Illuminate\Support\Collection */ - protected function pluckFromObjectColumn($queryResult, $column, $key) + public function pluck($column, $key = null) { - $results = []; - - if (is_null($key)) { - foreach ($queryResult as $row) { - $results[] = $row->$column; - } - } else { - foreach ($queryResult as $row) { - $results[$row->$key] = $row->$column; - } - } + $results = $this->get(is_null($key) ? [$column] : [$column, $key]); - return new Collection($results); + // 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) + ); } /** - * Retrieve column values from rows represented as arrays. + * Strip off the table name or alias from a column identifier. * - * @param array $queryResult * @param string $column - * @param string $key - * @return \Illuminate\Support\Collection + * @return string|null */ - protected function pluckFromArrayColumn($queryResult, $column, $key) + protected function stripTableForPluck($column) { - $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); + return is_null($column) ? $column : last(preg_split('~\.| ~', $column)); } /** @@ -3863,15 +1930,13 @@ public function implode($column, $glue = '') */ public function exists() { - $this->applyBeforeQueryCallbacks(); - $results = $this->connection->select( $this->grammar->compileExists($this), $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 the results has rows, we will get the row and see if the exists column is a + // boolean true. If there is 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,41 +1946,11 @@ 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 = '*') { @@ -3925,7 +1960,7 @@ public function count($columns = '*') /** * Retrieve the minimum value of a given column. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column + * @param string $column * @return mixed */ public function min($column) @@ -3936,7 +1971,7 @@ public function min($column) /** * Retrieve the maximum value of a given column. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column + * @param string $column * @return mixed */ public function max($column) @@ -3947,7 +1982,7 @@ public function max($column) /** * Retrieve the sum of the values of a given column. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column + * @param string $column * @return mixed */ public function sum($column) @@ -3960,7 +1995,7 @@ public function sum($column) /** * Retrieve the average of the values of a given column. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column + * @param string $column * @return mixed */ public function avg($column) @@ -3971,7 +2006,7 @@ public function avg($column) /** * Alias for the "avg" method. * - * @param \Illuminate\Contracts\Database\Query\Expression|string $column + * @param string $column * @return mixed */ public function average($column) @@ -3983,15 +2018,15 @@ public function average($column) * Execute an aggregate function on the database. * * @param string $function - * @param array $columns + * @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); + $results = $this->cloneWithout(['columns']) + ->cloneWithoutBindings(['select']) + ->setAggregate($function, $columns) + ->get($columns); if (! $results->isEmpty()) { return array_change_key_case((array) $results[0])['aggregate']; @@ -4002,7 +2037,7 @@ public function aggregate($function, $columns = ['*']) * Execute a numeric aggregate function on the database. * * @param string $function - * @param array $columns + * @param array $columns * @return float|int */ public function numericAggregate($function, $columns = ['*']) @@ -4023,34 +2058,28 @@ public function numericAggregate($function, $columns = ['*']) // 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; + return strpos((string) $result, '.') === false + ? (int) $result : (float) $result; } /** * Set the aggregate property without running the query. * * @param string $function - * @param array<\Illuminate\Contracts\Database\Query\Expression|string> $columns + * @param array $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. + * Insert a new record into the database. * + * @param array $values * @return bool */ public function insert(array $values) @@ -4059,88 +2088,17 @@ public function insert(array $values) // 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.'); + return true; } - if ($returning === []) { - throw new InvalidArgumentException('The returning columns must not be empty.'); + if (! is_array(reset($values))) { + $values = [$values]; } - if (! is_array(array_first($values))) { - $values = [$values]; - } else { + // 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); @@ -4148,27 +2106,24 @@ public function insertOrIgnoreReturning(array $values, array|string $uniqueBy, a } } - $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))) + // 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 a new record and get the value of the primary key. * - * @param string|null $sequence + * @param array $values + * @param string $sequence * @return int */ public function insertGetId(array $values, $sequence = null) { - $this->applyBeforeQueryCallbacks(); - $sql = $this->grammar->compileInsertGetId($this, $values, $sequence); $values = $this->cleanBindings($values); @@ -4177,172 +2132,43 @@ public function insertGetId(array $values, $sequence = null) } /** - * 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. + * Update a record in the database. * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|string $query + * @param array $values * @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) - ); - } - - /** - * Update records in the database. - * - * @return int<0, max> - */ public function update(array $values) { - $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()) - )); - } - - /** - * Update records in a PostgreSQL database using the update from syntax. - * - * @return int - * - * @throws \LogicException - */ - public function updateFrom(array $values) - { - if (! method_exists($this->grammar, 'compileUpdateFrom')) { - throw new LogicException('This database engine does not support the updateFrom method.'); - } - - $this->applyBeforeQueryCallbacks(); - - $sql = $this->grammar->compileUpdateFrom($this, $values); + $sql = $this->grammar->compileUpdate($this, $values); return $this->connection->update($sql, $this->cleanBindings( - $this->grammar->prepareBindingsForUpdateFrom($this->bindings, $values) + $this->grammar->prepareBindingsForUpdate($this->bindings, $values) )); } /** * Insert or update a record matching the attributes, and fill it with values. * + * @param array $attributes + * @param array $values * @return bool */ - public function updateOrInsert(array $attributes, array|callable $values = []) + public function updateOrInsert(array $attributes, array $values = []) { - $exists = $this->where($attributes)->exists(); - - if ($values instanceof Closure) { - $values = $values($exists); - } - - if (! $exists) { + if (! $this->where($attributes)->exists()) { return $this->insert(array_merge($attributes, $values)); } - if (empty($values)) { - return true; - } - - return (bool) $this->limit(1)->update($values); - } - - /** - * Insert new records or update the existing ones. - * - * @param non-empty-string|non-empty-array $uniqueBy - * @return int - */ - public function upsert(array $values, array|string $uniqueBy, ?array $update = null) - { - 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); - } - - if (! is_array(array_first($values))) { - $values = [$values]; - } else { - foreach ($values as $key => $value) { - ksort($value); - - $values[$key] = $value; - } - } - - if (is_null($update)) { - $update = array_keys(array_first($values)); - } - - $this->applyBeforeQueryCallbacks(); - - $bindings = $this->cleanBindings(array_merge( - Arr::flatten($values, 1), - (new Collection($update)) - ->reject(fn ($value, $key) => is_int($key)) - ->all() - )); - - return $this->connection->affectingStatement( - $this->grammar->compileUpsert($this, $values, (array) $uniqueBy, $update), - $bindings - ); + return (bool) $this->take(1)->update($values); } /** * Increment a column's value by a given amount. * * @param string $column - * @param float|int $amount - * @return int<0, max> - * - * @throws \InvalidArgumentException + * @param int $amount + * @param array $extra + * @return int */ public function increment($column, $amount = 1, array $extra = []) { @@ -4350,41 +2176,20 @@ public function increment($column, $amount = 1, array $extra = []) throw new InvalidArgumentException('Non-numeric value passed to increment method.'); } - return $this->incrementEach([$column => $amount], $extra); - } - - /** - * Increment the given column's values by the given amounts. - * - * @param array $columns - * @param array $extra - * @return int<0, max> - * - * @throws \InvalidArgumentException - */ - public function incrementEach(array $columns, array $extra = []) - { - 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.'); - } + $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); } /** * Decrement a column's value by a given amount. * * @param string $column - * @param float|int $amount - * @return int<0, max> - * - * @throws \InvalidArgumentException + * @param int $amount + * @param array $extra + * @return int */ public function decrement($column, $amount = 1, array $extra = []) { @@ -4392,35 +2197,15 @@ public function decrement($column, $amount = 1, array $extra = []) throw new InvalidArgumentException('Non-numeric value passed to decrement method.'); } - return $this->decrementEach([$column => $amount], $extra); - } - - /** - * Decrement the given column's values by the given amounts. - * - * @param array $columns - * @param array $extra - * @return int<0, max> - * - * @throws \InvalidArgumentException - */ - public function decrementEach(array $columns, 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 @@ -4434,24 +2219,18 @@ public function delete($id = null) $this->where($this->from.'.id', '=', $id); } - $this->applyBeforeQueryCallbacks(); - return $this->connection->delete( - $this->grammar->compileDelete($this), $this->cleanBindings( - $this->grammar->prepareBindingsForDelete($this->bindings) - ) + $this->grammar->compileDelete($this), $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); } @@ -4467,79 +2246,21 @@ public function newQuery() return new static($this->connection, $this->grammar, $this->processor); } - /** - * Create a new query instance for a sub-query. - * - * @return \Illuminate\Database\Query\Builder - */ - protected function forSubQuery() - { - return $this->newQuery(); - } - - /** - * Get all of the query builder's columns in a text-only array with all expressions evaluated. - * - * @return list - */ - public function getColumns() - { - return ! is_null($this->columns) - ? array_map(fn ($column) => $this->grammar->getValue($column), $this->columns) - : []; - } - /** * 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 +2270,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 +2280,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 +2300,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,35 +2313,21 @@ 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); @@ -4640,39 +2337,14 @@ public function mergeBindings(self $query) /** * 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 + * @param array $bindings + * @return array */ - protected function defaultKeyName() + protected function cleanBindings(array $bindings) { - return 'id'; + return array_values(array_filter($bindings, function ($binding) { + return ! $binding instanceof Expression; + })); } /** @@ -4685,20 +2357,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 +2378,7 @@ public function getGrammar() } /** - * Use the "write" PDO connection when executing the query. + * Use the write pdo for query. * * @return $this */ @@ -4731,52 +2389,16 @@ 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. * + * @param array $except * @return static */ - public function cloneWithout(array $properties) + public function cloneWithout(array $except) { - return tap($this->clone(), function ($clone) use ($properties) { - foreach ($properties as $property) { + return tap(clone $this, function ($clone) use ($except) { + foreach ($except as $property) { $clone->{$property} = null; } }); @@ -4785,71 +2407,23 @@ public function cloneWithout(array $properties) /** * Clone the query without the given bindings. * + * @param array $except * @return static */ public function cloneWithoutBindings(array $except) { - return tap($this->clone(), function ($clone) use ($except) { + return tap(clone $this, 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 +2434,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..b7e59733d3 100755 --- a/Query/Grammars/Grammar.php +++ b/Query/Grammars/Grammar.php @@ -2,20 +2,13 @@ namespace Illuminate\Database\Query\Grammars; -use Illuminate\Contracts\Database\Query\Expression; -use Illuminate\Database\Concerns\CompilesJsonPaths; -use Illuminate\Database\Grammar as BaseGrammar; +use Illuminate\Support\Arr; 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 +16,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 +32,7 @@ class Grammar extends BaseGrammar 'orders', 'limit', 'offset', + 'unions', 'lock', ]; @@ -58,21 +44,6 @@ 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. @@ -89,10 +60,6 @@ public function compileSelect(Builder $query) $this->compileComponents($query)) ); - if ($query->unions) { - $sql = $this->wrapUnion($sql).' '.$this->compileUnions($query); - } - $query->columns = $original; return $sql; @@ -109,7 +76,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 +93,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 +103,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 +126,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,58 +152,22 @@ protected function compileFrom(Builder $query, $table) */ protected function compileJoins(Builder $query, $joins) { - return (new Collection($joins))->map(function ($join) use ($query) { + return collect($joins)->map(function ($join) use ($query) { $table = $this->wrapTable($join->table); - $nestedJoins = is_null($join->joins) ? '' : ' '.$this->compileJoins($query, $join->joins); - - $tableAndNestedJoins = is_null($join->joins) ? $table : '('.$table.$nestedJoins.')'; - - if ($join instanceof JoinLateralClause) { - return $this->compileJoinLateral($join, $tableAndNestedJoins); - } - - $joinWord = ($join->type === 'straight_join' && $this->supportsStraightJoins()) ? '' : ' join'; - - return trim("{$join->type}{$joinWord} {$tableAndNestedJoins} {$this->compileWheres($join)}"); + return trim("{$join->type} join {$table} {$this->compileWheres($join)}"); })->implode(' '); } - /** - * Compile a "lateral join" clause. - * - * @param \Illuminate\Database\Query\JoinLateralClause $join - * @param string $expression - * @return string - * - * @throws \RuntimeException - */ - public function compileJoinLateral(JoinLateralClause $join, string $expression): string - { - throw new RuntimeException('This database engine does not support lateral joins.'); - } - - /** - * Determine if the grammar supports straight joins. - * - * @return bool - * - * @throws \RuntimeException - */ - protected function supportsStraightJoins() - { - throw new RuntimeException('This database engine does not support straight joins.'); - } - /** * Compile the "where" portions of the query. * * @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 + // 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. if (is_null($query->wheres)) { @@ -264,9 +192,9 @@ public function compileWheres(Builder $query) */ protected function compileWheresToArray($query) { - return (new Collection($query->wheres)) - ->map(fn ($where) => $where['boolean'].' '.$this->{"where{$where['type']}"}($query, $where)) - ->all(); + return collect($query->wheres)->map(function ($where) use ($query) { + return $where['boolean'].' '.$this->{"where{$where['type']}"}($query, $where); + })->all(); } /** @@ -292,7 +220,7 @@ protected function concatenateWhereClauses($query, $sql) */ protected function whereRaw(Builder $query, $where) { - return $where['sql'] instanceof Expression ? $where['sql']->getValue($this) : $where['sql']; + return $where['sql']; } /** @@ -306,41 +234,7 @@ protected function whereBasic(Builder $query, $where) { $value = $this->parameter($where['value']); - $operator = str_replace('?', '??', $where['operator']); - - return $this->wrap($where['column']).' '.$operator.' '.$value; - } - - /** - * Compile a bitwise operator where clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereBitwise(Builder $query, $where) - { - return $this->whereBasic($query, $where); - } - - /** - * Compile a "where like" clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - * - * @throws \RuntimeException - */ - protected function whereLike(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 $this->wrap($where['column']).' '.$where['operator'].' '.$value; } /** @@ -376,39 +270,27 @@ protected function whereNotIn(Builder $query, $where) } /** - * 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']).')'; - } - - return '1 = 1'; + return $this->wrap($where['column']).' in ('.$this->compileSelect($where['query']).')'; } /** - * 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']).')'; - } - - return '0 = 1'; + return $this->wrap($where['column']).' not in ('.$this->compileSelect($where['query']).')'; } /** @@ -446,47 +328,7 @@ 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; + return $this->wrap($where['column']).' '.$between.' ? and ?'; } /** @@ -565,7 +407,7 @@ protected function dateBasedWhere($type, Builder $query, $where) } /** - * Compile a where clause comparing two columns. + * Compile a where clause comparing two columns.. * * @param \Illuminate\Database\Query\Builder $query * @param array $where @@ -588,7 +430,7 @@ protected function whereNested(Builder $query, $where) // 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; + $offset = $query instanceof JoinClause ? 3 : 6; return '('.substr($this->compileWheres($where['query']), $offset).')'; } @@ -596,8 +438,8 @@ protected function whereNested(Builder $query, $where) /** * Compile a where condition with a sub-select. * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where + * @param \Illuminate\Database\Query\Builder $query + * @param array $where * @return string */ protected function whereSub(Builder $query, $where) @@ -631,210 +473,6 @@ protected function whereNotExists(Builder $query, $where) return 'not exists ('.$this->compileSelect($where['query']).')'; } - /** - * Compile a where row values condition. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereRowValues(Builder $query, $where) - { - $columns = $this->columnize($where['columns']); - - $values = $this->parameterize($where['values']); - - return '('.$columns.') '.$where['operator'].' ('.$values.')'; - } - - /** - * Compile a "where JSON boolean" clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereJsonBoolean(Builder $query, $where) - { - $column = $this->wrapJsonBooleanSelector($where['column']); - - $value = $this->wrapJsonBooleanValue( - $this->parameter($where['value']) - ); - - return $column.' '.$where['operator'].' '.$value; - } - - /** - * Compile a "where JSON contains" clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereJsonContains(Builder $query, $where) - { - $not = $where['not'] ? 'not ' : ''; - - return $not.$this->compileJsonContains( - $where['column'], - $this->parameter($where['value']) - ); - } - - /** - * Compile a "JSON contains" statement into SQL. - * - * @param string $column - * @param string $value - * @return string - * - * @throws \RuntimeException - */ - protected function compileJsonContains($column, $value) - { - throw new RuntimeException('This database engine does not support JSON contains operations.'); - } - - /** - * Compile a "where JSON overlaps" clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereJsonOverlaps(Builder $query, $where) - { - $not = $where['not'] ? 'not ' : ''; - - return $not.$this->compileJsonOverlaps( - $where['column'], - $this->parameter($where['value']) - ); - } - - /** - * Compile a "JSON overlaps" statement into SQL. - * - * @param string $column - * @param string $value - * @return string - * - * @throws \RuntimeException - */ - protected function compileJsonOverlaps($column, $value) - { - throw new RuntimeException('This database engine does not support JSON overlaps operations.'); - } - - /** - * Prepare the binding for a "JSON contains" statement. - * - * @param mixed $binding - * @return string - */ - public function prepareBindingForJsonContains($binding) - { - return json_encode($binding, JSON_UNESCAPED_UNICODE); - } - - /** - * Compile a "where JSON contains key" clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereJsonContainsKey(Builder $query, $where) - { - $not = $where['not'] ? 'not ' : ''; - - return $not.$this->compileJsonContainsKey( - $where['column'] - ); - } - - /** - * 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. * @@ -851,19 +489,20 @@ protected function compileGroups(Builder $query, $groups) * Compile the "having" portions of the query. * * @param \Illuminate\Database\Query\Builder $query + * @param array $havings * @return string */ - protected function compileHavings(Builder $query) + protected function compileHavings(Builder $query, $havings) { - return 'having '.$this->removeLeadingBoolean((new Collection($query->havings))->map(function ($having) { - return $having['boolean'].' '.$this->compileHaving($having); - })->implode(' ')); + $sql = implode(' ', array_map([$this, 'compileHaving'], $havings)); + + return 'having '.$this->removeLeadingBoolean($sql); } /** * Compile a single having clause. * - * @param array $having + * @param array $having * @return string */ protected function compileHaving(array $having) @@ -871,22 +510,17 @@ 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), - }; + if ($having['type'] === 'Raw') { + return $having['boolean'].' '.$having['sql']; + } + + return $this->compileBasicHaving($having); } /** * Compile a basic having clause. * - * @param array $having + * @param array $having * @return string */ protected function compileBasicHaving($having) @@ -895,89 +529,7 @@ protected function compileBasicHaving($having) $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).')'; + return $having['boolean'].' '.$column.' '.$having['operator'].' '.$parameter; } /** @@ -999,25 +551,23 @@ protected function compileOrders(Builder $query, $orders) /** * Compile the query orders to an array. * - * @param \Illuminate\Database\Query\Builder $query + * @param \Illuminate\Database\Query\Builder * @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']; + return array_map(function ($order) { + return ! isset($order['sql']) + ? $this->wrap($order['column']).' '.$order['direction'] + : $order['sql']; }, $orders); } /** * Compile the random statement into SQL. * - * @param string|int $seed + * @param string $seed * @return string */ public function compileRandom($seed) @@ -1037,66 +587,6 @@ 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. * @@ -1146,35 +636,9 @@ protected function compileUnions(Builder $query) */ 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); + $conjuction = $union['all'] ? ' union all ' : ' union '; - $query->aggregate = null; - - return $sql.' from ('.$this->compileSelect($query).') as '.$this->wrapTable('temp_table'); + return $conjuction.$union['query']->toSql(); } /** @@ -1204,62 +668,28 @@ public function compileInsert(Builder $query, array $values) // 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))) { + if (! is_array(reset($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 = collect($values)->map(function ($record) { + return '('.$this->parameterize($record).')'; + })->implode(', '); return "insert into $table ($columns) values $parameters"; } - /** - * 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.'); - } - - /** - * 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.'); - } - /** * 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 +697,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,79 +704,32 @@ 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) - ); - } + // 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 = collect($values)->map(function ($value, $key) { + return $this->wrap($key).' = '.$this->parameter($value); + })->implode(', '); - /** - * 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(', '); - } + // 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. + $joins = ''; - /** - * 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}"; - } + if (isset($query->joins)) { + $joins = ' '.$this->compileJoins($query, $query->joins); + } - /** - * 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); + // 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. + $wheres = $this->compileWheres($query); - return "update {$table} {$joins} set {$columns} {$where}"; - } - - /** - * 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.'); + return trim("update {$table}{$joins} set $columns $wheres"); } /** @@ -1392,12 +741,10 @@ public function compileUpsert(Builder $query, array $values, array $uniqueBy, ar */ public function prepareBindingsForUpdate(array $bindings, array $values) { - $cleanBindings = Arr::except($bindings, ['select', 'join']); - - $values = Arr::flatten(array_map(fn ($value) => value($value), $values)); + $bindingsWithoutJoin = Arr::except($bindings, 'join'); return array_values( - array_merge($bindings['join'], $values, Arr::flatten($cleanBindings)) + array_merge($bindings['join'], $values, Arr::flatten($bindingsWithoutJoin)) ); } @@ -1409,58 +756,9 @@ public function prepareBindingsForUpdate(array $bindings, array $values) */ public function compileDelete(Builder $query) { - $table = $this->wrapTable($query->from); - - $where = $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)); + $wheres = is_array($query->wheres) ? $this->compileWheres($query) : ''; - $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 {$this->wrapTable($query->from)} $wheres"); } /** @@ -1471,7 +769,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 +784,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 +816,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) @@ -1574,44 +840,6 @@ protected function removeLeadingBoolean($value) return preg_replace('/and |or /i', '', $value, 1); } - /** - * 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. * @@ -1621,14 +849,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..6943c147a0 100755 --- a/Query/Grammars/MySqlGrammar.php +++ b/Query/Grammars/MySqlGrammar.php @@ -2,20 +2,30 @@ 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; +use Illuminate\Database\Query\JsonExpression; 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 +37,35 @@ public function compileSelect(Builder $query) { $sql = parent::compileSelect($query); - if ($query->timeout === null) { - return $sql; - } - - $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.'); - } + if ($query->unions) { + $sql = '('.$sql.') '.$this->compileUnions($query); } - 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)'; + return $sql; } /** - * Compile a "JSON length" statement into SQL. + * Compile a single union statement. * - * @param string $column - * @param string $operator - * @param string $value + * @param array $union * @return string */ - protected function compileJsonLength($column, $operator, $value) + protected function compileUnion(array $union) { - [$field, $path] = $this->wrapJsonFieldAndPath($column); + $conjuction = $union['all'] ? ' union all ' : ' union '; - return 'json_length('.$field.$path.') '.$operator.' '.$value; - } - - /** - * Compile a "JSON value cast" statement into SQL. - * - * @param string $value - * @return string - */ - public function compileJsonValueCast($value) - { - return 'cast('.$value.' as json)'; + return $conjuction.'('.$union['query']->toSql().')'; } /** * Compile the random statement into SQL. * - * @param string|int $seed + * @param string $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 'RAND('.$seed.')'; } /** @@ -355,137 +85,87 @@ protected function compileLock(Builder $query, $value) } /** - * Compile an insert statement into SQL. + * Compile an update statement into SQL. * * @param \Illuminate\Database\Query\Builder $query * @param array $values * @return string */ - public function compileInsert(Builder $query, array $values) + public function compileUpdate(Builder $query, $values) { - if (empty($values)) { - $values = [[]]; - } + $table = $this->wrapTable($query->from); - return parent::compileInsert($query, $values); - } + // 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); - /** - * 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); - } + // 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. + $joins = ''; - return $this->wrap($key).' = '.$this->parameter($value); - })->implode(', '); - } + if (isset($query->joins)) { + $joins = ' '.$this->compileJoins($query, $query->joins); + } - /** - * 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) - { - $useUpsertAlias = $query->connection->getConfig('use_upsert_alias'); + // 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); - $sql = $this->compileInsert($query, $values); + $sql = rtrim("update {$table}{$joins} set $columns $where"); - if ($useUpsertAlias) { - $sql .= ' as laravel_upsert_alias'; + // If the query has an order by clause we will compile it since MySQL supports + // order bys on update statements. We'll compile them using the typical way + // of compiling order bys. Then they will be appended to the SQL queries. + if (! empty($query->orders)) { + $sql .= ' '.$this->compileOrders($query, $query->orders); } - $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(', '); + // Updates on MySQL also supports "limits", which allow you to easily update a + // single record very easily. This is not supported by all database engines + // so we have customized this update compiler here in order to add it in. + if (isset($query->limit)) { + $sql .= ' '.$this->compileLimit($query, $query->limit); + } - return $sql.$columns; + return rtrim($sql); } /** - * Compile a "lateral join" clause. + * Compile all of the columns for an update statement. * - * @param \Illuminate\Database\Query\JoinLateralClause $join - * @param string $expression + * @param array $values * @return string */ - public function compileJoinLateral(JoinLateralClause $join, string $expression): string - { - return trim("{$join->type} join lateral {$expression} on true"); - } - - /** - * {@inheritdoc} - */ - protected function supportsStraightJoins() + protected function compileUpdateColumns($values) { - return true; + return collect($values)->map(function ($value, $key) { + if ($this->isJsonSelector($key)) { + return $this->compileJsonUpdateColumn($key, new JsonExpression($value)); + } else { + return $this->wrap($key).' = '.$this->parameter($value); + } + })->implode(', '); } /** - * Prepare a JSON column being updated using the JSON_SET function. + * Prepares a JSON column being updated using the JSON_SET function. * * @param string $key - * @param mixed $value + * @param \Illuminate\Database\Query\JsonExpression $value * @return string */ - protected function compileJsonUpdateColumn($key, $value) + protected function compileJsonUpdateColumn($key, JsonExpression $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); + $path = explode('->', $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) - { - $sql = parent::compileUpdateWithoutJoins($query, $table, $columns, $where); - - if (! empty($query->orders)) { - $sql .= ' '.$this->compileOrders($query, $query->orders); - } + $field = $this->wrapValue(array_shift($path)); - if (isset($query->limit)) { - $sql .= ' '.$this->compileLimit($query, $query->limit); - } + $accessor = '"$.'.implode('.', $path).'"'; - return $sql; + return "{$field} = json_set({$field}, {$accessor}, {$value->getValue()})"; } /** @@ -497,56 +177,48 @@ protected function compileUpdateWithoutJoins(Builder $query, $table, $columns, $ * @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(); + $values = collect($values)->reject(function ($value, $column) { + return $this->isJsonSelector($column) && + in_array(gettype($value), ['boolean', 'integer', 'double']); + })->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; + return isset($query->joins) + ? $this->compileDeleteWithJoins($query, $table, $where) + : $this->compileDeleteWithoutJoins($query, $table, $where); } /** - * 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. + * Compile a delete query that does not use joins. * * @param \Illuminate\Database\Query\Builder $query * @param string $table - * @param string $where + * @param array $where * @return string */ - protected function compileDeleteWithJoins(Builder $query, $table, $where) + protected function compileDeleteWithoutJoins($query, $table, $where) { - $sql = parent::compileDeleteWithJoins($query, $table, $where); + $sql = trim("delete from {$table} {$where}"); + // When using MySQL, delete statements may contain order by statements and limits + // so we will compile both of those here. Once we have finished compiling this + // we will return the completed SQL statement so it will be executed for us. if (! empty($query->orders)) { $sql .= ' '.$this->compileOrders($query, $query->orders); } @@ -559,13 +231,21 @@ protected function compileDeleteWithJoins(Builder $query, $table, $where) } /** - * Compile a query to get the number of open connections for a database. + * Compile a delete query that uses joins. * + * @param \Illuminate\Database\Query\Builder $query + * @param string $table + * @param array $where * @return string */ - public function compileThreadCount() + protected function compileDeleteWithJoins($query, $table, $where) { - return 'select variable_value as `Value` from performance_schema.session_status where variable_name = \'threads_connected\''; + $joins = ' '.$this->compileJoins($query, $query->joins); + + $alias = strpos(strtolower($table), ' as ') !== false + ? explode(' as ', $table)[1] : $table; + + return trim("delete {$alias} from {$table}{$joins} {$where}"); } /** @@ -576,7 +256,18 @@ public function compileThreadCount() */ protected function wrapValue($value) { - return $value === '*' ? $value : '`'.str_replace('`', '``', $value).'`'; + if ($value === '*') { + return $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); + } + + return '`'.str_replace('`', '``', $value).'`'; } /** @@ -587,21 +278,23 @@ protected function wrapValue($value) */ protected function wrapJsonSelector($value) { - [$field, $path] = $this->wrapJsonFieldAndPath($value); + $path = explode('->', $value); + + $field = $this->wrapValue(array_shift($path)); - return 'json_unquote(json_extract('.$field.$path.'))'; + return sprintf('%s->\'$.%s\'', $field, collect($path)->map(function ($part) { + return '"'.$part.'"'; + })->implode('.')); } /** - * Wrap the given JSON selector for boolean values. + * Determine if the given string is a JSON selector. * * @param string $value - * @return string + * @return bool */ - protected function wrapJsonBooleanSelector($value) + protected function isJsonSelector($value) { - [$field, $path] = $this->wrapJsonFieldAndPath($value); - - return 'json_extract('.$field.$path.')'; + return Str::contains($value, '->'); } } diff --git a/Query/Grammars/PostgresGrammar.php b/Query/Grammars/PostgresGrammar.php index a21443523c..fa7f673cec 100755 --- a/Query/Grammars/PostgresGrammar.php +++ b/Query/Grammars/PostgresGrammar.php @@ -2,103 +2,24 @@ 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. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereBasic(Builder $query, $where) - { - if (str_contains(strtolower($where['operator']), 'like')) { - return sprintf( - '%s::text %s %s', - $this->wrap($where['column']), - $where['operator'], - $this->parameter($where['value']) - ); - } - - return parent::whereBasic($query, $where); - } - - /** - * Compile a bitwise operator where clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereBitwise(Builder $query, $where) - { - $value = $this->parameter($where['value']); - - $operator = str_replace('?', '??', $where['operator']); - - return '('.$this->wrap($where['column']).' '.$operator.' '.$value.')::bool'; - } - - /** - * 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' : 'ilike'; - - return $this->whereBasic($query, $where); - } - /** * Compile a "where date" clause. * @@ -108,33 +29,9 @@ protected function whereLike(Builder $query, $where) */ protected function whereDate(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; - } - - /** - * Compile a "where time" clause. - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereTime(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->wrap($where['column']).'::date '.$where['operator'].' '.$value; } /** @@ -152,199 +49,6 @@ protected function dateBasedWhere($type, Builder $query, $where) return 'extract('.$type.' from '.$this->wrap($where['column']).') '.$where['operator'].' '.$value; } - /** - * 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. - * - * @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 - * @return string - */ - protected function compileJsonContainsKey($column) - { - $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]; - } - - $column = str_replace('->>', '->', $this->wrap(implode('->', $segments))); - - 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), - ]); - } - - $key = "'".str_replace("'", "''", $lastSegment)."'"; - - return 'coalesce(('.$column.')::jsonb ?? '.$key.', false)'; - } - - /** - * Compile a "JSON length" statement into SQL. - * - * @param string $column - * @param string $operator - * @param string $value - * @return string - */ - protected function compileJsonLength($column, $operator, $value) - { - $column = str_replace('->>', '->', $this->wrap($column)); - - 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); - } - - 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.')::bool'; - } - /** * Compile the lock into SQL. * @@ -361,184 +65,84 @@ protected function compileLock(Builder $query, $value) 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'; - } - /** * 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) { - 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); + if (is_null($sequence)) { + $sequence = 'id'; } - 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(', '); + return $this->compileInsert($query, $values).' returning '.$this->wrap($sequence); } /** - * 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) + public function compileUpdate(Builder $query, $values) { - $sql = $this->compileInsert($query, $values); + $table = $this->wrapTable($query->from); - $sql .= ' on conflict ('.$this->columnize($uniqueBy).') do update set '; + // 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); - $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(', '); + $from = $this->compileUpdateFrom($query); - return $sql.$columns; - } + $where = $this->compileUpdateWheres($query); - /** - * 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"); + return trim("update {$table} set {$columns}{$from} {$where}"); } /** - * Prepares a JSON column being updated using the JSONB_SET function. + * Compile the columns for the update statement. * - * @param string $key - * @param mixed $value + * @param array $values * @return string */ - protected function compileJsonUpdateColumn($key, $value) + protected function compileUpdateColumns($values) { - $segments = explode('->', $key); - - $field = $this->wrap(array_shift($segments)); - - $path = "'{".implode(',', $this->wrapJsonPathAttributes($segments, '"'))."}'"; - - return "{$field} = jsonb_set({$field}::jsonb, {$path}, {$this->parameter($value)})"; + // 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. + return collect($values)->map(function ($value, $key) { + return $this->wrap($key).' = '.$this->parameter($value); + })->implode(', '); } /** - * Compile an update from statement into SQL. + * Compile the "from" clause for an update with a join. * * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @return string + * @return string|null */ - public function compileUpdateFrom(Builder $query, $values) + protected function compileUpdateFrom(Builder $query) { - $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 (! isset($query->joins)) { + return ''; } - $where = $this->compileUpdateWheres($query); + // 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 = collect($query->joins)->map(function ($join) { + return $this->wrapTable($join->table); + })->all(); - return trim("update {$table} set {$columns}{$from} {$where}"); + if (count($froms) > 0) { + return ' from '.implode(', ', $froms); + } } /** @@ -598,100 +202,18 @@ protected function compileUpdateJoinWheres(Builder $query) * @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)); + // Update statements with "joins" in Postgres use an interesting syntax. We need to + // take all of the bindings and put them on the end of this array since they are + // added to the end of the "where" clause statements as typical where clauses. + $bindingsWithoutJoin = Arr::except($bindings, 'join'); return array_values( - array_merge($values, Arr::flatten($cleanBindings)) + array_merge($values, $bindings['join'], Arr::flatten($bindingsWithoutJoin)) ); } - /** - * 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.'.ctid')); - - return "delete from {$table} where {$this->wrap('ctid')} in ({$selectSql})"; - } - /** * Compile a truncate table statement into SQL. * @@ -700,17 +222,29 @@ 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 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 (Str::contains($value, '->')) { + return $this->wrapJsonSelector($value); + } + + return '"'.str_replace('"', '""', $value).'"'; } /** @@ -723,7 +257,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 +271,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..6f5e9c30c6 100755 --- a/Query/Grammars/SQLiteGrammar.php +++ b/Query/Grammars/SQLiteGrammar.php @@ -3,97 +3,67 @@ 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 { + /** + * The components that make up a select clause. + * + * @var array + */ + protected $selectComponents = [ + 'aggregate', + 'columns', + 'from', + 'joins', + 'wheres', + 'groups', + 'havings', + 'orders', + 'limit', + 'offset', + 'lock', + ]; + /** * 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 a select query 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 */ - protected function wrapUnion($sql) + public function compileSelect(Builder $query) { - return 'select * from ('.$sql.')'; - } + $sql = parent::compileSelect($query); - /** - * 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']); - - return "{$column} IS {$value}"; + if ($query->unions) { + $sql = 'select * from ('.$sql.') '.$this->compileUnions($query); } - return parent::whereBasic($query, $where); + return $sql; } /** - * Compile a "where like" clause. + * Compile a single union statement. * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where + * @param array $union * @return string */ - protected function whereLike(Builder $query, $where) + protected function compileUnion(array $union) { - if ($where['caseSensitive'] == false) { - return parent::whereLike($query, $where); - } - $where['operator'] = $where['not'] ? 'not glob' : 'glob'; - - return $this->whereBasic($query, $where); - } + $conjuction = $union['all'] ? ' union all ' : ' union '; - /** - * 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 $conjuction.'select * from ('.$union['query']->toSql().')'; } /** @@ -144,18 +114,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,320 +124,52 @@ 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); + $value = str_pad($where['value'], 2, '0', STR_PAD_LEFT); - return (new Collection($values)) - ->reject(fn ($value, $key) => $this->isJsonSelector($key)) - ->merge($jsonGroups) - ->map(function ($value, $key) use ($jsonGroups) { - $column = last(explode('.', $key)); + $value = $this->parameter($value); - $value = isset($jsonGroups[$key]) ? $this->compileJsonPatch($column, $value) : $this->parameter($value); - - return $this->wrap($column).' = '.$value; - }) - ->implode(', '); + return "strftime('{$type}', {$this->wrap($where['column'])}) {$where['operator']} {$value}"; } /** - * Compile an "upsert" statement into SQL. + * Compile an insert 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) + 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); - $columns = $this->compileUpdateColumns($query, $values); - - $alias = last(preg_split('/\s+as\s+/i', $query->from)); - - $selectSql = $this->compileSelect($query->select($alias.'.rowid')); - - 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)) - ); - } + if (! is_array(reset($values))) { + $values = [$values]; + } - /** - * 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); + // 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)); } - return parent::compileDelete($query); - } + $names = $this->columnize(array_keys(reset($values))); - /** - * 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); + $columns = []; - $alias = last(preg_split('/\s+as\s+/i', $query->from)); + // 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); + } - $selectSql = $this->compileSelect($query->select($alias.'.rowid')); + $columns = array_fill(0, count($values), implode(', ', $columns)); - return "delete from {$table} where {$this->wrap('rowid')} in ({$selectSql})"; + return "insert into $table ($names) select ".implode(' union all select ', $columns); } /** @@ -490,26 +180,9 @@ protected function compileDeleteWithJoinsOrLimit(Builder $query) */ 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 sqlite_sequence where name = ?' => [$query->from], '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); - - return 'json_extract('.$field.$path.')'; - } } diff --git a/Query/Grammars/SqlServerGrammar.php b/Query/Grammars/SqlServerGrammar.php index f22f528e35..7fa2050a66 100755 --- a/Query/Grammars/SqlServerGrammar.php +++ b/Query/Grammars/SqlServerGrammar.php @@ -2,46 +2,22 @@ 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; +use Illuminate\Database\Query\Builder; 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 +26,20 @@ 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)']; + if (! $query->offset) { + return parent::compileSelect($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 (is_null($query->columns)) { + $query->columns = ['*']; } - return parent::compileSelect($query); + return $this->compileAnsiOffset( + $query, $this->compileComponents($query) + ); } /** @@ -76,8 +60,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); @@ -105,46 +89,6 @@ protected function compileFrom(Builder $query, $table) return $from; } - /** - * 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 "with (index([{$index}]))"; - } - - /** - * {@inheritdoc} - * - * @param \Illuminate\Database\Query\Builder $query - * @param array $where - * @return string - */ - protected function whereBitwise(Builder $query, $where) - { - $value = $this->parameter($where['value']); - - $operator = str_replace('?', '??', $where['operator']); - - return '('.$this->wrap($where['column']).' '.$operator.' '.$value.') != 0'; - } - /** * Compile a "where date" clause. * @@ -160,146 +104,84 @@ protected function whereDate(Builder $query, $where) } /** - * Compile a "where time" clause. + * Create a full ANSI offset clause for the query. * * @param \Illuminate\Database\Query\Builder $query - * @param array $where + * @param array $components * @return string */ - protected function whereTime(Builder $query, $where) + protected function compileAnsiOffset(Builder $query, $components) { - $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) - { - [$field, $path] = $this->wrapJsonFieldAndPath($column); - - 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); - - $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)."'"; + // 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 (empty($components['orders'])) { + $components['orders'] = 'order by (select 0)'; } - [$field, $path] = $this->wrapJsonFieldAndPath(implode('->', $segments)); + // 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. + $components['columns'] .= $this->compileOver($components['orders']); - return $key.' in (select [key] from openjson('.$field.$path.'))'; - } + unset($components['orders']); - /** - * 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); + // 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. + $sql = $this->concatenate($components); - return '(select count(*) from openjson('.$field.$path.')) '.$operator.' '.$value; + return $this->compileTableExpression($sql, $query); } /** - * Compile a "JSON value cast" statement into SQL. + * Compile the over statement for a table expression. * - * @param string $value + * @param string $orderings * @return string */ - public function compileJsonValueCast($value) + protected function compileOver($orderings) { - return 'json_query('.$value.')'; + return ", row_number() over ({$orderings}) as row_num"; } /** - * Compile a single having clause. + * Compile a common table expression for a query. * - * @param array $having + * @param string $sql + * @param \Illuminate\Database\Query\Builder $query * @return string */ - protected function compileHaving(array $having) + protected function compileTableExpression($sql, $query) { - if ($having['type'] === 'Bitwise') { - return $this->compileHavingBitwise($having); - } + $constraint = $this->compileRowConstraint($query); - return parent::compileHaving($having); + return "select * from ({$sql}) as temp_table where row_num {$constraint}"; } /** - * Compile a having clause involving a bitwise operator. + * Compile the limit / offset row constraint for a query. * - * @param array $having + * @param \Illuminate\Database\Query\Builder $query * @return string */ - protected function compileHavingBitwise($having) + protected function compileRowConstraint($query) { - $column = $this->wrap($having['column']); - - $parameter = $this->parameter($having['value']); + $start = $query->offset + 1; - return '('.$column.' '.$having['operator'].' '.$parameter.') != 0'; - } + if ($query->limit > 0) { + $finish = $query->offset + $query->limit; - /** - * 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) - { - $sql = parent::compileDeleteWithoutJoins($query, $table, $where); + return "between {$start} and {$finish}"; + } - return ! is_null($query->limit) && $query->limit > 0 && $query->offset <= 0 - ? Str::replaceFirst('delete', 'delete top ('.$query->limit.')', $sql) - : $sql; + return ">= {$start}"; } /** * Compile the random statement into SQL. * - * @param string|int $seed + * @param string $seed * @return string */ public function compileRandom($seed) @@ -316,31 +198,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,12 +210,6 @@ protected function compileRowNumber($partition, $orders) */ protected function compileOffset(Builder $query, $offset) { - $offset = (int) $offset; - - if ($offset) { - return "offset {$offset} rows"; - } - return ''; } @@ -371,17 +225,6 @@ protected function compileLock(Builder $query, $value) return ''; } - /** - * Wrap a union subquery in parentheses. - * - * @param string $sql - * @return string - */ - protected function wrapUnion($sql) - { - return 'select * from ('.$sql.') as '.$this->wrapTable('temp_table'); - } - /** * Compile an exists statement into SQL. * @@ -398,128 +241,134 @@ public function compileExists(Builder $query) } /** - * Compile an update statement with joins into SQL. + * Compile a delete statement 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) + public function compileDelete(Builder $query) { - $alias = last(explode(' as ', $table)); + $table = $this->wrapTable($query->from); - $joins = $this->compileJoins($query, $query->joins); + $where = is_array($query->wheres) ? $this->compileWheres($query) : ''; - return "update {$alias} set {$columns} from {$table} {$joins} {$where}"; + return isset($query->joins) + ? $this->compileDeleteWithJoins($query, $table, $where) + : trim("delete from {$table} {$where}"); } /** - * Compile an "upsert" statement into SQL. + * Compile a delete statement with joins into SQL. * * @param \Illuminate\Database\Query\Builder $query - * @param array $values - * @param array $uniqueBy - * @param array $update + * @param string $table + * @param string $where * @return string */ - public function compileUpsert(Builder $query, array $values, array $uniqueBy, array $update) + protected function compileDeleteWithJoins(Builder $query, $table, $where) { - $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(', '); + $joins = ' '.$this->compileJoins($query, $query->joins); - $sql .= 'when matched then update set '.$update.' '; - } - - $sql .= 'when not matched then insert ('.$columns.') values ('.$columns.');'; + $alias = strpos(strtolower($table), ' as ') !== false + ? explode(' as ', $table)[1] : $table; - return $sql; + return trim("delete {$alias} from {$table}{$joins} {$where}"); } /** - * Prepare the bindings for an update statement. + * Compile a truncate table statement into SQL. * - * @param array $bindings - * @param array $values + * @param \Illuminate\Database\Query\Builder $query * @return array */ - #[\Override] - public function prepareBindingsForUpdate(array $bindings, array $values) + public function compileTruncate(Builder $query) { - $cleanBindings = Arr::except($bindings, 'select'); - - $values = Arr::flatten(array_map(fn ($value) => value($value), $values)); - - return array_values( - array_merge($values, Arr::flatten($cleanBindings)) - ); + return ['truncate table '.$this->wrapTable($query->from) => []]; } /** - * Compile a "lateral join" clause. + * Compile an update statement into SQL. * - * @param \Illuminate\Database\Query\JoinLateralClause $join - * @param string $expression + * @param \Illuminate\Database\Query\Builder $query + * @param array $values * @return string */ - public function compileJoinLateral(JoinLateralClause $join, string $expression): string + public function compileUpdate(Builder $query, $values) { - $type = $join->type == 'left' ? 'outer' : 'cross'; + list($table, $alias) = $this->parseUpdateTable($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 = collect($values)->map(function ($value, $key) { + return $this->wrap($key).' = '.$this->parameter($value); + })->implode(', '); + + // 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. + $joins = ''; + + if (isset($query->joins)) { + $joins = ' '.$this->compileJoins($query, $query->joins); + } - return trim("{$type} apply {$expression}"); + // 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"); } /** - * Compile the SQL statement to define a savepoint. + * Get the table and alias for the given table. * - * @param string $name - * @return string + * @param string $table + * @return array */ - public function compileSavepoint($name) + protected function parseUpdateTable($table) { - return 'SAVE TRANSACTION '.$name; + $table = $alias = $this->wrapTable($table); + + if (strpos(strtolower($table), '] as [') !== false) { + $alias = '['.explode('] as [', $table)[1]; + } + + return [$table, $alias]; } /** - * Compile the SQL statement to execute a savepoint rollback. + * Prepare the bindings for an update statement. * - * @param string $name - * @return string + * @param array $bindings + * @param array $values + * @return array */ - public function compileSavepointRollBack($name) + public function prepareBindingsForUpdate(array $bindings, array $values) { - return 'ROLLBACK TRANSACTION '.$name; + // Update statements with joins in SQL Servers utilize an unique syntax. We need to + // take all of the bindings and put them on the end of this array since they are + // added to the end of the "where" clause statements as typical where clauses. + $bindingsWithoutJoin = Arr::except($bindings, 'join'); + + return array_values( + array_merge($values, $bindings['join'], Arr::flatten($bindingsWithoutJoin)) + ); } /** - * 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 +378,7 @@ public function compileThreadCount() */ public function getDateFormat() { - return 'Y-m-d H:i:s.v'; + return 'Y-m-d H:i:s.000'; } /** @@ -543,44 +392,15 @@ protected function wrapValue($value) return $value === '*' ? $value : '['.str_replace(']', ']]', $value).']'; } - /** - * Wrap the given JSON selector. - * - * @param string $value - * @return string - */ - protected function wrapJsonSelector($value) - { - [$field, $path] = $this->wrapJsonFieldAndPath($value); - - return 'json_value('.$field.$path.')'; - } - - /** - * Wrap the given JSON boolean value. - * - * @param string $value - * @return string - */ - protected function wrapJsonBooleanValue($value) - { - return "'".$value."'"; - } - /** * 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..ca7b4d4547 100755 --- a/Query/JoinClause.php +++ b/Query/JoinClause.php @@ -16,56 +16,33 @@ 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. - * - * @var \Illuminate\Database\Query\Grammars\Grammar - */ - protected $parentGrammar; - - /** - * The processor of the parent query builder. + * The parent query builder instance. * - * @var \Illuminate\Database\Query\Processors\Processor - */ - protected $parentProcessor; - - /** - * The class name of the parent query builder. - * - * @var string + * @var \Illuminate\Database\Query\Builder */ - protected $parentClass; + private $parentQuery; /** * Create a new join clause instance. * - * @param \Illuminate\Database\Query\Builder $parentQuery + * @param \Illuminate\Database\Query\Builder $parentQuery * @param string $type * @param string $table + * @return void */ public function __construct(Builder $parentQuery, $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(); + $this->parentQuery = $parentQuery; parent::__construct( - $this->parentConnection, $this->parentGrammar, $this->parentProcessor + $parentQuery->getConnection(), $parentQuery->getGrammar(), $parentQuery->getProcessor() ); } @@ -79,11 +56,11 @@ 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 * @return $this * @@ -101,9 +78,9 @@ public function on($first, $operator = null, $second = null, $boolean = 'and') /** * 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) @@ -118,28 +95,6 @@ public function orOn($first, $operator = null, $second = null) */ public function newQuery() { - return new static($this->newParentQuery(), $this->type, $this->table); - } - - /** - * Create a new query instance for sub-query. - * - * @return \Illuminate\Database\Query\Builder - */ - protected function forSubQuery() - { - return $this->newParentQuery()->newQuery(); - } - - /** - * Create a new parent query instance. - * - * @return \Illuminate\Database\Query\Builder - */ - protected function newParentQuery() - { - $class = $this->parentClass; - - return new $class($this->parentConnection, $this->parentGrammar, $this->parentProcessor); + return new static($this->parentQuery, $this->type, $this->table); } } 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 @@ -value = $this->getJsonBindingParameter($value); + } + + /** + * Translate the given value into the appropriate JSON binding parameter. + * + * @param mixed $value + * @return string + */ + protected function getJsonBindingParameter($value) + { + switch ($type = gettype($value)) { + case 'boolean': + return $value ? 'true' : 'false'; + case 'integer': + case 'double': + return $value; + case 'string': + return '?'; + case 'object': + case 'array': + return '?'; + } + + throw new InvalidArgumentException('JSON value is of illegal type: '.$type); + } + + /** + * Get the value of the expression. + * + * @return mixed + */ + 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/Processors/MariaDbProcessor.php b/Query/Processors/MariaDbProcessor.php deleted file mode 100644 index 7a549d57fc..0000000000 --- a/Query/Processors/MariaDbProcessor.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; - - 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 [ - '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); - } } diff --git a/Query/Processors/PostgresProcessor.php b/Query/Processors/PostgresProcessor.php index 2818f91d8c..90abf24682 100755 --- a/Query/Processors/PostgresProcessor.php +++ b/Query/Processors/PostgresProcessor.php @@ -11,17 +11,13 @@ 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]; + $result = $query->getConnection()->selectFromWriteConnection($sql, $values)[0]; $sequence = $sequence ?: 'id'; @@ -30,124 +26,16 @@ public function processInsertGetId(Builder $query, $sql, $values, $sequence = nu 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; - - $autoincrement = $result->default !== null && str_starts_with($result->default, 'nextval('); - - 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); - } - - /** @inheritDoc */ - public function processIndexes($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); - } - - /** @inheritDoc */ - public function processForeignKeys($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' => $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, - }, - ]; + return ((object) $result)->column_name; }, $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..65da1dff7c 100644 --- a/Query/Processors/SQLiteProcessor.php +++ b/Query/Processors/SQLiteProcessor.php @@ -4,96 +4,16 @@ class SQLiteProcessor extends Processor { - /** @inheritDoc */ - public function processColumns($results, $sql = '') - { - $hasPrimaryKey = array_sum(array_column($results, 'primary')) === 1; - - return array_map(function ($result) use ($hasPrimaryKey, $sql) { - $result = (object) $result; - - $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) + /** + * 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' => 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), - ]; + return ((object) $result)->name; }, $results); } } diff --git a/Query/Processors/SqlServerProcessor.php b/Query/Processors/SqlServerProcessor.php index 8d000c4579..65140c49b2 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,7 +37,6 @@ public function processInsertGetId(Builder $query, $sql, $values, $sequence = nu * * @param \Illuminate\Database\Connection $connection * @return int - * * @throws \Exception */ protected function processInsertGetIdForOdbc(Connection $connection) @@ -55,67 +54,16 @@ protected function processInsertGetIdForOdbc(Connection $connection) 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); - } - - /** @inheritDoc */ - public function processIndexes($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); - } - - /** @inheritDoc */ - public function processForeignKeys($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' => $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)), - ]; + return ((object) $result)->name; }, $results); } } diff --git a/QueryException.php b/QueryException.php index b9251d2fe0..9a3687d65b 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,22 @@ 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->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 +46,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 +66,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 +75,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..e7cdeed2c8 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(); 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,7 +17,7 @@ protected function isUniqueConstraintError(Exception $exception) */ protected function getDefaultQueryGrammar() { - return new QueryGrammar($this); + return $this->withTablePrefix(new QueryGrammar); } /** @@ -93,29 +41,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..695d1cc39f 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); } } @@ -159,32 +113,6 @@ public function toSql() return $statements; } - /** - * 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. * @@ -192,19 +120,15 @@ protected function commandsNamed(array $names) */ 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,34 +139,12 @@ 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; - } - + 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 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; - - 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; + $this->{$index}($column->name); continue 2; } @@ -251,12 +153,7 @@ protected function addFluentIndexes() // 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; + $this->{$index}($column->name, $column->{$index}); continue 2; } @@ -264,69 +161,16 @@ protected function addFluentIndexes() } } - /** - * 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. - * - * @return void - */ - public function addAlterCommands() - { - 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; - } - - $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() + protected function creating() { - return (new Collection($this->commands)) - ->contains(fn ($command) => ! $command instanceof ColumnDefinition && $command->name === 'create'); + return collect($this->commands)->contains(function ($command) { + return $command->name == 'create'; + }); } /** @@ -339,49 +183,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 +216,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 +241,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 +271,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 +282,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 +305,21 @@ public function dropTimestampsTz() /** * Indicate that the soft delete column should be dropped. * - * @param string $column * @return void */ - public function dropSoftDeletes($column = 'deleted_at') + public function dropSoftDeletes() { - $this->dropColumn($column); + $this->dropColumn('deleted_at'); } /** * Indicate that the soft delete column should be dropped. * - * @param string $column * @return void */ - public function dropSoftDeletesTz($column = 'deleted_at') + public function dropSoftDeletesTz() { - $this->dropSoftDeletes($column); + $this->dropSoftDeletes(); } /** @@ -612,20 +332,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 +347,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 +360,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,99 +373,32 @@ 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) { return $this->indexCommand('index', $columns, $name, $algorithm); } - /** - * Specify a fulltext index for the table. - * - * @param string|array $columns - * @param string|null $name - * @param string|null $algorithm - * @return \Illuminate\Database\Schema\IndexDefinition - */ - public function fullText($columns, $name = null, $algorithm = null) - { - return $this->indexCommand('fulltext', $columns, $name, $algorithm); - } - - /** - * Specify a spatial index for the table. - * - * @param string|array $columns - * @param string|null $name - * @param string|null $operatorClass - * @return \Illuminate\Database\Schema\IndexDefinition - */ - public function spatialIndex($columns, $name = null, $operatorClass = null) - { - return $this->indexCommand('spatialIndex', $columns, $name, null, $operatorClass); - } - - /** - * Specify a vector index for the table. - * - * @param string $column - * @param string|null $name - * @return \Illuminate\Database\Schema\IndexDefinition - */ - public function vectorIndex($column, $name = null) - { - return $this->indexCommand('vectorIndex', $column, $name, 'hnsw', 'vector_cosine_ops'); - } - - /** - * Specify a raw index for the table. - * - * @param string $expression - * @param string $name - * @return \Illuminate\Database\Schema\IndexDefinition - */ - public function rawIndex($expression, $name) - { - return $this->index([new Expression($expression)], $name); - } - /** * Specify a foreign key for the table. * * @param string|array $columns - * @param string|null $name - * @return \Illuminate\Database\Schema\ForeignKeyDefinition + * @param string $name + * @return \Illuminate\Support\Fluent */ public function foreign($columns, $name = null) { - $command = new ForeignKeyDefinition( - $this->indexCommand('foreign', $columns, $name)->getAttributes() - ); - - $this->commands[count($this->commands) - 1] = $command; - - return $command; - } - - /** - * 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 id($column = 'id') - { - return $this->bigIncrements($column); + return $this->indexCommand('foreign', $columns, $name); } /** - * Create a new auto-incrementing integer column on the table (4-byte, 0 to 4,294,967,295). + * Create a new auto-incrementing integer (4-byte) column on the table. * * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ public function increments($column) { @@ -767,21 +406,10 @@ public function increments($column) } /** - * Create a new auto-incrementing integer column on the table (4-byte, 0 to 4,294,967,295). - * - * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function integerIncrements($column) - { - return $this->unsignedInteger($column, true); - } - - /** - * Create a new auto-incrementing tiny integer column on the table (1-byte, 0 to 255). + * Create a new auto-incrementing tiny integer (1-byte) column on the table. * * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ public function tinyIncrements($column) { @@ -789,10 +417,10 @@ public function tinyIncrements($column) } /** - * Create a new auto-incrementing small integer column on the table (2-byte, 0 to 65,535). + * Create a new auto-incrementing small integer (2-byte) column on the table. * * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ public function smallIncrements($column) { @@ -800,10 +428,10 @@ public function smallIncrements($column) } /** - * Create a new auto-incrementing medium integer column on the table (3-byte, 0 to 16,777,215). + * Create a new auto-incrementing medium integer (3-byte) column on the table. * * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ public function mediumIncrements($column) { @@ -811,10 +439,10 @@ public function mediumIncrements($column) } /** - * 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 auto-incrementing big integer (8-byte) column on the table. * * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ public function bigIncrements($column) { @@ -825,12 +453,12 @@ public function bigIncrements($column) * Create a new char column on the table. * * @param string $column - * @param int|null $length - * @return \Illuminate\Database\Schema\ColumnDefinition + * @param int $length + * @return \Illuminate\Support\Fluent */ public function char($column, $length = null) { - $length = ! is_null($length) ? $length : Builder::$defaultStringLength; + $length = $length ?: Builder::$defaultStringLength; return $this->addColumn('char', $column, compact('length')); } @@ -839,8 +467,8 @@ public function char($column, $length = null) * Create a new string column on the table. * * @param string $column - * @param int|null $length - * @return \Illuminate\Database\Schema\ColumnDefinition + * @param int $length + * @return \Illuminate\Support\Fluent */ public function string($column, $length = null) { @@ -850,32 +478,21 @@ public function string($column, $length = null) } /** - * Create a new tiny text column on the table (up to 255 characters). + * Create a new text column on the table. * * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ - public function tinyText($column) + public function text($column) { - return $this->addColumn('tinyText', $column); + return $this->addColumn('text', $column); } /** - * Create a new text column on the table (up to 65,535 characters / ~64 KB). + * Create a new medium text column on the table. * * @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 + * @return \Illuminate\Support\Fluent */ public function mediumText($column) { @@ -883,10 +500,10 @@ public function mediumText($column) } /** - * Create a new long text column on the table (up to 4,294,967,295 characters / ~4 GB). + * Create a new long text column on the table. * * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ public function longText($column) { @@ -895,12 +512,11 @@ public function 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 + * @return \Illuminate\Support\Fluent */ public function integer($column, $autoIncrement = false, $unsigned = false) { @@ -909,12 +525,11 @@ public function integer($column, $autoIncrement = false, $unsigned = false) /** * 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 +538,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 +551,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 +564,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,11 +576,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 integer (4-byte) column on the table. * * @param string $column * @param bool $autoIncrement - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ public function unsignedInteger($column, $autoIncrement = false) { @@ -976,11 +588,11 @@ public function unsignedInteger($column, $autoIncrement = false) } /** - * Create a new unsigned tiny integer column on the table (1-byte, 0 to 255). + * Create a new unsigned tiny integer (1-byte) column on the table. * * @param string $column * @param bool $autoIncrement - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ public function unsignedTinyInteger($column, $autoIncrement = false) { @@ -988,11 +600,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 +612,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 +624,50 @@ 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 big integer (8-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) { return $this->bigInteger($column, $autoIncrement, true); } - /** - * Create a new unsigned big integer column on the table (8-byte, 0 to 18,446,744,073,709,551,615). - * - * @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 - */ - public function foreignIdFor($model, $column = null) - { - 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()); - } - /** * 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 +678,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 +689,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 +712,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 +723,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 +734,78 @@ 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 + * @return \Illuminate\Support\Fluent */ - public function timestampTz($column, $precision = null) + public function timestampTz($column) { - $precision ??= $this->defaultTimePrecision(); - - return $this->addColumn('timestampTz', $column, compact('precision')); + return $this->addColumn('timestampTz', $column); } /** * Add nullable creation and update timestamps to the table. * - * @param int|null $precision - * @return \Illuminate\Support\Collection + * @return void */ - public function timestamps($precision = null) + public function timestamps() { - return new Collection([ - $this->timestamp('created_at', $precision)->nullable(), - $this->timestamp('updated_at', $precision)->nullable(), - ]); + $this->timestamp('created_at')->nullable(); + + $this->timestamp('updated_at')->nullable(); } /** @@ -1282,175 +813,74 @@ 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 + * @return void */ - public function timestampsTz($precision = null) + public function nullableTimestamps() { - return new Collection([ - $this->timestampTz('created_at', $precision)->nullable(), - $this->timestampTz('updated_at', $precision)->nullable(), - ]); + $this->timestamps(); } /** - * Add nullable creation and update timestampTz columns to the table. - * - * Alias for self::timestampsTz(). + * Add creation and update timestampTz columns to the table. * - * @param int|null $precision - * @return \Illuminate\Support\Collection + * @return void */ - public function nullableTimestampsTz($precision = null) + public function timestampsTz() { - return $this->timestampsTz($precision); - } + $this->timestampTz('created_at')->nullable(); - /** - * Add creation and update datetime columns to the table. - * - * @param int|null $precision - * @return \Illuminate\Support\Collection - */ - public function datetimes($precision = null) - { - return new Collection([ - $this->datetime('created_at', $precision)->nullable(), - $this->datetime('updated_at', $precision)->nullable(), - ]); + $this->timestampTz('updated_at')->nullable(); } /** * Add a "deleted at" timestamp for the table. * - * @param string $column - * @param int|null $precision - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ - public function softDeletes($column = 'deleted_at', $precision = null) + public function softDeletes() { - return $this->timestamp($column, $precision)->nullable(); + return $this->timestamp('deleted_at')->nullable(); } /** * Add a "deleted at" timestampTz for the table. * - * @param string $column - * @param int|null $precision - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function softDeletesTz($column = 'deleted_at', $precision = null) - { - return $this->timestampTz($column, $precision)->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(); - } - - /** - * Create a new year column on the table. - * - * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition + * @return \Illuminate\Support\Fluent */ - public function year($column) + public function softDeletesTz() { - return $this->addColumn('year', $column); + return $this->timestampTz('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,227 +889,49 @@ 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) + public function morphs($name, $indexName = null) { - $this->string("{$name}_type") - ->nullable() - ->after($after); + $this->unsignedInteger("{$name}_id"); - $this->unsignedBigInteger("{$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 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. + * Add nullable columns for a polymorphic table. * * @param string $name * @param string|null $indexName - * @param string|null $after * @return void */ - public function nullableUlidMorphs($name, $indexName = null, $after = null) + public function nullableMorphs($name, $indexName = null) { - $this->string("{$name}_type") - ->nullable() - ->after($after); + $this->unsignedInteger("{$name}_id")->nullable(); - $this->ulid("{$name}_id") - ->nullable() - ->after(! is_null($after) ? "{$name}_type" : null); + $this->string("{$name}_type")->nullable(); - $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() { @@ -1687,39 +939,15 @@ public function rememberToken() } /** - * 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. + * Add a new index command to the blueprint. * - * @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 $type * @param string|array $columns - * @param string $index - * @param string|null $algorithm - * @param string|null $operatorClass + * @param string $index + * @param string|null $algorithm * @return \Illuminate\Support\Fluent */ - protected function indexCommand($type, $columns, $index, $algorithm = null, $operatorClass = null) + protected function indexCommand($type, $columns, $index, $algorithm = null) { $columns = (array) $columns; @@ -1729,7 +957,7 @@ protected function indexCommand($type, $columns, $index, $algorithm = null, $ope $index = $index ?: $this->createIndexName($type, $columns); return $this->addCommand( - $type, compact('index', 'columns', 'algorithm', 'operatorClass') + $type, compact('index', 'columns', 'algorithm') ); } @@ -1759,20 +987,12 @@ protected function dropIndexCommand($command, $type, $index) * Create a default index name for the table. * * @param string $type - * @param array $columns + * @param array $columns * @return string */ protected function createIndexName($type, array $columns) { - $table = $this->table; - - 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; - } - - $index = strtolower($table.'_'.implode('_', $columns).'_'.$type); + $index = strtolower($this->table.'_'.implode('_', $columns).'_'.$type); return str_replace(['-', '.'], '_', $index); } @@ -1782,53 +1002,16 @@ protected function createIndexName($type, array $columns) * * @param string $type * @param string $name - * @param array $parameters - * @return \Illuminate\Database\Schema\ColumnDefinition + * @param array $parameters + * @return \Illuminate\Support\Fluent */ public function addColumn($type, $name, array $parameters = []) { - return $this->addColumnDefinition(new ColumnDefinition( + $this->columns[] = $column = new Fluent( 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 - */ - protected function addColumnDefinition($definition) - { - $this->columns[] = $definition; - - if (! $this->creating()) { - $this->commands[] = $definition; - } - - if ($this->after) { - $definition->after($this->after); - - $this->after = $definition->name; - } - - return $definition; - } - - /** - * Add the columns from the callback after the given column. - * - * @param string $column - * @param (\Closure(self): void) $callback - * @return void - */ - public function after($column, Closure $callback) - { - $this->after = $column; - - $callback($this); + ); - $this->after = null; + return $column; } /** @@ -1840,11 +1023,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 +1047,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 +1065,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 +1078,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 +1100,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 +1108,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..e072df1112 100755 --- a/Schema/Builder.php +++ b/Schema/Builder.php @@ -3,18 +3,11 @@ 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; +use Illuminate\Database\Connection; class Builder { - use Macroable; - /** * The database connection instance. * @@ -32,33 +25,22 @@ 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 + * @var int */ 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) { @@ -69,7 +51,7 @@ public function __construct(Connection $connection) /** * Set the default string length for migrations. * - * @param non-negative-int $length + * @param int $length * @return void */ public static function defaultStringLength($length) @@ -77,89 +59,6 @@ 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 +67,11 @@ public function getSchemas() */ public function hasTable($table) { - [$schema, $table] = $this->parseSchemaAndTable($table); - $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( + $this->grammar->compileTableExists(), [$table] + )) > 0; } /** @@ -270,7 +84,7 @@ public function getTypes($schema = null) public function hasColumn($table, $column) { return in_array( - strtolower($column), array_map(strtolower(...), $this->getColumnListing($table)) + strtolower($column), array_map('strtolower', $this->getColumnListing($table)) ); } @@ -278,12 +92,12 @@ public function hasColumn($table, $column) * 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,200 +108,39 @@ 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) - ) - ); - } - - /** - * 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; + $results = $this->connection->select($this->grammar->compileColumnListing($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 */ @@ -499,7 +152,7 @@ public function table($table, Closure $callback) /** * Create a new table on the schema. * - * @param string $table + * @param string $table * @param \Closure $callback * @return void */ @@ -538,20 +191,6 @@ public function dropIfExists($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); - }); - } - /** * Drop all tables from the database. * @@ -564,30 +203,6 @@ 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.'); - } - /** * Rename a table on the schema. * @@ -626,59 +241,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 +249,7 @@ public function ensureExtensionExists($name, $schema = null) */ protected function build(Blueprint $blueprint) { - $blueprint->build(); + $blueprint->build($this->connection, $this->grammar); } /** @@ -697,82 +259,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')); + return new Blueprint($table, $callback); } /** - * Get the names of the current schemas for the connection. - * - * @return string[]|null - */ - public function getCurrentSchemaListing() - { - return null; - } - - /** - * 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. - * - * @param string $reference - * @param string|bool|null $withDefaultSchema - * @return array{string|null, string} + * Set the database connection instance. * - * @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/ChangeColumn.php b/Schema/Grammars/ChangeColumn.php new file mode 100644 index 0000000000..4751ecbed7 --- /dev/null +++ b/Schema/Grammars/ChangeColumn.php @@ -0,0 +1,205 @@ +isDoctrineAvailable()) { + throw new RuntimeException(sprintf( + 'Changing columns for table "%s" requires Doctrine DBAL; install "doctrine/dbal".', + $blueprint->getTable() + )); + } + + $tableDiff = static::getChangedDiff( + $grammar, $blueprint, $schema = $connection->getDoctrineSchemaManager() + ); + + if ($tableDiff !== false) { + return (array) $schema->getDatabasePlatform()->getAlterTableSQL($tableDiff); + } + + return []; + } + + /** + * Get the Doctrine table difference for the given changes. + * + * @param \Illuminate\Database\Schema\Grammars\Grammar $grammar + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Doctrine\DBAL\Schema\AbstractSchemaManager $schema + * @return \Doctrine\DBAL\Schema\TableDiff|bool + */ + protected static function getChangedDiff($grammar, Blueprint $blueprint, SchemaManager $schema) + { + $current = $schema->listTableDetails($grammar->getTablePrefix().$blueprint->getTable()); + + return (new Comparator)->diffTable( + $current, static::getTableWithColumnChanges($blueprint, $current) + ); + } + + /** + * Get a copy of the given Doctrine table after making the column changes. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Doctrine\DBAL\Schema\Table $table + * @return \Doctrine\DBAL\Schema\Table + */ + protected static function getTableWithColumnChanges(Blueprint $blueprint, Table $table) + { + $table = clone $table; + + foreach ($blueprint->getChangedColumns() as $fluent) { + $column = static::getDoctrineColumn($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 = static::mapFluentOptionToDoctrine($key))) { + if (method_exists($column, $method = 'set'.ucfirst($option))) { + $column->{$method}(static::mapFluentValueToDoctrine($option, $value)); + } + } + } + } + + return $table; + } + + /** + * Get the Doctrine column instance for a column change. + * + * @param \Doctrine\DBAL\Schema\Table $table + * @param \Illuminate\Support\Fluent $fluent + * @return \Doctrine\DBAL\Schema\Column + */ + protected static function getDoctrineColumn(Table $table, Fluent $fluent) + { + return $table->changeColumn( + $fluent['name'], static::getDoctrineColumnChangeOptions($fluent) + )->getColumn($fluent['name']); + } + + /** + * Get the Doctrine column change options. + * + * @param \Illuminate\Support\Fluent $fluent + * @return array + */ + protected static function getDoctrineColumnChangeOptions(Fluent $fluent) + { + $options = ['type' => static::getDoctrineColumnType($fluent['type'])]; + + if (in_array($fluent['type'], ['text', 'mediumText', 'longText'])) { + $options['length'] = static::calculateDoctrineTextLength($fluent['type']); + } + + return $options; + } + + /** + * Get the doctrine column type. + * + * @param string $type + * @return \Doctrine\DBAL\Types\Type + */ + protected static function getDoctrineColumnType($type) + { + $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); + } + + /** + * Calculate the proper column length to force the Doctrine text type. + * + * @param string $type + * @return int + */ + protected static function calculateDoctrineTextLength($type) + { + switch ($type) { + case 'mediumText': + return 65535 + 1; + case 'longText': + return 16777215 + 1; + default: + return 255 + 1; + } + } + + /** + * Get the matching Doctrine option for a given Fluent attribute name. + * + * @param string $attribute + * @return string|null + */ + protected static function mapFluentOptionToDoctrine($attribute) + { + switch ($attribute) { + case 'type': + case 'name': + return; + case 'nullable': + return 'notnull'; + case 'total': + return 'precision'; + case 'places': + return 'scale'; + default: + return $attribute; + } + } + + /** + * Get the matching Doctrine value for a given Fluent attribute. + * + * @param string $option + * @param mixed $value + * @return mixed + */ + protected static function mapFluentValueToDoctrine($option, $value) + { + return $option == 'notnull' ? ! $value : $value; + } +} diff --git a/Schema/Grammars/Grammar.php b/Schema/Grammars/Grammar.php index 5a1fd468e1..6e880aec0f 100755 --- a/Schema/Grammars/Grammar.php +++ b/Schema/Grammars/Grammar.php @@ -2,27 +2,16 @@ 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\Schema\TableDiff; +use Illuminate\Database\Connection; +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. * @@ -30,172 +19,17 @@ abstract class Grammar extends BaseGrammar */ 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 - * - * @throws \RuntimeException - */ - public function compileTables($schema) - { - throw new RuntimeException('This database driver does not support retrieving tables.'); - } - - /** - * 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.'); - } - - /** - * 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.'); - } - - /** - * 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.'); - } - - /** - * 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.'); - } - - /** - * Compile a vector index key command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return void - * - * @throws \RuntimeException - */ - public function compileVectorIndex(Blueprint $blueprint, Fluent $command) - { - throw new RuntimeException('The database driver in use does not support vector indexes.'); - } - - /** - * 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.'); - } - /** * Compile a rename column command. * * @param \Illuminate\Database\Schema\Blueprint $blueprint * @param \Illuminate\Support\Fluent $command - * @return list|string + * @param \Illuminate\Database\Connection $connection + * @return array */ - public function compileRenameColumn(Blueprint $blueprint, Fluent $command) + public function compileRenameColumn(Blueprint $blueprint, Fluent $command, Connection $connection) { - return sprintf('alter table %s rename column %s to %s', - $this->wrapTable($blueprint), - $this->wrap($command->from), - $this->wrap($command->to) - ); + return RenameColumn::compile($this, $blueprint, $command, $connection); } /** @@ -203,41 +37,14 @@ public function compileRenameColumn(Blueprint $blueprint, Fluent $command) * * @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.'); - } - - /** - * Compile a drop fulltext index command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string + * @param \Illuminate\Database\Connection $connection + * @return array * * @throws \RuntimeException */ - public function compileDropFullText(Blueprint $blueprint, Fluent $command) + public function compileChange(Blueprint $blueprint, Fluent $command, Connection $connection) { - throw new RuntimeException('This database driver does not support fulltext index removal.'); + return ChangeColumn::compile($this, $blueprint, $command, $connection); } /** @@ -281,23 +88,9 @@ public function compileForeign(Blueprint $blueprint, Fluent $command) } /** - * Compile a drop foreign key command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string + * Compile the blueprint's column definitions. * - * @throws \RuntimeException - */ - public function compileDropForeign(Blueprint $blueprint, Fluent $command) - { - throw new RuntimeException('This database driver does not support dropping foreign keys.'); - } - - /** - * Compile the blueprint's added column definitions. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Database\Schema\Blueprint $blueprint * @return array */ protected function getColumns(Blueprint $blueprint) @@ -305,29 +98,17 @@ protected function getColumns(Blueprint $blueprint) $columns = []; foreach ($blueprint->getAddedColumns() as $column) { - $columns[] = $this->getColumn($blueprint, $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; } - /** - * Compile the column definition. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Database\Schema\ColumnDefinition $column - * @return string - */ - protected function getColumn(Blueprint $blueprint, $column) - { - // 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); - } - /** * Get the SQL for the column data type. * @@ -339,43 +120,6 @@ protected function getType(Fluent $column) return $this->{'type'.ucfirst($column->type)}($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 does not support the computed type.'); - } - - /** - * Create the column definition for a vector type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - * - * @throws \RuntimeException - */ - protected function typeVector(Fluent $column) - { - throw new RuntimeException('This database driver does not support the vector type.'); - } - - /** - * Create the column definition for a raw column type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeRaw(Fluent $column) - { - return $column->offsetGet('definition'); - } - /** * Add the column modifiers to the definition. * @@ -396,7 +140,7 @@ protected function addModifiers($sql, Blueprint $blueprint, Fluent $column) } /** - * Get the command with a given name if it exists on the blueprint. + * Get the primary key command if it exists on the blueprint. * * @param \Illuminate\Database\Schema\Blueprint $blueprint * @param string $name @@ -407,7 +151,7 @@ protected function getCommandByName(Blueprint $blueprint, $name) $commands = $this->getCommandsByName($blueprint, $name); if (count($commands) > 0) { - return array_first($commands); + return reset($commands); } } @@ -425,30 +169,12 @@ protected function getCommandsByName(Blueprint $blueprint, $name) }); } - /* - * Determine if a command with a given name exists on the blueprint. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param string $name - * @return bool - */ - protected function hasCommand(Blueprint $blueprint, $name) - { - foreach ($blueprint->getCommands() as $command) { - if ($command->name === $name) { - return true; - } - } - - return false; - } - /** * Add a prefix to an array of values. * * @param string $prefix - * @param array $values - * @return array + * @param array $values + * @return array */ public function prefixArray($prefix, array $values) { @@ -460,60 +186,61 @@ public function prefixArray($prefix, array $values) /** * Wrap a table in keyword identifiers. * - * @param mixed $table - * @param string|null $prefix + * @param mixed $table * @return string */ - public function wrapTable($table, $prefix = null) + public function wrapTable($table) { return parent::wrapTable( - $table instanceof Blueprint ? $table->getTable() : $table, - $prefix + $table instanceof Blueprint ? $table->getTable() : $table ); } /** * Wrap a value in keyword identifiers. * - * @param \Illuminate\Support\Fluent|\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) { return parent::wrap( - $value instanceof Fluent ? $value->name : $value, + $value instanceof Fluent ? $value->name : $value, $prefixAlias ); } /** * Format a value so that it can be used in "default" clauses. * - * @param mixed $value + * @param mixed $value * @return string */ protected function getDefaultValue($value) { if ($value instanceof Expression) { - return $this->getValue($value); - } - - if ($value instanceof UnitEnum) { - return "'".str_replace("'", "''", enum_value($value))."'"; + return $value; } return is_bool($value) - ? "'".(int) $value."'" - : "'".str_replace("'", "''", $value)."'"; + ? "'".(int) $value."'" + : "'".strval($value)."'"; } /** - * Get the fluent commands for the grammar. + * Create an empty Doctrine DBAL TableDiff from the Blueprint. * - * @return array + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Doctrine\DBAL\Schema\AbstractSchemaManager $schema + * @return \Doctrine\DBAL\Schema\TableDiff */ - public function getFluentCommands() + public function getDoctrineTableDiff(Blueprint $blueprint, SchemaManager $schema) { - return $this->fluentCommands; + $table = $this->getTablePrefix().$blueprint->getTable(); + + return tap(new TableDiff($table), function ($tableDiff) use ($schema, $table) { + $tableDiff->fromTable = $schema->listTableDetails($table); + }); } /** 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..eba8b0399e 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. + * Compile the query to determine the list of tables. * - * @param string|string[]|null $schema * @return string */ - public function compileViews($schema) + public function compileTableExists() { - 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'; + return 'select * from information_schema.tables where table_schema = ? and table_name = ?'; } /** - * Compile the query to compare the schema. + * Compile the query to determine the list of columns. * - * @param string|string[]|null $schema - * @param string $column * @return string */ - protected function compileSchemaWhereClause($schema, $column) + public function compileColumnListing() { - 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. - * - * @param string|null $schema - * @param string $table - * @return string - */ - public function compileColumns($schema, $table) - { - 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) - ); - } - - /** - * Compile the query to determine the indexes. - * - * @param string|null $schema - * @param string $table - * @return string - */ - public function compileIndexes($schema, $table) - { - 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,25 +50,28 @@ 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 + $blueprint, $command, $connection ); // 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 + $sql, $connection, $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); + return $this->compileCreateEngine( + $sql, $connection, $blueprint + ); } /** @@ -230,26 +79,15 @@ public function compileCreate(Blueprint $blueprint, Fluent $command) * * @param \Illuminate\Database\Schema\Blueprint $blueprint * @param \Illuminate\Support\Fluent $command + * @param \Illuminate\Database\Connection $connection * @return string */ - protected function compileCreateTable($blueprint, $command) + protected function compileCreateTable($blueprint, $command, $connection) { - $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; - } - return sprintf('%s table %s (%s)', $blueprint->temporary ? 'create temporary' : 'create', $this->wrapTable($blueprint), - implode(', ', $tableStructure) + implode(', ', $this->getColumns($blueprint)) ); } @@ -257,17 +95,18 @@ protected function compileCreateTable($blueprint, $command) * 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; } @@ -275,9 +114,9 @@ protected function compileCreateEncoding($sql, Blueprint $blueprint) // 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}'"; + $sql .= ' collate '.$blueprint->collation; + } elseif (! is_null($collation = $connection->getConfig('collation'))) { + $sql .= ' collate '.$collation; } return $sql; @@ -287,14 +126,15 @@ protected function compileCreateEncoding($sql, Blueprint $blueprint) * Append the engine specifications to a command. * * @param string $sql + * @param \Illuminate\Database\Connection $connection * @param \Illuminate\Database\Schema\Blueprint $blueprint * @return string */ - protected function compileCreateEngine($sql, Blueprint $blueprint) + protected function compileCreateEngine($sql, Connection $connection, Blueprint $blueprint) { if (isset($blueprint->engine)) { return $sql.' engine = '.$blueprint->engine; - } elseif (! is_null($engine = $this->connection->getConfig('engine'))) { + } elseif (! is_null($engine = $connection->getConfig('engine'))) { return $sql.' engine = '.$engine; } @@ -310,112 +150,9 @@ 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(); - - if (($isMaria && version_compare($version, '10.5.2', '<')) || - (! $isMaria && version_compare($version, '8.0.3', '<'))) { - return $this->compileLegacyRenameColumn($blueprint, $command); - } - - 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); + $columns = $this->prefixArray('add', $this->getColumns($blueprint)); - if ($column->instant) { - $sql .= ', algorithm=instant'; - } - - if ($column->lock) { - $sql .= ', lock='.$column->lock; - } - - return $sql; + return 'alter table '.$this->wrapTable($blueprint).' '.implode(', ', $columns); } /** @@ -427,12 +164,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 +193,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,13 +203,12 @@ 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', + return sprintf('alter table %s add %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 : '' + $this->columnize($command->columns) ); } @@ -538,17 +247,7 @@ 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; - } - - return $sql; + return 'alter table '.$this->wrapTable($blueprint).' '.implode(', ', $columns); } /** @@ -591,48 +290,6 @@ public function compileDropIndex(Blueprint $blueprint, Fluent $command) 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; - } - - return $sql; - } - /** * Compile a drop foreign key command. * @@ -661,44 +318,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 +338,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 +360,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. * @@ -877,11 +456,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 +467,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 +497,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 +537,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 +548,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 +570,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 +592,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)); + return 'timestamp default CURRENT_TIMESTAMP'; } - if ($column->useCurrentOnUpdate) { - $column->onUpdate(new Expression($current)); - } - - 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 +622,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 +658,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 +667,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 +681,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 +724,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 +737,9 @@ 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)) { + if (is_null($column->virtualAs) && is_null($column->storedAs)) { 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'; - } } /** @@ -1333,20 +756,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 +766,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 +808,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."'"; } } @@ -1419,17 +826,4 @@ protected function wrapValue($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.'))'; - } } diff --git a/Schema/Grammars/PostgresGrammar.php b/Schema/Grammars/PostgresGrammar.php index 4a1660a20e..ccd56ff29d 100755 --- a/Schema/Grammars/PostgresGrammar.php +++ b/Schema/Grammars/PostgresGrammar.php @@ -2,11 +2,8 @@ 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 { @@ -20,211 +17,36 @@ class PostgresGrammar extends Grammar /** * 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. + * Compile the query to determine if a table exists. * - * @param string|string[]|null $schema * @return string */ - public function compileTypes($schema) + public function compileTableExists() { - 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'); + return 'select * from information_schema.tables where table_schema = ? and table_name = ?'; } /** - * Compile the query to compare the schema. + * Compile the query to determine the list of columns. * - * @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. - * - * @param string|null $schema - * @param string $table - * @return string - */ - public function compileIndexes($schema, $table) - { - 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()' - ); - } - - /** - * Compile the query to determine the foreign keys. - * - * @param string|null $schema * @param string $table * @return string */ - public function compileForeignKeys($schema, $table) + public function compileColumnListing($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'"; } /** @@ -252,56 +74,9 @@ 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; - - $changes = ['type '.$this->getType($column).$this->modifyCollate($blueprint, $column)]; - - 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)) + implode(', ', $this->prefixArray('add column', $this->getColumns($blueprint))) ); } @@ -324,184 +99,34 @@ public function compilePrimary(Blueprint $blueprint, Fluent $command) * * @param \Illuminate\Database\Schema\Blueprint $blueprint * @param \Illuminate\Support\Fluent $command - * @return string[] - */ - public function compileUnique(Blueprint $blueprint, Fluent $command) - { - $uniqueStatement = 'unique'; - - 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'; - } - - if ($command->deferrable && ! is_null($command->initiallyImmediate)) { - $sql .= $command->initiallyImmediate ? ' initially immediate' : ' initially deferred'; - } - - return isset($createIndexSql) ? [$createIndexSql, $sql] : [$sql]; - } - - /** - * Compile a plain index key command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command * @return string */ - public function compileIndex(Blueprint $blueprint, Fluent $command) + public function compileUnique(Blueprint $blueprint, Fluent $command) { - return sprintf('create index %s%s on %s%s (%s)', - $command->online ? 'concurrently ' : '', - $this->wrap($command->index), + return sprintf('alter table %s add constraint %s unique (%s)', $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) + $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) - { - $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. + * Compile a plain index key command. * * @param \Illuminate\Database\Schema\Blueprint $blueprint * @param \Illuminate\Support\Fluent $command * @return string */ - protected function compileIndexWithOperatorClass(Blueprint $blueprint, Fluent $command) + public function compileIndex(Blueprint $blueprint, Fluent $command) { - $columns = $this->columnizeWithOperatorClass($command->columns, $command->operatorClass); - - return sprintf('create index %s%s on %s%s (%s)', - $command->online ? 'concurrently ' : '', + return sprintf('create index %s on %s%s (%s)', $this->wrap($command->index), $this->wrapTable($blueprint), $command->algorithm ? ' using '.$command->algorithm : '', - $columns + $this->columnize($command->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'; - } - - if ($command->deferrable && ! is_null($command->initiallyImmediate)) { - $sql .= $command->initiallyImmediate ? ' initially immediate' : ' initially deferred'; - } - - if (! is_null($command->notValid)) { - $sql .= ' not valid'; - } - - return $sql; - } - /** * Compile a drop table command. * @@ -529,45 +154,23 @@ public function compileDropIfExists(Blueprint $blueprint, Fluent $command) /** * Compile the SQL needed to drop all tables. * - * @param array $tables + * @param string $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'; + return 'drop table "'.implode('","', $tables).'" cascade'; } /** - * Compile the SQL needed to drop all domains. + * Compile the SQL needed to retrieve all table names. * - * @param array $domains + * @param string $schema * @return string */ - public function compileDropAllDomains($domains) + public function compileGetAllTables($schema) { - return 'drop domain '.implode(', ', $this->escapeNames($domains)).' cascade'; + return "select tablename from pg_catalog.pg_tables where schemaname = '{$schema}'"; } /** @@ -593,8 +196,7 @@ 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"); + $index = $this->wrap("{$blueprint->getTable()}_pkey"); return 'alter table '.$this->wrapTable($blueprint)." drop constraint {$index}"; } @@ -625,30 +227,6 @@ 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); - } - - /** - * 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 drop foreign key command. * @@ -677,21 +255,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 string - */ - public function compileRenameIndex(Blueprint $blueprint, Fluent $command) - { - return sprintf('alter index %s rename to %s', - $this->wrap($command->from), - $this->wrap($command->to) - ); - } - /** * Compile the command to enable foreign key constraints. * @@ -712,53 +275,6 @@ public function compileDisableForeignKeyConstraints() return 'SET CONSTRAINTS ALL DEFERRED;'; } - /** - * 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. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @return string - */ - public function compileTableComment(Blueprint $blueprint, Fluent $command) - { - return sprintf('comment on table %s is %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. * @@ -767,11 +283,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 +294,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})"; } /** @@ -841,7 +338,7 @@ protected function typeLongText(Fluent $column) */ protected function typeInteger(Fluent $column) { - return $column->autoIncrement && is_null($column->generatedAs) && ! $column->change ? 'serial' : 'integer'; + return $column->autoIncrement ? 'serial' : 'integer'; } /** @@ -852,7 +349,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 +360,7 @@ protected function typeBigInteger(Fluent $column) */ protected function typeMediumInteger(Fluent $column) { - return $this->typeInteger($column); + return $column->autoIncrement ? 'serial' : 'integer'; } /** @@ -874,7 +371,7 @@ protected function typeMediumInteger(Fluent $column) */ protected function typeTinyInteger(Fluent $column) { - return $this->typeSmallInteger($column); + return $column->autoIncrement ? 'smallserial' : 'smallint'; } /** @@ -885,7 +382,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 +393,7 @@ protected function typeSmallInteger(Fluent $column) */ protected function typeFloat(Fluent $column) { - if ($column->precision) { - return "float({$column->precision})"; - } - - return 'float'; + return $this->typeDouble($column); } /** @@ -948,18 +441,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 +485,6 @@ protected function typeJsonb(Fluent $column) */ protected function typeDate(Fluent $column) { - if ($column->useCurrent) { - $column->default(new Expression('CURRENT_DATE')); - } - return 'date'; } @@ -1007,18 +496,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 +518,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 +541,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 +556,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 +606,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 +615,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 +627,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 +641,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/RenameColumn.php b/Schema/Grammars/RenameColumn.php new file mode 100644 index 0000000000..a07c4febb9 --- /dev/null +++ b/Schema/Grammars/RenameColumn.php @@ -0,0 +1,69 @@ +getDoctrineColumn( + $grammar->getTablePrefix().$blueprint->getTable(), $command->from + ); + + $schema = $connection->getDoctrineSchemaManager(); + + return (array) $schema->getDatabasePlatform()->getAlterTableSQL(static::getRenamedDiff( + $grammar, $blueprint, $command, $column, $schema + )); + } + + /** + * Get a new column instance with the new column name. + * + * @param \Illuminate\Database\Schema\Grammars\Grammar $grammar + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @param \Doctrine\DBAL\Schema\Column $column + * @param \Doctrine\DBAL\Schema\AbstractSchemaManager $schema + * @return \Doctrine\DBAL\Schema\TableDiff + */ + protected static function getRenamedDiff(Grammar $grammar, Blueprint $blueprint, Fluent $command, Column $column, SchemaManager $schema) + { + return static::setRenamedColumns( + $grammar->getDoctrineTableDiff($blueprint, $schema), $command, $column + ); + } + + /** + * Set the renamed columns on the table diff. + * + * @param \Doctrine\DBAL\Schema\TableDiff $tableDiff + * @param \Illuminate\Support\Fluent $command + * @param \Doctrine\DBAL\Schema\Column $column + * @return \Doctrine\DBAL\Schema\TableDiff + */ + protected static function setRenamedColumns(TableDiff $tableDiff, Fluent $command, Column $column) + { + $tableDiff->renamedColumns = [ + $command->from => new Column($command->to, $column->getType(), $column->toArray()), + ]; + + return $tableDiff; + } +} diff --git a/Schema/Grammars/SQLiteGrammar.php b/Schema/Grammars/SQLiteGrammar.php old mode 100644 new mode 100755 index c472266c43..a611ea8dd7 --- a/Schema/Grammars/SQLiteGrammar.php +++ b/Schema/Grammars/SQLiteGrammar.php @@ -2,223 +2,45 @@ 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. - * - * @return string - */ - public function compileSchemas() - { - return 'select name, file as path, name = \'main\' as "default" from pragma_database_list 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 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) - { - 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'; - } - - /** - * Compile the query for legacy versions of SQLite to determine the tables. - * - * @param string $schema - * @param bool $withSize - * @return string - */ - public function compileLegacyTables($schema, $withSize = false) - { - 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) - ); - } - - /** - * Compile the query to determine the views. + * Compile the query to determine if a table exists. * - * @param string $schema * @return string */ - public function compileViews($schema) + public function compileTableExists() { - 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) - ); + return "select * from sqlite_master where type = 'table' and name = ?"; } /** - * Compile the query to determine the columns. + * Compile the query to determine the list of columns. * - * @param string|null $schema * @param string $table * @return string */ - public function compileColumns($schema, $table) + public function compileColumnListing($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') - ); - } - - /** - * 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 - ); - } - - /** - * 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 - ); + return 'pragma table_info('.$this->wrapTable(str_replace('.', '__', $table)).')'; } /** @@ -234,24 +56,39 @@ public function compileCreate(Blueprint $blueprint, Fluent $command) $blueprint->temporary ? 'create temporary' : 'create', $this->wrapTable($blueprint), implode(', ', $this->getColumns($blueprint)), - $this->addForeignKeys($this->getCommandsByName($blueprint, 'foreign')), - $this->addPrimaryKeys($this->getCommandByName($blueprint, 'primary')) + (string) $this->addForeignKeys($blueprint), + (string) $this->addPrimaryKeys($blueprint) ); } /** * 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) { + $foreigns = $this->getCommandsByName($blueprint, 'foreign'); + + return collect($foreigns)->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 .= $this->getForeignKey($foreign); + + if (! is_null($foreign->onDelete)) { + $sql .= " on delete {$foreign->onDelete}"; + } + + // 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 + // the SQL so we can keep adding any other foreign consraints onto this. + if (! is_null($foreign->onUpdate)) { + $sql .= " on update {$foreign->onUpdate}"; + } + + return $sql; }, ''); } @@ -266,35 +103,22 @@ protected function getForeignKey($foreign) // 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)', + return 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}"; - } - - // 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}"; - } - - return $sql; } /** * 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) { - if (! is_null($primary)) { + if (! is_null($primary = $this->getCommandByName($blueprint, 'primary'))) { return ", primary key ({$this->columnize($primary->columns)})"; } } @@ -304,88 +128,15 @@ 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) - ); - } + $columns = $this->prefixArray('add column', $this->getColumns($blueprint)); - /** - * 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... - } - - /** - * 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... + return collect($columns)->map(function ($column) use ($blueprint) { + return 'alter table '.$this->wrapTable($blueprint).' '.$column; + })->all(); } /** @@ -397,12 +148,9 @@ 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).'.' : '', + return sprintf('create unique index %s on %s (%s)', $this->wrap($command->index), - $this->wrapTable($table), + $this->wrapTable($blueprint), $this->columnize($command->columns) ); } @@ -416,40 +164,23 @@ 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).'.' : '', + return sprintf('create index %s on %s (%s)', $this->wrap($command->index), - $this->wrapTable($table), + $this->wrapTable($blueprint), $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.'); - } - /** * Compile a foreign key 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... } /** @@ -479,40 +210,11 @@ public function compileDropIfExists(Blueprint $blueprint, Fluent $command) /** * 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) + public function compileDropAllTables() { - 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') - ); + return "delete from sqlite_master where type in ('table', 'index', 'trigger')"; } /** @@ -520,33 +222,22 @@ public function compileRebuild($schema = null) * * @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... - - return null; - } - - $table = $this->wrapTable($blueprint); + $tableDiff = $this->getDoctrineTableDiff( + $blueprint, $schema = $connection->getDoctrineSchemaManager() + ); - $columns = $this->prefixArray('drop column', $this->wrapArray($command->columns)); + foreach ($command->columns as $name) { + $column = $connection->getDoctrineColumn($blueprint->getTable(), $name); - return (new Collection($columns))->map(fn ($column) => 'alter table '.$table.' '.$column)->all(); - } + $tableDiff->removedColumns[$name] = $column; + } - /** - * 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 +249,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 +263,9 @@ public function compileDropUnique(Blueprint $blueprint, Fluent $command) */ public function compileDropIndex(Blueprint $blueprint, Fluent $command) { - [$schema] = $this->connection->getSchemaBuilder()->parseSchemaAndTable($blueprint->getTable()); + $index = $this->wrap($command->index); - 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.'); - } - - // Handled on table alteration... + return "drop index {$index}"; } /** @@ -624,46 +282,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 +289,7 @@ public function compileRenameIndex(Blueprint $blueprint, Fluent $command) */ public function compileEnableForeignKeyConstraints() { - return $this->pragma('foreign_keys', 1); + return 'PRAGMA foreign_keys = ON;'; } /** @@ -681,55 +299,49 @@ public function compileEnableForeignKeyConstraints() */ public function compileDisableForeignKeyConstraints() { - return $this->pragma('foreign_keys', 0); + return 'PRAGMA foreign_keys = OFF;'; } /** - * Get the SQL to get or set a PRAGMA value. + * Compile the SQL needed to enable a writable schema. * - * @param string $key - * @param mixed $value * @return string */ - public function pragma(string $key, mixed $value = null): string + public function compileEnableWriteableSchema() { - return sprintf('pragma %s%s', - $key, - is_null($value) ? '' : ' = '.$value - ); + return 'PRAGMA writable_schema = 1;'; } /** - * Create the column definition for a char type. + * Compile the SQL needed to disable a writable schema. * - * @param \Illuminate\Support\Fluent $column * @return string */ - protected function typeChar(Fluent $column) + public function compileDisableWriteableSchema() { - return 'varchar'; + return 'PRAGMA writable_schema = 0;'; } /** - * Create the column definition for a string type. + * Create the column definition for a char type. * * @param \Illuminate\Support\Fluent $column * @return string */ - protected function typeString(Fluent $column) + protected function typeChar(Fluent $column) { return 'varchar'; } /** - * Create the column definition for a tiny text type. + * Create the column definition for a string type. * * @param \Illuminate\Support\Fluent $column * @return string */ - protected function typeTinyText(Fluent $column) + protected function typeString(Fluent $column) { - return 'text'; + return 'varchar'; } /** @@ -766,7 +378,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 +451,7 @@ protected function typeFloat(Fluent $column) */ protected function typeDouble(Fluent $column) { - return 'double'; + return 'float'; } /** @@ -865,18 +477,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 +495,7 @@ protected function typeEnum(Fluent $column) */ protected function typeJson(Fluent $column) { - return $this->connection->getConfig('use_native_json') ? 'json' : 'text'; + return 'text'; } /** @@ -898,7 +506,7 @@ protected function typeJson(Fluent $column) */ protected function typeJsonb(Fluent $column) { - return $this->connection->getConfig('use_native_jsonb') ? 'jsonb' : 'text'; + return 'text'; } /** @@ -909,10 +517,6 @@ protected function typeJsonb(Fluent $column) */ protected function typeDate(Fluent $column) { - if ($column->useCurrent) { - $column->default(new Expression('CURRENT_DATE')); - } - return 'date'; } @@ -924,14 +528,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 +542,7 @@ protected function typeDateTime(Fluent $column) */ protected function typeDateTimeTz(Fluent $column) { - return $this->typeDateTime($column); + return 'datetime'; } /** @@ -954,14 +557,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 +576,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 +641,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 +650,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 +662,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 +680,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..d36d89bd2c 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', 'Collate', '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 compileColumnListing($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)"; } /** @@ -222,31 +69,10 @@ public function compileAdd(Blueprint $blueprint, Fluent $command) { return sprintf('alter table %s add %s', $this->wrapTable($blueprint), - $this->getColumn($blueprint, $command->column) - ); - } - - /** @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) + implode(', ', $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), - ), - ]; - } - /** * Compile a primary key command. * @@ -272,11 +98,10 @@ public function compilePrimary(Blueprint $blueprint, Fluent $command) */ public function compileUnique(Blueprint $blueprint, Fluent $command) { - return sprintf('create unique index %s on %s (%s)%s', + return sprintf('create unique index %s on %s (%s)', $this->wrap($command->index), $this->wrapTable($blueprint), - $this->columnize($command->columns), - $command->online ? ' with (online = on)' : '' + $this->columnize($command->columns) ); } @@ -289,48 +114,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)' : '' - ); - } - - /** - * 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)', + return sprintf('create index %s on %s (%s)', $this->wrap($command->index), $this->wrapTable($blueprint), $this->columnize($command->columns) ); } - /** - * 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) - ); - } - } - /** * Compile a drop table command. * @@ -352,8 +142,8 @@ 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)), + return sprintf('if exists (select * from INFORMATION_SCHEMA.TABLES where TABLE_NAME = %s) drop table %s', + "'".str_replace("'", "''", $this->getTablePrefix().$blueprint->getTable())."'", $this->wrapTable($blueprint) ); } @@ -379,34 +169,7 @@ 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 '.$this->wrapTable($blueprint).' drop column '.implode(', ', $columns); } /** @@ -451,18 +214,6 @@ public function compileDropIndex(Blueprint $blueprint, Fluent $command) return "drop index {$index} on {$this->wrapTable($blueprint)}"; } - /** - * 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 drop foreign key command. * @@ -486,25 +237,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 +262,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 +284,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. * @@ -686,10 +380,6 @@ protected function typeSmallInteger(Fluent $column) */ protected function typeFloat(Fluent $column) { - if ($column->precision) { - return "float({$column->precision})"; - } - return 'float'; } @@ -701,7 +391,7 @@ protected function typeFloat(Fluent $column) */ protected function typeDouble(Fluent $column) { - return 'double precision'; + return 'float'; } /** @@ -727,18 +417,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 +457,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 +468,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 +490,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 +513,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 +530,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 +544,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,39 +580,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. * @@ -973,9 +603,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 +615,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 +629,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'; + if (in_array($column->type, $this->serials) && $column->autoIncrement) { + return ' 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 ($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..41e215c317 100755 --- a/Schema/MySqlBuilder.php +++ b/Schema/MySqlBuilder.php @@ -5,54 +5,50 @@ 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()); - - if (empty($tables)) { - return; - } + $table = $this->connection->getTablePrefix().$table; - $this->disableForeignKeyConstraints(); - - try { - $this->connection->statement( - $this->grammar->compileDropAllTables($tables) - ); - } finally { - $this->enableForeignKeyConstraints(); - } + return count($this->connection->select( + $this->grammar->compileTableExists(), [$this->connection->getDatabaseName(), $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'); + $table = $this->connection->getTablePrefix().$table; - if (empty($views)) { - return; - } - - $this->connection->statement( - $this->grammar->compileDropAllViews($views) + $results = $this->connection->select( + $this->grammar->compileColumnListing(), [$this->connection->getDatabaseName(), $table] ); + + return $this->connection->getPostProcessor()->processColumnListing($results); } /** - * Get the names of current schemas for the connection. + * Drop all tables from the database. * - * @return string[]|null + * @return void */ - public function getCurrentSchemaListing() + public function dropAllTables() { - return [$this->connection->getDatabaseName()]; + $this->disableForeignKeyConstraints(); + + foreach ($this->connection->select('SHOW TABLES') as $table) { + $this->drop(get_object_vars($table)[key($table)]); + } + + $this->enableForeignKeyConstraints(); } } 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..f8f21d499d 100755 --- a/Schema/PostgresBuilder.php +++ b/Schema/PostgresBuilder.php @@ -2,99 +2,60 @@ namespace Illuminate\Database\Schema; -use Illuminate\Database\Concerns\ParsesSearchPath; - class PostgresBuilder extends Builder { - use ParsesSearchPath; - /** - * 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 = []; - - $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 (is_array($schema = $this->connection->getConfig('schema'))) { + $schema = head($schema); } - if (empty($tables)) { - return; - } + $schema = $schema ? $schema : 'public'; - $this->connection->statement( - $this->grammar->compileDropAllTables($tables) - ); - } + $table = $this->connection->getTablePrefix().$table; - /** - * 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) - ); + return count($this->connection->select( + $this->grammar->compileTableExists(), [$schema, $table] + )) > 0; } /** - * Drop all types from the database. + * Drop all tables from the database. * * @return void */ - public function dropAllTypes() + public function dropAllTables() { - $types = []; - $domains = []; + $tables = []; - 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']; - } - } + foreach ($this->getAllTables() as $table) { + $tables[] = get_object_vars($table)[key($table)]; } - if (! empty($types)) { - $this->connection->statement($this->grammar->compileDropAllTypes($types)); + if (empty($tables)) { + return; } - if (! empty($domains)) { - $this->connection->statement($this->grammar->compileDropAllDomains($domains)); - } + $this->connection->statement( + $this->grammar->compileDropAllTables($tables) + ); } /** - * Get the current schemas for the connection. + * Get all of the table names for the database. * - * @return string[] + * @return array */ - public function getCurrentSchemaListing() + protected function getAllTables() { - 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 $this->connection->select( + $this->grammar->compileGetAllTables($this->connection->getConfig('schema')) ); } } 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 index f750e97edf..21b43366ef 100644 --- a/Schema/SQLiteBuilder.php +++ b/Schema/SQLiteBuilder.php @@ -2,93 +2,8 @@ namespace Illuminate\Database\Schema; -use Illuminate\Database\QueryException; -use Illuminate\Support\Arr; -use Illuminate\Support\Facades\File; - class SQLiteBuilder extends Builder { - /** - * Create a database in the schema. - * - * @param string $name - * @return bool - */ - public function createDatabase($name) - { - return File::put($name, '') !== false; - } - - /** - * Drop a database from the schema if the database exists. - * - * @param string $name - * @return bool - */ - public function dropDatabaseIfExists($name) - { - return ! File::exists($name) || File::delete($name); - } - - /** @inheritDoc */ - public function getTables($schema = null) - { - try { - $withSize = $this->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. * @@ -96,78 +11,10 @@ public function getColumns($table) */ public function dropAllTables() { - foreach ($this->getCurrentSchemaListing() as $schema) { - $database = $schema === 'main' - ? $this->connection->getDatabaseName() - : (array_column($this->getSchemas(), 'path', 'name')[$schema] ?: ':memory:'); + $this->connection->select($this->grammar->compileEnableWriteableSchema()); - if ($database !== ':memory:' && - ! str_contains($database, '?mode=memory') && - ! str_contains($database, '&mode=memory') - ) { - $this->refreshDatabaseFile($database); - } else { - $this->pragma('writable_schema', 1); + $this->connection->select($this->grammar->compileDropAllTables()); - $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']; + $this->connection->select($this->grammar->compileDisableWriteableSchema()); } } 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 index 9161bc61af..2c54282dd2 100644 --- a/Schema/SqlServerBuilder.php +++ b/Schema/SqlServerBuilder.php @@ -2,8 +2,6 @@ namespace Illuminate\Database\Schema; -use Illuminate\Support\Arr; - class SqlServerBuilder extends Builder { /** @@ -13,28 +11,10 @@ class SqlServerBuilder extends Builder */ public function dropAllTables() { - $this->connection->statement($this->grammar->compileDropAllForeignKeys()); + $this->disableForeignKeyConstraints(); $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']; + $this->enableForeignKeyConstraints(); } } 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/Seeder.php b/Seeder.php index cac50afb57..bb33876546 100755 --- a/Seeder.php +++ b/Seeder.php @@ -2,19 +2,16 @@ 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\Console\Command; +use Illuminate\Container\Container; abstract class Seeder { /** * The container instance. * - * @var \Illuminate\Contracts\Container\Container + * @var \Illuminate\Container\Container */ protected $container; @@ -26,95 +23,18 @@ 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. + * Seed the given connection from the given path. * - * @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. - * - * @param array|string $class - * @param array $parameters - * @return void - */ - public function callSilent($class, array $parameters = []) - { - $this->call($class, true, $parameters); - } - - /** - * Run the given seeder class once. - * - * @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->call($class, $silent, $parameters); + if (isset($this->command)) { + $this->command->getOutput()->writeln("Seeding: $class"); } + + $this->resolve($class)->__invoke(); } /** @@ -143,7 +63,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) @@ -169,27 +89,18 @@ public function setCommand(Command $command) /** * Run the database seeds. * - * @param array $parameters - * @return mixed + * @return void * * @throws \InvalidArgumentException */ - public function __invoke(array $parameters = []) + public function __invoke() { 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(); + return isset($this->container) + ? $this->container->call([$this, 'run']) + : $this->run(); } } diff --git a/SqlServerConnection.php b/SqlServerConnection.php index 7b3d0c5f01..95395637d6 100755 --- a/SqlServerConnection.php +++ b/SqlServerConnection.php @@ -4,24 +4,15 @@ use Closure; use Exception; -use Illuminate\Database\Query\Grammars\SqlServerGrammar as QueryGrammar; +use Throwable; +use Illuminate\Database\Schema\SqlServerBuilder; +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. * @@ -29,13 +20,13 @@ public function getDriverTitle() * @param int $attempts * @return mixed * - * @throws \Throwable + * @throws \Exception|\Throwable */ public function transaction(Closure $callback, $attempts = 1) { 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'); @@ -49,10 +40,14 @@ public function transaction(Closure $callback, $attempts = 1) $this->getPdo()->exec('COMMIT TRAN'); } - // If we catch an exception, we will rollback so nothing gets messed + // 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 (Throwable $e) { + catch (Exception $e) { + $this->getPdo()->exec('ROLLBACK TRAN'); + + throw $e; + } catch (Throwable $e) { $this->getPdo()->exec('ROLLBACK TRAN'); throw $e; @@ -62,30 +57,6 @@ public function transaction(Closure $callback, $attempts = 1) } } - /** - * 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()); - } - /** * Get the default query grammar instance. * @@ -93,7 +64,7 @@ protected function isUniqueConstraintError(Exception $exception) */ protected function getDefaultQueryGrammar() { - return new QueryGrammar($this); + return $this->withTablePrefix(new QueryGrammar); } /** @@ -117,29 +88,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 @@ -=7.0", + "illuminate/container": "5.5.*", + "illuminate/contracts": "5.5.*", + "illuminate/support": "5.5.*", + "nesbot/carbon": "~1.20" }, - "suggest": { - "ext-filter": "Required to use the Postgres database driver.", - "fakerphp/faker": "Required to use the eloquent factory builder (^1.24).", - "illuminate/console": "Required to use the database commands (^13.0).", - "illuminate/events": "Required to use the observers with Eloquent (^13.0).", - "illuminate/filesystem": "Required to use the migrations (^13.0).", - "illuminate/http": "Required to convert Eloquent models to API resources (^13.0).", - "illuminate/pagination": "Required to paginate the result set (^13.0).", - "symfony/finder": "Required to use Eloquent model factories (^7.4 || ^8.0)." - }, - "minimum-stability": "dev", "autoload": { "psr-4": { "Illuminate\\Database\\": "" } }, - "config": { - "sort-packages": true - }, "extra": { "branch-alias": { - "dev-master": "13.0.x-dev" + "dev-master": "5.5-dev" } - } + }, + "suggest": { + "doctrine/dbal": "Required to rename columns and drop SQLite columns (~2.5).", + "fzaninotto/faker": "Required to use the eloquent factory builder (~1.4).", + "illuminate/console": "Required to use the database commands (5.5.*).", + "illuminate/events": "Required to use the observers with Eloquent (5.5.*).", + "illuminate/filesystem": "Required to use the migrations (5.5.*).", + "illuminate/pagination": "Required to paginate the result set (5.5.*)." + }, + "config": { + "sort-packages": true + }, + "minimum-stability": "dev" }