Upgrade framework

This commit is contained in:
2023-11-14 16:54:35 +01:00
parent 1648a5cd42
commit 4fcf6fffcc
10548 changed files with 693138 additions and 466698 deletions

View File

@@ -2,13 +2,13 @@
namespace Illuminate\Database\Capsule;
use PDO;
use Illuminate\Container\Container;
use Illuminate\Database\DatabaseManager;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Traits\CapsuleManagerTrait;
use Illuminate\Database\Eloquent\Model as Eloquent;
use Illuminate\Database\Connectors\ConnectionFactory;
use Illuminate\Database\DatabaseManager;
use Illuminate\Database\Eloquent\Model as Eloquent;
use Illuminate\Support\Traits\CapsuleManagerTrait;
use PDO;
class Manager
{
@@ -66,7 +66,7 @@ class Manager
/**
* Get a connection instance from the global manager.
*
* @param string $connection
* @param string|null $connection
* @return \Illuminate\Database\Connection
*/
public static function connection($connection = null)
@@ -77,19 +77,20 @@ class Manager
/**
* Get a fluent query builder instance.
*
* @param string $table
* @param string $connection
* @param \Closure|\Illuminate\Database\Query\Builder|string $table
* @param string|null $as
* @param string|null $connection
* @return \Illuminate\Database\Query\Builder
*/
public static function table($table, $connection = null)
public static function table($table, $as = null, $connection = null)
{
return static::$instance->connection($connection)->table($table);
return static::$instance->connection($connection)->table($table, $as);
}
/**
* Get a schema builder instance.
*
* @param string $connection
* @param string|null $connection
* @return \Illuminate\Database\Schema\Builder
*/
public static function schema($connection = null)
@@ -100,7 +101,7 @@ class Manager
/**
* Get a registered connection instance.
*
* @param string $name
* @param string|null $name
* @return \Illuminate\Database\Connection
*/
public function getConnection($name = null)
@@ -111,7 +112,7 @@ class Manager
/**
* Register a connection with the manager.
*
* @param array $config
* @param array $config
* @param string $name
* @return void
*/
@@ -191,7 +192,7 @@ class Manager
* Dynamically pass methods to the default connection.
*
* @param string $method
* @param array $parameters
* @param array $parameters
* @return mixed
*/
public static function __callStatic($method, $parameters)

View File

@@ -0,0 +1,29 @@
<?php
namespace Illuminate\Database;
use RuntimeException;
class ClassMorphViolationException extends RuntimeException
{
/**
* The name of the affected Eloquent model.
*
* @var string
*/
public $model;
/**
* Create a new exception instance.
*
* @param object $model
*/
public function __construct($model)
{
$class = get_class($model);
parent::__construct("No morph map defined for model [{$class}].");
$this->model = $class;
}
}

View File

@@ -3,11 +3,25 @@
namespace Illuminate\Database\Concerns;
use Illuminate\Container\Container;
use Illuminate\Pagination\Paginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\MultipleRecordsFoundException;
use Illuminate\Database\Query\Expression;
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;
trait BuildsQueries
{
use Conditionable;
/**
* Chunk the results of the query.
*
@@ -36,22 +50,46 @@ trait BuildsQueries
// 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) {
if ($callback($results, $page) === false) {
return false;
}
unset($results);
$page++;
} while ($countResults == $count);
return true;
}
/**
* Run a map over each item while chunking.
*
* @param callable $callback
* @param int $count
* @return \Illuminate\Support\Collection
*/
public function chunkMap(callable $callback, $count = 1000)
{
$collection = Collection::make();
$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 $callback
* @param int $count
* @return bool
*
* @throws \RuntimeException
*/
public function each(callable $callback, $count = 1000)
{
@@ -64,11 +102,194 @@ trait BuildsQueries
});
}
/**
* Chunk the results of a query by comparing 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->defaultKeyName();
$alias ??= $column;
$lastId = null;
$page = 1;
do {
$clone = clone $this;
// 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, $page) === false) {
return false;
}
$lastId = data_get($results->last(), $alias);
if ($lastId === null) {
throw new RuntimeException("The chunkById operation was aborted because the [{$alias}] column is not present in the query result.");
}
unset($results);
$page++;
} while ($countResults == $count);
return true;
}
/**
* Execute a callback over each item while chunking by ID.
*
* @param callable $callback
* @param int $count
* @param string|null $column
* @param string|null $alias
* @return bool
*/
public function eachById(callable $callback, $count = 1000, $column = null, $alias = null)
{
return $this->chunkById($count, function ($results, $page) use ($callback, $count) {
foreach ($results as $key => $value) {
if ($callback($value, (($page - 1) * $count) + $key) === false) {
return false;
}
}
}, $column, $alias);
}
/**
* Query lazily, by chunks of the given size.
*
* @param int $chunkSize
* @return \Illuminate\Support\LazyCollection
*
* @throws \InvalidArgumentException
*/
public function lazy($chunkSize = 1000)
{
if ($chunkSize < 1) {
throw new InvalidArgumentException('The chunk size should be at least 1');
}
$this->enforceOrderBy();
return LazyCollection::make(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
*/
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 LazyCollection::make(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};
}
});
}
/**
* Execute the query and get the first result.
*
* @param array $columns
* @return mixed
* @param array|string $columns
* @return \Illuminate\Database\Eloquent\Model|object|static|null
*/
public function first($columns = ['*'])
{
@@ -76,41 +297,149 @@ trait BuildsQueries
}
/**
* Apply the callback's query changes if the given "value" is true.
* Execute the query and get the first result if it's the sole matching record.
*
* @param mixed $value
* @param callable $callback
* @param callable $default
* @return mixed
* @param array|string $columns
* @return \Illuminate\Database\Eloquent\Model|object|static|null
*
* @throws \Illuminate\Database\RecordsNotFoundException
* @throws \Illuminate\Database\MultipleRecordsFoundException
*/
public function when($value, $callback, $default = null)
public function sole($columns = ['*'])
{
if ($value) {
return $callback($this, $value) ?: $this;
} elseif ($default) {
return $default($this, $value) ?: $this;
$result = $this->take(2)->get($columns);
$count = $result->count();
if ($count === 0) {
throw new RecordsNotFoundException;
}
return $this;
if ($count > 1) {
throw new MultipleRecordsFoundException($count);
}
return $result->first();
}
/**
* Apply the callback's query changes if the given "value" is false.
* Paginate the given query using a cursor paginator.
*
* @param mixed $value
* @param callable $callback
* @param callable $default
* @return mixed
* @param int $perPage
* @param array|string $columns
* @param string $cursorName
* @param \Illuminate\Pagination\Cursor|string|null $cursor
* @return \Illuminate\Contracts\Pagination\CursorPaginator
*/
public function unless($value, $callback, $default = null)
protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
{
if (! $value) {
return $callback($this, $value) ?: $this;
} elseif ($default) {
return $default($this, $value) ?: $this;
if (! $cursor instanceof Cursor) {
$cursor = is_string($cursor)
? Cursor::fromEncoded($cursor)
: CursorPaginator::resolveCurrentCursor($cursorName, $cursor);
}
return $this;
$orders = $this->ensureOrderForCursorPagination(! is_null($cursor) && $cursor->pointsToPreviousItems());
if (! is_null($cursor)) {
$addCursorConditions = function (self $builder, $previousColumn, $i) use (&$addCursorConditions, $cursor, $orders) {
$unionBuilders = isset($builder->unions) ? collect($builder->unions)->pluck('query') : collect();
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($this, $previousColumn),
'=',
$cursor->parameter($previousColumn)
);
$this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
});
}
$builder->where(function (self $builder) use ($addCursorConditions, $cursor, $orders, $i, $unionBuilders) {
['column' => $column, 'direction' => $direction] = $orders[$i];
$originalColumn = $this->getOriginalColumnNameForCursorPagination($this, $column);
$builder->where(
Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn,
$direction === 'asc' ? '>' : '<',
$cursor->parameter($column)
);
if ($i < $orders->count() - 1) {
$builder->orWhere(function (self $builder) use ($addCursorConditions, $column, $i) {
$addCursorConditions($builder, $column, $i + 1);
});
}
$unionBuilders->each(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions) {
$unionBuilder->where(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions) {
$unionBuilder->where(
$this->getOriginalColumnNameForCursorPagination($this, $column),
$direction === 'asc' ? '>' : '<',
$cursor->parameter($column)
);
if ($i < $orders->count() - 1) {
$unionBuilder->orWhere(function (self $builder) use ($addCursorConditions, $column, $i) {
$addCursorConditions($builder, $column, $i + 1);
});
}
$this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
});
});
});
};
$addCursorConditions($this, null, 0);
}
$this->limit($perPage + 1);
return $this->cursorPaginator($this->get($columns), $perPage, $cursor, [
'path' => Paginator::resolveCurrentPath(),
'cursorName' => $cursorName,
'parameters' => $orders->pluck('column')->toArray(),
]);
}
/**
* Get the original column name of the given column, without any aliasing.
*
* @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
* @param string $parameter
* @return string
*/
protected function getOriginalColumnNameForCursorPagination($builder, string $parameter)
{
$columns = $builder instanceof Builder ? $builder->getQuery()->columns : $builder->columns;
if (! is_null($columns)) {
foreach ($columns as $column) {
if (($position = strripos($column, ' as ')) !== false) {
$original = substr($column, 0, $position);
$alias = substr($column, $position + 4);
if ($parameter === $alias || $builder->getGrammar()->wrap($parameter) === $alias) {
return $original;
}
}
}
}
return $parameter;
}
/**
@@ -134,8 +463,8 @@ trait BuildsQueries
* Create a new simple paginator instance.
*
* @param \Illuminate\Support\Collection $items
* @param int $perPage
* @param int $currentPage
* @param int $perPage
* @param int $currentPage
* @param array $options
* @return \Illuminate\Pagination\Paginator
*/
@@ -145,4 +474,33 @@ trait BuildsQueries
'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.
*
* @param callable $callback
* @return $this
*/
public function tap($callback)
{
$callback($this);
return $this;
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Illuminate\Database\Concerns;
use Illuminate\Support\Str;
trait CompilesJsonPaths
{
/**
* Split the given JSON selector into the field and the optional path and wrap them separately.
*
* @param string $column
* @return array
*/
protected function wrapJsonFieldAndPath($column)
{
$parts = explode('->', $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 = collect(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.'"';
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Illuminate\Database\Concerns;
use Illuminate\Support\Collection;
trait ExplainsQueries
{
/**
* Explains the query.
*
* @return \Illuminate\Support\Collection
*/
public function explain()
{
$sql = $this->toSql();
$bindings = $this->getBindings();
$explanation = $this->getConnection()->select('EXPLAIN '.$sql, $bindings);
return new Collection($explanation);
}
}

View File

@@ -3,7 +3,8 @@
namespace Illuminate\Database\Concerns;
use Closure;
use Exception;
use Illuminate\Database\DeadlockException;
use RuntimeException;
use Throwable;
trait ManagesTransactions
@@ -15,7 +16,7 @@ trait ManagesTransactions
* @param int $attempts
* @return mixed
*
* @throws \Exception|\Throwable
* @throws \Throwable
*/
public function transaction(Closure $callback, $attempts = 1)
{
@@ -26,46 +27,69 @@ trait ManagesTransactions
// 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 {
return tap($callback($this), function ($result) {
$this->commit();
});
$callbackResult = $callback($this);
}
// If we catch an exception we'll rollback this transaction and try again if we
// are not out of attempts. If we are out of attempts we will just throw the
// exception back out and let the developer handle an uncaught exceptions.
catch (Exception $e) {
// exception back out, and let the developer handle an uncaught exception.
catch (Throwable $e) {
$this->handleTransactionException(
$e, $currentAttempt, $attempts
);
} catch (Throwable $e) {
$this->rollBack();
throw $e;
continue;
}
try {
if ($this->transactions == 1) {
$this->fireConnectionEvent('committing');
$this->getPdo()->commit();
}
$this->transactions = max(0, $this->transactions - 1);
if ($this->afterCommitCallbacksShouldBeExecuted()) {
$this->transactionsManager?->commit($this->getName());
}
} catch (Throwable $e) {
$this->handleCommitTransactionException(
$e, $currentAttempt, $attempts
);
continue;
}
$this->fireConnectionEvent('committed');
return $callbackResult;
}
}
/**
* Handle an exception encountered when running a transacted statement.
*
* @param \Exception $e
* @param \Throwable $e
* @param int $currentAttempt
* @param int $maxAttempts
* @return void
*
* @throws \Exception
* @throws \Throwable
*/
protected function handleTransactionException($e, $currentAttempt, $maxAttempts)
protected function handleTransactionException(Throwable $e, $currentAttempt, $maxAttempts)
{
// On a deadlock, MySQL rolls back the entire transaction so we can't just
// retry the query. We have to throw this exception all the way out and
// let the developer handle it in another way. We will decrement too.
if ($this->causedByDeadlock($e) &&
if ($this->causedByConcurrencyError($e) &&
$this->transactions > 1) {
--$this->transactions;
$this->transactions--;
throw $e;
$this->transactionsManager?->rollback(
$this->getName(), $this->transactions
);
throw new DeadlockException($e->getMessage(), is_int($e->getCode()) ? $e->getCode() : 0, $e);
}
// If there was an exception we will rollback this transaction and then we
@@ -73,7 +97,7 @@ trait ManagesTransactions
// if we haven't we will return and try this query again in our loop.
$this->rollBack();
if ($this->causedByDeadlock($e) &&
if ($this->causedByConcurrencyError($e) &&
$currentAttempt < $maxAttempts) {
return;
}
@@ -85,13 +109,18 @@ trait ManagesTransactions
* Start a new database transaction.
*
* @return void
* @throws \Exception
*
* @throws \Throwable
*/
public function beginTransaction()
{
$this->createTransaction();
++$this->transactions;
$this->transactions++;
$this->transactionsManager?->begin(
$this->getName(), $this->transactions
);
$this->fireConnectionEvent('beganTransaction');
}
@@ -100,13 +129,17 @@ trait ManagesTransactions
* Create a transaction within the database.
*
* @return void
*
* @throws \Throwable
*/
protected function createTransaction()
{
if ($this->transactions == 0) {
$this->reconnectIfMissingConnection();
try {
$this->getPdo()->beginTransaction();
} catch (Exception $e) {
} catch (Throwable $e) {
$this->handleBeginTransactionException($e);
}
} elseif ($this->transactions >= 1 && $this->queryGrammar->supportsSavepoints()) {
@@ -118,6 +151,8 @@ trait ManagesTransactions
* Create a save point within the database.
*
* @return void
*
* @throws \Throwable
*/
protected function createSavepoint()
{
@@ -129,17 +164,17 @@ trait ManagesTransactions
/**
* Handle an exception from a transaction beginning.
*
* @param \Exception $e
* @param \Throwable $e
* @return void
*
* @throws \Exception
* @throws \Throwable
*/
protected function handleBeginTransactionException($e)
protected function handleBeginTransactionException(Throwable $e)
{
if ($this->causedByLostConnection($e)) {
$this->reconnect();
$this->pdo->beginTransaction();
$this->getPdo()->beginTransaction();
} else {
throw $e;
}
@@ -149,23 +184,69 @@ trait ManagesTransactions
* Commit the active database transaction.
*
* @return void
*
* @throws \Throwable
*/
public function commit()
{
if ($this->transactions == 1) {
if ($this->transactionLevel() == 1) {
$this->fireConnectionEvent('committing');
$this->getPdo()->commit();
}
$this->transactions = max(0, $this->transactions - 1);
if ($this->afterCommitCallbacksShouldBeExecuted()) {
$this->transactionsManager?->commit($this->getName());
}
$this->fireConnectionEvent('committed');
}
/**
* Determine if after commit callbacks should be executed.
*
* @return bool
*/
protected function afterCommitCallbacksShouldBeExecuted()
{
return $this->transactions == 0 ||
($this->transactionsManager &&
$this->transactionsManager->callbackApplicableTransactions()->count() === 1);
}
/**
* Handle an exception encountered when committing a transaction.
*
* @param \Throwable $e
* @param int $currentAttempt
* @param int $maxAttempts
* @return void
*
* @throws \Throwable
*/
protected function handleCommitTransactionException(Throwable $e, $currentAttempt, $maxAttempts)
{
$this->transactions = max(0, $this->transactions - 1);
if ($this->causedByConcurrencyError($e) && $currentAttempt < $maxAttempts) {
return;
}
if ($this->causedByLostConnection($e)) {
$this->transactions = 0;
}
throw $e;
}
/**
* Rollback the active database transaction.
*
* @param int|null $toLevel
* @return void
*
* @throws \Throwable
*/
public function rollBack($toLevel = null)
{
@@ -183,10 +264,18 @@ trait ManagesTransactions
// 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.
$this->performRollBack($toLevel);
try {
$this->performRollBack($toLevel);
} catch (Throwable $e) {
$this->handleRollBackException($e);
}
$this->transactions = $toLevel;
$this->transactionsManager?->rollback(
$this->getName(), $this->transactions
);
$this->fireConnectionEvent('rollingBack');
}
@@ -195,6 +284,8 @@ trait ManagesTransactions
*
* @param int $toLevel
* @return void
*
* @throws \Throwable
*/
protected function performRollBack($toLevel)
{
@@ -207,6 +298,27 @@ trait ManagesTransactions
}
}
/**
* 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.
*
@@ -216,4 +328,21 @@ trait ManagesTransactions
{
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.');
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Illuminate\Database\Concerns;
trait ParsesSearchPath
{
/**
* Parse the Postgres "search_path" configuration value into an array.
*
* @param string|array|null $searchPath
* @return array
*/
protected function parseSearchPath($searchPath)
{
if (is_string($searchPath)) {
preg_match_all('/[^\s,"\']+/', $searchPath, $matches);
$searchPath = $matches[0];
}
return array_map(function ($schema) {
return trim($schema, '\'"');
}, $searchPath ?? []);
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Illuminate\Database;
use Illuminate\Support\ConfigurationUrlParser as BaseConfigurationUrlParser;
class ConfigurationUrlParser extends BaseConfigurationUrlParser
{
//
}

View File

@@ -2,27 +2,38 @@
namespace Illuminate\Database;
use PDO;
use Carbon\CarbonInterval;
use Closure;
use Exception;
use PDOStatement;
use LogicException;
use DateTimeInterface;
use Illuminate\Support\Arr;
use Illuminate\Database\Query\Expression;
use Doctrine\DBAL\Connection as DoctrineConnection;
use Doctrine\DBAL\Types\Type;
use Exception;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Events\QueryExecuted;
use Doctrine\DBAL\Connection as DoctrineConnection;
use Illuminate\Database\Query\Processors\Processor;
use Illuminate\Database\Events\StatementPrepared;
use Illuminate\Database\Events\TransactionBeginning;
use Illuminate\Database\Events\TransactionCommitted;
use Illuminate\Database\Events\TransactionCommitting;
use Illuminate\Database\Events\TransactionRolledBack;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Database\Schema\Builder as SchemaBuilder;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Query\Grammars\Grammar as QueryGrammar;
use Illuminate\Database\Query\Processors\Processor;
use Illuminate\Database\Schema\Builder as SchemaBuilder;
use Illuminate\Support\Arr;
use Illuminate\Support\InteractsWithTime;
use Illuminate\Support\Traits\Macroable;
use PDO;
use PDOStatement;
use RuntimeException;
class Connection implements ConnectionInterface
{
use DetectsDeadlocks,
use DetectsConcurrencyErrors,
DetectsLostConnections,
Concerns\ManagesTransactions;
Concerns\ManagesTransactions,
InteractsWithTime,
Macroable;
/**
* The active PDO connection.
@@ -45,6 +56,13 @@ class Connection implements ConnectionInterface
*/
protected $database;
/**
* The type of the connection.
*
* @var string|null
*/
protected $readWriteType;
/**
* The table prefix for the connection.
*
@@ -108,6 +126,27 @@ class Connection implements ConnectionInterface
*/
protected $transactions = 0;
/**
* The transaction manager instance.
*
* @var \Illuminate\Database\DatabaseTransactionsManager
*/
protected $transactionsManager;
/**
* Indicates if changes have been made to the database.
*
* @var bool
*/
protected $recordsModified = false;
/**
* Indicates if the connection should use the "write" PDO connection.
*
* @var bool
*/
protected $readOnWriteConnection = false;
/**
* All of the queries run against the connection.
*
@@ -122,6 +161,20 @@ class Connection implements ConnectionInterface
*/
protected $loggingQueries = false;
/**
* The duration of all executed queries in milliseconds.
*
* @var float
*/
protected $totalQueryDuration = 0.0;
/**
* All of the registered query duration handlers.
*
* @var array
*/
protected $queryDurationHandlers = [];
/**
* Indicates if the connection is in a "dry run".
*
@@ -129,6 +182,13 @@ class Connection implements ConnectionInterface
*/
protected $pretending = false;
/**
* All of the callbacks that should be invoked before a query is executed.
*
* @var \Closure[]
*/
protected $beforeExecutingCallbacks = [];
/**
* The instance of Doctrine connection.
*
@@ -136,20 +196,27 @@ class Connection implements ConnectionInterface
*/
protected $doctrineConnection;
/**
* Type mappings that should be registered with new Doctrine connections.
*
* @var array<string, string>
*/
protected $doctrineTypeMappings = [];
/**
* The connection resolvers.
*
* @var array
* @var \Closure[]
*/
protected static $resolvers = [];
/**
* Create a new database connection instance.
*
* @param \PDO|\Closure $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 = [])
@@ -250,12 +317,13 @@ class Connection implements ConnectionInterface
/**
* Begin a fluent query against a database table.
*
* @param string $table
* @param \Closure|\Illuminate\Database\Query\Builder|string $table
* @param string|null $as
* @return \Illuminate\Database\Query\Builder
*/
public function table($table)
public function table($table, $as = null)
{
return $this->query()->from($table);
return $this->query()->from($table, $as);
}
/**
@@ -274,7 +342,7 @@ class Connection implements ConnectionInterface
* Run a select statement and return a single result.
*
* @param string $query
* @param array $bindings
* @param array $bindings
* @param bool $useReadPdo
* @return mixed
*/
@@ -285,11 +353,38 @@ class Connection implements ConnectionInterface
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 reset($record);
}
/**
* Run a select statement against the database.
*
* @param string $query
* @param array $bindings
* @param array $bindings
* @return array
*/
public function selectFromWriteConnection($query, $bindings = [])
@@ -315,8 +410,9 @@ class Connection implements ConnectionInterface
// 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));
$statement = $this->prepared(
$this->getPdoForSelect($useReadPdo)->prepare($query)
);
$this->bindValues($statement, $this->prepareBindings($bindings));
@@ -374,9 +470,7 @@ class Connection implements ConnectionInterface
{
$statement->setFetchMode($this->fetchMode);
$this->event(new Events\StatementPrepared(
$this, $statement
));
$this->event(new StatementPrepared($this, $statement));
return $statement;
}
@@ -396,7 +490,7 @@ class Connection implements ConnectionInterface
* Run an insert statement against the database.
*
* @param string $query
* @param array $bindings
* @param array $bindings
* @return bool
*/
public function insert($query, $bindings = [])
@@ -408,7 +502,7 @@ class Connection implements ConnectionInterface
* Run an update statement against the database.
*
* @param string $query
* @param array $bindings
* @param array $bindings
* @return int
*/
public function update($query, $bindings = [])
@@ -420,7 +514,7 @@ class Connection implements ConnectionInterface
* Run a delete statement against the database.
*
* @param string $query
* @param array $bindings
* @param array $bindings
* @return int
*/
public function delete($query, $bindings = [])
@@ -432,7 +526,7 @@ class Connection implements ConnectionInterface
* 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 = [])
@@ -446,6 +540,8 @@ class Connection implements ConnectionInterface
$this->bindValues($statement, $this->prepareBindings($bindings));
$this->recordsHaveBeenModified();
return $statement->execute();
});
}
@@ -454,7 +550,7 @@ class Connection implements ConnectionInterface
* 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 = [])
@@ -473,7 +569,11 @@ class Connection implements ConnectionInterface
$statement->execute();
return $statement->rowCount();
$this->recordsHaveBeenModified(
($count = $statement->rowCount()) > 0
);
return $count;
});
}
@@ -490,7 +590,11 @@ class Connection implements ConnectionInterface
return true;
}
return $this->getPdo()->exec($query) === false ? false : true;
$this->recordsHaveBeenModified(
$change = $this->getPdo()->exec($query) !== false
);
return $change;
});
}
@@ -535,7 +639,7 @@ class Connection implements ConnectionInterface
// 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 hte callback so the original callers can have the results.
// value of the callback so the original callers can have the results.
$result = $callback();
$this->loggingQueries = $loggingQueries;
@@ -546,7 +650,7 @@ class Connection implements ConnectionInterface
/**
* Bind values to their parameters in the given statement.
*
* @param \PDOStatement $statement
* @param \PDOStatement $statement
* @param array $bindings
* @return void
*/
@@ -554,8 +658,13 @@ class Connection implements ConnectionInterface
{
foreach ($bindings as $key => $value) {
$statement->bindValue(
is_string($key) ? $key : $key + 1, $value,
is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR
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
},
);
}
}
@@ -576,8 +685,8 @@ class Connection implements ConnectionInterface
// so we'll just ask the grammar for the format to get from the date.
if ($value instanceof DateTimeInterface) {
$bindings[$key] = $value->format($grammar->getDateFormat());
} elseif ($value === false) {
$bindings[$key] = 0;
} elseif (is_bool($value)) {
$bindings[$key] = (int) $value;
}
}
@@ -587,8 +696,8 @@ class Connection implements ConnectionInterface
/**
* 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
*
@@ -596,6 +705,10 @@ class Connection implements ConnectionInterface
*/
protected function run($query, $bindings, Closure $callback)
{
foreach ($this->beforeExecutingCallbacks as $beforeExecutingCallback) {
$beforeExecutingCallback($query, $bindings, $this);
}
$this->reconnectIfMissingConnection();
$start = microtime(true);
@@ -624,8 +737,8 @@ class Connection implements ConnectionInterface
/**
* Run a SQL statement.
*
* @param string $query
* @param array $bindings
* @param string $query
* @param array $bindings
* @param \Closure $callback
* @return mixed
*
@@ -637,7 +750,7 @@ class Connection implements ConnectionInterface
// 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 {
$result = $callback($query, $bindings);
return $callback($query, $bindings);
}
// If an exception occurs when attempting to run a query, we'll format the error
@@ -648,20 +761,20 @@ class Connection implements ConnectionInterface
$query, $this->prepareBindings($bindings), $e
);
}
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;
$this->event(new QueryExecuted($query, $bindings, $time, $this));
if ($this->loggingQueries) {
@@ -672,7 +785,7 @@ class Connection implements ConnectionInterface
/**
* Get the elapsed time since a given starting point.
*
* @param int $start
* @param int $start
* @return float
*/
protected function getElapsedTime($start)
@@ -680,17 +793,83 @@ class Connection implements ConnectionInterface
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 $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 \Exception $e
* @param \Illuminate\Database\QueryException $e
* @param string $query
* @param array $bindings
* @param \Closure $callback
* @return mixed
* @throws \Exception
*
* @throws \Illuminate\Database\QueryException
*/
protected function handleQueryException($e, $query, $bindings, Closure $callback)
protected function handleQueryException(QueryException $e, $query, $bindings, Closure $callback)
{
if ($this->transactions >= 1) {
throw $e;
@@ -705,8 +884,8 @@ class Connection implements ConnectionInterface
* 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
*
@@ -726,17 +905,19 @@ class Connection implements ConnectionInterface
/**
* Reconnect to the database.
*
* @return void
* @return mixed|false
*
* @throws \LogicException
* @throws \Illuminate\Database\LostConnectionException
*/
public function reconnect()
{
if (is_callable($this->reconnector)) {
$this->doctrineConnection = null;
return call_user_func($this->reconnector, $this);
}
throw new LogicException('Lost connection and no reconnector available.');
throw new LostConnectionException('Lost connection and no reconnector available.');
}
/**
@@ -759,6 +940,21 @@ class Connection implements ConnectionInterface
public function disconnect()
{
$this->setPdo(null)->setReadPdo(null);
$this->doctrineConnection = null;
}
/**
* 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;
}
/**
@@ -769,9 +965,7 @@ class Connection implements ConnectionInterface
*/
public function listen(Closure $callback)
{
if (isset($this->events)) {
$this->events->listen(Events\QueryExecuted::class, $callback);
}
$this->events?->listen(Events\QueryExecuted::class, $callback);
}
/**
@@ -782,18 +976,13 @@ class Connection implements ConnectionInterface
*/
protected function fireConnectionEvent($event)
{
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));
}
return $this->events?->dispatch(match ($event) {
'beganTransaction' => new TransactionBeginning($this),
'committed' => new TransactionCommitted($this),
'committing' => new TransactionCommitting($this),
'rollingBack' => new TransactionRolledBack($this),
default => null,
});
}
/**
@@ -804,9 +993,7 @@ class Connection implements ConnectionInterface
*/
protected function event($event)
{
if (isset($this->events)) {
$this->events->dispatch($event);
}
$this->events?->dispatch($event);
}
/**
@@ -820,6 +1007,65 @@ class Connection implements ConnectionInterface
return new Expression($value);
}
/**
* Determine if the database connection has modified any database records.
*
* @return bool
*/
public function hasModifiedRecords()
{
return $this->recordsModified;
}
/**
* Indicate if any records have been modified.
*
* @param bool $value
* @return void
*/
public function recordsHaveBeenModified($value = true)
{
if (! $this->recordsModified) {
$this->recordsModified = $value;
}
}
/**
* Set the record modification state.
*
* @param bool $value
* @return $this
*/
public function setRecordModificationState(bool $value)
{
$this->recordsModified = $value;
return $this;
}
/**
* Reset the record modification state.
*
* @return void
*/
public function forgetRecordModificationState()
{
$this->recordsModified = false;
}
/**
* Indicate that the connection should use the write PDO connection for reads.
*
* @param bool $value
* @return $this
*/
public function useWriteConnectionWhenReading($value = true)
{
$this->readOnWriteConnection = $value;
return $this;
}
/**
* Is Doctrine available?
*
@@ -830,6 +1076,16 @@ class Connection implements ConnectionInterface
return class_exists('Doctrine\DBAL\Connection');
}
/**
* Indicates whether native alter operations will be used when dropping or renaming columns, even if Doctrine DBAL is installed.
*
* @return bool
*/
public function usingNativeSchemaOperations()
{
return ! $this->isDoctrineAvailable() || SchemaBuilder::$alwaysUsesNativeSchemaOperationsIfPossible;
}
/**
* Get a Doctrine Schema Column instance.
*
@@ -851,7 +1107,13 @@ class Connection implements ConnectionInterface
*/
public function getDoctrineSchemaManager()
{
return $this->getDoctrineDriver()->getSchemaManager($this->getDoctrineConnection());
$connection = $this->getDoctrineConnection();
// Doctrine v2 expects one parameter while v3 expects two. 2nd will be ignored on v2...
return $this->getDoctrineDriver()->getSchemaManager(
$connection,
$connection->getDatabasePlatform()
);
}
/**
@@ -862,16 +1124,52 @@ class Connection implements ConnectionInterface
public function getDoctrineConnection()
{
if (is_null($this->doctrineConnection)) {
$data = ['pdo' => $this->getPdo(), 'dbname' => $this->getConfig('database')];
$driver = $this->getDoctrineDriver();
$this->doctrineConnection = new DoctrineConnection(
$data, $this->getDoctrineDriver()
);
$this->doctrineConnection = new DoctrineConnection(array_filter([
'pdo' => $this->getPdo(),
'dbname' => $this->getDatabaseName(),
'driver' => $driver->getName(),
'serverVersion' => $this->getConfig('server_version'),
]), $driver);
foreach ($this->doctrineTypeMappings as $name => $type) {
$this->doctrineConnection
->getDatabasePlatform()
->registerDoctrineTypeMapping($type, $name);
}
}
return $this->doctrineConnection;
}
/**
* Register a custom Doctrine mapping type.
*
* @param Type|class-string<Type> $class
* @param string $name
* @param string $type
* @return void
*
* @throws \Doctrine\DBAL\DBALException
* @throws \RuntimeException
*/
public function registerDoctrineType(Type|string $class, string $name, string $type): void
{
if (! $this->isDoctrineAvailable()) {
throw new RuntimeException(
'Registering a custom Doctrine type requires Doctrine DBAL (doctrine/dbal).'
);
}
if (! Type::hasType($name)) {
Type::getTypeRegistry()
->register($name, is_string($class) ? new $class() : $class);
}
$this->doctrineTypeMappings[$name] = $type;
}
/**
* Get the current PDO connection.
*
@@ -886,6 +1184,16 @@ class Connection implements ConnectionInterface
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.
*
@@ -893,7 +1201,12 @@ class Connection implements ConnectionInterface
*/
public function getReadPdo()
{
if ($this->transactions >= 1) {
if ($this->transactions > 0) {
return $this->getPdo();
}
if ($this->readOnWriteConnection ||
($this->recordsModified && $this->getConfig('sticky'))) {
return $this->getPdo();
}
@@ -904,6 +1217,16 @@ class Connection implements ConnectionInterface
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.
*
@@ -922,7 +1245,7 @@ class Connection implements ConnectionInterface
/**
* Set the PDO connection used for reading.
*
* @param \PDO||\Closure|null $pdo
* @param \PDO|\Closure|null $pdo
* @return $this
*/
public function setReadPdo($pdo)
@@ -955,6 +1278,16 @@ class Connection implements ConnectionInterface
return $this->getConfig('name');
}
/**
* Get the database connection full name.
*
* @return string|null
*/
public function getNameWithReadWriteType()
{
return $this->getName().($this->readWriteType ? '::'.$this->readWriteType : '');
}
/**
* Get an option from the configuration options.
*
@@ -990,11 +1323,13 @@ class Connection implements ConnectionInterface
* Set the query grammar used by the connection.
*
* @param \Illuminate\Database\Query\Grammars\Grammar $grammar
* @return void
* @return $this
*/
public function setQueryGrammar(Query\Grammars\Grammar $grammar)
{
$this->queryGrammar = $grammar;
return $this;
}
/**
@@ -1011,11 +1346,13 @@ class Connection implements ConnectionInterface
* Set the schema grammar used by the connection.
*
* @param \Illuminate\Database\Schema\Grammars\Grammar $grammar
* @return void
* @return $this
*/
public function setSchemaGrammar(Schema\Grammars\Grammar $grammar)
{
$this->schemaGrammar = $grammar;
return $this;
}
/**
@@ -1032,11 +1369,13 @@ class Connection implements ConnectionInterface
* Set the query post processor used by the connection.
*
* @param \Illuminate\Database\Query\Processors\Processor $processor
* @return void
* @return $this
*/
public function setPostProcessor(Processor $processor)
{
$this->postProcessor = $processor;
return $this;
}
/**
@@ -1053,15 +1392,50 @@ class Connection implements ConnectionInterface
* Set the event dispatcher instance on the connection.
*
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @return void
* @return $this
*/
public function setEventDispatcher(Dispatcher $events)
{
$this->events = $events;
return $this;
}
/**
* Determine if the connection in a "dry run".
* Unset the event dispatcher for this connection.
*
* @return void
*/
public function unsetEventDispatcher()
{
$this->events = null;
}
/**
* 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".
*
* @return bool
*/
@@ -1134,11 +1508,26 @@ class Connection implements ConnectionInterface
* Set the name of the connected database.
*
* @param string $database
* @return string
* @return $this
*/
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;
}
/**
@@ -1155,13 +1544,15 @@ class Connection implements ConnectionInterface
* Set the table prefix in use by the connection.
*
* @param string $prefix
* @return void
* @return $this
*/
public function setTablePrefix($prefix)
{
$this->tablePrefix = $prefix;
$this->getQueryGrammar()->setTablePrefix($prefix);
return $this;
}
/**
@@ -1197,7 +1588,6 @@ class Connection implements ConnectionInterface
*/
public static function getResolver($driver)
{
return isset(static::$resolvers[$driver]) ?
static::$resolvers[$driver] : null;
return static::$resolvers[$driver] ?? null;
}
}

View File

@@ -9,10 +9,11 @@ interface ConnectionInterface
/**
* Begin a fluent query against a database table.
*
* @param string $table
* @param \Closure|\Illuminate\Database\Query\Builder|string $table
* @param string|null $as
* @return \Illuminate\Database\Query\Builder
*/
public function table($table);
public function table($table, $as = null);
/**
* Get a new raw query expression.
@@ -26,25 +27,37 @@ interface ConnectionInterface
* Run a select statement and return a single result.
*
* @param string $query
* @param array $bindings
* @param array $bindings
* @param bool $useReadPdo
* @return mixed
*/
public function selectOne($query, $bindings = []);
public function selectOne($query, $bindings = [], $useReadPdo = true);
/**
* Run a select statement against the database.
*
* @param string $query
* @param array $bindings
* @param array $bindings
* @param bool $useReadPdo
* @return array
*/
public function select($query, $bindings = []);
public function select($query, $bindings = [], $useReadPdo = true);
/**
* Run a select statement against the database and returns a generator.
*
* @param string $query
* @param array $bindings
* @param bool $useReadPdo
* @return \Generator
*/
public function cursor($query, $bindings = [], $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 = []);
@@ -53,7 +66,7 @@ interface ConnectionInterface
* Run an update statement against the database.
*
* @param string $query
* @param array $bindings
* @param array $bindings
* @return int
*/
public function update($query, $bindings = []);
@@ -62,7 +75,7 @@ interface ConnectionInterface
* Run a delete statement against the database.
*
* @param string $query
* @param array $bindings
* @param array $bindings
* @return int
*/
public function delete($query, $bindings = []);
@@ -71,7 +84,7 @@ interface ConnectionInterface
* 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 = []);
@@ -80,7 +93,7 @@ interface ConnectionInterface
* 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 = []);
@@ -147,4 +160,11 @@ interface ConnectionInterface
* @return array
*/
public function pretend(Closure $callback);
/**
* Get the name of the connected database.
*
* @return string
*/
public function getDatabaseName();
}

View File

@@ -7,7 +7,7 @@ class ConnectionResolver implements ConnectionResolverInterface
/**
* All of the registered connections.
*
* @var array
* @var \Illuminate\Database\ConnectionInterface[]
*/
protected $connections = [];
@@ -21,7 +21,7 @@ class ConnectionResolver implements ConnectionResolverInterface
/**
* Create a new connection resolver instance.
*
* @param array $connections
* @param array<string, \Illuminate\Database\ConnectionInterface> $connections
* @return void
*/
public function __construct(array $connections = [])
@@ -34,7 +34,7 @@ class ConnectionResolver implements ConnectionResolverInterface
/**
* Get a database connection instance.
*
* @param string $name
* @param string|null $name
* @return \Illuminate\Database\ConnectionInterface
*/
public function connection($name = null)

View File

@@ -7,7 +7,7 @@ interface ConnectionResolverInterface
/**
* Get a database connection instance.
*
* @param string $name
* @param string|null $name
* @return \Illuminate\Database\ConnectionInterface
*/
public function connection($name = null);

View File

@@ -2,16 +2,15 @@
namespace Illuminate\Database\Connectors;
use PDOException;
use Illuminate\Support\Arr;
use InvalidArgumentException;
use Illuminate\Contracts\Container\Container;
use Illuminate\Database\Connection;
use Illuminate\Database\MySqlConnection;
use Illuminate\Database\SQLiteConnection;
use Illuminate\Database\PostgresConnection;
use Illuminate\Database\SQLiteConnection;
use Illuminate\Database\SqlServerConnection;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Support\Arr;
use InvalidArgumentException;
use PDOException;
class ConnectionFactory
{
@@ -36,8 +35,8 @@ class ConnectionFactory
/**
* Establish a PDO connection based on the configuration.
*
* @param array $config
* @param string $name
* @param array $config
* @param string|null $name
* @return \Illuminate\Database\Connection
*/
public function make(array $config, $name = null)
@@ -54,7 +53,7 @@ class ConnectionFactory
/**
* Parse and prepare the database configuration.
*
* @param array $config
* @param array $config
* @param string $name
* @return array
*/
@@ -79,7 +78,7 @@ class ConnectionFactory
}
/**
* Create a single database connection instance.
* Create a read / write database connection instance.
*
* @param array $config
* @return \Illuminate\Database\Connection
@@ -116,7 +115,7 @@ class ConnectionFactory
}
/**
* Get the read configuration for a read / write connection.
* Get the write configuration for a read / write connection.
*
* @param array $config
* @return array
@@ -131,7 +130,7 @@ class ConnectionFactory
/**
* Get a read / write level configuration.
*
* @param array $config
* @param array $config
* @param string $type
* @return array
*/
@@ -172,19 +171,19 @@ class ConnectionFactory
*
* @param array $config
* @return \Closure
*
* @throws \PDOException
*/
protected function createPdoResolverWithHosts(array $config)
{
return function () use ($config) {
foreach (Arr::shuffle($hosts = $this->parseHosts($config)) as $key => $host) {
foreach (Arr::shuffle($this->parseHosts($config)) as $host) {
$config['host'] = $host;
try {
return $this->createConnector($config)->connect($config);
} catch (PDOException $e) {
if (count($hosts) - 1 === $key && $this->container->bound(ExceptionHandler::class)) {
$this->container->make(ExceptionHandler::class)->report($e);
}
continue;
}
}
@@ -197,10 +196,12 @@ class ConnectionFactory
*
* @param array $config
* @return array
*
* @throws \InvalidArgumentException
*/
protected function parseHosts(array $config)
{
$hosts = array_wrap($config['host']);
$hosts = Arr::wrap($config['host']);
if (empty($hosts)) {
throw new InvalidArgumentException('Database hosts array is empty.');
@@ -217,9 +218,7 @@ class ConnectionFactory
*/
protected function createPdoResolverWithoutHosts(array $config)
{
return function () use ($config) {
return $this->createConnector($config)->connect($config);
};
return fn () => $this->createConnector($config)->connect($config);
}
/**
@@ -240,28 +239,23 @@ class ConnectionFactory
return $this->container->make($key);
}
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']}]");
return match ($config['driver']) {
'mysql' => new MySqlConnector,
'pgsql' => new PostgresConnector,
'sqlite' => new SQLiteConnector,
'sqlsrv' => new SqlServerConnector,
default => 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
@@ -272,17 +266,12 @@ class ConnectionFactory
return $resolver($connection, $database, $prefix, $config);
}
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]");
return match ($driver) {
'mysql' => new MySqlConnection($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}]."),
};
}
}

View File

@@ -2,11 +2,11 @@
namespace Illuminate\Database\Connectors;
use PDO;
use Exception;
use Illuminate\Support\Arr;
use Doctrine\DBAL\Driver\PDOConnection;
use Exception;
use Illuminate\Database\DetectsLostConnections;
use PDO;
use Throwable;
class Connector
{
@@ -29,14 +29,16 @@ 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)
{
list($username, $password) = [
Arr::get($config, 'username'), Arr::get($config, 'password'),
[$username, $password] = [
$config['username'] ?? null, $config['password'] ?? null,
];
try {
@@ -83,16 +85,16 @@ class Connector
/**
* Handle an exception that occurred during connect execution.
*
* @param \Exception $e
* @param \Throwable $e
* @param string $dsn
* @param string $username
* @param string $password
* @param array $options
* @param array $options
* @return \PDO
*
* @throws \Exception
*/
protected function tryAgainIfCausedByLostConnection(Exception $e, $dsn, $username, $password, $options)
protected function tryAgainIfCausedByLostConnection(Throwable $e, $dsn, $username, $password, $options)
{
if ($this->causedByLostConnection($e)) {
return $this->createPdoConnection($dsn, $username, $password, $options);
@@ -109,7 +111,7 @@ class Connector
*/
public function getOptions(array $config)
{
$options = Arr::get($config, 'options', []);
$options = $config['options'] ?? [];
return array_diff_key($this->options, $options) + $options;
}

View File

@@ -27,6 +27,8 @@ class MySqlConnector extends Connector implements ConnectorInterface
$connection->exec("use `{$config['database']}`;");
}
$this->configureIsolationLevel($connection, $config);
$this->configureEncoding($connection, $config);
// Next, we will check to see if a timezone has been specified in this config
@@ -40,12 +42,30 @@ class MySqlConnector extends Connector implements ConnectorInterface
}
/**
* Set the connection character set and collation.
* Set the connection transaction isolation level.
*
* @param \PDO $connection
* @param array $config
* @return void
*/
protected function configureIsolationLevel($connection, array $config)
{
if (! isset($config['isolation_level'])) {
return;
}
$connection->prepare(
"SET SESSION TRANSACTION ISOLATION LEVEL {$config['isolation_level']}"
)->execute();
}
/**
* Set the connection character set and collation.
*
* @param \PDO $connection
* @param array $config
* @return void|\PDO
*/
protected function configureEncoding($connection, array $config)
{
if (! isset($config['charset'])) {
@@ -87,7 +107,7 @@ class MySqlConnector extends Connector implements ConnectorInterface
*
* 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)
@@ -147,7 +167,7 @@ class MySqlConnector extends Connector implements ConnectorInterface
$this->setCustomModes($connection, $config);
} elseif (isset($config['strict'])) {
if ($config['strict']) {
$connection->prepare($this->strictMode())->execute();
$connection->prepare($this->strictMode($connection, $config))->execute();
} else {
$connection->prepare("set session sql_mode='NO_ENGINE_SUBSTITUTION'")->execute();
}
@@ -171,10 +191,18 @@ class MySqlConnector extends Connector implements ConnectorInterface
/**
* Get the query to enable strict mode.
*
* @param \PDO $connection
* @param array $config
* @return string
*/
protected function strictMode()
protected function strictMode(PDO $connection, $config)
{
$version = $config['version'] ?? $connection->getAttribute(PDO::ATTR_SERVER_VERSION);
if (version_compare($version, '8.0.11') >= 0) {
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_ENGINE_SUBSTITUTION'";
}
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'";
}
}

View File

@@ -2,10 +2,13 @@
namespace Illuminate\Database\Connectors;
use Illuminate\Database\Concerns\ParsesSearchPath;
use PDO;
class PostgresConnector extends Connector implements ConnectorInterface
{
use ParsesSearchPath;
/**
* The default PDO connection options.
*
@@ -33,6 +36,8 @@ class PostgresConnector extends Connector implements ConnectorInterface
$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
@@ -40,16 +45,32 @@ class PostgresConnector extends Connector implements ConnectorInterface
// database. Setting this DB timezone is an optional configuration item.
$this->configureTimezone($connection, $config);
$this->configureSchema($connection, $config);
$this->configureSearchPath($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);
$this->configureSynchronousCommit($connection, $config);
return $connection;
}
/**
* Set the connection transaction isolation level.
*
* @param \PDO $connection
* @param array $config
* @return void
*/
protected function configureIsolationLevel($connection, array $config)
{
if (isset($config['isolation_level'])) {
$connection->prepare("set session characteristics as transaction isolation level {$config['isolation_level']}")->execute();
}
}
/**
* Set the connection character set and collation.
*
@@ -59,9 +80,11 @@ class PostgresConnector extends Connector implements ConnectorInterface
*/
protected function configureEncoding($connection, $config)
{
$charset = $config['charset'];
if (! isset($config['charset'])) {
return;
}
$connection->prepare("set names '$charset'")->execute();
$connection->prepare("set names '{$config['charset']}'")->execute();
}
/**
@@ -81,38 +104,36 @@ class PostgresConnector extends Connector implements ConnectorInterface
}
/**
* Set the schema on the connection.
* Set the "search_path" on the database connection.
*
* @param \PDO $connection
* @param array $config
* @return void
*/
protected function configureSchema($connection, $config)
protected function configureSearchPath($connection, $config)
{
if (isset($config['schema'])) {
$schema = $this->formatSchema($config['schema']);
if (isset($config['search_path']) || isset($config['schema'])) {
$searchPath = $this->quoteSearchPath(
$this->parseSearchPath($config['search_path'] ?? $config['schema'])
);
$connection->prepare("set search_path to {$schema}")->execute();
$connection->prepare("set search_path to {$searchPath}")->execute();
}
}
/**
* Format the schema for the DSN.
* Format the search path for the DSN.
*
* @param array|string $schema
* @param array $searchPath
* @return string
*/
protected function formatSchema($schema)
protected function quoteSearchPath($searchPath)
{
if (is_array($schema)) {
return '"'.implode('", "', $schema).'"';
} else {
return '"'.$schema.'"';
}
return count($searchPath) === 1 ? '"'.$searchPath[0].'"' : '"'.implode('", "', $searchPath).'"';
}
/**
* Set the schema on the connection.
* Set the application name on the connection.
*
* @param \PDO $connection
* @param array $config
@@ -130,7 +151,7 @@ class PostgresConnector extends Connector implements ConnectorInterface
/**
* Create a DSN string from a configuration.
*
* @param array $config
* @param array $config
* @return string
*/
protected function getDsn(array $config)
@@ -142,7 +163,12 @@ class PostgresConnector extends Connector implements ConnectorInterface
$host = isset($host) ? "host={$host};" : '';
$dsn = "pgsql:{$host}dbname={$database}";
// 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;
$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
@@ -171,4 +197,20 @@ class PostgresConnector extends Connector implements ConnectorInterface
return $dsn;
}
/**
* Configure the synchronous_commit setting.
*
* @param \PDO $connection
* @param array $config
* @return void
*/
protected function configureSynchronousCommit($connection, array $config)
{
if (! isset($config['synchronous_commit'])) {
return;
}
$connection->prepare("set synchronous_commit to '{$config['synchronous_commit']}'")->execute();
}
}

View File

@@ -2,7 +2,7 @@
namespace Illuminate\Database\Connectors;
use InvalidArgumentException;
use Illuminate\Database\SQLiteDatabaseDoesNotExistException;
class SQLiteConnector extends Connector implements ConnectorInterface
{
@@ -12,7 +12,7 @@ class SQLiteConnector extends Connector implements ConnectorInterface
* @param array $config
* @return \PDO
*
* @throws \InvalidArgumentException
* @throws \Illuminate\Database\SQLiteDatabaseDoesNotExistException
*/
public function connect(array $config)
{
@@ -21,7 +21,7 @@ class SQLiteConnector extends Connector implements ConnectorInterface
// 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 may only have a single open connection.
if ($config['database'] == ':memory:') {
if ($config['database'] === ':memory:') {
return $this->createConnection('sqlite::memory:', $config, $options);
}
@@ -31,7 +31,7 @@ class SQLiteConnector extends Connector implements ConnectorInterface
// 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 InvalidArgumentException("Database (${config['database']}) does not exist.");
throw new SQLiteDatabaseDoesNotExistException($config['database']);
}
return $this->createConnection("sqlite:{$path}", $config, $options);

View File

@@ -2,8 +2,8 @@
namespace Illuminate\Database\Connectors;
use PDO;
use Illuminate\Support\Arr;
use PDO;
class SqlServerConnector extends Connector implements ConnectorInterface
{
@@ -29,13 +29,37 @@ class SqlServerConnector extends Connector implements ConnectorInterface
{
$options = $this->getOptions($config);
return $this->createConnection($this->getDsn($config), $config, $options);
$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();
}
/**
* Create a DSN string from a configuration.
*
* @param array $config
* @param array $config
* @return string
*/
protected function getDsn(array $config)
@@ -43,12 +67,14 @@ class SqlServerConnector extends Connector implements ConnectorInterface
// 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 (in_array('dblib', $this->getAvailableDrivers())) {
return $this->getDblibDsn($config);
} elseif ($this->prefersOdbc($config)) {
if ($this->prefersOdbc($config)) {
return $this->getOdbcDsn($config);
} else {
}
if (in_array('sqlsrv', $this->getAvailableDrivers())) {
return $this->getSqlSrvDsn($config);
} else {
return $this->getDblibDsn($config);
}
}
@@ -61,7 +87,7 @@ class SqlServerConnector extends Connector implements ConnectorInterface
protected function prefersOdbc(array $config)
{
return in_array('odbc', $this->getAvailableDrivers()) &&
array_get($config, 'odbc') === true;
($config['odbc'] ?? null) === true;
}
/**
@@ -75,7 +101,7 @@ class SqlServerConnector extends Connector implements ConnectorInterface
return $this->buildConnectString('dblib', array_merge([
'host' => $this->buildHostString($config, ':'),
'dbname' => $config['database'],
], Arr::only($config, ['appname', 'charset'])));
], Arr::only($config, ['appname', 'charset', 'version'])));
}
/**
@@ -130,6 +156,38 @@ class SqlServerConnector extends Connector implements ConnectorInterface
$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);
}
@@ -156,11 +214,11 @@ class SqlServerConnector extends Connector implements ConnectorInterface
*/
protected function buildHostString(array $config, $separator)
{
if (isset($config['port']) && ! empty($config['port'])) {
return $config['host'].$separator.$config['port'];
} else {
if (empty($config['port'])) {
return $config['host'];
}
return $config['host'].$separator.$config['port'];
}
/**

View File

@@ -0,0 +1,246 @@
<?php
namespace Illuminate\Database\Console;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Illuminate\Console\Command;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\MySqlConnection;
use Illuminate\Database\PostgresConnection;
use Illuminate\Database\QueryException;
use Illuminate\Database\SQLiteConnection;
use Illuminate\Database\SqlServerConnection;
use Illuminate\Support\Arr;
use Illuminate\Support\Composer;
use Symfony\Component\Process\Exception\ProcessSignaledException;
use Symfony\Component\Process\Exception\RuntimeException;
use Symfony\Component\Process\Process;
abstract class DatabaseInspectionCommand extends Command
{
/**
* A map of database column types.
*
* @var array
*/
protected $typeMappings = [
'bit' => 'string',
'citext' => 'string',
'enum' => 'string',
'geometry' => 'string',
'geomcollection' => 'string',
'linestring' => 'string',
'ltree' => 'string',
'multilinestring' => 'string',
'multipoint' => 'string',
'multipolygon' => 'string',
'point' => 'string',
'polygon' => 'string',
'sysname' => 'string',
];
/**
* The Composer instance.
*
* @var \Illuminate\Support\Composer
*/
protected $composer;
/**
* Create a new command instance.
*
* @param \Illuminate\Support\Composer|null $composer
* @return void
*/
public function __construct(Composer $composer = null)
{
parent::__construct();
$this->composer = $composer ?? $this->laravel->make(Composer::class);
}
/**
* Register the custom Doctrine type mappings for inspection commands.
*
* @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform
* @return void
*/
protected function registerTypeMappings(AbstractPlatform $platform)
{
foreach ($this->typeMappings as $type => $value) {
$platform->registerDoctrineTypeMapping($type, $value);
}
}
/**
* Get a human-readable platform name for the given platform.
*
* @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform
* @param string $database
* @return string
*/
protected function getPlatformName(AbstractPlatform $platform, $database)
{
return match (class_basename($platform)) {
'MySQLPlatform' => 'MySQL <= 5',
'MySQL57Platform' => 'MySQL 5.7',
'MySQL80Platform' => 'MySQL 8',
'PostgreSQL100Platform', 'PostgreSQLPlatform' => 'Postgres',
'SqlitePlatform' => 'SQLite',
'SQLServerPlatform' => 'SQL Server',
'SQLServer2012Platform' => 'SQL Server 2012',
default => $database,
};
}
/**
* Get the size of a table in bytes.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param string $table
* @return int|null
*/
protected function getTableSize(ConnectionInterface $connection, string $table)
{
return match (true) {
$connection instanceof MySqlConnection => $this->getMySQLTableSize($connection, $table),
$connection instanceof PostgresConnection => $this->getPostgresTableSize($connection, $table),
$connection instanceof SQLiteConnection => $this->getSqliteTableSize($connection, $table),
default => null,
};
}
/**
* Get the size of a MySQL table in bytes.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param string $table
* @return mixed
*/
protected function getMySQLTableSize(ConnectionInterface $connection, string $table)
{
$result = $connection->selectOne('SELECT (data_length + index_length) AS size FROM information_schema.TABLES WHERE table_schema = ? AND table_name = ?', [
$connection->getDatabaseName(),
$table,
]);
return Arr::wrap((array) $result)['size'];
}
/**
* Get the size of a Postgres table in bytes.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param string $table
* @return mixed
*/
protected function getPostgresTableSize(ConnectionInterface $connection, string $table)
{
$result = $connection->selectOne('SELECT pg_total_relation_size(?) AS size;', [
$table,
]);
return Arr::wrap((array) $result)['size'];
}
/**
* Get the size of a SQLite table in bytes.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param string $table
* @return mixed
*/
protected function getSqliteTableSize(ConnectionInterface $connection, string $table)
{
try {
$result = $connection->selectOne('SELECT SUM(pgsize) AS size FROM dbstat WHERE name=?', [
$table,
]);
return Arr::wrap((array) $result)['size'];
} catch (QueryException $e) {
return null;
}
}
/**
* Get the number of open connections for a database.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @return int|null
*/
protected function getConnectionCount(ConnectionInterface $connection)
{
$result = match (true) {
$connection instanceof MySqlConnection => $connection->selectOne('show status where variable_name = "threads_connected"'),
$connection instanceof PostgresConnection => $connection->selectOne('select count(*) AS "Value" from pg_stat_activity'),
$connection instanceof SqlServerConnection => $connection->selectOne('SELECT COUNT(*) Value FROM sys.dm_exec_sessions WHERE status = ?', ['running']),
default => null,
};
if (! $result) {
return null;
}
return Arr::wrap((array) $result)['Value'];
}
/**
* Get the connection configuration details for the given connection.
*
* @param string $database
* @return array
*/
protected function getConfigFromDatabase($database)
{
$database ??= config('database.default');
return Arr::except(config('database.connections.'.$database), ['password']);
}
/**
* Ensure the dependencies for the database commands are available.
*
* @return bool
*/
protected function ensureDependenciesExist()
{
return tap(interface_exists('Doctrine\DBAL\Driver'), function ($dependenciesExist) {
if (! $dependenciesExist && $this->components->confirm('Inspecting database information requires the Doctrine DBAL (doctrine/dbal) package. Would you like to install it?')) {
$this->installDependencies();
}
});
}
/**
* Install the command's dependencies.
*
* @return void
*
* @throws \Symfony\Component\Process\Exception\ProcessSignaledException
*/
protected function installDependencies()
{
$command = collect($this->composer->findComposer())
->push('require doctrine/dbal')
->implode(' ');
$process = Process::fromShellCommandline($command, null, null, null, null);
if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) {
try {
$process->setTty(true);
} catch (RuntimeException $e) {
$this->components->warn($e->getMessage());
}
}
try {
$process->run(fn ($type, $line) => $this->output->write($line));
} catch (ProcessSignaledException $e) {
if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) {
throw $e;
}
}
}
}

View File

@@ -0,0 +1,227 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Console\Command;
use Illuminate\Support\ConfigurationUrlParser;
use Symfony\Component\Process\Process;
use UnexpectedValueException;
class DbCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'db {connection? : The database connection that should be used}
{--read : Connect to the read connection}
{--write : Connect to the write connection}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Start a new database CLI session';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$connection = $this->getConnection();
if (! isset($connection['host']) && $connection['driver'] !== 'sqlite') {
$this->components->error('No host specified for this database connection.');
$this->line(' Use the <options=bold>[--read]</> and <options=bold>[--write]</> options to specify a read or write connection.');
$this->newLine();
return Command::FAILURE;
}
(new Process(
array_merge([$this->getCommand($connection)], $this->commandArguments($connection)),
null,
$this->commandEnvironment($connection)
))->setTimeout(null)->setTty(true)->mustRun(function ($type, $buffer) {
$this->output->write($buffer);
});
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',
'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)
{
return array_merge([
'--host='.$connection['host'],
'--port='.$connection['port'],
'--user='.$connection['username'],
], $this->getOptionalArguments([
'password' => '--password='.$connection['password'],
'unix_socket' => '--socket='.($connection['unix_socket'] ?? ''),
'charset' => '--default-character-set='.($connection['charset'] ?? ''),
], $connection), [$connection['database']]);
}
/**
* 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'] : ''), ],
], $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));
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Console\Command;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Connection;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\Events\SchemaDumped;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Facades\Config;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'schema:dump')]
class DumpCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $signature = 'schema:dump
{--database= : The database connection to use}
{--path= : The path where the schema dump file should be stored}
{--prune : Delete all existing migration files}';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'schema:dump';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Dump the given database schema';
/**
* Execute the console command.
*
* @param \Illuminate\Database\ConnectionResolverInterface $connections
* @param \Illuminate\Contracts\Events\Dispatcher $dispatcher
* @return int
*/
public function handle(ConnectionResolverInterface $connections, Dispatcher $dispatcher)
{
$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(
database_path('migrations'), $preserve = false
);
$info .= ' and pruned';
}
$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)
{
return $connection->getSchemaState()
->withMigrationTable($connection->getTablePrefix().Config::get('database.migrations', 'migrations'))
->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.dump'), function ($path) {
(new Filesystem)->ensureDirectoryExists(dirname($path));
});
}
}

View File

@@ -0,0 +1,154 @@
<?php
namespace Illuminate\Database\Console\Factories;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'make:factory')]
class FactoryMakeCommand extends GeneratorCommand
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'make:factory';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'make:factory';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new model factory';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Factory';
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub()
{
return $this->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 = (string) Str::of($name)->replaceFirst($this->rootNamespace(), '')->finish('Factory');
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'],
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace {{ factoryNamespace }};
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\{{ namespacedModel }}>
*/
class {{ factory }}Factory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
//
];
}
}

View File

@@ -18,15 +18,27 @@ class BaseCommand extends Command
// migrations may be run for any customized path from within the application.
if ($this->input->hasOption('path') && $this->option('path')) {
return collect($this->option('path'))->map(function ($path) {
return $this->laravel->basePath().'/'.$path;
return ! $this->usingRealPath()
? $this->laravel->basePath().'/'.$path
: $path;
})->all();
}
return array_merge(
[$this->getMigrationPath()], $this->migrator->paths()
$this->migrator->paths(), [$this->getMigrationPath()]
);
}
/**
* Determine if the given path(s) are pre-resolved "real" paths.
*
* @return bool
*/
protected function usingRealPath()
{
return $this->input->hasOption('realpath') && $this->option('realpath');
}
/**
* Get the path to the migration directory.
*

View File

@@ -0,0 +1,120 @@
<?php
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Events\DatabaseRefreshed;
use Symfony\Component\Console\Input\InputOption;
class FreshCommand extends Command
{
use ConfirmableTrait;
/**
* The console command name.
*
* @var string
*/
protected $name = 'migrate:fresh';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Drop all tables and re-run all migrations';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if (! $this->confirmToProceed()) {
return 1;
}
$database = $this->input->getOption('database');
$this->newLine();
$this->components->task('Dropping all tables', fn () => $this->callSilent('db:wipe', array_filter([
'--database' => $database,
'--drop-views' => $this->option('drop-views'),
'--drop-types' => $this->option('drop-types'),
'--force' => true,
])) == 0);
$this->newLine();
$this->call('migrate', array_filter([
'--database' => $database,
'--path' => $this->input->getOption('path'),
'--realpath' => $this->input->getOption('realpath'),
'--schema-path' => $this->input->getOption('schema-path'),
'--force' => true,
'--step' => $this->option('step'),
]));
if ($this->laravel->bound(Dispatcher::class)) {
$this->laravel[Dispatcher::class]->dispatch(
new DatabaseRefreshed
);
}
if ($this->needsSeeding()) {
$this->runSeeder($database);
}
return 0;
}
/**
* Determine if the developer has requested database seeding.
*
* @return bool
*/
protected function needsSeeding()
{
return $this->option('seed') || $this->option('seeder');
}
/**
* Run the database seeder command.
*
* @param string $database
* @return void
*/
protected function runSeeder($database)
{
$this->call('db:seed', array_filter([
'--database' => $database,
'--class' => $this->option('seeder') ?: 'Database\\Seeders\\DatabaseSeeder',
'--force' => true,
]));
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'],
['drop-views', null, InputOption::VALUE_NONE, 'Drop all tables and views'],
['drop-types', null, InputOption::VALUE_NONE, 'Drop all tables and types (Postgres only)'],
['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'],
['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to the migrations files to be executed'],
['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'],
['schema-path', null, InputOption::VALUE_OPTIONAL, 'The path to a schema dump file'],
['seed', null, InputOption::VALUE_NONE, 'Indicates if the seed task should be re-run'],
['seeder', null, InputOption::VALUE_OPTIONAL, 'The class name of the root seeder'],
['step', null, InputOption::VALUE_NONE, 'Force the migrations to be run so they can be rolled back individually'],
];
}
}

View File

@@ -3,8 +3,8 @@
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use Illuminate\Database\Migrations\MigrationRepositoryInterface;
use Symfony\Component\Console\Input\InputOption;
class InstallCommand extends Command
{
@@ -47,13 +47,13 @@ class InstallCommand extends Command
*
* @return void
*/
public function fire()
public function handle()
{
$this->repository->setSource($this->input->getOption('database'));
$this->repository->createRepository();
$this->info('Migration table created successfully.');
$this->components->info('Migration table created successfully.');
}
/**
@@ -64,7 +64,7 @@ class InstallCommand extends Command
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use.'],
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'],
];
}
}

View File

@@ -3,9 +3,16 @@
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 PDOException;
use Throwable;
class MigrateCommand extends BaseCommand
class MigrateCommand extends BaseCommand implements Isolatable
{
use ConfirmableTrait;
@@ -14,12 +21,15 @@ class MigrateCommand extends BaseCommand
*
* @var string
*/
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.}';
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}';
/**
* The console command description.
@@ -35,53 +45,63 @@ class MigrateCommand extends BaseCommand
*/
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)
public function __construct(Migrator $migrator, Dispatcher $dispatcher)
{
parent::__construct();
$this->migrator = $migrator;
$this->dispatcher = $dispatcher;
}
/**
* Execute the console command.
*
* @return void
* @return int
*/
public function fire()
public function handle()
{
if (! $this->confirmToProceed()) {
return;
return 1;
}
$this->prepareDatabase();
$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->run($this->getMigrationPaths(), [
'pretend' => $this->option('pretend'),
'step' => $this->option('step'),
]);
// Next, we will check to see if a path option has been defined. If it has
// we will use the path relative to the root of this installation folder
// so that migrations may be run for any path within the applications.
$migrations = $this->migrator->setOutput($this->output)
->run($this->getMigrationPaths(), [
'pretend' => $this->option('pretend'),
'step' => $this->option('step'),
]);
// Once the migrator has run we will grab the note output and send it out to
// the console screen, since the migrator itself functions without having
// any instances of the OutputInterface contract passed into the class.
foreach ($this->migrator->getNotes() as $note) {
$this->output->writeln($note);
}
// Finally, if the "seed" option has been given, we will re-run the database
// seed task to re-populate the database, which is convenient when adding
// a migration and a seed at the same time, as it is only this command.
if ($this->option('seed') && ! $this->option('pretend')) {
$this->call('db:seed', [
'--class' => $this->option('seeder') ?: 'Database\\Seeders\\DatabaseSeeder',
'--force' => true,
]);
}
});
// Finally, if the "seed" option has been given, we will re-run the database
// seed task to re-populate the database, which is convenient when adding
// a migration and a seed at the same time, as it is only this command.
if ($this->option('seed')) {
$this->call('db:seed', ['--force' => true]);
}
return 0;
}
/**
@@ -91,12 +111,171 @@ class MigrateCommand extends BaseCommand
*/
protected function prepareDatabase()
{
$this->migrator->setConnection($this->option('database'));
if (! $this->repositoryExists()) {
$this->components->info('Preparing database.');
if (! $this->migrator->repositoryExists()) {
$this->call(
'migrate:install', ['--database' => $this->option('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 {
if ($e->getPrevious() instanceof SQLiteDatabaseDoesNotExistException) {
return $this->createMissingSqliteDatbase($e->getPrevious()->path);
}
$connection = $this->migrator->resolveConnection($this->option('database'));
if (
$e->getPrevious() instanceof PDOException &&
$e->getPrevious()->getCode() === 1049 &&
$connection->getDriverName() === 'mysql') {
return $this->createMissingMysqlDatabase($connection);
}
return false;
} catch (Throwable) {
return false;
}
});
}
/**
* Create a missing SQLite database.
*
* @param string $path
* @return bool
*/
protected function createMissingSqliteDatbase($path)
{
if ($this->option('force')) {
return touch($path);
}
if ($this->option('no-interaction')) {
return false;
}
$this->components->warn('The SQLite database does not exist: '.$path);
if (! $this->components->confirm('Would you like to create it?')) {
return false;
}
return touch($path);
}
/**
* Create a missing MySQL database.
*
* @return bool
*/
protected function createMissingMysqlDatabase($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 (! $this->components->confirm('Would you like to create it?')) {
return false;
}
}
try {
$this->laravel['config']->set("database.connections.{$connection->getName()}.database", null);
$this->laravel['db']->purge();
$freshConnection = $this->migrator->resolveConnection($this->option('database'));
return tap($freshConnection->unprepared("CREATE DATABASE IF NOT EXISTS `{$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');
}
}

View File

@@ -2,8 +2,9 @@
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Support\Composer;
use Illuminate\Database\Migrations\MigrationCreator;
use Illuminate\Support\Composer;
use Illuminate\Support\Str;
class MigrateMakeCommand extends BaseCommand
{
@@ -12,10 +13,12 @@ class MigrateMakeCommand extends BaseCommand
*
* @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.}';
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}';
/**
* The console command description.
@@ -58,12 +61,12 @@ class MigrateMakeCommand extends BaseCommand
*
* @return void
*/
public function fire()
public function handle()
{
// 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 = trim($this->input->getArgument('name'));
$name = Str::snake(trim($this->input->getArgument('name')));
$table = $this->input->getOption('table');
@@ -78,6 +81,13 @@ class MigrateMakeCommand extends BaseCommand
$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.
@@ -91,16 +101,20 @@ class MigrateMakeCommand extends BaseCommand
*
* @param string $name
* @param string $table
* @param bool $create
* @param bool $create
* @return string
*/
protected function writeMigration($name, $table, $create)
{
$file = pathinfo($this->creator->create(
$file = $this->creator->create(
$name, $this->getMigrationPath(), $table, $create
), PATHINFO_FILENAME);
);
$this->line("<info>Created Migration:</info> {$file}");
if (! $this->option('fullpath')) {
$file = pathinfo($file, PATHINFO_FILENAME);
}
$this->components->info(sprintf('Migration [%s] created successfully.', $file));
}
/**
@@ -111,7 +125,9 @@ class MigrateMakeCommand extends BaseCommand
protected function getMigrationPath()
{
if (! is_null($targetPath = $this->input->getOption('path'))) {
return $this->laravel->basePath().'/'.$targetPath;
return ! $this->usingRealPath()
? $this->laravel->basePath().'/'.$targetPath
: $targetPath;
}
return parent::getMigrationPath();

View File

@@ -4,6 +4,8 @@ namespace Illuminate\Database\Console\Migrations;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Events\DatabaseRefreshed;
use Symfony\Component\Console\Input\InputOption;
class RefreshCommand extends Command
@@ -27,12 +29,12 @@ class RefreshCommand extends Command
/**
* Execute the console command.
*
* @return void
* @return int
*/
public function fire()
public function handle()
{
if (! $this->confirmToProceed()) {
return;
return 1;
}
// Next we'll gather some of the options so that we can have the right options
@@ -42,31 +44,38 @@ class RefreshCommand extends Command
$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, $force);
$this->runRollback($database, $path, $step);
} else {
$this->runReset($database, $path, $force);
$this->runReset($database, $path);
}
// 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', [
$this->call('migrate', array_filter([
'--database' => $database,
'--path' => $path,
'--force' => $force,
]);
'--realpath' => $this->input->getOption('realpath'),
'--force' => true,
]));
if ($this->laravel->bound(Dispatcher::class)) {
$this->laravel[Dispatcher::class]->dispatch(
new DatabaseRefreshed
);
}
if ($this->needsSeeding()) {
$this->runSeeder($database);
}
return 0;
}
/**
@@ -74,18 +83,18 @@ class RefreshCommand extends Command
*
* @param string $database
* @param string $path
* @param bool $step
* @param bool $force
* @param int $step
* @return void
*/
protected function runRollback($database, $path, $step, $force)
protected function runRollback($database, $path, $step)
{
$this->call('migrate:rollback', [
$this->call('migrate:rollback', array_filter([
'--database' => $database,
'--path' => $path,
'--realpath' => $this->input->getOption('realpath'),
'--step' => $step,
'--force' => $force,
]);
'--force' => true,
]));
}
/**
@@ -93,16 +102,16 @@ class RefreshCommand extends Command
*
* @param string $database
* @param string $path
* @param bool $force
* @return void
*/
protected function runReset($database, $path, $force)
protected function runReset($database, $path)
{
$this->call('migrate:reset', [
$this->call('migrate:reset', array_filter([
'--database' => $database,
'--path' => $path,
'--force' => $force,
]);
'--realpath' => $this->input->getOption('realpath'),
'--force' => true,
]));
}
/**
@@ -123,11 +132,11 @@ class RefreshCommand extends Command
*/
protected function runSeeder($database)
{
$this->call('db:seed', [
$this->call('db:seed', array_filter([
'--database' => $database,
'--class' => $this->option('seeder') ?: 'DatabaseSeeder',
'--force' => $this->option('force'),
]);
'--class' => $this->option('seeder') ?: 'Database\\Seeders\\DatabaseSeeder',
'--force' => true,
]));
}
/**
@@ -138,17 +147,13 @@ class RefreshCommand extends Command
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, '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.'],
['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'],
];
}
}

View File

@@ -47,33 +47,26 @@ class ResetCommand extends BaseCommand
/**
* Execute the console command.
*
* @return void
* @return int
*/
public function fire()
public function handle()
{
if (! $this->confirmToProceed()) {
return;
return 1;
}
$this->migrator->setConnection($this->option('database'));
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.');
}
// 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);
}
$this->migrator->setOutput($this->output)->reset(
$this->getMigrationPaths(), $this->option('pretend')
);
});
}
/**
@@ -84,13 +77,15 @@ class ResetCommand extends BaseCommand
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use.'],
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'],
['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'],
['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) of migrations files to be executed.'],
['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to the migrations files to be executed'],
['pretend', null, InputOption::VALUE_NONE, 'Dump the SQL queries that would be run.'],
['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'],
];
}
}

View File

@@ -47,29 +47,24 @@ class RollbackCommand extends BaseCommand
/**
* Execute the console command.
*
* @return void
* @return int
*/
public function fire()
public function handle()
{
if (! $this->confirmToProceed()) {
return;
return 1;
}
$this->migrator->setConnection($this->option('database'));
$this->migrator->usingConnection($this->option('database'), function () {
$this->migrator->setOutput($this->output)->rollback(
$this->getMigrationPaths(), [
'pretend' => $this->option('pretend'),
'step' => (int) $this->option('step'),
]
);
});
$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);
}
return 0;
}
/**
@@ -80,15 +75,17 @@ class RollbackCommand extends BaseCommand
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use.'],
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'],
['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'],
['path', null, InputOption::VALUE_OPTIONAL, 'The path of migrations files to be executed.'],
['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to the migrations files to be executed'],
['pretend', null, InputOption::VALUE_NONE, 'Dump the SQL queries that would be run.'],
['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'],
['step', null, InputOption::VALUE_OPTIONAL, 'The number of migrations to be reverted.'],
['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'],
];
}
}

View File

@@ -2,8 +2,8 @@
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Support\Collection;
use Illuminate\Database\Migrations\Migrator;
use Illuminate\Support\Collection;
use Symfony\Component\Console\Input\InputOption;
class StatusCommand extends BaseCommand
@@ -32,8 +32,8 @@ class StatusCommand extends BaseCommand
/**
* Create a new migration rollback command instance.
*
* @param \Illuminate\Database\Migrations\Migrator $migrator
* @return \Illuminate\Database\Console\Migrations\StatusCommand
* @param \Illuminate\Database\Migrations\Migrator $migrator
* @return void
*/
public function __construct(Migrator $migrator)
{
@@ -45,40 +45,59 @@ class StatusCommand extends BaseCommand
/**
* Execute the console command.
*
* @return void
* @return int|null
*/
public function fire()
public function handle()
{
$this->migrator->setConnection($this->option('database'));
return $this->migrator->usingConnection($this->option('database'), function () {
if (! $this->migrator->repositoryExists()) {
$this->components->error('Migration table not found.');
if (! $this->migrator->repositoryExists()) {
return $this->error('No migrations found.');
}
return 1;
}
$ran = $this->migrator->getRepository()->getRan();
$ran = $this->migrator->getRepository()->getRan();
if (count($migrations = $this->getStatusFor($ran)) > 0) {
$this->table(['Ran?', 'Migration'], $migrations);
} else {
$this->error('No migrations found');
}
$batches = $this->migrator->getRepository()->getMigrationBatches();
if (count($migrations = $this->getStatusFor($ran, $batches)) > 0) {
$this->newLine();
$this->components->twoColumnDetail('<fg=gray>Migration name</>', '<fg=gray>Batch / Status</>');
$migrations->each(
fn ($migration) => $this->components->twoColumnDetail($migration[0], $migration[1])
);
$this->newLine();
} else {
$this->components->info('No migrations found');
}
});
}
/**
* Get the status for the given ran migrations.
* Get the status for the given run migrations.
*
* @param array $ran
* @param array $batches
* @return \Illuminate\Support\Collection
*/
protected function getStatusFor(array $ran)
protected function getStatusFor(array $ran, array $batches)
{
return Collection::make($this->getAllMigrationFiles())
->map(function ($migration) use ($ran) {
->map(function ($migration) use ($ran, $batches) {
$migrationName = $this->migrator->getMigrationName($migration);
return in_array($migrationName, $ran)
? ['<info>Y</info>', $migrationName]
: ['<fg=red>N</fg=red>', $migrationName];
$status = in_array($migrationName, $ran)
? '<fg=green;options=bold>Ran</>'
: '<fg=yellow;options=bold>Pending</>';
if (in_array($migrationName, $ran)) {
$status = '['.$batches[$migrationName].'] '.$status;
}
return [$migrationName, $status];
});
}
@@ -100,9 +119,11 @@ class StatusCommand extends BaseCommand
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use.'],
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'],
['path', null, InputOption::VALUE_OPTIONAL, 'The path of migrations files to use.'],
['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'],
];
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Illuminate\Database\Console\Migrations;
class TableGuesser
{
const CREATE_PATTERNS = [
'/^create_(\w+)_table$/',
'/^create_(\w+)$/',
];
const CHANGE_PATTERNS = [
'/_(to|from|in)_(\w+)_table$/',
'/_(to|from|in)_(\w+)$/',
];
/**
* Attempt to guess the table name and "creation" status of the given migration.
*
* @param string $migration
* @return array
*/
public static function guess($migration)
{
foreach (self::CREATE_PATTERNS as $pattern) {
if (preg_match($pattern, $migration, $matches)) {
return [$matches[1], $create = true];
}
}
foreach (self::CHANGE_PATTERNS as $pattern) {
if (preg_match($pattern, $migration, $matches)) {
return [$matches[2], $create = false];
}
}
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\Events\DatabaseBusy;
use Illuminate\Support\Composer;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'db:monitor')]
class MonitorCommand extends DatabaseInspectionCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'db:monitor
{--databases= : The database connections to monitor}
{--max= : The maximum number of connections that can be open before an event is dispatched}';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'db:monitor';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Monitor the number of connections on the specified database';
/**
* The connection resolver instance.
*
* @var \Illuminate\Database\ConnectionResolverInterface
*/
protected $connection;
/**
* The events dispatcher instance.
*
* @var \Illuminate\Contracts\Events\Dispatcher
*/
protected $events;
/**
* Create a new command instance.
*
* @param \Illuminate\Database\ConnectionResolverInterface $connection
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @param \Illuminate\Support\Composer $composer
*/
public function __construct(ConnectionResolverInterface $connection, Dispatcher $events, Composer $composer)
{
parent::__construct($composer);
$this->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 collect(explode(',', $databases))->map(function ($database) {
if (! $database) {
$database = $this->laravel['config']['database.default'];
}
$maxConnections = $this->option('max');
return [
'database' => $database,
'connections' => $connections = $this->getConnectionCount($this->connection->connection($database)),
'status' => $maxConnections && $connections >= $maxConnections ? '<fg=yellow;options=bold>ALERT</>' : '<fg=green;options=bold>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('<fg=gray>Database name</>', '<fg=gray>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'] === '<fg=green;options=bold>OK</>') {
return;
}
$this->events->dispatch(
new DatabaseBusy(
$database['database'],
$database['connections']
)
);
});
}
}

View File

@@ -0,0 +1,186 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Console\Command;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\MassPrunable;
use Illuminate\Database\Eloquent\Prunable;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Events\ModelsPruned;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Symfony\Component\Finder\Finder;
class PruneCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $signature = 'model:prune
{--model=* : Class names of the models to be pruned}
{--except=* : Class names of the models to be excluded from pruning}
{--chunk=1000 : The number of models to retrieve per chunk of models to be deleted}
{--pretend : Display the number of prunable records found instead of deleting them}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Prune models that are no longer needed';
/**
* Execute the console command.
*
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @return void
*/
public function handle(Dispatcher $events)
{
$models = $this->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");
});
$models->each(function ($model) {
$this->pruneModel($model);
});
$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 = $this->isPrunable($model)
? $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
*/
protected function models()
{
if (! empty($models = $this->option('model'))) {
return collect($models)->filter(function ($model) {
return class_exists($model);
})->values();
}
$except = $this->option('except');
if (! empty($models) && ! empty($except)) {
throw new InvalidArgumentException('The --models and --except options cannot be combined.');
}
return collect((new Finder)->in($this->getDefaultPath())->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), function ($models) use ($except) {
return $models->reject(function ($model) use ($except) {
return in_array($model, $except);
});
})->filter(function ($model) {
return $this->isPrunable($model);
})->filter(function ($model) {
return class_exists($model);
})->values();
}
/**
* Get the default path where models are located.
*
* @return string|string[]
*/
protected function getDefaultPath()
{
return app_path('Models');
}
/**
* Determine if the given model class is prunable.
*
* @param string $model
* @return bool
*/
protected function isPrunable($model)
{
$uses = class_uses_recursive($model);
return in_array(Prunable::class, $uses) || in_array(MassPrunable::class, $uses);
}
/**
* Display how many models will be pruned.
*
* @param string $model
* @return void
*/
protected function pretendToPrune($model)
{
$instance = new $model;
$count = $instance->prunable()
->when(in_array(SoftDeletes::class, class_uses_recursive(get_class($instance))), 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.");
}
}
}

View File

@@ -3,11 +3,14 @@
namespace Illuminate\Database\Console\Seeds;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Console\ConfirmableTrait;
use Symfony\Component\Console\Input\InputOption;
use Illuminate\Database\ConnectionResolverInterface as Resolver;
use Illuminate\Database\Eloquent\Model;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'db:seed')]
class SeedCommand extends Command
{
use ConfirmableTrait;
@@ -19,6 +22,17 @@ class SeedCommand extends Command
*/
protected $name = 'db:seed';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'db:seed';
/**
* The console command description.
*
@@ -49,19 +63,29 @@ class SeedCommand extends Command
/**
* Execute the console command.
*
* @return void
* @return int
*/
public function fire()
public function handle()
{
if (! $this->confirmToProceed()) {
return;
return 1;
}
$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;
}
/**
@@ -71,9 +95,20 @@ class SeedCommand extends Command
*/
protected function getSeeder()
{
$class = $this->laravel->make($this->input->getOption('class'));
$class = $this->input->getArgument('class') ?? $this->input->getOption('class');
return $class->setContainer($this->laravel)->setCommand($this);
if (! str_contains($class, '\\')) {
$class = 'Database\\Seeders\\'.$class;
}
if ($class === 'Database\\Seeders\\DatabaseSeeder' &&
! class_exists($class)) {
$class = 'DatabaseSeeder';
}
return $this->laravel->make($class)
->setContainer($this->laravel)
->setCommand($this);
}
/**
@@ -88,6 +123,18 @@ class SeedCommand extends Command
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.
*
@@ -96,11 +143,9 @@ class SeedCommand extends Command
protected function getOptions()
{
return [
['class', null, InputOption::VALUE_OPTIONAL, 'The class name of the root seeder', 'DatabaseSeeder'],
['class', null, InputOption::VALUE_OPTIONAL, 'The class name of the root seeder', 'Database\\Seeders\\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'],
];
}
}

View File

@@ -2,10 +2,11 @@
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
{
/**
@@ -15,6 +16,17 @@ class SeederMakeCommand extends GeneratorCommand
*/
protected $name = 'make:seeder';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'make:seeder';
/**
* The console command description.
*
@@ -29,37 +41,14 @@ class SeederMakeCommand extends GeneratorCommand
*/
protected $type = 'Seeder';
/**
* 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 __construct(Filesystem $files, Composer $composer)
{
parent::__construct($files);
$this->composer = $composer;
}
/**
* Execute the console command.
*
* @return void
*/
public function fire()
public function handle()
{
parent::fire();
$this->composer->dumpAutoloads();
parent::handle();
}
/**
@@ -69,7 +58,20 @@ class SeederMakeCommand extends GeneratorCommand
*/
protected function getStub()
{
return __DIR__.'/stubs/seeder.stub';
return $this->resolveStubPath('/stubs/seeder.stub');
}
/**
* Resolve the fully-qualified path to the stub.
*
* @param string $stub
* @return string
*/
protected function resolveStubPath($stub)
{
return is_file($customPath = $this->laravel->basePath(trim($stub, '/')))
? $customPath
: __DIR__.$stub;
}
/**
@@ -80,17 +82,22 @@ class SeederMakeCommand extends GeneratorCommand
*/
protected function getPath($name)
{
return $this->laravel->databasePath().'/seeds/'.$name.'.php';
$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';
}
/**
* Parse the class name and format according to the root namespace.
* Get the root namespace for the class.
*
* @param string $name
* @return string
*/
protected function qualifyClass($name)
protected function rootNamespace()
{
return $name;
return 'Database\Seeders\\';
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Illuminate\Database\Console\Seeds;
use Illuminate\Database\Eloquent\Model;
trait WithoutModelEvents
{
/**
* Prevent model events from being dispatched by the given callback.
*
* @param callable $callback
* @return callable
*/
public function withoutModelEvents(callable $callback)
{
return fn () => Model::withoutEvents($callback);
}
}

View File

@@ -1,8 +1,11 @@
<?php
namespace {{ namespace }};
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class DummyClass extends Seeder
class {{ class }} extends Seeder
{
/**
* Run the database seeds.

View File

@@ -0,0 +1,189 @@
<?php
namespace Illuminate\Database\Console;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\View;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Support\Arr;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'db:show')]
class ShowCommand extends DatabaseInspectionCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'db:show {--database= : The database connection}
{--json : Output the database information as JSON}
{--counts : Show the table row count <bg=red;options=bold> Note: This can be slow on large databases </>};
{--views : Show the database views <bg=red;options=bold> Note: This can be slow on large databases </>}';
/**
* 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)
{
if (! $this->ensureDependenciesExist()) {
return 1;
}
$connection = $connections->connection($database = $this->input->getOption('database'));
$schema = $connection->getDoctrineSchemaManager();
$this->registerTypeMappings($schema->getDatabasePlatform());
$data = [
'platform' => [
'config' => $this->getConfigFromDatabase($database),
'name' => $this->getPlatformName($schema->getDatabasePlatform(), $database),
'open_connections' => $this->getConnectionCount($connection),
],
'tables' => $this->tables($connection, $schema),
];
if ($this->option('views')) {
$data['views'] = $this->collectViews($connection, $schema);
}
$this->display($data);
return 0;
}
/**
* Get information regarding the tables within the database.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Doctrine\DBAL\Schema\AbstractSchemaManager $schema
* @return \Illuminate\Support\Collection
*/
protected function tables(ConnectionInterface $connection, AbstractSchemaManager $schema)
{
return collect($schema->listTables())->map(fn (Table $table, $index) => [
'table' => $table->getName(),
'size' => $this->getTableSize($connection, $table->getName()),
'rows' => $this->option('counts') ? $connection->table($table->getName())->count() : null,
'engine' => rescue(fn () => $table->getOption('engine'), null, false),
'comment' => $table->getComment(),
]);
}
/**
* Get information regarding the views within the database.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Doctrine\DBAL\Schema\AbstractSchemaManager $schema
* @return \Illuminate\Support\Collection
*/
protected function collectViews(ConnectionInterface $connection, AbstractSchemaManager $schema)
{
return collect($schema->listViews())
->reject(fn (View $view) => str($view->getName())
->startsWith(['pg_catalog', 'information_schema', 'spt_']))
->map(fn (View $view) => [
'view' => $view->getName(),
'rows' => $connection->table($view->getName())->count(),
]);
}
/**
* 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;
$this->newLine();
$this->components->twoColumnDetail('<fg=green;options=bold>'.$platform['name'].'</>');
$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_format($tableSizeSum / 1024 / 1024, 2).'MiB');
}
$this->newLine();
if ($tables->isNotEmpty()) {
$this->components->twoColumnDetail('<fg=green;options=bold>Table</>', 'Size (MiB)'.($this->option('counts') ? ' <fg=gray;options=bold>/</> <fg=yellow;options=bold>Rows</>' : ''));
$tables->each(function ($table) {
if ($tableSize = $table['size']) {
$tableSize = number_format($tableSize / 1024 / 1024, 2);
}
$this->components->twoColumnDetail(
$table['table'].($this->output->isVerbose() ? ' <fg=gray>'.$table['engine'].'</>' : null),
($tableSize ? $tableSize : '—').($this->option('counts') ? ' <fg=gray;options=bold>/</> <fg=yellow;options=bold>'.number_format($table['rows']).'</>' : '')
);
if ($this->output->isVerbose()) {
if ($table['comment']) {
$this->components->bulletList([
$table['comment'],
]);
}
}
});
$this->newLine();
}
if ($views && $views->isNotEmpty()) {
$this->components->twoColumnDetail('<fg=green;options=bold>View</>', '<fg=green;options=bold>Rows</>');
$views->each(fn ($view) => $this->components->twoColumnDetail($view['view'], number_format($view['rows'])));
$this->newLine();
}
}
}

View File

@@ -0,0 +1,246 @@
<?php
namespace Illuminate\Database\Console;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Table;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Support\Str;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'db:table')]
class TableCommand extends DatabaseInspectionCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'db:table
{table? : The name of the table}
{--database= : The database connection}
{--json : Output the table information as JSON}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Display information about the given database table';
/**
* Execute the console command.
*
* @return int
*/
public function handle(ConnectionResolverInterface $connections)
{
if (! $this->ensureDependenciesExist()) {
return 1;
}
$connection = $connections->connection($this->input->getOption('database'));
$schema = $connection->getDoctrineSchemaManager();
$this->registerTypeMappings($schema->getDatabasePlatform());
$table = $this->argument('table') ?: $this->components->choice(
'Which table would you like to inspect?',
collect($schema->listTables())->flatMap(fn (Table $table) => [$table->getName()])->toArray()
);
if (! $schema->tablesExist([$table])) {
return $this->components->warn("Table [{$table}] doesn't exist.");
}
$table = $schema->listTableDetails($table);
$columns = $this->columns($table);
$indexes = $this->indexes($table);
$foreignKeys = $this->foreignKeys($table);
$data = [
'table' => [
'name' => $table->getName(),
'columns' => $columns->count(),
'size' => $this->getTableSize($connection, $table->getName()),
],
'columns' => $columns,
'indexes' => $indexes,
'foreign_keys' => $foreignKeys,
];
$this->display($data);
return 0;
}
/**
* Get the information regarding the table's columns.
*
* @param \Doctrine\DBAL\Schema\Table $table
* @return \Illuminate\Support\Collection
*/
protected function columns(Table $table)
{
return collect($table->getColumns())->map(fn (Column $column) => [
'column' => $column->getName(),
'attributes' => $this->getAttributesForColumn($column),
'default' => $column->getDefault(),
'type' => $column->getType()->getName(),
]);
}
/**
* Get the attributes for a table column.
*
* @param \Doctrine\DBAL\Schema\Column $column
* @return \Illuminate\Support\Collection
*/
protected function getAttributesForColumn(Column $column)
{
return collect([
$column->getAutoincrement() ? 'autoincrement' : null,
'type' => $column->getType()->getName(),
$column->getUnsigned() ? 'unsigned' : null,
! $column->getNotNull() ? 'nullable' : null,
])->filter();
}
/**
* Get the information regarding the table's indexes.
*
* @param \Doctrine\DBAL\Schema\Table $table
* @return \Illuminate\Support\Collection
*/
protected function indexes(Table $table)
{
return collect($table->getIndexes())->map(fn (Index $index) => [
'name' => $index->getName(),
'columns' => collect($index->getColumns()),
'attributes' => $this->getAttributesForIndex($index),
]);
}
/**
* Get the attributes for a table index.
*
* @param \Doctrine\DBAL\Schema\Index $index
* @return \Illuminate\Support\Collection
*/
protected function getAttributesForIndex(Index $index)
{
return collect([
'compound' => count($index->getColumns()) > 1,
'unique' => $index->isUnique(),
'primary' => $index->isPrimary(),
])->filter()->keys()->map(fn ($attribute) => Str::lower($attribute));
}
/**
* Get the information regarding the table's foreign keys.
*
* @param \Doctrine\DBAL\Schema\Table $table
* @return \Illuminate\Support\Collection
*/
protected function foreignKeys(Table $table)
{
return collect($table->getForeignKeys())->map(fn (ForeignKeyConstraint $foreignKey) => [
'name' => $foreignKey->getName(),
'local_table' => $table->getName(),
'local_columns' => collect($foreignKey->getLocalColumns()),
'foreign_table' => $foreignKey->getForeignTableName(),
'foreign_columns' => collect($foreignKey->getForeignColumns()),
'on_update' => Str::lower(rescue(fn () => $foreignKey->getOption('onUpdate'), 'N/A')),
'on_delete' => Str::lower(rescue(fn () => $foreignKey->getOption('onDelete'), 'N/A')),
]);
}
/**
* 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('<fg=green;options=bold>'.$table['name'].'</>');
$this->components->twoColumnDetail('Columns', $table['columns']);
if ($size = $table['size']) {
$this->components->twoColumnDetail('Size', number_format($size / 1024 / 1024, 2).'MiB');
}
$this->newLine();
if ($columns->isNotEmpty()) {
$this->components->twoColumnDetail('<fg=green;options=bold>Column</>', 'Type');
$columns->each(function ($column) {
$this->components->twoColumnDetail(
$column['column'].' <fg=gray>'.$column['attributes']->implode(', ').'</>',
($column['default'] ? '<fg=gray>'.$column['default'].'</> ' : '').''.$column['type'].''
);
});
$this->newLine();
}
if ($indexes->isNotEmpty()) {
$this->components->twoColumnDetail('<fg=green;options=bold>Index</>');
$indexes->each(function ($index) {
$this->components->twoColumnDetail(
$index['name'].' <fg=gray>'.$index['columns']->implode(', ').'</>',
$index['attributes']->implode(', ')
);
});
$this->newLine();
}
if ($foreignKeys->isNotEmpty()) {
$this->components->twoColumnDetail('<fg=green;options=bold>Foreign Key</>', 'On Update / On Delete');
$foreignKeys->each(function ($foreignKey) {
$this->components->twoColumnDetail(
$foreignKey['name'].' <fg=gray;options=bold>'.$foreignKey['local_columns']->implode(', ').' references '.$foreignKey['foreign_columns']->implode(', ').' on '.$foreignKey['foreign_table'].'</>',
$foreignKey['on_update'].' / '.$foreignKey['on_delete'],
);
});
$this->newLine();
}
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'db:wipe')]
class WipeCommand extends Command
{
use ConfirmableTrait;
/**
* The console command name.
*
* @var string
*/
protected $name = 'db:wipe';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'db:wipe';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Drop all tables, views, and types';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if (! $this->confirmToProceed()) {
return 1;
}
$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.');
}
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();
}
/**
* 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'],
];
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Illuminate\Database\DBAL;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\PhpDateTimeMappingType;
use Doctrine\DBAL\Types\Type;
class TimestampType extends Type implements PhpDateTimeMappingType
{
/**
* {@inheritdoc}
*/
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return match ($name = $platform->getName()) {
'mysql',
'mysql2' => $this->getMySqlPlatformSQLDeclaration($fieldDeclaration),
'postgresql',
'pgsql',
'postgres' => $this->getPostgresPlatformSQLDeclaration($fieldDeclaration),
'mssql' => $this->getSqlServerPlatformSQLDeclaration($fieldDeclaration),
'sqlite',
'sqlite3' => $this->getSQLitePlatformSQLDeclaration($fieldDeclaration),
default => throw new DBALException('Invalid platform: '.$name),
};
}
/**
* Get the SQL declaration for MySQL.
*
* @param array $fieldDeclaration
* @return string
*/
protected function getMySqlPlatformSQLDeclaration(array $fieldDeclaration)
{
$columnType = 'TIMESTAMP';
if ($fieldDeclaration['precision']) {
$columnType = 'TIMESTAMP('.$fieldDeclaration['precision'].')';
}
$notNull = $fieldDeclaration['notnull'] ?? false;
if (! $notNull) {
return $columnType.' NULL';
}
return $columnType;
}
/**
* Get the SQL declaration for PostgreSQL.
*
* @param array $fieldDeclaration
* @return string
*/
protected function getPostgresPlatformSQLDeclaration(array $fieldDeclaration)
{
return 'TIMESTAMP('.(int) $fieldDeclaration['precision'].')';
}
/**
* Get the SQL declaration for SQL Server.
*
* @param array $fieldDeclaration
* @return string
*/
protected function getSqlServerPlatformSQLDeclaration(array $fieldDeclaration)
{
return $fieldDeclaration['precision'] ?? false
? 'DATETIME2('.$fieldDeclaration['precision'].')'
: 'DATETIME';
}
/**
* Get the SQL declaration for SQLite.
*
* @param array $fieldDeclaration
* @return string
*/
protected function getSQLitePlatformSQLDeclaration(array $fieldDeclaration)
{
return 'DATETIME';
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'timestamp';
}
}

View File

@@ -2,18 +2,29 @@
namespace Illuminate\Database;
use PDO;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Doctrine\DBAL\Types\Type;
use Illuminate\Database\Connectors\ConnectionFactory;
use Illuminate\Support\Arr;
use Illuminate\Support\ConfigurationUrlParser;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use InvalidArgumentException;
use PDO;
use RuntimeException;
/**
* @mixin \Illuminate\Database\Connection
*/
class DatabaseManager implements ConnectionResolverInterface
{
use Macroable {
__call as macroCall;
}
/**
* The application instance.
*
* @var \Illuminate\Foundation\Application
* @var \Illuminate\Contracts\Foundation\Application
*/
protected $app;
@@ -27,21 +38,35 @@ class DatabaseManager implements ConnectionResolverInterface
/**
* The active connection instances.
*
* @var array
* @var array<string, \Illuminate\Database\Connection>
*/
protected $connections = [];
/**
* The custom connection resolvers.
*
* @var array
* @var array<string, callable>
*/
protected $extensions = [];
/**
* The callback to be executed to reconnect to a database.
*
* @var callable
*/
protected $reconnector;
/**
* The custom Doctrine column types.
*
* @var array<string, array>
*/
protected $doctrineTypes = [];
/**
* Create a new database manager instance.
*
* @param \Illuminate\Foundation\Application $app
* @param \Illuminate\Contracts\Foundation\Application $app
* @param \Illuminate\Database\Connectors\ConnectionFactory $factory
* @return void
*/
@@ -49,17 +74,21 @@ class DatabaseManager implements ConnectionResolverInterface
{
$this->app = $app;
$this->factory = $factory;
$this->reconnector = function ($connection) {
$this->reconnect($connection->getNameWithReadWriteType());
};
}
/**
* Get a database connection instance.
*
* @param string $name
* @param string|null $name
* @return \Illuminate\Database\Connection
*/
public function connection($name = null)
{
list($database, $type) = $this->parseConnectionName($name);
[$database, $type] = $this->parseConnectionName($name);
$name = $name ?: $database;
@@ -68,7 +97,7 @@ class DatabaseManager implements ConnectionResolverInterface
// set the "fetch mode" for PDO which determines the query return types.
if (! isset($this->connections[$name])) {
$this->connections[$name] = $this->configure(
$connection = $this->makeConnection($database), $type
$this->makeConnection($database), $type
);
}
@@ -134,10 +163,11 @@ class DatabaseManager implements ConnectionResolverInterface
$connections = $this->app['config']['database.connections'];
if (is_null($config = Arr::get($connections, $name))) {
throw new InvalidArgumentException("Database [$name] not configured.");
throw new InvalidArgumentException("Database connection [{$name}] not configured.");
}
return $config;
return (new ConfigurationUrlParser)
->parseConfiguration($config);
}
/**
@@ -149,7 +179,7 @@ class DatabaseManager implements ConnectionResolverInterface
*/
protected function configure(Connection $connection, $type)
{
$connection = $this->setPdoForType($connection, $type);
$connection = $this->setPdoForType($connection, $type)->setReadWriteType($type);
// First we'll set the fetch mode and a few other dependencies of the database
// connection. This method basically just configures and prepares it to get
@@ -158,12 +188,16 @@ class DatabaseManager implements ConnectionResolverInterface
$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(function ($connection) {
$this->reconnect($connection->getName());
});
$connection->setReconnector($this->reconnector);
$this->registerConfiguredDoctrineTypes($connection);
return $connection;
}
@@ -172,24 +206,67 @@ class DatabaseManager implements ConnectionResolverInterface
* Prepare the read / write mode for database connection instance.
*
* @param \Illuminate\Database\Connection $connection
* @param string $type
* @param string|null $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());
}
return $connection;
}
/**
* Register custom Doctrine types with the connection.
*
* @param \Illuminate\Database\Connection $connection
* @return void
*/
protected function registerConfiguredDoctrineTypes(Connection $connection): void
{
foreach ($this->app['config']->get('database.dbal.types', []) as $name => $class) {
$this->registerDoctrineType($class, $name, $name);
}
foreach ($this->doctrineTypes as $name => [$type, $class]) {
$connection->registerDoctrineType($class, $name, $type);
}
}
/**
* Register a custom Doctrine type.
*
* @param string $class
* @param string $name
* @param string $type
* @return void
*
* @throws \Doctrine\DBAL\DBALException
* @throws \RuntimeException
*/
public function registerDoctrineType(string $class, string $name, string $type): void
{
if (! class_exists('Doctrine\DBAL\Connection')) {
throw new RuntimeException(
'Registering a custom Doctrine type requires Doctrine DBAL (doctrine/dbal).'
);
}
if (! Type::hasType($name)) {
Type::addType($name, $class);
}
$this->doctrineTypes[$name] = [$type, $class];
}
/**
* Disconnect from the given database and remove from local cache.
*
* @param string $name
* @param string|null $name
* @return void
*/
public function purge($name = null)
@@ -204,7 +281,7 @@ class DatabaseManager implements ConnectionResolverInterface
/**
* Disconnect from the given database.
*
* @param string $name
* @param string|null $name
* @return void
*/
public function disconnect($name = null)
@@ -217,7 +294,7 @@ class DatabaseManager implements ConnectionResolverInterface
/**
* Reconnect to the given database.
*
* @param string $name
* @param string|null $name
* @return \Illuminate\Database\Connection
*/
public function reconnect($name = null)
@@ -231,6 +308,24 @@ class DatabaseManager implements ConnectionResolverInterface
return $this->refreshPdoConnections($name);
}
/**
* Set the default database connection for the callback execution.
*
* @param string $name
* @param callable $callback
* @return mixed
*/
public function usingConnection($name, callable $callback)
{
$previousName = $this->getDefaultConnection();
$this->setDefaultConnection($name);
return tap($callback(), function () use ($previousName) {
$this->setDefaultConnection($previousName);
});
}
/**
* Refresh the PDO connections on a given connection.
*
@@ -239,11 +334,15 @@ class DatabaseManager implements ConnectionResolverInterface
*/
protected function refreshPdoConnections($name)
{
$fresh = $this->makeConnection($name);
[$database, $type] = $this->parseConnectionName($name);
$fresh = $this->configure(
$this->makeConnection($database), $type
);
return $this->connections[$name]
->setPdo($fresh->getPdo())
->setReadPdo($fresh->getReadPdo());
->setPdo($fresh->getRawPdo())
->setReadPdo($fresh->getRawReadPdo());
}
/**
@@ -270,7 +369,7 @@ class DatabaseManager implements ConnectionResolverInterface
/**
* Get all of the support drivers.
*
* @return array
* @return string[]
*/
public function supportedDrivers()
{
@@ -280,7 +379,7 @@ class DatabaseManager implements ConnectionResolverInterface
/**
* Get all of the drivers that are actually available.
*
* @return array
* @return string[]
*/
public function availableDrivers()
{
@@ -293,7 +392,7 @@ class DatabaseManager implements ConnectionResolverInterface
/**
* Register an extension connection resolver.
*
* @param string $name
* @param string $name
* @param callable $resolver
* @return void
*/
@@ -302,25 +401,64 @@ class DatabaseManager implements ConnectionResolverInterface
$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<string, \Illuminate\Database\Connection>
*/
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);
}
}

View File

@@ -4,15 +4,21 @@ namespace Illuminate\Database;
use Faker\Factory as FakerFactory;
use Faker\Generator as FakerGenerator;
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\Database\Eloquent\Factory as EloquentFactory;
use Illuminate\Support\ServiceProvider;
class DatabaseServiceProvider extends ServiceProvider
{
/**
* The array of resolved Faker instances.
*
* @var array
*/
protected static $fakers = [];
/**
* Bootstrap the application events.
*
@@ -35,9 +41,7 @@ class DatabaseServiceProvider extends ServiceProvider
Model::clearBootedModels();
$this->registerConnectionServices();
$this->registerEloquentFactory();
$this->registerQueueableEntityResolver();
}
@@ -65,6 +69,14 @@ class DatabaseServiceProvider extends ServiceProvider
$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 ($app) {
return new DatabaseTransactionsManager;
});
}
/**
@@ -74,14 +86,16 @@ class DatabaseServiceProvider extends ServiceProvider
*/
protected function registerEloquentFactory()
{
$this->app->singleton(FakerGenerator::class, function ($app) {
return FakerFactory::create($app['config']->get('app.faker_locale', 'en_US'));
});
$this->app->singleton(FakerGenerator::class, function ($app, $parameters) {
$locale = $parameters['locale'] ?? $app['config']->get('app.faker_locale', 'en_US');
$this->app->singleton(EloquentFactory::class, function ($app) {
return EloquentFactory::construct(
$app->make(FakerGenerator::class), $this->app->databasePath('factories')
);
if (! isset(static::$fakers[$locale])) {
static::$fakers[$locale] = FakerFactory::create($locale);
}
static::$fakers[$locale]->unique(true);
return static::$fakers[$locale];
});
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Illuminate\Database;
class DatabaseTransactionRecord
{
/**
* The name of the database connection.
*
* @var string
*/
public $connection;
/**
* The transaction level.
*
* @var int
*/
public $level;
/**
* The callbacks that should be executed after committing.
*
* @var array
*/
protected $callbacks = [];
/**
* Create a new database transaction record instance.
*
* @param string $connection
* @param int $level
* @return void
*/
public function __construct($connection, $level)
{
$this->connection = $connection;
$this->level = $level;
}
/**
* Register a callback to be executed after committing.
*
* @param callable $callback
* @return void
*/
public function addCallback($callback)
{
$this->callbacks[] = $callback;
}
/**
* Execute all of the callbacks.
*
* @return void
*/
public function executeCallbacks()
{
foreach ($this->callbacks as $callback) {
$callback();
}
}
/**
* Get all of the callbacks.
*
* @return array
*/
public function getCallbacks()
{
return $this->callbacks;
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace Illuminate\Database;
class DatabaseTransactionsManager
{
/**
* All of the recorded transactions.
*
* @var \Illuminate\Support\Collection
*/
protected $transactions;
/**
* The database transaction that should be ignored by callbacks.
*
* @var \Illuminate\Database\DatabaseTransactionRecord
*/
protected $callbacksShouldIgnore;
/**
* Create a new database transactions manager instance.
*
* @return void
*/
public function __construct()
{
$this->transactions = collect();
}
/**
* Start a new database transaction.
*
* @param string $connection
* @param int $level
* @return void
*/
public function begin($connection, $level)
{
$this->transactions->push(
new DatabaseTransactionRecord($connection, $level)
);
}
/**
* Rollback the active database transaction.
*
* @param string $connection
* @param int $level
* @return void
*/
public function rollback($connection, $level)
{
$this->transactions = $this->transactions->reject(
fn ($transaction) => $transaction->connection == $connection && $transaction->level > $level
)->values();
if ($this->transactions->isEmpty()) {
$this->callbacksShouldIgnore = null;
}
}
/**
* Commit the active database transaction.
*
* @param string $connection
* @return void
*/
public function commit($connection)
{
[$forThisConnection, $forOtherConnections] = $this->transactions->partition(
fn ($transaction) => $transaction->connection == $connection
);
$this->transactions = $forOtherConnections->values();
$forThisConnection->map->executeCallbacks();
if ($this->transactions->isEmpty()) {
$this->callbacksShouldIgnore = null;
}
}
/**
* Register a transaction callback.
*
* @param callable $callback
* @return void
*/
public function addCallback($callback)
{
if ($current = $this->callbackApplicableTransactions()->last()) {
return $current->addCallback($callback);
}
$callback();
}
/**
* Specify that callbacks should ignore the given transaction when determining if they should be executed.
*
* @param \Illuminate\Database\DatabaseTransactionRecord $transaction
* @return $this
*/
public function callbacksShouldIgnore(DatabaseTransactionRecord $transaction)
{
$this->callbacksShouldIgnore = $transaction;
return $this;
}
/**
* Get the transactions that are applicable to callbacks.
*
* @return \Illuminate\Support\Collection
*/
public function callbackApplicableTransactions()
{
return $this->transactions->reject(function ($transaction) {
return $transaction === $this->callbacksShouldIgnore;
})->values();
}
/**
* Get all the transactions.
*
* @return \Illuminate\Support\Collection
*/
public function getTransactions()
{
return $this->transactions;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Illuminate\Database;
use PDOException;
class DeadlockException extends PDOException
{
//
}

View File

@@ -2,19 +2,24 @@
namespace Illuminate\Database;
use Exception;
use Illuminate\Support\Str;
use PDOException;
use Throwable;
trait DetectsDeadlocks
trait DetectsConcurrencyErrors
{
/**
* Determine if the given exception was caused by a deadlock.
* Determine if the given exception was caused by a concurrency error such as a deadlock or serialization failure.
*
* @param \Exception $e
* @param \Throwable $e
* @return bool
*/
protected function causedByDeadlock(Exception $e)
protected function causedByConcurrencyError(Throwable $e)
{
if ($e instanceof PDOException && ($e->getCode() === 40001 || $e->getCode() === '40001')) {
return true;
}
$message = $e->getMessage();
return Str::contains($message, [
@@ -26,6 +31,7 @@ trait DetectsDeadlocks
'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',
]);
}
}

View File

@@ -2,18 +2,18 @@
namespace Illuminate\Database;
use Exception;
use Illuminate\Support\Str;
use Throwable;
trait DetectsLostConnections
{
/**
* Determine if the given exception was caused by a lost connection.
*
* @param \Exception $e
* @param \Throwable $e
* @return bool
*/
protected function causedByLostConnection(Exception $e)
protected function causedByLostConnection(Throwable $e)
{
$message = $e->getMessage();
@@ -29,6 +29,38 @@ trait DetectsLostConnections
'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',
'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',
'SSL: Broken pipe',
'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.',
]);
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace Illuminate\Database\Eloquent;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;
class BroadcastableModelEventOccurred implements ShouldBroadcast
{
use InteractsWithSockets, SerializesModels;
/**
* The model instance corresponding to the event.
*
* @var \Illuminate\Database\Eloquent\Model
*/
public $model;
/**
* The event name (created, updated, etc.).
*
* @var string
*/
protected $event;
/**
* The channels that the event should be broadcast on.
*
* @var array
*/
protected $channels = [];
/**
* The queue connection that should be used to queue the broadcast job.
*
* @var string
*/
public $connection;
/**
* The queue that should be used to queue the broadcast job.
*
* @var string
*/
public $queue;
/**
* Indicates whether the job should be dispatched after all database transactions have committed.
*
* @var bool|null
*/
public $afterCommit;
/**
* Create a new event instance.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $event
* @return void
*/
public function __construct($model, $event)
{
$this->model = $model;
$this->event = $event;
}
/**
* The channels the event should broadcast on.
*
* @return array
*/
public function broadcastOn()
{
$channels = empty($this->channels)
? ($this->model->broadcastOn($this->event) ?: [])
: $this->channels;
return collect($channels)->map(function ($channel) {
return $channel instanceof Model ? new PrivateChannel($channel) : $channel;
})->all();
}
/**
* The name the event should broadcast as.
*
* @return string
*/
public function broadcastAs()
{
$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;
}
}

View File

@@ -0,0 +1,197 @@
<?php
namespace Illuminate\Database\Eloquent;
use Illuminate\Support\Arr;
trait BroadcastsEvents
{
/**
* Boot the event broadcasting trait.
*
* @return void
*/
public static function bootBroadcastsEvents()
{
static::created(function ($model) {
$model->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($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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use ArrayObject as BaseArrayObject;
use Illuminate\Contracts\Support\Arrayable;
use JsonSerializable;
class ArrayObject extends BaseArrayObject implements Arrayable, JsonSerializable
{
/**
* Get a collection containing the underlying array.
*
* @return \Illuminate\Support\Collection
*/
public function collect()
{
return collect($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();
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class AsArrayObject implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @param array $arguments
* @return object|string
*/
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], true);
return is_array($data) ? new ArrayObject($data) : 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();
}
};
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Collection;
class AsCollection implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @param array $arguments
* @return object|string
*/
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], true);
return is_array($data) ? new Collection($data) : null;
}
public function set($model, $key, $value, $attributes)
{
return [$key => json_encode($value)];
}
};
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Facades\Crypt;
class AsEncryptedArrayObject implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @param array $arguments
* @return object|string
*/
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]), true));
}
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;
}
};
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Crypt;
class AsEncryptedCollection implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @param array $arguments
* @return object|string
*/
public static function castUsing(array $arguments)
{
return new class implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
if (isset($attributes[$key])) {
return new Collection(json_decode(Crypt::decryptString($attributes[$key]), true));
}
return null;
}
public function set($model, $key, $value, $attributes)
{
if (! is_null($value)) {
return [$key => Crypt::encryptString(json_encode($value))];
}
return null;
}
};
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Str;
class AsStringable implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @param array $arguments
* @return object|string
*/
public static function castUsing(array $arguments)
{
return new class implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return isset($value) ? Str::of($value) : null;
}
public function set($model, $key, $value, $attributes)
{
return isset($value) ? (string) $value : null;
}
};
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
class Attribute
{
/**
* The attribute accessor.
*
* @var callable
*/
public $get;
/**
* The attribute mutator.
*
* @var callable
*/
public $set;
/**
* Indicates if caching is enabled for this attribute.
*
* @var bool
*/
public $withCaching = false;
/**
* Indicates if caching of objects is enabled for this attribute.
*
* @var bool
*/
public $withObjectCaching = true;
/**
* Create a new attribute accessor / mutator.
*
* @param callable|null $get
* @param callable|null $set
* @return void
*/
public function __construct(callable $get = null, callable $set = null)
{
$this->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;
}
}

View File

@@ -2,19 +2,29 @@
namespace Illuminate\Database\Eloquent;
use LogicException;
use Illuminate\Support\Arr;
use Illuminate\Contracts\Queue\QueueableCollection;
use Illuminate\Contracts\Queue\QueueableEntity;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection as BaseCollection;
use LogicException;
/**
* @template TKey of array-key
* @template TModel of \Illuminate\Database\Eloquent\Model
*
* @extends \Illuminate\Support\Collection<TKey, TModel>
*/
class Collection extends BaseCollection implements QueueableCollection
{
/**
* Find a model in the collection by key.
*
* @template TFindDefault
*
* @param mixed $key
* @param mixed $default
* @return \Illuminate\Database\Eloquent\Model|static
* @param TFindDefault $default
* @return static<TKey|TModel>|TModel|TFindDefault
*/
public function find($key, $default = null)
{
@@ -22,6 +32,10 @@ class Collection extends BaseCollection implements QueueableCollection
$key = $key->getKey();
}
if ($key instanceof Arrayable) {
$key = $key->toArray();
}
if (is_array($key)) {
if ($this->isEmpty()) {
return new static;
@@ -38,17 +52,17 @@ class Collection extends BaseCollection implements QueueableCollection
/**
* Load a set of relationships onto the collection.
*
* @param mixed $relations
* @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string>|string $relations
* @return $this
*/
public function load($relations)
{
if (count($this->items) > 0) {
if ($this->isNotEmpty()) {
if (is_string($relations)) {
$relations = func_get_args();
}
$query = $this->first()->newQuery()->with($relations);
$query = $this->first()->newQueryWithoutRelationships()->with($relations);
$this->items = $query->eagerLoadRelations($this->items);
}
@@ -57,14 +71,223 @@ class Collection extends BaseCollection implements QueueableCollection
}
/**
* Add an item to the collection.
* Load a set of aggregations over relationship's column onto the collection.
*
* @param mixed $item
* @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string>|string $relations
* @param string $column
* @param string|null $function
* @return $this
*/
public function add($item)
public function loadAggregate($relations, $column, $function = null)
{
$this->items[] = $item;
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<array-key, (callable(\Illuminate\Database\Eloquent\Builder): 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<array-key, (callable(\Illuminate\Database\Eloquent\Builder): 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<array-key, (callable(\Illuminate\Database\Eloquent\Builder): 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<array-key, (callable(\Illuminate\Database\Eloquent\Builder): 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<array-key, (callable(\Illuminate\Database\Eloquent\Builder): 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<array-key, (callable(\Illuminate\Database\Eloquent\Builder): 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<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string>|string $relations
* @return $this
*/
public function loadMissing($relations)
{
if (is_string($relations)) {
$relations = func_get_args();
}
foreach ($relations as $key => $value) {
if (is_numeric($key)) {
$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][end($segments)] = $value;
}
$this->loadMissingRelation($this, $path);
}
return $this;
}
/**
* 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(function ($model) use ($name) {
return ! is_null($model) && ! $model->relationLoaded($name);
})->load($relation);
if (empty($path)) {
return;
}
$models = $models->pluck($name)->whereNotNull();
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<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string> $relations
* @return $this
*/
public function loadMorph($relation, $relations)
{
$this->pluck($relation)
->filter()
->groupBy(function ($model) {
return get_class($model);
})
->each(function ($models, $className) use ($relations) {
static::make($models)->load($relations[$className] ?? []);
});
return $this;
}
/**
* Load a set of relationship counts onto the mixed relationship collection.
*
* @param string $relation
* @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string> $relations
* @return $this
*/
public function loadMorphCount($relation, $relations)
{
$this->pluck($relation)
->filter()
->groupBy(function ($model) {
return get_class($model);
})
->each(function ($models, $className) use ($relations) {
static::make($models)->loadCount($relations[$className] ?? []);
});
return $this;
}
@@ -72,7 +295,7 @@ class Collection extends BaseCollection implements QueueableCollection
/**
* Determine if a key exists in the collection.
*
* @param mixed $key
* @param (callable(TModel, TKey): bool)|TModel|string|int $key
* @param mixed $operator
* @param mixed $value
* @return bool
@@ -97,7 +320,7 @@ class Collection extends BaseCollection implements QueueableCollection
/**
* Get the array of primary keys.
*
* @return array
* @return array<int, array-key>
*/
public function modelKeys()
{
@@ -109,7 +332,7 @@ class Collection extends BaseCollection implements QueueableCollection
/**
* Merge the collection with the given items.
*
* @param \ArrayAccess|array $items
* @param iterable<array-key, TModel> $items
* @return static
*/
public function merge($items)
@@ -126,8 +349,10 @@ class Collection extends BaseCollection implements QueueableCollection
/**
* Run a map over each of the items.
*
* @param callable $callback
* @return \Illuminate\Support\Collection|static
* @template TMapValue
*
* @param callable(TModel, TKey): TMapValue $callback
* @return \Illuminate\Support\Collection<TKey, TMapValue>|static<TKey, TMapValue>
*/
public function map(callable $callback)
{
@@ -138,10 +363,30 @@ class Collection extends BaseCollection implements QueueableCollection
}) ? $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<TMapWithKeysKey, TMapWithKeysValue> $callback
* @return \Illuminate\Support\Collection<TMapWithKeysKey, TMapWithKeysValue>|static<TMapWithKeysKey, TMapWithKeysValue>
*/
public function mapWithKeys(callable $callback)
{
$result = parent::mapWithKeys($callback);
return $result->contains(function ($item) {
return ! $item instanceof Model;
}) ? $result->toBase() : $result;
}
/**
* Reload a fresh model instance from the database for all the entities.
*
* @param array|string $with
* @param array<array-key, string>|string $with
* @return static
*/
public function fresh($with = [])
@@ -158,15 +403,18 @@ class Collection extends BaseCollection implements QueueableCollection
->get()
->getDictionary();
return $this->map(function ($model) use ($freshModels) {
return $model->exists ? $freshModels[$model->getKey()] : null;
return $this->filter(function ($model) use ($freshModels) {
return $model->exists && isset($freshModels[$model->getKey()]);
})
->map(function ($model) use ($freshModels) {
return $freshModels[$model->getKey()];
});
}
/**
* Diff the collection with the given items.
*
* @param \ArrayAccess|array $items
* @param iterable<array-key, TModel> $items
* @return static
*/
public function diff($items)
@@ -187,13 +435,17 @@ class Collection extends BaseCollection implements QueueableCollection
/**
* Intersect the collection with the given items.
*
* @param \ArrayAccess|array $items
* @param iterable<array-key, TModel> $items
* @return static
*/
public function intersect($items)
{
$intersect = new static;
if (empty($items)) {
return $intersect;
}
$dictionary = $this->getDictionary($items);
foreach ($this->items as $item) {
@@ -208,9 +460,9 @@ class Collection extends BaseCollection implements QueueableCollection
/**
* Return only unique items from the collection.
*
* @param string|callable|null $key
* @param (callable(TModel, TKey): mixed)|string|null $key
* @param bool $strict
* @return static|\Illuminate\Support\Collection
* @return static<int, TModel>
*/
public function unique($key = null, $strict = false)
{
@@ -224,8 +476,8 @@ class Collection extends BaseCollection implements QueueableCollection
/**
* Returns only the models from the collection with the specified keys.
*
* @param mixed $keys
* @return static
* @param array<array-key, mixed>|null $keys
* @return static<int, TModel>
*/
public function only($keys)
{
@@ -241,8 +493,8 @@ class Collection extends BaseCollection implements QueueableCollection
/**
* Returns all models in the collection except the models with specified keys.
*
* @param mixed $keys
* @return static
* @param array<array-key, mixed>|null $keys
* @return static<int, TModel>
*/
public function except($keys)
{
@@ -254,34 +506,41 @@ class Collection extends BaseCollection implements QueueableCollection
/**
* Make the given, typically visible, attributes hidden across the entire collection.
*
* @param array|string $attributes
* @param array<array-key, string>|string $attributes
* @return $this
*/
public function makeHidden($attributes)
{
return $this->each(function ($model) use ($attributes) {
$model->addHidden($attributes);
});
return $this->each->makeHidden($attributes);
}
/**
* Make the given, typically hidden, attributes visible across the entire collection.
*
* @param array|string $attributes
* @param array<array-key, string>|string $attributes
* @return $this
*/
public function makeVisible($attributes)
{
return $this->each(function ($model) use ($attributes) {
$model->makeVisible($attributes);
});
return $this->each->makeVisible($attributes);
}
/**
* Append an attribute across the entire collection.
*
* @param array<array-key, string>|string $attributes
* @return $this
*/
public function append($attributes)
{
return $this->each->append($attributes);
}
/**
* Get a dictionary keyed by primary keys.
*
* @param \ArrayAccess|array|null $items
* @return array
* @param iterable<array-key, TModel>|null $items
* @return array<array-key, TModel>
*/
public function getDictionary($items = null)
{
@@ -303,9 +562,9 @@ class Collection extends BaseCollection implements QueueableCollection
/**
* Get an array with the values of a given key.
*
* @param string $value
* @param string|array<array-key, string> $value
* @param string|null $key
* @return \Illuminate\Support\Collection
* @return \Illuminate\Support\Collection<int, mixed>
*/
public function pluck($value, $key = null)
{
@@ -315,7 +574,7 @@ class Collection extends BaseCollection implements QueueableCollection
/**
* Get the keys of the collection items.
*
* @return \Illuminate\Support\Collection
* @return \Illuminate\Support\Collection<int, TKey>
*/
public function keys()
{
@@ -325,18 +584,20 @@ class Collection extends BaseCollection implements QueueableCollection
/**
* Zip the collection together with one or more arrays.
*
* @param mixed ...$items
* @return \Illuminate\Support\Collection
* @template TZipValue
*
* @param \Illuminate\Contracts\Support\Arrayable<array-key, TZipValue>|iterable<array-key, TZipValue> ...$items
* @return \Illuminate\Support\Collection<int, \Illuminate\Support\Collection<int, TModel|TZipValue>>
*/
public function zip($items)
{
return call_user_func_array([$this->toBase(), 'zip'], func_get_args());
return $this->toBase()->zip(...func_get_args());
}
/**
* Collapse the collection of items into a single array.
*
* @return \Illuminate\Support\Collection
* @return \Illuminate\Support\Collection<int, mixed>
*/
public function collapse()
{
@@ -347,7 +608,7 @@ class Collection extends BaseCollection implements QueueableCollection
* Get a flattened array of the items in the collection.
*
* @param int $depth
* @return \Illuminate\Support\Collection
* @return \Illuminate\Support\Collection<int, mixed>
*/
public function flatten($depth = INF)
{
@@ -357,28 +618,57 @@ class Collection extends BaseCollection implements QueueableCollection
/**
* Flip the items in the collection.
*
* @return \Illuminate\Support\Collection
* @return \Illuminate\Support\Collection<TModel, TKey>
*/
public function flip()
{
return $this->toBase()->flip();
}
/**
* Pad collection to the specified length with a value.
*
* @template TPadValue
*
* @param int $size
* @param TPadValue $value
* @return \Illuminate\Support\Collection<int, TModel|TPadValue>
*/
public function pad($size, $value)
{
return $this->toBase()->pad($size, $value);
}
/**
* Get the comparison function to detect duplicates.
*
* @param bool $strict
* @return callable(TValue, TValue): bool
*/
protected function duplicateComparator($strict)
{
return function ($a, $b) {
return $a->is($b);
};
}
/**
* Get the type of the entities being queued.
*
* @return string|null
*
* @throws \LogicException
*/
public function getQueueableClass()
{
if ($this->count() === 0) {
if ($this->isEmpty()) {
return;
}
$class = get_class($this->first());
$class = $this->getQueueableModelClass($this->first());
$this->each(function ($model) use ($class) {
if (get_class($model) !== $class) {
if ($this->getQueueableModelClass($model) !== $class) {
throw new LogicException('Queueing collections with multiple model types is not supported.');
}
});
@@ -386,13 +676,104 @@ class Collection extends BaseCollection implements QueueableCollection
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<int, mixed>
*/
public function getQueueableIds()
{
return $this->modelKeys();
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<int, string>
*/
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->filter(function ($model) use ($class) {
return ! $model instanceof $class;
})->isNotEmpty()) {
throw new LogicException('Unable to create query for collection with mixed types.');
}
return $model->newModelQuery()->whereKey($this->modelKeys());
}
}

View File

@@ -2,21 +2,19 @@
namespace Illuminate\Database\Eloquent\Concerns;
use Illuminate\Support\Str;
trait GuardsAttributes
{
/**
* The attributes that are mass assignable.
*
* @var array
* @var array<string>
*/
protected $fillable = [];
/**
* The attributes that aren't mass assignable.
*
* @var array
* @var array<string>|bool
*/
protected $guarded = ['*'];
@@ -27,10 +25,17 @@ trait GuardsAttributes
*/
protected static $unguarded = false;
/**
* The actual columns that exist on the database and can be guarded.
*
* @var array<string>
*/
protected static $guardableColumns = [];
/**
* Get the fillable attributes for the model.
*
* @return array
* @return array<string>
*/
public function getFillable()
{
@@ -40,7 +45,7 @@ trait GuardsAttributes
/**
* Set the fillable attributes for the model.
*
* @param array $fillable
* @param array<string> $fillable
* @return $this
*/
public function fillable(array $fillable)
@@ -50,20 +55,35 @@ trait GuardsAttributes
return $this;
}
/**
* Merge new fillable attributes with existing fillable attributes on the model.
*
* @param array<string> $fillable
* @return $this
*/
public function mergeFillable(array $fillable)
{
$this->fillable = array_merge($this->fillable, $fillable);
return $this;
}
/**
* Get the guarded attributes for the model.
*
* @return array
* @return array<string>
*/
public function getGuarded()
{
return $this->guarded;
return $this->guarded === false
? []
: $this->guarded;
}
/**
* Set the guarded attributes for the model.
*
* @param array $guarded
* @param array<string> $guarded
* @return $this
*/
public function guard(array $guarded)
@@ -73,6 +93,19 @@ trait GuardsAttributes
return $this;
}
/**
* Merge new guarded attributes with existing guarded attributes on the model.
*
* @param array<string> $guarded
* @return $this
*/
public function mergeGuarded(array $guarded)
{
$this->guarded = array_merge($this->guarded, $guarded);
return $this;
}
/**
* Disable all mass assignable restrictions.
*
@@ -95,7 +128,7 @@ trait GuardsAttributes
}
/**
* Determine if current state is "unguarded".
* Determine if the current state is "unguarded".
*
* @return bool
*/
@@ -152,7 +185,8 @@ trait GuardsAttributes
}
return empty($this->getFillable()) &&
! Str::startsWith($key, '_');
! str_contains($key, '.') &&
! str_starts_with($key, '_');
}
/**
@@ -163,7 +197,35 @@ trait GuardsAttributes
*/
public function isGuarded($key)
{
return in_array($key, $this->getGuarded()) || $this->getGuarded() == ['*'];
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 (! 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)]);
}
/**
@@ -173,7 +235,7 @@ trait GuardsAttributes
*/
public function totallyGuarded()
{
return count($this->getFillable()) == 0 && $this->getGuarded() == ['*'];
return count($this->getFillable()) === 0 && $this->getGuarded() == ['*'];
}
/**

View File

@@ -3,6 +3,9 @@
namespace Illuminate\Database\Eloquent\Concerns;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Events\NullDispatcher;
use Illuminate\Support\Arr;
use InvalidArgumentException;
trait HasEvents
{
@@ -13,7 +16,7 @@ trait HasEvents
*
* @var array
*/
protected $events = [];
protected $dispatchesEvents = [];
/**
* User exposed observable events.
@@ -25,27 +28,65 @@ trait HasEvents
protected $observables = [];
/**
* Register an observer with the Model.
* Register observers with the model.
*
* @param object|string $class
* @param object|array|string $classes
* @return void
*
* @throws \RuntimeException
*/
public static function observe($class)
public static function observe($classes)
{
$instance = new static;
$className = is_string($class) ? $class : get_class($class);
foreach (Arr::wrap($classes) as $class) {
$instance->registerObserver($class);
}
}
/**
* Register a single observer with the model.
*
* @param object|string $class
* @return void
*
* @throws \RuntimeException
*/
protected function registerObserver($class)
{
$className = $this->resolveObserverClassName($class);
// When registering a model observer, we will spin through the possible events
// and determine if this observer has that method. If it does, we will hook
// it into the model's event system, making it convenient to watch these.
foreach ($instance->getObservableEvents() as $event) {
foreach ($this->getObservableEvents() as $event) {
if (method_exists($class, $event)) {
static::registerModelEvent($event, $className.'@'.$event);
}
}
}
/**
* Resolve the observer's class name from an object or string.
*
* @param object|string $class
* @return 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.
*
@@ -55,9 +96,9 @@ trait HasEvents
{
return array_merge(
[
'creating', 'created', 'updating', 'updated',
'deleting', 'deleted', 'saving', 'saved',
'restoring', 'restored',
'retrieved', 'creating', 'created', 'updating', 'updated',
'saving', 'saved', 'restoring', 'restored', 'replicating',
'deleting', 'deleted', 'forceDeleted',
],
$this->observables
);
@@ -106,7 +147,7 @@ trait HasEvents
* Register a model event with the dispatcher.
*
* @param string $event
* @param \Closure|string $callback
* @param \Illuminate\Events\QueuedClosure|\Closure|string|array $callback
* @return void
*/
protected static function registerModelEvent($event, $callback)
@@ -134,7 +175,7 @@ trait HasEvents
// 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' : 'fire';
$method = $halt ? 'until' : 'dispatch';
$result = $this->filterModelEventResults(
$this->fireCustomModelEvent($event, $method)
@@ -158,11 +199,11 @@ trait HasEvents
*/
protected function fireCustomModelEvent($event, $method)
{
if (! isset($this->events[$event])) {
if (! isset($this->dispatchesEvents[$event])) {
return;
}
$result = static::$dispatcher->$method(new $this->events[$event]($this));
$result = static::$dispatcher->$method(new $this->dispatchesEvents[$event]($this));
if (! is_null($result)) {
return $result;
@@ -186,10 +227,21 @@ trait HasEvents
return $result;
}
/**
* Register a retrieved model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|\Closure|string|array $callback
* @return void
*/
public static function retrieved($callback)
{
static::registerModelEvent('retrieved', $callback);
}
/**
* Register a saving model event with the dispatcher.
*
* @param \Closure|string $callback
* @param \Illuminate\Events\QueuedClosure|\Closure|string|array $callback
* @return void
*/
public static function saving($callback)
@@ -200,7 +252,7 @@ trait HasEvents
/**
* Register a saved model event with the dispatcher.
*
* @param \Closure|string $callback
* @param \Illuminate\Events\QueuedClosure|\Closure|string|array $callback
* @return void
*/
public static function saved($callback)
@@ -211,7 +263,7 @@ trait HasEvents
/**
* Register an updating model event with the dispatcher.
*
* @param \Closure|string $callback
* @param \Illuminate\Events\QueuedClosure|\Closure|string|array $callback
* @return void
*/
public static function updating($callback)
@@ -222,7 +274,7 @@ trait HasEvents
/**
* Register an updated model event with the dispatcher.
*
* @param \Closure|string $callback
* @param \Illuminate\Events\QueuedClosure|\Closure|string|array $callback
* @return void
*/
public static function updated($callback)
@@ -233,7 +285,7 @@ trait HasEvents
/**
* Register a creating model event with the dispatcher.
*
* @param \Closure|string $callback
* @param \Illuminate\Events\QueuedClosure|\Closure|string|array $callback
* @return void
*/
public static function creating($callback)
@@ -244,7 +296,7 @@ trait HasEvents
/**
* Register a created model event with the dispatcher.
*
* @param \Closure|string $callback
* @param \Illuminate\Events\QueuedClosure|\Closure|string|array $callback
* @return void
*/
public static function created($callback)
@@ -252,10 +304,21 @@ trait HasEvents
static::registerModelEvent('created', $callback);
}
/**
* Register a replicating model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|\Closure|string|array $callback
* @return void
*/
public static function replicating($callback)
{
static::registerModelEvent('replicating', $callback);
}
/**
* Register a deleting model event with the dispatcher.
*
* @param \Closure|string $callback
* @param \Illuminate\Events\QueuedClosure|\Closure|string|array $callback
* @return void
*/
public static function deleting($callback)
@@ -266,7 +329,7 @@ trait HasEvents
/**
* Register a deleted model event with the dispatcher.
*
* @param \Closure|string $callback
* @param \Illuminate\Events\QueuedClosure|\Closure|string|array $callback
* @return void
*/
public static function deleted($callback)
@@ -275,7 +338,7 @@ trait HasEvents
}
/**
* Remove all of the event listeners for the model.
* Remove all the event listeners for the model.
*
* @return void
*/
@@ -290,6 +353,10 @@ trait HasEvents
foreach ($instance->getObservableEvents() as $event) {
static::$dispatcher->forget("eloquent.{$event}: ".static::class);
}
foreach (array_values($instance->dispatchesEvents) as $event) {
static::$dispatcher->forget($event);
}
}
/**
@@ -322,4 +389,27 @@ trait HasEvents
{
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);
}
}
}
}

View File

@@ -3,9 +3,9 @@
namespace Illuminate\Database\Eloquent\Concerns;
use Closure;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Support\Arr;
use InvalidArgumentException;
use Illuminate\Database\Eloquent\Scope;
trait HasGlobalScopes
{
@@ -13,14 +13,14 @@ trait HasGlobalScopes
* Register a new global scope on the model.
*
* @param \Illuminate\Database\Eloquent\Scope|\Closure|string $scope
* @param \Closure|null $implementation
* @param \Illuminate\Database\Eloquent\Scope|\Closure|null $implementation
* @return mixed
*
* @throws \InvalidArgumentException
*/
public static function addGlobalScope($scope, Closure $implementation = null)
public static function addGlobalScope($scope, $implementation = null)
{
if (is_string($scope) && ! is_null($implementation)) {
if (is_string($scope) && ($implementation instanceof Closure || $implementation instanceof Scope)) {
return static::$globalScopes[static::class][$scope] = $implementation;
} elseif ($scope instanceof Closure) {
return static::$globalScopes[static::class][spl_object_hash($scope)] = $scope;

View File

@@ -2,20 +2,25 @@
namespace Illuminate\Database\Eloquent\Concerns;
use Closure;
use Illuminate\Database\ClassMorphViolationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
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\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\MorphTo;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\Relations\Relation;
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
{
@@ -36,19 +41,60 @@ trait HasRelationships
/**
* The many to many relationship methods.
*
* @var array
* @var string[]
*/
public static $manyMethods = [
'belongsToMany', 'morphToMany', 'morphedByMany',
'guessBelongsToManyRelation', 'findFirstMethodThatIsntRelation',
];
/**
* The relation resolver callbacks.
*
* @var array
*/
protected static $relationResolvers = [];
/**
* Get the dynamic relation resolver if defined or inherited, or return null.
*
* @param string $class
* @param string $key
* @return mixed
*/
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]]
);
}
/**
* Define a one-to-one relationship.
*
* @param string $related
* @param string $foreignKey
* @param string $localKey
* @param string|null $foreignKey
* @param string|null $localKey
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function hasOne($related, $foreignKey = null, $localKey = null)
@@ -59,7 +105,64 @@ trait HasRelationships
$localKey = $localKey ?: $this->getKeyName();
return new HasOne($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);
return $this->newHasOne($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);
}
/**
* Instantiate a new HasOne relationship.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $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.
*
* @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\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.
*
* @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
* @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);
}
/**
@@ -67,31 +170,46 @@ trait HasRelationships
*
* @param string $related
* @param string $name
* @param string $type
* @param string $id
* @param string $localKey
* @param string|null $type
* @param string|null $id
* @param string|null $localKey
* @return \Illuminate\Database\Eloquent\Relations\MorphOne
*/
public function morphOne($related, $name, $type = null, $id = null, $localKey = null)
{
$instance = $this->newRelatedInstance($related);
list($type, $id) = $this->getMorphs($name, $type, $id);
[$type, $id] = $this->getMorphs($name, $type, $id);
$table = $instance->getTable();
$localKey = $localKey ?: $this->getKeyName();
return new MorphOne($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $localKey);
return $this->newMorphOne($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $localKey);
}
/**
* Instantiate a new MorphOne relationship.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $parent
* @param string $type
* @param string $id
* @param string $localKey
* @return \Illuminate\Database\Eloquent\Relations\MorphOne
*/
protected function newMorphOne(Builder $query, Model $parent, $type, $id, $localKey)
{
return new MorphOne($query, $parent, $type, $id, $localKey);
}
/**
* Define an inverse one-to-one or many relationship.
*
* @param string $related
* @param string $foreignKey
* @param string $ownerKey
* @param string $relation
* @param string|null $foreignKey
* @param string|null $ownerKey
* @param string|null $relation
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function belongsTo($related, $foreignKey = null, $ownerKey = null, $relation = null)
@@ -112,41 +230,57 @@ trait HasRelationships
$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 returns the relationship instance which will
// actually be responsible for retrieving and hydrating every relations.
// Once we have the foreign key names we'll just create a new Eloquent query
// for the related models and return the relationship instance which will
// actually be responsible for retrieving and hydrating every relation.
$ownerKey = $ownerKey ?: $instance->getKeyName();
return new BelongsTo(
return $this->newBelongsTo(
$instance->newQuery(), $this, $foreignKey, $ownerKey, $relation
);
}
/**
* Instantiate a new BelongsTo relationship.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $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 $name
* @param string $type
* @param string $id
* @param string|null $name
* @param string|null $type
* @param string|null $id
* @param string|null $ownerKey
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function morphTo($name = null, $type = null, $id = null)
public function morphTo($name = null, $type = null, $id = null, $ownerKey = null)
{
// If no name is provided, we will use the backtrace to get the function name
// since that is most likely the name of the polymorphic interface. We can
// use that to get both the class and foreign key that will be utilized.
$name = $name ?: $this->guessBelongsToRelation();
list($type, $id) = $this->getMorphs(
[$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 empty($class = $this->{$type})
? $this->morphEagerTo($name, $type, $id)
: $this->morphInstanceTo($class, $name, $type, $id);
return is_null($class = $this->getAttributeFromArray($type)) || $class === ''
? $this->morphEagerTo($name, $type, $id, $ownerKey)
: $this->morphInstanceTo($class, $name, $type, $id, $ownerKey);
}
/**
@@ -155,12 +289,13 @@ trait HasRelationships
* @param string $name
* @param string $type
* @param string $id
* @param string $ownerKey
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
protected function morphEagerTo($name, $type, $id)
protected function morphEagerTo($name, $type, $id, $ownerKey)
{
return new MorphTo(
$this->newQuery()->setEagerLoads([]), $this, $id, null, $type, $name
return $this->newMorphTo(
$this->newQuery()->setEagerLoads([]), $this, $id, $ownerKey, $type, $name
);
}
@@ -171,19 +306,36 @@ trait HasRelationships
* @param string $name
* @param string $type
* @param string $id
* @param string $ownerKey
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
protected function morphInstanceTo($target, $name, $type, $id)
protected function morphInstanceTo($target, $name, $type, $id, $ownerKey)
{
$instance = $this->newRelatedInstance(
static::getActualClassNameForMorph($target)
);
return new MorphTo(
$instance->newQuery(), $this, $id, $instance->getKeyName(), $type, $name
return $this->newMorphTo(
$instance->newQuery(), $this, $id, $ownerKey ?? $instance->getKeyName(), $type, $name
);
}
/**
* Instantiate a new MorphTo relationship.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $parent
* @param string $foreignKey
* @param string $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.
*
@@ -202,7 +354,7 @@ trait HasRelationships
*/
protected function guessBelongsToRelation()
{
list($one, $two, $caller) = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
[$one, $two, $caller] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
return $caller['function'];
}
@@ -211,8 +363,8 @@ trait HasRelationships
* Define a one-to-many relationship.
*
* @param string $related
* @param string $foreignKey
* @param string $localKey
* @param string|null $foreignKey
* @param string|null $localKey
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function hasMany($related, $foreignKey = null, $localKey = null)
@@ -223,11 +375,25 @@ trait HasRelationships
$localKey = $localKey ?: $this->getKeyName();
return new HasMany(
return $this->newHasMany(
$instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey
);
}
/**
* Instantiate a new HasMany relationship.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $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.
*
@@ -236,21 +402,43 @@ trait HasRelationships
* @param string|null $firstKey
* @param string|null $secondKey
* @param string|null $localKey
* @param string|null $secondLocalKey
* @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
*/
public function hasManyThrough($related, $through, $firstKey = null, $secondKey = null, $localKey = null)
public function hasManyThrough($related, $through, $firstKey = null, $secondKey = null, $localKey = null, $secondLocalKey = null)
{
$through = new $through;
$through = $this->newRelatedThroughInstance($through);
$firstKey = $firstKey ?: $this->getForeignKey();
$secondKey = $secondKey ?: $through->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
return $this->newHasManyThrough(
$this->newRelatedInstance($related)->newQuery(),
$this,
$through,
$firstKey,
$secondKey,
$localKey ?: $this->getKeyName(),
$secondLocalKey ?: $through->getKeyName()
);
}
$instance = $this->newRelatedInstance($related);
return new HasManyThrough($instance->newQuery(), $this, $through, $firstKey, $secondKey, $localKey);
/**
* Instantiate a new HasManyThrough relationship.
*
* @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
* @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);
}
/**
@@ -258,9 +446,9 @@ trait HasRelationships
*
* @param string $related
* @param string $name
* @param string $type
* @param string $id
* @param string $localKey
* @param string|null $type
* @param string|null $id
* @param string|null $localKey
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
*/
public function morphMany($related, $name, $type = null, $id = null, $localKey = null)
@@ -270,26 +458,44 @@ trait HasRelationships
// Here we will gather up the morph type and ID for the relationship so that we
// can properly query the intermediate table of a relation. Finally, we will
// get the table and create the relationship instances for the developers.
list($type, $id) = $this->getMorphs($name, $type, $id);
[$type, $id] = $this->getMorphs($name, $type, $id);
$table = $instance->getTable();
$localKey = $localKey ?: $this->getKeyName();
return new MorphMany($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $localKey);
return $this->newMorphMany($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $localKey);
}
/**
* Instantiate a new MorphMany relationship.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $parent
* @param string $type
* @param string $id
* @param string $localKey
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
*/
protected function newMorphMany(Builder $query, Model $parent, $type, $id, $localKey)
{
return new MorphMany($query, $parent, $type, $id, $localKey);
}
/**
* Define a many-to-many relationship.
*
* @param string $related
* @param string $table
* @param string $foreignKey
* @param string $relatedKey
* @param string $relation
* @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\BelongsToMany
*/
public function belongsToMany($related, $table = null, $foreignKey = 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
@@ -303,34 +509,59 @@ trait HasRelationships
// instances as well as the relationship instances we need for this.
$instance = $this->newRelatedInstance($related);
$foreignKey = $foreignKey ?: $this->getForeignKey();
$foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey();
$relatedKey = $relatedKey ?: $instance->getForeignKey();
$relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey();
// If no table name was provided, we can guess it by concatenating the two
// models using underscores in alphabetical order. The two model names
// are transformed to snake case from their default CamelCase also.
if (is_null($table)) {
$table = $this->joiningTable($related);
$table = $this->joiningTable($related, $instance);
}
return new BelongsToMany(
$instance->newQuery(), $this, $table, $foreignKey, $relatedKey, $relation
return $this->newBelongsToMany(
$instance->newQuery(), $this, $table, $foreignPivotKey,
$relatedPivotKey, $parentKey ?: $this->getKeyName(),
$relatedKey ?: $instance->getKeyName(), $relation
);
}
/**
* Instantiate a new BelongsToMany relationship.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $parent
* @param string $table
* @param string $foreignPivotKey
* @param string $relatedPivotKey
* @param string $parentKey
* @param string $relatedKey
* @param string|null $relationName
* @return \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.
*
* @param string $related
* @param string $name
* @param string $table
* @param string $foreignKey
* @param string $relatedKey
* @param string|null $table
* @param string|null $foreignPivotKey
* @param string|null $relatedPivotKey
* @param string|null $parentKey
* @param string|null $relatedKey
* @param bool $inverse
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany
*/
public function morphToMany($related, $name, $table = null, $foreignKey = null, $relatedKey = null, $inverse = false)
public function morphToMany($related, $name, $table = null, $foreignPivotKey = null,
$relatedPivotKey = null, $parentKey = null,
$relatedKey = null, $inverse = false)
{
$caller = $this->guessBelongsToManyRelation();
@@ -339,52 +570,91 @@ trait HasRelationships
// instances, as well as the relationship instances we need for these.
$instance = $this->newRelatedInstance($related);
$foreignKey = $foreignKey ?: $name.'_id';
$foreignPivotKey = $foreignPivotKey ?: $name.'_id';
$relatedKey = $relatedKey ?: $instance->getForeignKey();
$relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey();
// Now we're ready to create a new query builder for this related model and
// the relationship instances for this relation. This relations will set
// appropriate query constraints then entirely manages the hydrations.
$table = $table ?: Str::plural($name);
// 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);
return new MorphToMany(
$lastWord = array_pop($words);
$table = implode('', $words).Str::plural($lastWord);
}
return $this->newMorphToMany(
$instance->newQuery(), $this, $name, $table,
$foreignKey, $relatedKey, $caller, $inverse
$foreignPivotKey, $relatedPivotKey, $parentKey ?: $this->getKeyName(),
$relatedKey ?: $instance->getKeyName(), $caller, $inverse
);
}
/**
* Instantiate a new MorphToMany relationship.
*
* @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 bool $inverse
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany
*/
protected function newMorphToMany(Builder $query, Model $parent, $name, $table, $foreignPivotKey,
$relatedPivotKey, $parentKey, $relatedKey,
$relationName = null, $inverse = false)
{
return new MorphToMany($query, $parent, $name, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey,
$relationName, $inverse);
}
/**
* Define a polymorphic, inverse many-to-many relationship.
*
* @param string $related
* @param string $name
* @param string $table
* @param string $foreignKey
* @param string $relatedKey
* @param string|null $table
* @param string|null $foreignPivotKey
* @param string|null $relatedPivotKey
* @param string|null $parentKey
* @param string|null $relatedKey
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany
*/
public function morphedByMany($related, $name, $table = null, $foreignKey = null, $relatedKey = null)
public function morphedByMany($related, $name, $table = null, $foreignPivotKey = null,
$relatedPivotKey = null, $parentKey = null, $relatedKey = null)
{
$foreignKey = $foreignKey ?: $this->getForeignKey();
$foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey();
// For the inverse of the polymorphic many-to-many relations, we will change
// the way we determine the foreign and other keys, as it is the opposite
// of the morph-to-many method since we're figuring out these inverses.
$relatedKey = $relatedKey ?: $name.'_id';
$relatedPivotKey = $relatedPivotKey ?: $name.'_id';
return $this->morphToMany($related, $name, $table, $foreignKey, $relatedKey, true);
return $this->morphToMany(
$related, $name, $table, $foreignPivotKey,
$relatedPivotKey, $parentKey, $relatedKey, true
);
}
/**
* Get the relationship name of the belongs to many.
* Get the relationship name of the belongsToMany relationship.
*
* @return string
* @return string|null
*/
protected function guessBelongsToManyRelation()
{
$caller = Arr::first(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), function ($trace) {
return ! in_array($trace['function'], Model::$manyMethods);
return ! in_array(
$trace['function'],
array_merge(static::$manyMethods, ['guessBelongsToManyRelation'])
);
});
return ! is_null($caller) ? $caller['function'] : null;
@@ -394,24 +664,36 @@ trait HasRelationships
* 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)
public function joiningTable($related, $instance = null)
{
// The joining table name, by convention, is simply the snake cased models
// sorted alphabetically and concatenated with an underscore, so we can
// just sort the models and join them together to get the table name.
$models = [
Str::snake(class_basename($related)),
Str::snake(class_basename($this)),
$segments = [
$instance ? $instance->joiningTableSegment()
: Str::snake(class_basename($related)),
$this->joiningTableSegment(),
];
// Now that we have the model names in an array we can just sort them and
// use the implode function to join them together with an underscores,
// which is typically used by convention within the database system.
sort($models);
sort($segments);
return strtolower(implode('_', $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));
}
/**
@@ -422,7 +704,7 @@ trait HasRelationships
*/
public function touches($relation)
{
return in_array($relation, $this->touches);
return in_array($relation, $this->getTouchedRelations());
}
/**
@@ -432,7 +714,7 @@ trait HasRelationships
*/
public function touchOwners()
{
foreach ($this->touches as $relation) {
foreach ($this->getTouchedRelations() as $relation) {
$this->$relation()->touch();
if ($this->$relation instanceof self) {
@@ -440,9 +722,7 @@ trait HasRelationships
$this->$relation->touchOwners();
} elseif ($this->$relation instanceof Collection) {
$this->$relation->each(function (Model $relation) {
$relation->touchOwners();
});
$this->$relation->each->touchOwners();
}
}
}
@@ -473,6 +753,14 @@ trait HasRelationships
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;
}
@@ -491,6 +779,17 @@ trait HasRelationships
});
}
/**
* Create a new model instance for a related "through" model.
*
* @param string $class
* @return mixed
*/
protected function newRelatedThroughInstance($class)
{
return new $class;
}
/**
* Get all the loaded relations for the instance.
*
@@ -524,7 +823,7 @@ trait HasRelationships
}
/**
* Set the specific relationship in the model.
* Set the given relationship on the model.
*
* @param string $relation
* @param mixed $value
@@ -537,6 +836,19 @@ trait HasRelationships
return $this;
}
/**
* Unset a loaded relationship.
*
* @param string $relation
* @return $this
*/
public function unsetRelation($relation)
{
unset($this->relations[$relation]);
return $this;
}
/**
* Set the entire relations array on the model.
*
@@ -550,6 +862,30 @@ trait HasRelationships
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.
*

View File

@@ -2,7 +2,7 @@
namespace Illuminate\Database\Eloquent\Concerns;
use Carbon\Carbon;
use Illuminate\Support\Facades\Date;
trait HasTimestamps
{
@@ -13,13 +13,27 @@ trait HasTimestamps
*/
public $timestamps = true;
/**
* The list of models classes that have timestamps temporarily disabled.
*
* @var array
*/
protected static $ignoreTimestampsOn = [];
/**
* Update the model's update timestamp.
*
* @param string|null $attribute
* @return bool
*/
public function touch()
public function touch($attribute = null)
{
if ($attribute) {
$this->$attribute = $this->freshTimestamp();
return $this->save();
}
if (! $this->usesTimestamps()) {
return false;
}
@@ -29,22 +43,39 @@ trait HasTimestamps
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 void
* @return $this
*/
protected function updateTimestamps()
public function updateTimestamps()
{
$time = $this->freshTimestamp();
if (! $this->isDirty(static::UPDATED_AT)) {
$updatedAtColumn = $this->getUpdatedAtColumn();
if (! is_null($updatedAtColumn) && ! $this->isDirty($updatedAtColumn)) {
$this->setUpdatedAt($time);
}
if (! $this->exists && ! $this->isDirty(static::CREATED_AT)) {
$createdAtColumn = $this->getCreatedAtColumn();
if (! $this->exists && ! is_null($createdAtColumn) && ! $this->isDirty($createdAtColumn)) {
$this->setCreatedAt($time);
}
return $this;
}
/**
@@ -55,7 +86,7 @@ trait HasTimestamps
*/
public function setCreatedAt($value)
{
$this->{static::CREATED_AT} = $value;
$this->{$this->getCreatedAtColumn()} = $value;
return $this;
}
@@ -68,7 +99,7 @@ trait HasTimestamps
*/
public function setUpdatedAt($value)
{
$this->{static::UPDATED_AT} = $value;
$this->{$this->getUpdatedAtColumn()} = $value;
return $this;
}
@@ -76,11 +107,11 @@ trait HasTimestamps
/**
* Get a fresh timestamp for the model.
*
* @return \Carbon\Carbon
* @return \Illuminate\Support\Carbon
*/
public function freshTimestamp()
{
return new Carbon;
return Date::now();
}
/**
@@ -100,13 +131,13 @@ trait HasTimestamps
*/
public function usesTimestamps()
{
return $this->timestamps;
return $this->timestamps && ! static::isIgnoringTimestamps($this::class);
}
/**
* Get the name of the "created at" column.
*
* @return string
* @return string|null
*/
public function getCreatedAtColumn()
{
@@ -116,10 +147,78 @@ trait HasTimestamps
/**
* Get the name of the "updated at" column.
*
* @return string
* @return string|null
*/
public function getUpdatedAtColumn()
{
return static::UPDATED_AT;
}
/**
* Get the fully qualified "created at" column.
*
* @return string|null
*/
public function getQualifiedCreatedAtColumn()
{
return $this->qualifyColumn($this->getCreatedAtColumn());
}
/**
* Get the fully qualified "updated at" column.
*
* @return string|null
*/
public function getQualifiedUpdatedAtColumn()
{
return $this->qualifyColumn($this->getUpdatedAtColumn());
}
/**
* 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 {
static::$ignoreTimestampsOn = array_values(array_diff(static::$ignoreTimestampsOn, $models));
}
}
/**
* 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;
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Str;
trait HasUlids
{
/**
* Boot the trait.
*
* @return void
*/
public static function bootHasUlids()
{
static::creating(function (self $model) {
foreach ($model->uniqueIds() as $column) {
if (empty($model->{$column})) {
$model->{$column} = $model->newUniqueId();
}
}
});
}
/**
* Generate a new ULID for the model.
*
* @return string
*/
public function newUniqueId()
{
return strtolower((string) Str::ulid());
}
/**
* 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\Database\Eloquent\Relations\Relation
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function resolveRouteBindingQuery($query, $value, $field = null)
{
if ($field && in_array($field, $this->uniqueIds()) && ! Str::isUlid($value)) {
throw (new ModelNotFoundException)->setModel(get_class($this), $value);
}
if (! $field && in_array($this->getRouteKeyName(), $this->uniqueIds()) && ! Str::isUlid($value)) {
throw (new ModelNotFoundException)->setModel(get_class($this), $value);
}
return parent::resolveRouteBindingQuery($query, $value, $field);
}
/**
* Get the columns that should receive a unique identifier.
*
* @return array
*/
public function uniqueIds()
{
return [$this->getKeyName()];
}
/**
* Get the auto-incrementing key type.
*
* @return string
*/
public function getKeyType()
{
if (in_array($this->getKeyName(), $this->uniqueIds())) {
return 'string';
}
return $this->keyType;
}
/**
* Get the value indicating whether the IDs are incrementing.
*
* @return bool
*/
public function getIncrementing()
{
if (in_array($this->getKeyName(), $this->uniqueIds())) {
return false;
}
return $this->incrementing;
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Str;
trait HasUuids
{
/**
* Generate a primary UUID for the model.
*
* @return void
*/
public static function bootHasUuids()
{
static::creating(function (self $model) {
foreach ($model->uniqueIds() as $column) {
if (empty($model->{$column})) {
$model->{$column} = $model->newUniqueId();
}
}
});
}
/**
* Generate a new UUID for the model.
*
* @return string
*/
public function newUniqueId()
{
return (string) Str::orderedUuid();
}
/**
* Get the columns that should receive a unique identifier.
*
* @return array
*/
public function uniqueIds()
{
return [$this->getKeyName()];
}
/**
* 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\Database\Eloquent\Relations\Relation
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function resolveRouteBindingQuery($query, $value, $field = null)
{
if ($field && in_array($field, $this->uniqueIds()) && ! Str::isUuid($value)) {
throw (new ModelNotFoundException)->setModel(get_class($this), $value);
}
if (! $field && in_array($this->getRouteKeyName(), $this->uniqueIds()) && ! Str::isUuid($value)) {
throw (new ModelNotFoundException)->setModel(get_class($this), $value);
}
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 $this->keyType;
}
/**
* Get the value indicating whether the IDs are incrementing.
*
* @return bool
*/
public function getIncrementing()
{
if (in_array($this->getKeyName(), $this->uniqueIds())) {
return false;
}
return $this->incrementing;
}
}

View File

@@ -7,21 +7,21 @@ trait HidesAttributes
/**
* The attributes that should be hidden for serialization.
*
* @var array
* @var array<string>
*/
protected $hidden = [];
/**
* The attributes that should be visible in serialization.
*
* @var array
* @var array<string>
*/
protected $visible = [];
/**
* Get the hidden attributes for the model.
*
* @return array
* @return array<string>
*/
public function getHidden()
{
@@ -31,7 +31,7 @@ trait HidesAttributes
/**
* Set the hidden attributes for the model.
*
* @param array $hidden
* @param array<string> $hidden
* @return $this
*/
public function setHidden(array $hidden)
@@ -41,23 +41,10 @@ trait HidesAttributes
return $this;
}
/**
* Add hidden attributes for the model.
*
* @param array|string|null $attributes
* @return void
*/
public function addHidden($attributes = null)
{
$this->hidden = array_merge(
$this->hidden, is_array($attributes) ? $attributes : func_get_args()
);
}
/**
* Get the visible attributes for the model.
*
* @return array
* @return array<string>
*/
public function getVisible()
{
@@ -67,7 +54,7 @@ trait HidesAttributes
/**
* Set the visible attributes for the model.
*
* @param array $visible
* @param array<string> $visible
* @return $this
*/
public function setVisible(array $visible)
@@ -77,50 +64,61 @@ trait HidesAttributes
return $this;
}
/**
* Add visible attributes for the model.
*
* @param array|string|null $attributes
* @return void
*/
public function addVisible($attributes = null)
{
$this->visible = array_merge(
$this->visible, is_array($attributes) ? $attributes : func_get_args()
);
}
/**
* Make the given, typically hidden, attributes visible.
*
* @param array|string $attributes
* @param array<string>|string|null $attributes
* @return $this
*/
public function makeVisible($attributes)
{
$this->hidden = array_diff($this->hidden, (array) $attributes);
$attributes = is_array($attributes) ? $attributes : func_get_args();
$this->hidden = array_diff($this->hidden, $attributes);
if (! empty($this->visible)) {
$this->addVisible($attributes);
$this->visible = array_merge($this->visible, $attributes);
}
return $this;
}
/**
* Make the given, typically hidden, attributes visible if the given truth test passes.
*
* @param bool|\Closure $condition
* @param array<string>|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 $attributes
* @param array<string>|string|null $attributes
* @return $this
*/
public function makeHidden($attributes)
{
$attributes = (array) $attributes;
$this->visible = array_diff($this->visible, $attributes);
$this->hidden = array_unique(array_merge($this->hidden, $attributes));
$this->hidden = array_merge(
$this->hidden, is_array($attributes) ? $attributes : func_get_args()
);
return $this;
}
/**
* Make the given, typically visible, attributes hidden if the given truth test passes.
*
* @param bool|\Closure $condition
* @param array<string>|string|null $attributes
* @return $this
*/
public function makeHiddenIf($condition, $attributes)
{
return value($condition, $this) ? $this->makeHidden($attributes) : $this;
}
}

View File

@@ -2,33 +2,46 @@
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\Query\Expression;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\RelationNotFoundException;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Database\Query\Expression;
use Illuminate\Support\Str;
use InvalidArgumentException;
trait QueriesRelationships
{
/**
* Add a relationship count / exists condition to the query.
*
* @param string $relation
* @param \Illuminate\Database\Eloquent\Relations\Relation|string $relation
* @param string $operator
* @param int $count
* @param int $count
* @param string $boolean
* @param \Closure|null $callback
* @return \Illuminate\Database\Eloquent\Builder|static
*
* @throws \RuntimeException
*/
public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', Closure $callback = null)
{
if (strpos($relation, '.') !== false) {
return $this->hasNested($relation, $operator, $count, $boolean, $callback);
if (is_string($relation)) {
if (str_contains($relation, '.')) {
return $this->hasNested($relation, $operator, $count, $boolean, $callback);
}
$relation = $this->getRelationWithoutConstraints($relation);
}
$relation = $this->getRelationWithoutConstraints($relation);
if ($relation instanceof MorphTo) {
return $this->hasMorph($relation, ['*'], $operator, $count, $boolean, $callback);
}
// If we only need to check for the existence of the relation, then we can optimize
// the subquery to only run a "where exists" clause instead of this full "count"
@@ -38,7 +51,7 @@ trait QueriesRelationships
: 'getRelationExistenceCountQuery';
$hasQuery = $relation->{$method}(
$relation->getRelated()->newQuery(), $this
$relation->getRelated()->newQueryWithoutRelationships(), $this
);
// Next we will call any given callback as an "anonymous" scope so they can get the
@@ -60,7 +73,7 @@ trait QueriesRelationships
*
* @param string $relations
* @param string $operator
* @param int $count
* @param int $count
* @param string $boolean
* @param \Closure|null $callback
* @return \Illuminate\Database\Eloquent\Builder|static
@@ -69,7 +82,14 @@ trait QueriesRelationships
{
$relations = explode('.', $relations);
$closure = function ($q) use (&$closure, &$relations, $operator, $count, $boolean, $callback) {
$doesntHave = $operator === '<' && $count === 1;
if ($doesntHave) {
$operator = '>=';
$count = 1;
}
$closure = function ($q) use (&$closure, &$relations, $operator, $count, $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.
@@ -78,7 +98,7 @@ trait QueriesRelationships
: $q->has(array_shift($relations), $operator, $count, 'and', $callback);
};
return $this->has(array_shift($relations), '>=', 1, $boolean, $closure);
return $this->has(array_shift($relations), $doesntHave ? '<' : '>=', 1, $boolean, $closure);
}
/**
@@ -86,7 +106,7 @@ trait QueriesRelationships
*
* @param string $relation
* @param string $operator
* @param int $count
* @param int $count
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function orHas($relation, $operator = '>=', $count = 1)
@@ -107,13 +127,24 @@ trait QueriesRelationships
return $this->has($relation, '<', 1, $boolean, $callback);
}
/**
* Add a relationship count / exists condition to the query with an "or".
*
* @param string $relation
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function orDoesntHave($relation)
{
return $this->doesntHave($relation, 'or');
}
/**
* Add a relationship count / exists condition to the query with where clauses.
*
* @param string $relation
* @param \Closure|null $callback
* @param string $operator
* @param int $count
* @param int $count
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function whereHas($relation, Closure $callback = null, $operator = '>=', $count = 1)
@@ -121,13 +152,30 @@ trait QueriesRelationships
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 same condition.
*
* @param string $relation
* @param \Closure|null $callback
* @param string $operator
* @param int $count
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function withWhereHas($relation, Closure $callback = null, $operator = '>=', $count = 1)
{
return $this->whereHas(Str::before($relation, ':'), $callback, $operator, $count)
->with($callback ? [$relation => fn ($query) => $callback($query)] : $relation);
}
/**
* Add a relationship count / exists condition to the query with where clauses and an "or".
*
* @param string $relation
* @param \Closure $callback
* @param string $operator
* @param int $count
* @param string $relation
* @param \Closure|null $callback
* @param string $operator
* @param int $count
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function orWhereHas($relation, Closure $callback = null, $operator = '>=', $count = 1)
@@ -148,12 +196,406 @@ trait QueriesRelationships
}
/**
* Add subselect queries to count the relations.
* Add a relationship count / exists condition to the query with where clauses and an "or".
*
* @param string $relation
* @param \Closure|null $callback
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function orWhereDoesntHave($relation, Closure $callback = null)
{
return $this->doesntHave($relation, 'or', $callback);
}
/**
* Add a polymorphic relationship count / exists condition to the query.
*
* @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
* @param string|array $types
* @param string $operator
* @param int $count
* @param string $boolean
* @param \Closure|null $callback
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function hasMorph($relation, $types, $operator = '>=', $count = 1, $boolean = 'and', Closure $callback = null)
{
if (is_string($relation)) {
$relation = $this->getRelationWithoutConstraints($relation);
}
$types = (array) $types;
if ($types === ['*']) {
$types = $this->model->newModelQuery()->distinct()->pluck($relation->getMorphType())->filter()->all();
}
foreach ($types as &$type) {
$type = Relation::getMorphedModel($type) ?? $type;
}
return $this->where(function ($query) use ($relation, $callback, $operator, $count, $types) {
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);
});
}
}, null, null, $boolean);
}
/**
* Get the BelongsTo relationship for a single polymorphic type.
*
* @param \Illuminate\Database\Eloquent\Relations\MorphTo $relation
* @param 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 int $count
* @return \Illuminate\Database\Eloquent\Builder|static
*/
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.
*
* @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
* @param string|array $types
* @param string $boolean
* @param \Closure|null $callback
* @return \Illuminate\Database\Eloquent\Builder|static
*/
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 \Illuminate\Database\Eloquent\Builder|static
*/
public function orDoesntHaveMorph($relation, $types)
{
return $this->doesntHaveMorph($relation, $types, 'or');
}
/**
* Add a polymorphic relationship count / exists condition to the query with where clauses.
*
* @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
* @param string|array $types
* @param \Closure|null $callback
* @param string $operator
* @param int $count
* @return \Illuminate\Database\Eloquent\Builder|static
*/
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".
*
* @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
* @param string|array $types
* @param \Closure|null $callback
* @param string $operator
* @param int $count
* @return \Illuminate\Database\Eloquent\Builder|static
*/
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.
*
* @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
* @param string|array $types
* @param \Closure|null $callback
* @return \Illuminate\Database\Eloquent\Builder|static
*/
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".
*
* @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
* @param string|array $types
* @param \Closure|null $callback
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function orWhereDoesntHaveMorph($relation, $types, Closure $callback = null)
{
return $this->doesntHaveMorph($relation, $types, 'or', $callback);
}
/**
* Add a basic where clause to a relationship query.
*
* @param string $relation
* @param \Closure|string|array|\Illuminate\Database\Query\Expression $column
* @param mixed $operator
* @param mixed $value
* @return \Illuminate\Database\Eloquent\Builder|static
*/
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 an "or where" clause to a relationship query.
*
* @param string $relation
* @param \Closure|string|array|\Illuminate\Database\Query\Expression $column
* @param mixed $operator
* @param mixed $value
* @return \Illuminate\Database\Eloquent\Builder|static
*/
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 polymorphic relationship condition to the query with a where clause.
*
* @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
* @param string|array $types
* @param \Closure|string|array|\Illuminate\Database\Query\Expression $column
* @param mixed $operator
* @param mixed $value
* @return \Illuminate\Database\Eloquent\Builder|static
*/
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.
*
* @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
* @param string|array $types
* @param \Closure|string|array|\Illuminate\Database\Query\Expression $column
* @param mixed $operator
* @param mixed $value
* @return \Illuminate\Database\Eloquent\Builder|static
*/
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 morph-to relationship condition to the query.
*
* @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
* @param \Illuminate\Database\Eloquent\Model|string $model
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function whereMorphedTo($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->where($relation->getMorphType(), $model, null, $boolean);
}
return $this->where(function ($query) use ($relation, $model) {
$query->where($relation->getMorphType(), $model->getMorphClass())
->where($relation->getForeignKeyName(), $model->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|string $model
* @return \Illuminate\Database\Eloquent\Builder|static
*/
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->getMorphType(), '<=>', $model, $boolean);
}
return $this->whereNot(function ($query) use ($relation, $model) {
$query->where($relation->getMorphType(), '<=>', $model->getMorphClass())
->where($relation->getForeignKeyName(), '<=>', $model->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|string $model
* @return \Illuminate\Database\Eloquent\Builder|static
*/
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|string $model
* @return \Illuminate\Database\Eloquent\Builder|static
*/
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<\Illuminate\Database\Eloquent\Model> $related
* @param string|null $relationshipName
* @param string $boolean
* @return $this
*
* @throws \Illuminate\Database\Eloquent\RelationNotFoundException
*/
public function whereBelongsTo($related, $relationshipName = null, $boolean = 'and')
{
if (! $related instanceof Collection) {
$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 $exception) {
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 an "BelongsTo" 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 orWhereBelongsTo($related, $relationshipName = null)
{
return $this->whereBelongsTo($related, $relationshipName, 'or');
}
/**
* Add subselect queries to include an aggregate value for a relationship.
*
* @param mixed $relations
* @param string $column
* @param string $function
* @return $this
*/
public function withCount($relations)
public function withAggregate($relations, $column, $function = null)
{
if (empty($relations)) {
return $this;
@@ -163,44 +605,167 @@ trait QueriesRelationships
$this->query->select([$this->query->from.'.*']);
}
$relations = is_array($relations) ? $relations : func_get_args();
$relations = is_array($relations) ? $relations : [$relations];
foreach ($this->parseWithRelations($relations) as $name => $constraints) {
// First we will determine if the name has been aliased using an "as" clause on the name
// and if it has we will extract the actual relationship name and the desired name of
// the resulting column. This allows multiple counts on the same relationship name.
// the resulting column. This allows multiple aggregates on the same relationships.
$segments = explode(' ', $name);
unset($alias);
if (count($segments) == 3 && Str::lower($segments[1]) == 'as') {
list($name, $alias) = [$segments[0], $segments[2]];
if (count($segments) === 3 && Str::lower($segments[1]) === 'as') {
[$name, $alias] = [$segments[0], $segments[2]];
}
$relation = $this->getRelationWithoutConstraints($name);
// Here we will get the relationship count query and prepare to add it to the main query
if ($function) {
$hashedColumn = $this->getRelationHashedColumn($column, $relation);
$wrappedColumn = $this->getQuery()->getGrammar()->wrap(
$column === '*' ? $column : $relation->getRelated()->qualifyColumn($hashedColumn)
);
$expression = $function === 'exists' ? $wrappedColumn : sprintf('%s(%s)', $function, $wrappedColumn);
} else {
$expression = $column;
}
// Here, we will grab the relationship sub-query and prepare to add it to the main query
// as a sub-select. First, we'll get the "has" query and use that to get the relation
// count query. We will normalize the relation name then append _count as the name.
$query = $relation->getRelationExistenceCountQuery(
$relation->getRelated()->newQuery(), $this
);
// sub-query. We'll format this relationship name and append this column if needed.
$query = $relation->getRelationExistenceQuery(
$relation->getRelated()->newQuery(), $this, new Expression($expression)
)->setBindings([], 'select');
$query->callScope($constraints);
$query->mergeConstraintsFrom($relation->getQuery());
$query = $query->mergeConstraintsFrom($relation->getQuery())->toBase();
// 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 = snake_case(isset($alias) ? $alias : $name).'_count';
// 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');
$this->selectSub($query->toBase(), $column);
if (count($query->columns) > 1) {
$query->columns = [$query->columns[0]];
$query->bindings['select'] = [];
}
// Finally, we will make the proper column alias to the query and run this sub-select on
// the query builder. Then, we will return the builder instance back to the developer
// for further constraint chaining that needs to take place on the query as needed.
$alias ??= Str::snake(
preg_replace('/[^[:alnum:][:space:]_]/u', '', "$name $function $column")
);
if ($function === 'exists') {
$this->selectRaw(
sprintf('exists(%s) as %s', $query->toSql(), $this->getQuery()->grammar->wrap($alias)),
$query->getBindings()
)->withCasts([$alias => 'bool']);
} else {
$this->selectSub(
$function ? $query : $query->limit(1),
$alias
);
}
}
return $this;
}
/**
* Get the relation hashed column name for the given column and relation.
*
* @param string $column
* @param \Illuminate\Database\Eloquent\Relations\Relationship $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 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 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 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 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.
*
@@ -216,7 +781,7 @@ trait QueriesRelationships
$hasQuery->mergeConstraintsFrom($relation->getQuery());
return $this->canUseExistsForExistenceCheck($operator, $count)
? $this->addWhereExistsQuery($hasQuery->toBase(), $boolean, $not = ($operator === '<' && $count === 1))
? $this->addWhereExistsQuery($hasQuery->toBase(), $boolean, $operator === '<' && $count === 1)
: $this->addWhereCountQuery($hasQuery->toBase(), $operator, $count, $boolean);
}
@@ -228,9 +793,14 @@ trait QueriesRelationships
*/
public function mergeConstraintsFrom(Builder $from)
{
$whereBindings = Arr::get(
$from->getQuery()->getRawBindings(), 'where', []
);
$whereBindings = $from->getQuery()->getRawBindings()['where'] ?? [];
$wheres = $from->getQuery()->from !== $this->getQuery()->from
? $this->requalifyWhereTables(
$from->getQuery()->wheres,
$from->getQuery()->from,
$this->getModel()->getTable()
) : $from->getQuery()->wheres;
// Here we have some other query that we want to merge the where constraints from. We will
// copy over any where constraints on the query as well as remove any global scopes the
@@ -238,14 +808,33 @@ trait QueriesRelationships
return $this->withoutGlobalScopes(
$from->removedScopes()
)->mergeWheres(
$from->getQuery()->wheres, $whereBindings
$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 collect($wheres)->map(function ($where) use ($from, $to) {
return collect($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 int $count
* @param string $boolean

View File

@@ -0,0 +1,76 @@
<?php
namespace Illuminate\Database\Eloquent\Factories;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
class BelongsToManyRelationship
{
/**
* The related factory instance.
*
* @var \Illuminate\Database\Eloquent\Factories\Factory|\Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array
*/
protected $factory;
/**
* The pivot attributes / attribute resolver.
*
* @var callable|array
*/
protected $pivot;
/**
* The relationship name.
*
* @var string
*/
protected $relationship;
/**
* Create a new attached relationship definition.
*
* @param \Illuminate\Database\Eloquent\Factories\Factory|\Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $factory
* @param callable|array $pivot
* @param string $relationship
* @return void
*/
public function __construct($factory, $pivot, $relationship)
{
$this->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)
{
Collection::wrap($this->factory instanceof Factory ? $this->factory->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;
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Illuminate\Database\Eloquent\Factories;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class BelongsToRelationship
{
/**
* The related factory instance.
*
* @var \Illuminate\Database\Eloquent\Factories\Factory|\Illuminate\Database\Eloquent\Model
*/
protected $factory;
/**
* The relationship name.
*
* @var string
*/
protected $relationship;
/**
* The cached, resolved parent instance ID.
*
* @var mixed
*/
protected $resolved;
/**
* Create a new "belongs to" relationship definition.
*
* @param \Illuminate\Database\Eloquent\Factories\Factory|\Illuminate\Database\Eloquent\Model $factory
* @param string $relationship
* @return void
*/
public function __construct($factory, $relationship)
{
$this->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;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Illuminate\Database\Eloquent\Factories;
use Illuminate\Support\Arr;
class CrossJoinSequence extends Sequence
{
/**
* Create a new cross join sequence instance.
*
* @param array $sequences
* @return void
*/
public function __construct(...$sequences)
{
$crossJoined = array_map(
function ($a) {
return array_merge(...$a);
},
Arr::crossJoin(...$sequences),
);
parent::__construct(...$crossJoined);
}
}

View File

@@ -0,0 +1,926 @@
<?php
namespace Illuminate\Database\Eloquent\Factories;
use Closure;
use Faker\Generator;
use Illuminate\Container\Container;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Enumerable;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Conditionable;
use Illuminate\Support\Traits\ForwardsCalls;
use Illuminate\Support\Traits\Macroable;
use Throwable;
/**
* @template TModel of \Illuminate\Database\Eloquent\Model
*
* @method $this trashed()
*/
abstract class Factory
{
use Conditionable, ForwardsCalls, Macroable {
__call as macroCall;
}
/**
* The name of the factory's corresponding model.
*
* @var class-string<\Illuminate\Database\Eloquent\Model|TModel>
*/
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;
/**
* The name of the database connection that will be used to create the models.
*
* @var string|null
*/
protected $connection;
/**
* The current Faker instance.
*
* @var \Faker\Generator
*/
protected $faker;
/**
* The default namespace where factories reside.
*
* @var string
*/
protected static $namespace = 'Database\\Factories\\';
/**
* The default model name resolver.
*
* @var callable
*/
protected static $modelNameResolver;
/**
* The factory name resolver.
*
* @var callable
*/
protected static $factoryNameResolver;
/**
* 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 string|null $connection
* @param \Illuminate\Support\Collection|null $recycle
* @return void
*/
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)
{
$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();
}
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
abstract public function definition();
/**
* Get a new factory instance for the given attributes.
*
* @param (callable(array<string, mixed>): array<string, mixed>)|array<string, mixed> $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 $this
*/
public function configure()
{
return $this;
}
/**
* Get the raw attributes generated by the factory.
*
* @param (callable(array<string, mixed>): array<string, mixed>)|array<string, mixed> $attributes
* @param \Illuminate\Database\Eloquent\Model|null $parent
* @return array<int|string, mixed>
*/
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<string, mixed>): array<string, mixed>)|array<string, mixed> $attributes
* @return \Illuminate\Database\Eloquent\Model|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<string, mixed>): array<string, mixed>)|array<string, mixed> $attributes
* @return \Illuminate\Database\Eloquent\Model|TModel
*/
public function createOneQuietly($attributes = [])
{
return $this->count(null)->createQuietly($attributes);
}
/**
* Create a collection of models and persist them to the database.
*
* @param iterable<int, array<string, mixed>> $records
* @return \Illuminate\Database\Eloquent\Collection<int, \Illuminate\Database\Eloquent\Model|TModel>
*/
public function createMany(iterable $records)
{
return new EloquentCollection(
collect($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 iterable<int, array<string, mixed>> $records
* @return \Illuminate\Database\Eloquent\Collection<int, \Illuminate\Database\Eloquent\Model|TModel>
*/
public function createManyQuietly(iterable $records)
{
return Model::withoutEvents(function () use ($records) {
return $this->createMany($records);
});
}
/**
* Create a collection of models and persist them to the database.
*
* @param (callable(array<string, mixed>): array<string, mixed>)|array<string, mixed> $attributes
* @param \Illuminate\Database\Eloquent\Model|null $parent
* @return \Illuminate\Database\Eloquent\Collection<int, \Illuminate\Database\Eloquent\Model|TModel>|\Illuminate\Database\Eloquent\Model|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(collect([$results]));
$this->callAfterCreating(collect([$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<string, mixed>): array<string, mixed>)|array<string, mixed> $attributes
* @param \Illuminate\Database\Eloquent\Model|null $parent
* @return \Illuminate\Database\Eloquent\Collection<int, \Illuminate\Database\Eloquent\Model|TModel>|\Illuminate\Database\Eloquent\Model|TModel
*/
public function createQuietly($attributes = [], ?Model $parent = null)
{
return Model::withoutEvents(function () use ($attributes, $parent) {
return $this->create($attributes, $parent);
});
}
/**
* Create a callback that persists a model in the database when invoked.
*
* @param array<string, mixed> $attributes
* @param \Illuminate\Database\Eloquent\Model|null $parent
* @return \Closure(): (\Illuminate\Database\Eloquent\Collection<int, \Illuminate\Database\Eloquent\Model|TModel>|\Illuminate\Database\Eloquent\Model|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<string, mixed>): array<string, mixed>)|array<string, mixed> $attributes
* @return \Illuminate\Database\Eloquent\Model|TModel
*/
public function makeOne($attributes = [])
{
return $this->count(null)->make($attributes);
}
/**
* Create a collection of models.
*
* @param (callable(array<string, mixed>): array<string, mixed>)|array<string, mixed> $attributes
* @param \Illuminate\Database\Eloquent\Model|null $parent
* @return \Illuminate\Database\Eloquent\Collection<int, \Illuminate\Database\Eloquent\Model|TModel>|\Illuminate\Database\Eloquent\Model|TModel
*/
public function make($attributes = [], ?Model $parent = null)
{
if (! empty($attributes)) {
return $this->state($attributes)->make([], $parent);
}
if ($this->count === null) {
return tap($this->makeInstance($parent), function ($instance) {
$this->callAfterMaking(collect([$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;
}
/**
* 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()
{
$model = $this->newModel();
return $this->for->map(function (BelongsToRelationship $for) use ($model) {
return $for->recycle($this->recycle)->attributesFor($model);
})->collapse()->all();
}
/**
* Expand all attributes to their underlying values.
*
* @param array $definition
* @return array
*/
protected function expandAttributes(array $definition)
{
return collect($definition)
->map($evaluateRelations = function ($attribute) {
if ($attribute instanceof self) {
$attribute = $this->getRandomRecycledModel($attribute->modelName())
?? $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);
$definition[$key] = $attribute;
return $attribute;
})
->all();
}
/**
* Add a new state transformation to the model definition.
*
* @param (callable(array<string, mixed>, \Illuminate\Database\Eloquent\Model|null): array<string, mixed>)|array<string, mixed> $state
* @return static
*/
public function state($state)
{
return $this->newInstance([
'states' => $this->states->concat([
is_callable($state) ? $state : function () use ($state) {
return $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 array $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<string, mixed>)|array<string, mixed> $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.
*
* @param string $modelClassName
* @return \Illuminate\Database\Eloquent\Model|null
*/
public function getRandomRecycledModel($modelClassName)
{
return $this->recycle->get($modelClassName)?->random();
}
/**
* Add a new "after making" callback to the model definition.
*
* @param \Closure(\Illuminate\Database\Eloquent\Model|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(\Illuminate\Database\Eloquent\Model|TModel): mixed $callback
* @return static
*/
public function afterCreating(Closure $callback)
{
return $this->newInstance(['afterCreating' => $this->afterCreating->concat([$callback])]);
}
/**
* 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]);
}
/**
* Specify the database connection that should be used to generate models.
*
* @param string $connection
* @return static
*/
public function connection(string $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,
], $arguments)));
}
/**
* Get a new model instance.
*
* @param array<string, mixed> $attributes
* @return \Illuminate\Database\Eloquent\Model|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<\Illuminate\Database\Eloquent\Model|TModel>
*/
public function modelName()
{
$resolver = static::$modelNameResolver ?? function (self $factory) {
$namespacedFactoryBasename = Str::replaceLast(
'Factory', '', Str::replaceFirst(static::$namespace, '', get_class($factory))
);
$factoryBasename = Str::replaceLast('Factory', '', class_basename($factory));
$appNamespace = static::appNamespace();
return class_exists($appNamespace.'Models\\'.$namespacedFactoryBasename)
? $appNamespace.'Models\\'.$namespacedFactoryBasename
: $appNamespace.$factoryBasename;
};
return $this->model ?? $resolver($this);
}
/**
* Specify the callback that should be invoked to guess model names based on factory names.
*
* @param callable(self): class-string<\Illuminate\Database\Eloquent\Model|TModel> $callback
* @return void
*/
public static function guessModelNamesUsing(callable $callback)
{
static::$modelNameResolver = $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.
*
* @param class-string<\Illuminate\Database\Eloquent\Model> $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;
}
/**
* Get a new Faker instance.
*
* @return \Faker\Generator
*/
protected function withFaker()
{
return Container::getInstance()->make(Generator::class);
}
/**
* Get the factory name for the given model name.
*
* @param class-string<\Illuminate\Database\Eloquent\Model> $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 $e) {
return 'App\\';
}
}
/**
* 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' && in_array(SoftDeletes::class, class_uses_recursive($this->modelName()))) {
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
);
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Illuminate\Database\Eloquent\Factories;
trait HasFactory
{
/**
* Get a new factory instance for the model.
*
* @param callable|array|int|null $count
* @param callable|array $state
* @return \Illuminate\Database\Eloquent\Factories\Factory<static>
*/
public static function factory($count = null, $state = [])
{
$factory = static::newFactory() ?: Factory::factoryForModel(get_called_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 \Illuminate\Database\Eloquent\Factories\Factory<static>
*/
protected static function newFactory()
{
//
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Illuminate\Database\Eloquent\Factories;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
use Illuminate\Database\Eloquent\Relations\MorphOneOrMany;
class Relationship
{
/**
* The related factory instance.
*
* @var \Illuminate\Database\Eloquent\Factories\Factory
*/
protected $factory;
/**
* The relationship name.
*
* @var string
*/
protected $relationship;
/**
* Create a new child relationship instance.
*
* @param \Illuminate\Database\Eloquent\Factories\Factory $factory
* @param string $relationship
* @return void
*/
public function __construct(Factory $factory, $relationship)
{
$this->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(),
])->create([], $parent);
} elseif ($relationship instanceof HasOneOrMany) {
$this->factory->state([
$relationship->getForeignKeyName() => $relationship->getParentKey(),
])->create([], $parent);
} elseif ($relationship instanceof BelongsToMany) {
$relationship->attach($this->factory->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;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Illuminate\Database\Eloquent\Factories;
use Countable;
class Sequence implements Countable
{
/**
* The sequence of return values.
*
* @var array
*/
protected $sequence;
/**
* The count of the sequence items.
*
* @var int
*/
public $count;
/**
* The current index of the sequence iteration.
*
* @var int
*/
public $index = 0;
/**
* Create a new sequence instance.
*
* @param array $sequence
* @return void
*/
public function __construct(...$sequence)
{
$this->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.
*
* @return mixed
*/
public function __invoke()
{
return tap(value($this->sequence[$this->index % $this->count], $this), function () {
$this->index = $this->index + 1;
});
}
}

View File

@@ -1,253 +0,0 @@
<?php
namespace Illuminate\Database\Eloquent;
use ArrayAccess;
use Faker\Generator as Faker;
use Symfony\Component\Finder\Finder;
class Factory implements ArrayAccess
{
/**
* The model definitions in the container.
*
* @var array
*/
protected $definitions = [];
/**
* The registered model states.
*
* @var array
*/
protected $states = [];
/**
* The Faker instance for the builder.
*
* @var \Faker\Generator
*/
protected $faker;
/**
* Create a new factory instance.
*
* @param \Faker\Generator $faker
* @return void
*/
public function __construct(Faker $faker)
{
$this->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]);
}
}

View File

@@ -1,283 +0,0 @@
<?php
namespace Illuminate\Database\Eloquent;
use Closure;
use Faker\Generator as Faker;
use InvalidArgumentException;
use Illuminate\Support\Traits\Macroable;
class FactoryBuilder
{
use Macroable;
/**
* The model definitions in the container.
*
* @var array
*/
protected $definitions;
/**
* The model being built.
*
* @var string
*/
protected $class;
/**
* The name of the model being built.
*
* @var string
*/
protected $name = 'default';
/**
* The model states.
*
* @var array
*/
protected $states;
/**
* The states to apply.
*
* @var array
*/
protected $activeStates = [];
/**
* The Faker instance for the builder.
*
* @var \Faker\Generator
*/
protected $faker;
/**
* The number of models to build.
*
* @var int|null
*/
protected $amount = null;
/**
* Create an new builder instance.
*
* @param string $class
* @param string $name
* @param array $definitions
* @param array $states
* @param \Faker\Generator $faker
* @return void
*/
public function __construct($class, $name, array $definitions, array $states, Faker $faker)
{
$this->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|mixed $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->newQueryWithoutScopes()->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);
}
if ($attribute instanceof static) {
$attribute = $attribute->create()->getKey();
}
if ($attribute instanceof Model) {
$attribute = $attribute->getKey();
}
}
return $attributes;
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Illuminate\Database\Eloquent;
/**
* @mixin \Illuminate\Database\Eloquent\Builder
*/
class HigherOrderBuilderProxy
{
/**
* The collection being operated on.
*
* @var \Illuminate\Database\Eloquent\Builder
*/
protected $builder;
/**
* The method being proxied.
*
* @var string
*/
protected $method;
/**
* Create a new proxy instance.
*
* @param \Illuminate\Database\Eloquent\Builder $builder
* @param string $method
* @return void
*/
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);
});
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Illuminate\Database\Eloquent;
use RuntimeException;
class InvalidCastException extends RuntimeException
{
/**
* The name of the affected Eloquent model.
*
* @var string
*/
public $model;
/**
* The name of the column.
*
* @var string
*/
public $column;
/**
* The name of the cast type.
*
* @var string
*/
public $castType;
/**
* Create a new exception instance.
*
* @param object $model
* @param string $column
* @param string $castType
* @return static
*/
public function __construct($model, $column, $castType)
{
$class = get_class($model);
parent::__construct("Call to undefined cast [{$castType}] on column [{$column}] in model [{$class}].");
$this->model = $class;
$this->column = $column;
$this->castType = $castType;
}
}

View File

@@ -18,12 +18,26 @@ class JsonEncodingException extends RuntimeException
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)

View File

@@ -0,0 +1,48 @@
<?php
namespace Illuminate\Database\Eloquent;
use Illuminate\Database\Events\ModelsPruned;
use LogicException;
trait MassPrunable
{
/**
* Prune all prunable models in the database.
*
* @param int $chunkSize
* @return int
*/
public function pruneAll(int $chunkSize = 1000)
{
$query = tap($this->prunable(), function ($query) use ($chunkSize) {
$query->when(! $query->getQuery()->limit, function ($query) use ($chunkSize) {
$query->limit($chunkSize);
});
});
$total = 0;
do {
$total += $count = in_array(SoftDeletes::class, class_uses_recursive(get_class($this)))
? $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
*/
public function prunable()
{
throw new LogicException('Please implement the prunable method on your model.');
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Illuminate\Database\Eloquent;
use OutOfBoundsException;
class MissingAttributeException extends OutOfBoundsException
{
/**
* Create a new missing attribute exception instance.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @return void
*/
public function __construct($model, $key)
{
parent::__construct(sprintf(
'The attribute [%s] either does not exist or was not retrieved for model [%s].',
$key, get_class($model)
));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,35 +2,39 @@
namespace Illuminate\Database\Eloquent;
use RuntimeException;
use Illuminate\Database\RecordsNotFoundException;
use Illuminate\Support\Arr;
class ModelNotFoundException extends RuntimeException
/**
* @template TModel of \Illuminate\Database\Eloquent\Model
*/
class ModelNotFoundException extends RecordsNotFoundException
{
/**
* Name of the affected Eloquent model.
*
* @var string
* @var class-string<TModel>
*/
protected $model;
/**
* The affected model IDs.
*
* @var int|array
* @var array<int, int|string>
*/
protected $ids;
/**
* Set the affected Eloquent model and instance ids.
*
* @param string $model
* @param int|array $ids
* @param class-string<TModel> $model
* @param array<int, int|string>|int|string $ids
* @return $this
*/
public function setModel($model, $ids = [])
{
$this->model = $model;
$this->ids = array_wrap($ids);
$this->ids = Arr::wrap($ids);
$this->message = "No query results for model [{$model}]";
@@ -46,7 +50,7 @@ class ModelNotFoundException extends RuntimeException
/**
* Get the affected Eloquent model.
*
* @return string
* @return class-string<TModel>
*/
public function getModel()
{
@@ -56,7 +60,7 @@ class ModelNotFoundException extends RuntimeException
/**
* Get the affected Eloquent model IDs.
*
* @return int|array
* @return array<int, int|string>
*/
public function getIds()
{

View File

@@ -0,0 +1,67 @@
<?php
namespace Illuminate\Database\Eloquent;
use Illuminate\Database\Events\ModelsPruned;
use LogicException;
trait Prunable
{
/**
* Prune all prunable models in the database.
*
* @param int $chunkSize
* @return int
*/
public function pruneAll(int $chunkSize = 1000)
{
$total = 0;
$this->prunable()
->when(in_array(SoftDeletes::class, class_uses_recursive(get_class($this))), function ($query) {
$query->withTrashed();
})->chunkById($chunkSize, function ($models) use (&$total) {
$models->each->prune();
$total += $models->count();
event(new ModelsPruned(static::class, $total));
});
return $total;
}
/**
* Get the prunable model query.
*
* @return \Illuminate\Database\Eloquent\Builder
*/
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 in_array(SoftDeletes::class, class_uses_recursive(get_class($this)))
? $this->forceDelete()
: $this->delete();
}
/**
* Prepare the model for pruning.
*
* @return void
*/
protected function pruning()
{
//
}
}

View File

@@ -6,17 +6,41 @@ use RuntimeException;
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 mixed $model
* @param object $model
* @param string $relation
* @param string|null $type
* @return static
*/
public static function make($model, $relation)
public static function make($model, $relation, $type = null)
{
$class = get_class($model);
return new static("Call to undefined relationship [{$relation}] on model [{$class}].");
$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;
}
}

View File

@@ -2,17 +2,23 @@
namespace Illuminate\Database\Eloquent\Relations;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
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;
class BelongsTo extends Relation
{
use SupportsDefaultModels;
use ComparesRelatedModels,
InteractsWithDictionary,
SupportsDefaultModels;
/**
* The child model instance of the relation.
*
* @var \Illuminate\Database\Eloquent\Model
*/
protected $child;
@@ -35,14 +41,7 @@ class BelongsTo extends Relation
*
* @var string
*/
protected $relation;
/**
* The count of self joins.
*
* @var int
*/
protected static $selfJoinCount = 0;
protected $relationName;
/**
* Create a new belongs to relationship instance.
@@ -51,13 +50,13 @@ class BelongsTo extends Relation
* @param \Illuminate\Database\Eloquent\Model $child
* @param string $foreignKey
* @param string $ownerKey
* @param string $relation
* @param string $relationName
* @return void
*/
public function __construct(Builder $query, Model $child, $foreignKey, $ownerKey, $relation)
public function __construct(Builder $query, Model $child, $foreignKey, $ownerKey, $relationName)
{
$this->ownerKey = $ownerKey;
$this->relation = $relation;
$this->relationName = $relationName;
$this->foreignKey = $foreignKey;
// In the underlying base relationship class, this variable is referred to as
@@ -75,6 +74,10 @@ class BelongsTo extends Relation
*/
public function getResults()
{
if (is_null($this->child->{$this->foreignKey})) {
return $this->getDefaultFor($this->parent);
}
return $this->query->first() ?: $this->getDefaultFor($this->parent);
}
@@ -108,7 +111,9 @@ class BelongsTo extends Relation
// our eagerly loading query so it returns the proper models from execution.
$key = $this->related->getTable().'.'.$this->ownerKey;
$this->query->whereIn($key, $this->getEagerModelKeys($models));
$whereIn = $this->whereInMethod($this->related, $this->ownerKey);
$this->query->{$whereIn}($key, $this->getEagerModelKeys($models));
}
/**
@@ -130,13 +135,6 @@ class BelongsTo extends Relation
}
}
// If there are no keys that were not null we will just return an array with null
// so this query wont fail plus returns zero results, which should be what the
// developer expects to happen in this situation. Otherwise we'll sort them.
if (count($keys) === 0) {
return [null];
}
sort($keys);
return array_values(array_unique($keys));
@@ -145,7 +143,7 @@ class BelongsTo extends Relation
/**
* Initialize the relation on a set of models.
*
* @param array $models
* @param array $models
* @param string $relation
* @return array
*/
@@ -161,7 +159,7 @@ class BelongsTo extends Relation
/**
* Match the eagerly loaded results to their parents.
*
* @param array $models
* @param array $models
* @param \Illuminate\Database\Eloquent\Collection $results
* @param string $relation
* @return array
@@ -178,36 +176,29 @@ class BelongsTo extends Relation
$dictionary = [];
foreach ($results as $result) {
$dictionary[$result->getAttribute($owner)] = $result;
$attribute = $this->getDictionaryKey($result->getAttribute($owner));
$dictionary[$attribute] = $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) {
if (isset($dictionary[$model->{$foreign}])) {
$model->setRelation($relation, $dictionary[$model->{$foreign}]);
$attribute = $this->getDictionaryKey($model->{$foreign});
if (isset($dictionary[$attribute])) {
$model->setRelation($relation, $dictionary[$attribute]);
}
}
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 \Illuminate\Database\Eloquent\Model|int|string $model
* @param \Illuminate\Database\Eloquent\Model|int|string|null $model
* @return \Illuminate\Database\Eloquent\Model
*/
public function associate($model)
@@ -217,7 +208,9 @@ class BelongsTo extends Relation
$this->child->setAttribute($this->foreignKey, $ownerKey);
if ($model instanceof Model) {
$this->child->setRelation($this->relation, $model);
$this->child->setRelation($this->relationName, $model);
} else {
$this->child->unsetRelation($this->relationName);
}
return $this->child;
@@ -232,7 +225,17 @@ class BelongsTo extends Relation
{
$this->child->setAttribute($this->foreignKey, null);
return $this->child->setRelation($this->relation, null);
return $this->child->setRelation($this->relationName, null);
}
/**
* Alias of "dissociate" method.
*
* @return \Illuminate\Database\Eloquent\Model
*/
public function disassociate()
{
return $this->dissociate();
}
/**
@@ -250,7 +253,7 @@ class BelongsTo extends Relation
}
return $query->select($columns)->whereColumn(
$this->getQualifiedForeignKey(), '=', $query->getModel()->getTable().'.'.$this->ownerKey
$this->getQualifiedForeignKeyName(), '=', $query->qualifyColumn($this->ownerKey)
);
}
@@ -271,20 +274,10 @@ class BelongsTo extends Relation
$query->getModel()->setTable($hash);
return $query->whereColumn(
$hash.'.'.$query->getModel()->getKeyName(), '=', $this->getQualifiedForeignKey()
$hash.'.'.$this->ownerKey, '=', $this->getQualifiedForeignKeyName()
);
}
/**
* Get a relationship join table hash.
*
* @return string
*/
public function getRelationCountHash()
{
return 'laravel_reserved_'.static::$selfJoinCount++;
}
/**
* Determine if the related model has an auto-incrementing ID.
*
@@ -293,7 +286,7 @@ class BelongsTo extends Relation
protected function relationHasIncrementingId()
{
return $this->related->getIncrementing() &&
$this->related->getKeyType() === 'int';
in_array($this->related->getKeyType(), ['int', 'integer']);
}
/**
@@ -307,12 +300,22 @@ class BelongsTo extends Relation
return $this->related->newInstance();
}
/**
* Get the child of the relationship.
*
* @return \Illuminate\Database\Eloquent\Model
*/
public function getChild()
{
return $this->child;
}
/**
* Get the foreign key of the relationship.
*
* @return string
*/
public function getForeignKey()
public function getForeignKeyName()
{
return $this->foreignKey;
}
@@ -322,9 +325,19 @@ class BelongsTo extends Relation
*
* @return string
*/
public function getQualifiedForeignKey()
public function getQualifiedForeignKeyName()
{
return $this->child->getTable().'.'.$this->foreignKey;
return $this->child->qualifyColumn($this->foreignKey);
}
/**
* Get the key value of the child's foreign key.
*
* @return mixed
*/
public function getParentKey()
{
return $this->child->{$this->foreignKey};
}
/**
@@ -332,7 +345,7 @@ class BelongsTo extends Relation
*
* @return string
*/
public function getOwnerKey()
public function getOwnerKeyName()
{
return $this->ownerKey;
}
@@ -344,7 +357,18 @@ class BelongsTo extends Relation
*/
public function getQualifiedOwnerKeyName()
{
return $this->related->getTable().'.'.$this->ownerKey;
return $this->related->qualifyColumn($this->ownerKey);
}
/**
* Get the value of the model's associated key.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return mixed
*/
protected function getRelatedKeyFrom(Model $model)
{
return $model->{$this->ownerKey};
}
/**
@@ -352,8 +376,8 @@ class BelongsTo extends Relation
*
* @return string
*/
public function getRelation()
public function getRelationName()
{
return $this->relation;
return $this->relationName;
}
}

View File

@@ -0,0 +1,333 @@
<?php
namespace Illuminate\Database\Eloquent\Relations\Concerns;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
trait 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;
/**
* The name of the "other key" column.
*
* @var string
*/
protected $relatedKey;
/**
* Create a new pivot model instance.
*
* @param \Illuminate\Database\Eloquent\Model $parent
* @param array $attributes
* @param string $table
* @param bool $exists
* @return static
*/
public static function fromAttributes(Model $parent, $attributes, $table, $exists = false)
{
$instance = new static;
$instance->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->setTable(str_replace(
'\\', '', Str::snake(Str::singular(class_basename($this)))
));
}
return $this->table;
}
/**
* 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 or given attributes has timestamp attributes.
*
* @param array|null $attributes
* @return bool
*/
public function hasTimestampAttributes($attributes = null)
{
return array_key_exists($this->getCreatedAtColumn(), $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->relations = [];
return $this;
}
}

View File

@@ -0,0 +1,310 @@
<?php
namespace Illuminate\Database\Eloquent\Relations\Concerns;
use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Arr;
use InvalidArgumentException;
trait CanBeOneOfMany
{
/**
* Determines whether the relationship is one-of-many.
*
* @var bool
*/
protected $isOneOfMany = false;
/**
* The name of the relationship.
*
* @var string
*/
protected $relationName;
/**
* The one of many inner join subselect query builder instance.
*
* @var \Illuminate\Database\Eloquent\Builder|null
*/
protected $oneOfManySubQuery;
/**
* Add constraints for inner join subselect for one of many relationships.
*
* @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(),
$column, $aggregate
);
if (isset($previous)) {
$this->addOneOfManyJoinSubQuery($subQuery, $previous['subQuery'], $previous['column']);
}
if (isset($closure)) {
$closure($subQuery);
}
if (! isset($previous)) {
$this->oneOfManySubQuery = $subQuery;
}
if (array_key_last($columns) == $column) {
$this->addOneOfManyJoinSubQuery($this->query, $subQuery, $column);
}
$previous = [
'subQuery' => $subQuery,
'column' => $column,
];
}
$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(collect(Arr::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(collect(Arr::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 string|null $column
* @param string|null $aggregate
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function newOneOfManySubQuery($groupBy, $column = 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($column)) {
$subQuery->selectRaw($aggregate.'('.$subQuery->getQuery()->grammar->wrap($subQuery->qualifyColumn($column)).') as '.$subQuery->getQuery()->grammar->wrap($column.'_aggregate'));
}
$this->addOneOfManySubQueryConstraints($subQuery, $groupBy, $column, $aggregate);
return $subQuery;
}
/**
* Add the join subquery to the given query on the given column and the relationship's foreign key.
*
* @param \Illuminate\Database\Eloquent\Builder $parent
* @param \Illuminate\Database\Eloquent\Builder $subQuery
* @param string $on
* @return void
*/
protected function addOneOfManyJoinSubQuery(Builder $parent, Builder $subQuery, $on)
{
$parent->beforeQuery(function ($parent) use ($subQuery, $on) {
$subQuery->applyBeforeQueryCallbacks();
$parent->joinSub($subQuery, $this->relationName, function ($join) use ($on) {
$join->on($this->qualifySubSelectColumn($on.'_aggregate'), '=', $this->qualifyRelatedColumn($on));
$this->addOneOfManyJoinSubQueryConstraints($join, $on);
});
});
}
/**
* 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 str_contains($column, '.') ? $column : $this->query->getModel()->getTable().'.'.$column;
}
/**
* Guess the "hasOne" relationship's name via backtrace.
*
* @return string
*/
protected function guessRelationship()
{
return debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['function'];
}
/**
* Determine whether the relationship is a one-of-many relationship.
*
* @return bool
*/
public function isOneOfMany()
{
return $this->isOneOfMany;
}
/**
* Get the name of the relationship.
*
* @return string
*/
public function getRelationName()
{
return $this->relationName;
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Illuminate\Database\Eloquent\Relations\Concerns;
use Illuminate\Contracts\Database\Eloquent\SupportsPartialRelations;
use Illuminate\Database\Eloquent\Model;
trait ComparesRelatedModels
{
/**
* Determine if the model is the related instance of the relationship.
*
* @param \Illuminate\Database\Eloquent\Model|null $model
* @return bool
*/
public function is($model)
{
$match = ! is_null($model) &&
$this->compareKeys($this->getParentKey(), $this->getRelatedKeyFrom($model)) &&
$this->related->getTable() === $model->getTable() &&
$this->related->getConnectionName() === $model->getConnectionName();
if ($match && $this instanceof SupportsPartialRelations && $this->isOneOfMany()) {
return $this->query
->whereKey($model->getKey())
->exists();
}
return $match;
}
/**
* 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;
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Illuminate\Database\Eloquent\Relations\Concerns;
use BackedEnum;
use Doctrine\Instantiator\Exception\InvalidArgumentException;
use UnitEnum;
trait InteractsWithDictionary
{
/**
* Get a dictionary key attribute - casting it to a string if necessary.
*
* @param mixed $attribute
* @return mixed
*
* @throws \Doctrine\Instantiator\Exception\InvalidArgumentException
*/
protected function getDictionaryKey($attribute)
{
if (is_object($attribute)) {
if (method_exists($attribute, '__toString')) {
return $attribute->__toString();
}
if (function_exists('enum_exists') &&
$attribute instanceof UnitEnum) {
return $attribute instanceof BackedEnum ? $attribute->value : $attribute->name;
}
throw new InvalidArgumentException('Model attribute value is an object but does not have a __toString method.');
}
return $attribute;
}
}

View File

@@ -2,8 +2,9 @@
namespace Illuminate\Database\Eloquent\Relations\Concerns;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Support\Collection as BaseCollection;
trait InteractsWithPivotTable
@@ -14,7 +15,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)
@@ -23,13 +24,13 @@ trait InteractsWithPivotTable
'attached' => [], 'detached' => [],
];
$records = $this->formatRecordsList((array) $this->parseIds($ids));
$records = $this->formatRecordsList($this->parseIds($ids));
// Next, we will determine which IDs should get removed from the join table by
// checking which of the given ID/records is in the list of current records
// and removing all of those rows from this "intermediate" joining table.
$detach = array_values(array_intersect(
$this->newPivotQuery()->pluck($this->relatedKey)->all(),
$this->newPivotQuery()->pluck($this->relatedPivotKey)->all(),
array_keys($records)
));
@@ -64,7 +65,7 @@ trait InteractsWithPivotTable
/**
* Sync the intermediate tables with a list of IDs without detaching.
*
* @param \Illuminate\Database\Eloquent\Collection|array $ids
* @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids
* @return array
*/
public function syncWithoutDetaching($ids)
@@ -75,8 +76,8 @@ trait InteractsWithPivotTable
/**
* Sync the intermediate tables with a list of IDs or collection of models.
*
* @param \Illuminate\Database\Eloquent\Collection|\Illuminate\Support\Collection|array $ids
* @param bool $detaching
* @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids
* @param bool $detaching
* @return array
*/
public function sync($ids, $detaching = true)
@@ -88,21 +89,22 @@ trait InteractsWithPivotTable
// First we need to attach any of the associated models that are not currently
// in this joining table. We'll spin through the given IDs, checking to see
// if they exist in the array of current ones, and if not we will insert.
$current = $this->newPivotQuery()->pluck(
$this->relatedKey
)->all();
$current = $this->getCurrentlyAttachedPivots()
->pluck($this->relatedPivotKey)->all();
$detach = array_diff($current, array_keys(
$records = $this->formatRecordsList((array) $this->parseIds($ids))
));
$records = $this->formatRecordsList($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 && count($detach) > 0) {
$this->detach($detach);
if ($detaching) {
$detach = array_diff($current, array_keys($records));
$changes['detached'] = $this->castKeys($detach);
if (count($detach) > 0) {
$this->detach($detach);
$changes['detached'] = $this->castKeys($detach);
}
}
// Now we are finally ready to attach the new records. Note that we'll disable
@@ -116,13 +118,29 @@ trait InteractsWithPivotTable
// 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['updated']) ||
count($changes['detached'])) {
$this->touchIfTouching();
}
return $changes;
}
/**
* Sync the intermediate tables with a list of IDs or collection of models with the given pivot values.
*
* @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids
* @param array $values
* @param bool $detaching
* @return array
*/
public function syncWithPivotValues($ids, array $values, bool $detaching = true)
{
return $this->sync(collect($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.
*
@@ -133,7 +151,7 @@ trait InteractsWithPivotTable
{
return collect($records)->mapWithKeys(function ($attributes, $id) {
if (! is_array($attributes)) {
list($id, $attributes) = [$attributes, []];
[$id, $attributes] = [$attributes, []];
}
return [$id => $attributes];
@@ -145,7 +163,7 @@ trait InteractsWithPivotTable
*
* @param array $records
* @param array $current
* @param bool $touch
* @param bool $touch
* @return array
*/
protected function attachNew(array $records, array $current, $touch = true)
@@ -179,16 +197,25 @@ trait InteractsWithPivotTable
*
* @param mixed $id
* @param array $attributes
* @param bool $touch
* @param bool $touch
* @return int
*/
public function updateExistingPivot($id, array $attributes, $touch = true)
{
if (in_array($this->updatedAt(), $this->pivotColumns)) {
if ($this->using &&
empty($this->pivotWheres) &&
empty($this->pivotWhereIns) &&
empty($this->pivotWhereNulls)) {
return $this->updateExistingPivotUsingCustomClass($id, $attributes, $touch);
}
if ($this->hasPivotColumn($this->updatedAt())) {
$attributes = $this->addTimestampsToAttachment($attributes, true);
}
$updated = $this->newPivotStatementForId($id)->update($attributes);
$updated = $this->newPivotStatementForId($this->parseId($id))->update(
$this->castAttributes($attributes)
);
if ($touch) {
$this->touchIfTouching();
@@ -197,28 +224,78 @@ trait InteractsWithPivotTable
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->getCurrentlyAttachedPivots()
->where($this->foreignPivotKey, $this->parent->{$this->parentKey})
->where($this->relatedPivotKey, $this->parseId($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 $id
* @param array $attributes
* @param bool $touch
* @param bool $touch
* @return void
*/
public function attach($id, array $attributes = [], $touch = true)
{
// 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 ($this->using) {
$this->attachUsingCustomClass($id, $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($id), $attributes
));
}
if ($touch) {
$this->touchIfTouching();
}
}
/**
* Attach a model to the parent using a custom class.
*
* @param mixed $id
* @param array $attributes
* @return void
*/
protected function attachUsingCustomClass($id, array $attributes)
{
$records = $this->formatAttachRecords(
$this->parseIds($id), $attributes
);
foreach ($records as $record) {
$this->newPivot($record, false)->save();
}
}
/**
* Create an array of records to insert into the pivot table.
*
@@ -248,18 +325,18 @@ trait InteractsWithPivotTable
/**
* 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)
{
list($id, $attributes) = $this->extractAttachIdAndAttributes($key, $value, $attributes);
[$id, $attributes] = $this->extractAttachIdAndAttributes($key, $value, $attributes);
return array_merge(
$this->baseAttachRecord($id, $hasTimestamps), $attributes
$this->baseAttachRecord($id, $hasTimestamps), $this->castAttributes($attributes)
);
}
@@ -281,15 +358,15 @@ trait InteractsWithPivotTable
/**
* Create a new pivot attachment record.
*
* @param int $id
* @param int $id
* @param bool $timed
* @return array
*/
protected function baseAttachRecord($id, $timed)
{
$record[$this->relatedKey] = $id;
$record[$this->relatedPivotKey] = $id;
$record[$this->foreignKey] = $this->parent->getKey();
$record[$this->foreignPivotKey] = $this->parent->{$this->parentKey};
// If the record needs to have creation and update timestamps, we will make
// them by calling the parent model's "freshTimestamp" method which will
@@ -298,6 +375,10 @@ trait InteractsWithPivotTable
$record = $this->addTimestampsToAttachment($record);
}
foreach ($this->pivotValues as $value) {
$record[$value['column']] = $value['value'];
}
return $record;
}
@@ -305,13 +386,19 @@ trait InteractsWithPivotTable
* 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 = $fresh->format($pivotModel->getDateFormat());
}
if (! $exists && $this->hasPivotColumn($this->createdAt())) {
$record[$this->createdAt()] = $fresh;
}
@@ -329,7 +416,7 @@ trait InteractsWithPivotTable
* @param string $column
* @return bool
*/
protected function hasPivotColumn($column)
public function hasPivotColumn($column)
{
return in_array($column, $this->pivotColumns);
}
@@ -343,24 +430,34 @@ trait InteractsWithPivotTable
*/
public function detach($ids = null, $touch = true)
{
$query = $this->newPivotQuery();
if ($this->using &&
! empty($ids) &&
empty($this->pivotWheres) &&
empty($this->pivotWhereIns) &&
empty($this->pivotWhereNulls)) {
$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 = $this->parseIds($ids))) {
if (count($ids) === 0) {
return 0;
// 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->whereIn($this->relatedKey, (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();
}
// 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();
}
@@ -368,11 +465,47 @@ trait InteractsWithPivotTable
return $results;
}
/**
* Detach models from the relationship using a custom class.
*
* @param mixed $ids
* @return int
*/
protected function detachUsingCustomClass($ids)
{
$results = 0;
foreach ($this->parseIds($ids) as $id) {
$results += $this->newPivot([
$this->foreignPivotKey => $this->parent->{$this->parentKey},
$this->relatedPivotKey => $id,
], true)->delete();
}
return $results;
}
/**
* Get the pivot models that are currently attached.
*
* @return \Illuminate\Support\Collection
*/
protected function getCurrentlyAttachedPivots()
{
return $this->newPivotQuery()->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);
});
}
/**
* 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)
@@ -381,7 +514,7 @@ trait InteractsWithPivotTable
$this->parent, $attributes, $this->table, $exists, $this->using
);
return $pivot->setPivotKeys($this->foreignKey, $this->relatedKey);
return $pivot->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey);
}
/**
@@ -413,7 +546,7 @@ trait InteractsWithPivotTable
*/
public function newPivotStatementForId($id)
{
return $this->newPivotQuery()->where($this->relatedKey, $id);
return $this->newPivotQuery()->whereIn($this->relatedPivotKey, $this->parseIds($id));
}
/**
@@ -421,19 +554,23 @@ trait InteractsWithPivotTable
*
* @return \Illuminate\Database\Query\Builder
*/
protected function newPivotQuery()
public function newPivotQuery()
{
$query = $this->newPivotStatement();
foreach ($this->pivotWheres as $arguments) {
call_user_func_array([$query, 'where'], $arguments);
$query->where(...$arguments);
}
foreach ($this->pivotWhereIns as $arguments) {
call_user_func_array([$query, 'whereIn'], $arguments);
$query->whereIn(...$arguments);
}
return $query->where($this->foreignKey, $this->parent->getKey());
foreach ($this->pivotWhereNulls as $arguments) {
$query->whereNull(...$arguments);
}
return $query->where($this->getQualifiedForeignPivotKeyName(), $this->parent->{$this->parentKey});
}
/**
@@ -460,18 +597,29 @@ trait InteractsWithPivotTable
protected function parseIds($value)
{
if ($value instanceof Model) {
return $value->getKey();
return [$value->{$this->relatedKey}];
}
if ($value instanceof Collection) {
return $value->modelKeys();
return $value->pluck($this->relatedKey)->all();
}
if ($value instanceof BaseCollection) {
return $value->toArray();
}
return $value;
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;
}
/**
@@ -482,19 +630,52 @@ trait InteractsWithPivotTable
*/
protected function castKeys(array $keys)
{
return (array) array_map(function ($v) {
return array_map(function ($v) {
return $this->castKey($v);
}, $keys);
}
/**
* Cast the given key to an integer if it is numeric.
* Cast the given key to convert to primary key type.
*
* @param mixed $key
* @return mixed
*/
protected function castKey($key)
{
return is_numeric($key) ? (int) $key : (string) $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,
};
}
}

View File

@@ -51,7 +51,7 @@ trait SupportsDefaultModels
$instance = $this->newRelatedInstanceFor($parent);
if (is_callable($this->withDefault)) {
return call_user_func($this->withDefault, $instance) ?: $instance;
return call_user_func($this->withDefault, $instance, $parent) ?: $instance;
}
if (is_array($this->withDefault)) {

View File

@@ -13,13 +13,15 @@ class HasMany extends HasOneOrMany
*/
public function getResults()
{
return $this->query->get();
return ! is_null($this->getParentKey())
? $this->query->get()
: $this->related->newCollection();
}
/**
* Initialize the relation on a set of models.
*
* @param array $models
* @param array $models
* @param string $relation
* @return array
*/
@@ -35,7 +37,7 @@ class HasMany extends HasOneOrMany
/**
* Match the eagerly loaded results to their parents.
*
* @param array $models
* @param array $models
* @param \Illuminate\Database\Eloquent\Collection $results
* @param string $relation
* @return array

View File

@@ -2,14 +2,19 @@
namespace Illuminate\Database\Eloquent\Relations;
use Illuminate\Database\Eloquent\Model;
use Closure;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
use Illuminate\Database\Eloquent\SoftDeletes;
class HasManyThrough extends Relation
{
use InteractsWithDictionary;
/**
* The "through" parent model instance.
*
@@ -45,6 +50,13 @@ class HasManyThrough extends Relation
*/
protected $localKey;
/**
* The local key on the intermediary model.
*
* @var string
*/
protected $secondLocalKey;
/**
* Create a new has many through relationship instance.
*
@@ -54,15 +66,17 @@ class HasManyThrough extends Relation
* @param string $firstKey
* @param string $secondKey
* @param string $localKey
* @param string $secondLocalKey
* @return void
*/
public function __construct(Builder $query, Model $farParent, Model $throughParent, $firstKey, $secondKey, $localKey)
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);
}
@@ -98,10 +112,22 @@ class HasManyThrough extends Relation
$query->join($this->throughParent->getTable(), $this->getQualifiedParentKeyName(), '=', $farKey);
if ($this->throughParentSoftDeletes()) {
$query->whereNull($this->throughParent->getQualifiedDeletedAtColumn());
$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.
*
@@ -109,9 +135,19 @@ class HasManyThrough extends Relation
*/
public function throughParentSoftDeletes()
{
return in_array(SoftDeletes::class, class_uses_recursive(
get_class($this->throughParent)
));
return in_array(SoftDeletes::class, class_uses_recursive($this->throughParent));
}
/**
* Indicate that trashed "through" parents should be included in the query.
*
* @return $this
*/
public function withTrashedParents()
{
$this->query->withoutGlobalScope('SoftDeletableHasManyThrough');
return $this;
}
/**
@@ -122,7 +158,9 @@ class HasManyThrough extends Relation
*/
public function addEagerConstraints(array $models)
{
$this->query->whereIn(
$whereIn = $this->whereInMethod($this->farParent, $this->localKey);
$this->query->{$whereIn}(
$this->getQualifiedFirstKeyName(), $this->getKeys($models, $this->localKey)
);
}
@@ -130,7 +168,7 @@ class HasManyThrough extends Relation
/**
* Initialize the relation on a set of models.
*
* @param array $models
* @param array $models
* @param string $relation
* @return array
*/
@@ -146,7 +184,7 @@ class HasManyThrough extends Relation
/**
* Match the eagerly loaded results to their parents.
*
* @param array $models
* @param array $models
* @param \Illuminate\Database\Eloquent\Collection $results
* @param string $relation
* @return array
@@ -159,7 +197,7 @@ class HasManyThrough extends 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) {
if (isset($dictionary[$key = $model->getKey()])) {
if (isset($dictionary[$key = $this->getDictionaryKey($model->getAttribute($this->localKey))])) {
$model->setRelation(
$relation, $this->related->newCollection($dictionary[$key])
);
@@ -183,7 +221,7 @@ class HasManyThrough extends Relation
// 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;
$dictionary[$result->laravel_through_key][] = $result;
}
return $dictionary;
@@ -220,10 +258,24 @@ class HasManyThrough extends Relation
return $instance;
}
/**
* Add a basic where clause to the query, and return the first result.
*
* @param \Closure|string|array $column
* @param mixed $operator
* @param mixed $value
* @param string $boolean
* @return \Illuminate\Database\Eloquent\Model|static
*/
public function firstWhere($column, $operator = null, $value = null, $boolean = 'and')
{
return $this->where($column, $operator, $value, $boolean)->first();
}
/**
* Execute the query and get the first related model.
*
* @param array $columns
* @param array $columns
* @return mixed
*/
public function first($columns = ['*'])
@@ -239,7 +291,7 @@ class HasManyThrough extends Relation
* @param array $columns
* @return \Illuminate\Database\Eloquent\Model|static
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException<\Illuminate\Database\Eloquent\Model>
*/
public function firstOrFail($columns = ['*'])
{
@@ -250,6 +302,28 @@ class HasManyThrough extends Relation
throw (new ModelNotFoundException)->setModel(get_class($this->related));
}
/**
* Execute the query and get the first result or call a callback.
*
* @param \Closure|array $columns
* @param \Closure|null $callback
* @return \Illuminate\Database\Eloquent\Model|static|mixed
*/
public function firstOr($columns = ['*'], Closure $callback = null)
{
if ($columns instanceof Closure) {
$callback = $columns;
$columns = ['*'];
}
if (! is_null($model = $this->first($columns))) {
return $model;
}
return $callback();
}
/**
* Find a related model by its primary key.
*
@@ -259,7 +333,7 @@ class HasManyThrough extends Relation
*/
public function find($id, $columns = ['*'])
{
if (is_array($id)) {
if (is_array($id) || $id instanceof Arrayable) {
return $this->findMany($id, $columns);
}
@@ -271,12 +345,14 @@ class HasManyThrough extends Relation
/**
* Find multiple related models by their primary keys.
*
* @param mixed $ids
* @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();
}
@@ -293,21 +369,54 @@ class HasManyThrough extends Relation
* @param array $columns
* @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException<\Illuminate\Database\Eloquent\Model>
*/
public function findOrFail($id, $columns = ['*'])
{
$result = $this->find($id, $columns);
$id = $id instanceof Arrayable ? $id->toArray() : $id;
if (is_array($id)) {
if (count($result) == count(array_unique($id))) {
if (count($result) === count(array_unique($id))) {
return $result;
}
} elseif (! is_null($result)) {
return $result;
}
throw (new ModelNotFoundException)->setModel(get_class($this->related));
throw (new ModelNotFoundException)->setModel(get_class($this->related), $id);
}
/**
* Find a related model by its primary key or call a callback.
*
* @param mixed $id
* @param \Closure|array $columns
* @param \Closure|null $callback
* @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|mixed
*/
public function findOr($id, $columns = ['*'], Closure $callback = null)
{
if ($columns instanceof Closure) {
$callback = $columns;
$columns = ['*'];
}
$result = $this->find($id, $columns);
$id = $id instanceof Arrayable ? $id->toArray() : $id;
if (is_array($id)) {
if (count($result) === count(array_unique($id))) {
return $result;
}
} elseif (! is_null($result)) {
return $result;
}
return $callback();
}
/**
@@ -317,7 +426,9 @@ class HasManyThrough extends Relation
*/
public function getResults()
{
return $this->get();
return ! is_null($this->farParent->{$this->localKey})
? $this->get()
: $this->related->newCollection();
}
/**
@@ -328,16 +439,9 @@ class HasManyThrough extends Relation
*/
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->prepareQueryBuilder($columns);
$builder = $this->query->applyScopes();
$models = $builder->addSelect(
$this->shouldSelect($columns)
)->getModels();
$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
@@ -352,7 +456,7 @@ class HasManyThrough extends Relation
/**
* Get a paginator for the "select" statement.
*
* @param int $perPage
* @param int|null $perPage
* @param array $columns
* @param string $pageName
* @param int $page
@@ -368,7 +472,7 @@ class HasManyThrough extends Relation
/**
* Paginate the given query into a simple paginator.
*
* @param int $perPage
* @param int|null $perPage
* @param array $columns
* @param string $pageName
* @param int|null $page
@@ -381,6 +485,22 @@ class HasManyThrough extends Relation
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.
*
@@ -393,7 +513,108 @@ class HasManyThrough extends Relation
$columns = [$this->related->getTable().'.*'];
}
return array_merge($columns, [$this->getQualifiedFirstKeyName()]);
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);
}
/**
* Get a generator for the given query.
*
* @return \Generator
*/
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);
}
/**
* 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)
);
}
/**
@@ -406,21 +627,67 @@ class HasManyThrough extends Relation
*/
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->getExistenceCompareKey(), '=', $this->getQualifiedFirstKeyName()
$this->getQualifiedLocalKeyName(), '=', $this->getQualifiedFirstKeyName()
);
}
/**
* Get the key for comparing against the parent key in "has" query.
* Add the constraints for a relationship query on the same table.
*
* @return string
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Builder $parentQuery
* @param array|mixed $columns
* @return \Illuminate\Database\Eloquent\Builder
*/
public function getExistenceCompareKey()
public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])
{
return $this->farParent->getQualifiedKeyName();
$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 array|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
);
}
/**
@@ -434,13 +701,13 @@ class HasManyThrough extends Relation
}
/**
* Get the qualified foreign key on the related model.
* Get the foreign key on the "through" model.
*
* @return string
*/
public function getQualifiedForeignKeyName()
public function getFirstKeyName()
{
return $this->related->getTable().'.'.$this->secondKey;
return $this->firstKey;
}
/**
@@ -450,6 +717,56 @@ class HasManyThrough extends Relation
*/
public function getQualifiedFirstKeyName()
{
return $this->throughParent->getTable().'.'.$this->firstKey;
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;
}
}

View File

@@ -2,13 +2,18 @@
namespace Illuminate\Database\Eloquent\Relations;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Database\Eloquent\SupportsPartialRelations;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Concerns\CanBeOneOfMany;
use Illuminate\Database\Eloquent\Relations\Concerns\ComparesRelatedModels;
use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels;
use Illuminate\Database\Query\JoinClause;
class HasOne extends HasOneOrMany
class HasOne extends HasOneOrMany implements SupportsPartialRelations
{
use SupportsDefaultModels;
use ComparesRelatedModels, CanBeOneOfMany, SupportsDefaultModels;
/**
* Get the results of the relationship.
@@ -17,13 +22,17 @@ class HasOne extends HasOneOrMany
*/
public function getResults()
{
if (is_null($this->getParentKey())) {
return $this->getDefaultFor($this->parent);
}
return $this->query->first() ?: $this->getDefaultFor($this->parent);
}
/**
* Initialize the relation on a set of models.
*
* @param array $models
* @param array $models
* @param string $relation
* @return array
*/
@@ -49,6 +58,59 @@ class HasOne extends HasOneOrMany
return $this->matchOne($models, $results, $relation);
}
/**
* Add the constraints for an internal relationship existence query.
*
* Essentially, these queries compare on column names like "whereColumn".
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Builder $parentQuery
* @param array|mixed $columns
* @return \Illuminate\Database\Eloquent\Builder
*/
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
if ($this->isOneOfMany()) {
$this->mergeOneOfManyJoinsTo($query);
}
return parent::getRelationExistenceQuery($query, $parentQuery, $columns);
}
/**
* Add constraints for inner join subselect for one of many relationships.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string|null $column
* @param string|null $aggregate
* @return void
*/
public function addOneOfManySubQueryConstraints(Builder $query, $column = null, $aggregate = null)
{
$query->addSelect($this->foreignKey);
}
/**
* Get the columns that should be selected by the one of many subquery.
*
* @return array|string
*/
public function getOneOfManySubQuerySelectColumns()
{
return $this->foreignKey;
}
/**
* 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));
}
/**
* Make a new related instance for the given model.
*
@@ -61,4 +123,15 @@ class HasOne extends HasOneOrMany
$this->getForeignKeyName(), $parent->{$this->localKey}
);
}
/**
* Get the value of the model's foreign key.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return mixed
*/
protected function getRelatedKeyFrom(Model $model)
{
return $model->getAttribute($this->getForeignKeyName());
}
}

Some files were not shown because too many files have changed in this diff Show More