vendor and env first commit

This commit is contained in:
2025-03-28 08:52:46 +01:00
parent f8388bc81b
commit 8f26283832
10976 changed files with 1349952 additions and 2 deletions
@@ -0,0 +1,202 @@
<?php
namespace Illuminate\Database\Capsule;
use Illuminate\Container\Container;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Connectors\ConnectionFactory;
use Illuminate\Database\DatabaseManager;
use Illuminate\Database\Eloquent\Model as Eloquent;
use Illuminate\Support\Traits\CapsuleManagerTrait;
use PDO;
class Manager
{
use CapsuleManagerTrait;
/**
* The database manager instance.
*
* @var \Illuminate\Database\DatabaseManager
*/
protected $manager;
/**
* Create a new database capsule manager.
*
* @param \Illuminate\Container\Container|null $container
* @return void
*/
public function __construct(?Container $container = null)
{
$this->setupContainer($container ?: new Container);
// Once we have the container setup, we will setup the default configuration
// options in the container "config" binding. This will make the database
// manager work correctly out of the box without extreme configuration.
$this->setupDefaultConfiguration();
$this->setupManager();
}
/**
* Setup the default database configuration options.
*
* @return void
*/
protected function setupDefaultConfiguration()
{
$this->container['config']['database.fetch'] = PDO::FETCH_OBJ;
$this->container['config']['database.default'] = 'default';
}
/**
* Build the database manager instance.
*
* @return void
*/
protected function setupManager()
{
$factory = new ConnectionFactory($this->container);
$this->manager = new DatabaseManager($this->container, $factory);
}
/**
* Get a connection instance from the global manager.
*
* @param string|null $connection
* @return \Illuminate\Database\Connection
*/
public static function connection($connection = null)
{
return static::$instance->getConnection($connection);
}
/**
* Get a fluent query builder instance.
*
* @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, $as = null, $connection = null)
{
return static::$instance->connection($connection)->table($table, $as);
}
/**
* Get a schema builder instance.
*
* @param string|null $connection
* @return \Illuminate\Database\Schema\Builder
*/
public static function schema($connection = null)
{
return static::$instance->connection($connection)->getSchemaBuilder();
}
/**
* Get a registered connection instance.
*
* @param string|null $name
* @return \Illuminate\Database\Connection
*/
public function getConnection($name = null)
{
return $this->manager->connection($name);
}
/**
* Register a connection with the manager.
*
* @param array $config
* @param string $name
* @return void
*/
public function addConnection(array $config, $name = 'default')
{
$connections = $this->container['config']['database.connections'];
$connections[$name] = $config;
$this->container['config']['database.connections'] = $connections;
}
/**
* Bootstrap Eloquent so it is ready for usage.
*
* @return void
*/
public function bootEloquent()
{
Eloquent::setConnectionResolver($this->manager);
// If we have an event dispatcher instance, we will go ahead and register it
// with the Eloquent ORM, allowing for model callbacks while creating and
// updating "model" instances; however, it is not necessary to operate.
if ($dispatcher = $this->getEventDispatcher()) {
Eloquent::setEventDispatcher($dispatcher);
}
}
/**
* Set the fetch mode for the database connections.
*
* @param int $fetchMode
* @return $this
*/
public function setFetchMode($fetchMode)
{
$this->container['config']['database.fetch'] = $fetchMode;
return $this;
}
/**
* Get the database manager instance.
*
* @return \Illuminate\Database\DatabaseManager
*/
public function getDatabaseManager()
{
return $this->manager;
}
/**
* Get the current event dispatcher instance.
*
* @return \Illuminate\Contracts\Events\Dispatcher|null
*/
public function getEventDispatcher()
{
if ($this->container->bound('events')) {
return $this->container['events'];
}
}
/**
* Set the event dispatcher instance to be used by connections.
*
* @param \Illuminate\Contracts\Events\Dispatcher $dispatcher
* @return void
*/
public function setEventDispatcher(Dispatcher $dispatcher)
{
$this->container->instance('events', $dispatcher);
}
/**
* Dynamically pass methods to the default connection.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public static function __callStatic($method, $parameters)
{
return static::connection()->$method(...$parameters);
}
}
@@ -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;
}
}
@@ -0,0 +1,560 @@
<?php
namespace Illuminate\Database\Concerns;
use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\MultipleRecordsFoundException;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\RecordsNotFoundException;
use Illuminate\Pagination\Cursor;
use Illuminate\Pagination\CursorPaginator;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Conditionable;
use InvalidArgumentException;
use RuntimeException;
/**
* @template TValue
*
* @mixin \Illuminate\Database\Eloquent\Builder
* @mixin \Illuminate\Database\Query\Builder
*/
trait BuildsQueries
{
use Conditionable;
/**
* Chunk the results of the query.
*
* @param int $count
* @param callable(\Illuminate\Support\Collection<int, TValue>, int): mixed $callback
* @return bool
*/
public function chunk($count, callable $callback)
{
$this->enforceOrderBy();
$page = 1;
do {
// 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 = $this->forPage($page, $count)->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;
}
unset($results);
$page++;
} while ($countResults == $count);
return true;
}
/**
* Run a map over each item while chunking.
*
* @template TReturn
*
* @param callable(TValue): TReturn $callback
* @param int $count
* @return \Illuminate\Support\Collection<int, TReturn>
*/
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(TValue, int): mixed $callback
* @param int $count
* @return bool
*
* @throws \RuntimeException
*/
public function each(callable $callback, $count = 1000)
{
return $this->chunk($count, function ($results) use ($callback) {
foreach ($results as $key => $value) {
if ($callback($value, $key) === false) {
return false;
}
}
});
}
/**
* Chunk the results of a query by comparing IDs.
*
* @param int $count
* @param callable(\Illuminate\Support\Collection<int, TValue>, int): mixed $callback
* @param string|null $column
* @param string|null $alias
* @return bool
*/
public function chunkById($count, callable $callback, $column = null, $alias = null)
{
return $this->orderedChunkById($count, $callback, $column, $alias);
}
/**
* Chunk the results of a query by comparing IDs in descending order.
*
* @param int $count
* @param callable(\Illuminate\Support\Collection<int, TValue>, int): mixed $callback
* @param string|null $column
* @param string|null $alias
* @return bool
*/
public function chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
{
return $this->orderedChunkById($count, $callback, $column, $alias, descending: true);
}
/**
* Chunk the results of a query by comparing IDs in a given order.
*
* @param int $count
* @param callable(\Illuminate\Support\Collection<int, TValue>, int): mixed $callback
* @param string|null $column
* @param string|null $alias
* @param bool $descending
* @return bool
*
* @throws \RuntimeException
*/
public function orderedChunkById($count, callable $callback, $column = null, $alias = null, $descending = false)
{
$column ??= $this->defaultKeyName();
$alias ??= $column;
$lastId = null;
$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.
if ($descending) {
$results = $clone->forPageBeforeId($count, $lastId, $column)->get();
} else {
$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(TValue, int): mixed $callback
* @param int $count
* @param string|null $column
* @param string|null $alias
* @return bool
*/
public function eachById(callable $callback, $count = 1000, $column = null, $alias = null)
{
return $this->chunkById($count, function ($results, $page) use ($callback, $count) {
foreach ($results as $key => $value) {
if ($callback($value, (($page - 1) * $count) + $key) === false) {
return false;
}
}
}, $column, $alias);
}
/**
* Query lazily, by chunks of the given size.
*
* @param int $chunkSize
* @return \Illuminate\Support\LazyCollection
*
* @throws \InvalidArgumentException
*/
public function lazy($chunkSize = 1000)
{
if ($chunkSize < 1) {
throw new InvalidArgumentException('The chunk size should be at least 1');
}
$this->enforceOrderBy();
return 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};
if ($lastId === null) {
throw new RuntimeException("The lazyById operation was aborted because the [{$alias}] column is not present in the query result.");
}
}
});
}
/**
* Execute the query and get the first result.
*
* @param array|string $columns
* @return TValue|null
*/
public function first($columns = ['*'])
{
return $this->take(1)->get($columns)->first();
}
/**
* Execute the query and get the first result if it's the sole matching record.
*
* @param array|string $columns
* @return TValue
*
* @throws \Illuminate\Database\RecordsNotFoundException
* @throws \Illuminate\Database\MultipleRecordsFoundException
*/
public function sole($columns = ['*'])
{
$result = $this->take(2)->get($columns);
$count = $result->count();
if ($count === 0) {
throw new RecordsNotFoundException;
}
if ($count > 1) {
throw new MultipleRecordsFoundException($count);
}
return $result->first();
}
/**
* Paginate the given query using a cursor paginator.
*
* @param int $perPage
* @param array|string $columns
* @param string $cursorName
* @param \Illuminate\Pagination\Cursor|string|null $cursor
* @return \Illuminate\Contracts\Pagination\CursorPaginator
*/
protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
{
if (! $cursor instanceof Cursor) {
$cursor = is_string($cursor)
? Cursor::fromEncoded($cursor)
: CursorPaginator::resolveCurrentCursor($cursorName, $cursor);
}
$orders = $this->ensureOrderForCursorPagination(! is_null($cursor) && $cursor->pointsToPreviousItems());
if (! is_null($cursor)) {
// Reset the union bindings so we can add the cursor where in the correct position...
$this->setBindings([], 'union');
$addCursorConditions = function (self $builder, $previousColumn, $originalColumn, $i) use (&$addCursorConditions, $cursor, $orders) {
$unionBuilders = $builder->getUnionBuilders();
if (! is_null($previousColumn)) {
$originalColumn ??= $this->getOriginalColumnNameForCursorPagination($this, $previousColumn);
$builder->where(
Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn,
'=',
$cursor->parameter($previousColumn)
);
$unionBuilders->each(function ($unionBuilder) use ($previousColumn, $cursor) {
$unionBuilder->where(
$this->getOriginalColumnNameForCursorPagination($unionBuilder, $previousColumn),
'=',
$cursor->parameter($previousColumn)
);
$this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
});
}
$builder->where(function (self $secondBuilder) use ($addCursorConditions, $cursor, $orders, $i, $unionBuilders) {
['column' => $column, 'direction' => $direction] = $orders[$i];
$originalColumn = $this->getOriginalColumnNameForCursorPagination($this, $column);
$secondBuilder->where(
Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn,
$direction === 'asc' ? '>' : '<',
$cursor->parameter($column)
);
if ($i < $orders->count() - 1) {
$secondBuilder->orWhere(function (self $thirdBuilder) use ($addCursorConditions, $column, $originalColumn, $i) {
$addCursorConditions($thirdBuilder, $column, $originalColumn, $i + 1);
});
}
$unionBuilders->each(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions) {
$unionWheres = $unionBuilder->getRawBindings()['where'];
$originalColumn = $this->getOriginalColumnNameForCursorPagination($unionBuilder, $column);
$unionBuilder->where(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions, $originalColumn, $unionWheres) {
$unionBuilder->where(
$originalColumn,
$direction === 'asc' ? '>' : '<',
$cursor->parameter($column)
);
if ($i < $orders->count() - 1) {
$unionBuilder->orWhere(function (self $fourthBuilder) use ($addCursorConditions, $column, $originalColumn, $i) {
$addCursorConditions($fourthBuilder, $column, $originalColumn, $i + 1);
});
}
$this->addBinding($unionWheres, 'union');
$this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
});
});
});
};
$addCursorConditions($this, null, null, 0);
}
$this->limit($perPage + 1);
return $this->cursorPaginator($this->get($columns), $perPage, $cursor, [
'path' => Paginator::resolveCurrentPath(),
'cursorName' => $cursorName,
'parameters' => $orders->pluck('column')->toArray(),
]);
}
/**
* Get the original column name of the given column, without any aliasing.
*
* @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*> $builder
* @param string $parameter
* @return string
*/
protected function getOriginalColumnNameForCursorPagination($builder, string $parameter)
{
$columns = $builder instanceof Builder ? $builder->getQuery()->getColumns() : $builder->getColumns();
if (! is_null($columns)) {
foreach ($columns as $column) {
if (($position = strripos($column, ' as ')) !== false) {
$original = substr($column, 0, $position);
$alias = substr($column, $position + 4);
if ($parameter === $alias || $builder->getGrammar()->wrap($parameter) === $alias) {
return $original;
}
}
}
}
return $parameter;
}
/**
* Create a new length-aware paginator instance.
*
* @param \Illuminate\Support\Collection $items
* @param int $total
* @param int $perPage
* @param int $currentPage
* @param array $options
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
protected function paginator($items, $total, $perPage, $currentPage, $options)
{
return Container::getInstance()->makeWith(LengthAwarePaginator::class, compact(
'items', 'total', 'perPage', 'currentPage', 'options'
));
}
/**
* Create a new simple paginator instance.
*
* @param \Illuminate\Support\Collection $items
* @param int $perPage
* @param int $currentPage
* @param array $options
* @return \Illuminate\Pagination\Paginator
*/
protected function simplePaginator($items, $perPage, $currentPage, $options)
{
return Container::getInstance()->makeWith(Paginator::class, compact(
'items', 'perPage', 'currentPage', 'options'
));
}
/**
* Create a new cursor paginator instance.
*
* @param \Illuminate\Support\Collection $items
* @param int $perPage
* @param \Illuminate\Pagination\Cursor $cursor
* @param array $options
* @return \Illuminate\Pagination\CursorPaginator
*/
protected function cursorPaginator($items, $perPage, $cursor, $options)
{
return Container::getInstance()->makeWith(CursorPaginator::class, compact(
'items', 'perPage', 'cursor', 'options'
));
}
/**
* Pass the query to a given callback.
*
* @param callable($this): mixed $callback
* @return $this
*/
public function tap($callback)
{
$callback($this);
return $this;
}
}
@@ -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.'"';
}
}
@@ -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);
}
}
@@ -0,0 +1,351 @@
<?php
namespace Illuminate\Database\Concerns;
use Closure;
use Illuminate\Database\DeadlockException;
use RuntimeException;
use Throwable;
trait ManagesTransactions
{
/**
* Execute a Closure within a transaction.
*
* @param \Closure $callback
* @param int $attempts
* @return mixed
*
* @throws \Throwable
*/
public function transaction(Closure $callback, $attempts = 1)
{
for ($currentAttempt = 1; $currentAttempt <= $attempts; $currentAttempt++) {
$this->beginTransaction();
// We'll simply execute the given callback within a try / catch block and if we
// catch any exception we can rollback this transaction so that none of this
// gets actually persisted to a database or stored in a permanent fashion.
try {
$callbackResult = $callback($this);
}
// If we catch an exception we'll rollback this transaction and try again if we
// are not out of attempts. If we are out of attempts we will just throw the
// exception back out, and let the developer handle an uncaught exception.
catch (Throwable $e) {
$this->handleTransactionException(
$e, $currentAttempt, $attempts
);
continue;
}
$levelBeingCommitted = $this->transactions;
try {
if ($this->transactions == 1) {
$this->fireConnectionEvent('committing');
$this->getPdo()->commit();
}
$this->transactions = max(0, $this->transactions - 1);
} catch (Throwable $e) {
$this->handleCommitTransactionException(
$e, $currentAttempt, $attempts
);
continue;
}
$this->transactionsManager?->commit(
$this->getName(),
$levelBeingCommitted,
$this->transactions
);
$this->fireConnectionEvent('committed');
return $callbackResult;
}
}
/**
* Handle an exception encountered when running a transacted statement.
*
* @param \Throwable $e
* @param int $currentAttempt
* @param int $maxAttempts
* @return void
*
* @throws \Throwable
*/
protected function handleTransactionException(Throwable $e, $currentAttempt, $maxAttempts)
{
// On a deadlock, MySQL rolls back the entire transaction so we can't just
// retry the query. We have to throw this exception all the way out and
// let the developer handle it in another way. We will decrement too.
if ($this->causedByConcurrencyError($e) &&
$this->transactions > 1) {
$this->transactions--;
$this->transactionsManager?->rollback(
$this->getName(), $this->transactions
);
throw new DeadlockException($e->getMessage(), is_int($e->getCode()) ? $e->getCode() : 0, $e);
}
// If there was an exception we will rollback this transaction and then we
// can check if we have exceeded the maximum attempt count for this and
// if we haven't we will return and try this query again in our loop.
$this->rollBack();
if ($this->causedByConcurrencyError($e) &&
$currentAttempt < $maxAttempts) {
return;
}
throw $e;
}
/**
* Start a new database transaction.
*
* @return void
*
* @throws \Throwable
*/
public function beginTransaction()
{
foreach ($this->beforeStartingTransaction as $callback) {
$callback($this);
}
$this->createTransaction();
$this->transactions++;
$this->transactionsManager?->begin(
$this->getName(), $this->transactions
);
$this->fireConnectionEvent('beganTransaction');
}
/**
* Create a transaction within the database.
*
* @return void
*
* @throws \Throwable
*/
protected function createTransaction()
{
if ($this->transactions == 0) {
$this->reconnectIfMissingConnection();
try {
$this->getPdo()->beginTransaction();
} catch (Throwable $e) {
$this->handleBeginTransactionException($e);
}
} elseif ($this->transactions >= 1 && $this->queryGrammar->supportsSavepoints()) {
$this->createSavepoint();
}
}
/**
* Create a save point within the database.
*
* @return void
*
* @throws \Throwable
*/
protected function createSavepoint()
{
$this->getPdo()->exec(
$this->queryGrammar->compileSavepoint('trans'.($this->transactions + 1))
);
}
/**
* Handle an exception from a transaction beginning.
*
* @param \Throwable $e
* @return void
*
* @throws \Throwable
*/
protected function handleBeginTransactionException(Throwable $e)
{
if ($this->causedByLostConnection($e)) {
$this->reconnect();
$this->getPdo()->beginTransaction();
} else {
throw $e;
}
}
/**
* Commit the active database transaction.
*
* @return void
*
* @throws \Throwable
*/
public function commit()
{
if ($this->transactionLevel() == 1) {
$this->fireConnectionEvent('committing');
$this->getPdo()->commit();
}
[$levelBeingCommitted, $this->transactions] = [
$this->transactions,
max(0, $this->transactions - 1),
];
$this->transactionsManager?->commit(
$this->getName(), $levelBeingCommitted, $this->transactions
);
$this->fireConnectionEvent('committed');
}
/**
* Handle an exception encountered when committing a transaction.
*
* @param \Throwable $e
* @param int $currentAttempt
* @param int $maxAttempts
* @return void
*
* @throws \Throwable
*/
protected function handleCommitTransactionException(Throwable $e, $currentAttempt, $maxAttempts)
{
$this->transactions = max(0, $this->transactions - 1);
if ($this->causedByConcurrencyError($e) && $currentAttempt < $maxAttempts) {
return;
}
if ($this->causedByLostConnection($e)) {
$this->transactions = 0;
}
throw $e;
}
/**
* Rollback the active database transaction.
*
* @param int|null $toLevel
* @return void
*
* @throws \Throwable
*/
public function rollBack($toLevel = null)
{
// We allow developers to rollback to a certain transaction level. We will verify
// that this given transaction level is valid before attempting to rollback to
// that level. If it's not we will just return out and not attempt anything.
$toLevel = is_null($toLevel)
? $this->transactions - 1
: $toLevel;
if ($toLevel < 0 || $toLevel >= $this->transactions) {
return;
}
// Next, we will actually perform this rollback within this database and fire the
// rollback event. We will also set the current transaction level to the given
// level that was passed into this method so it will be right from here out.
try {
$this->performRollBack($toLevel);
} catch (Throwable $e) {
$this->handleRollBackException($e);
}
$this->transactions = $toLevel;
$this->transactionsManager?->rollback(
$this->getName(), $this->transactions
);
$this->fireConnectionEvent('rollingBack');
}
/**
* Perform a rollback within the database.
*
* @param int $toLevel
* @return void
*
* @throws \Throwable
*/
protected function performRollBack($toLevel)
{
if ($toLevel == 0) {
$pdo = $this->getPdo();
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
} elseif ($this->queryGrammar->supportsSavepoints()) {
$this->getPdo()->exec(
$this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1))
);
}
}
/**
* Handle an exception from a rollback.
*
* @param \Throwable $e
* @return void
*
* @throws \Throwable
*/
protected function handleRollBackException(Throwable $e)
{
if ($this->causedByLostConnection($e)) {
$this->transactions = 0;
$this->transactionsManager?->rollback(
$this->getName(), $this->transactions
);
}
throw $e;
}
/**
* Get the number of active transactions.
*
* @return int
*/
public function transactionLevel()
{
return $this->transactions;
}
/**
* Execute the callback after a transaction commits.
*
* @param callable $callback
* @return void
*
* @throws \RuntimeException
*/
public function afterCommit($callback)
{
if ($this->transactionsManager) {
return $this->transactionsManager->addCallback($callback);
}
throw new RuntimeException('Transactions Manager has not been set.');
}
}
@@ -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 ?? []);
}
}
@@ -0,0 +1,10 @@
<?php
namespace Illuminate\Database;
use Illuminate\Support\ConfigurationUrlParser as BaseConfigurationUrlParser;
class ConfigurationUrlParser extends BaseConfigurationUrlParser
{
//
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,182 @@
<?php
namespace Illuminate\Database;
use Closure;
interface ConnectionInterface
{
/**
* Begin a fluent query against a database table.
*
* @param \Closure|\Illuminate\Database\Query\Builder|string $table
* @param string|null $as
* @return \Illuminate\Database\Query\Builder
*/
public function table($table, $as = null);
/**
* Get a new raw query expression.
*
* @param mixed $value
* @return \Illuminate\Contracts\Database\Query\Expression
*/
public function raw($value);
/**
* Run a select statement and return a single result.
*
* @param string $query
* @param array $bindings
* @param bool $useReadPdo
* @return mixed
*/
public function selectOne($query, $bindings = [], $useReadPdo = true);
/**
* Run a select statement and return the first column of the first row.
*
* @param string $query
* @param array $bindings
* @param bool $useReadPdo
* @return mixed
*
* @throws \Illuminate\Database\MultipleColumnsSelectedException
*/
public function scalar($query, $bindings = [], $useReadPdo = true);
/**
* Run a select statement against the database.
*
* @param string $query
* @param array $bindings
* @param bool $useReadPdo
* @return array
*/
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
* @return bool
*/
public function insert($query, $bindings = []);
/**
* Run an update statement against the database.
*
* @param string $query
* @param array $bindings
* @return int
*/
public function update($query, $bindings = []);
/**
* Run a delete statement against the database.
*
* @param string $query
* @param array $bindings
* @return int
*/
public function delete($query, $bindings = []);
/**
* Execute an SQL statement and return the boolean result.
*
* @param string $query
* @param array $bindings
* @return bool
*/
public function statement($query, $bindings = []);
/**
* Run an SQL statement and get the number of rows affected.
*
* @param string $query
* @param array $bindings
* @return int
*/
public function affectingStatement($query, $bindings = []);
/**
* Run a raw, unprepared query against the PDO connection.
*
* @param string $query
* @return bool
*/
public function unprepared($query);
/**
* Prepare the query bindings for execution.
*
* @param array $bindings
* @return array
*/
public function prepareBindings(array $bindings);
/**
* Execute a Closure within a transaction.
*
* @param \Closure $callback
* @param int $attempts
* @return mixed
*
* @throws \Throwable
*/
public function transaction(Closure $callback, $attempts = 1);
/**
* Start a new database transaction.
*
* @return void
*/
public function beginTransaction();
/**
* Commit the active database transaction.
*
* @return void
*/
public function commit();
/**
* Rollback the active database transaction.
*
* @return void
*/
public function rollBack();
/**
* Get the number of active transactions.
*
* @return int
*/
public function transactionLevel();
/**
* Execute the given callback in "dry run" mode.
*
* @param \Closure $callback
* @return array
*/
public function pretend(Closure $callback);
/**
* Get the name of the connected database.
*
* @return string
*/
public function getDatabaseName();
}
@@ -0,0 +1,92 @@
<?php
namespace Illuminate\Database;
class ConnectionResolver implements ConnectionResolverInterface
{
/**
* All of the registered connections.
*
* @var \Illuminate\Database\ConnectionInterface[]
*/
protected $connections = [];
/**
* The default connection name.
*
* @var string
*/
protected $default;
/**
* Create a new connection resolver instance.
*
* @param array<string, \Illuminate\Database\ConnectionInterface> $connections
* @return void
*/
public function __construct(array $connections = [])
{
foreach ($connections as $name => $connection) {
$this->addConnection($name, $connection);
}
}
/**
* Get a database connection instance.
*
* @param string|null $name
* @return \Illuminate\Database\ConnectionInterface
*/
public function connection($name = null)
{
if (is_null($name)) {
$name = $this->getDefaultConnection();
}
return $this->connections[$name];
}
/**
* Add a connection to the resolver.
*
* @param string $name
* @param \Illuminate\Database\ConnectionInterface $connection
* @return void
*/
public function addConnection($name, ConnectionInterface $connection)
{
$this->connections[$name] = $connection;
}
/**
* Check if a connection has been registered.
*
* @param string $name
* @return bool
*/
public function hasConnection($name)
{
return isset($this->connections[$name]);
}
/**
* Get the default connection name.
*
* @return string
*/
public function getDefaultConnection()
{
return $this->default;
}
/**
* Set the default connection name.
*
* @param string $name
* @return void
*/
public function setDefaultConnection($name)
{
$this->default = $name;
}
}
@@ -0,0 +1,29 @@
<?php
namespace Illuminate\Database;
interface ConnectionResolverInterface
{
/**
* Get a database connection instance.
*
* @param string|null $name
* @return \Illuminate\Database\ConnectionInterface
*/
public function connection($name = null);
/**
* Get the default connection name.
*
* @return string
*/
public function getDefaultConnection();
/**
* Set the default connection name.
*
* @param string $name
* @return void
*/
public function setDefaultConnection($name);
}
@@ -0,0 +1,280 @@
<?php
namespace Illuminate\Database\Connectors;
use Illuminate\Contracts\Container\Container;
use Illuminate\Database\Connection;
use Illuminate\Database\MariaDbConnection;
use Illuminate\Database\MySqlConnection;
use Illuminate\Database\PostgresConnection;
use Illuminate\Database\SQLiteConnection;
use Illuminate\Database\SqlServerConnection;
use Illuminate\Support\Arr;
use InvalidArgumentException;
use PDOException;
class ConnectionFactory
{
/**
* The IoC container instance.
*
* @var \Illuminate\Contracts\Container\Container
*/
protected $container;
/**
* Create a new connection factory instance.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return void
*/
public function __construct(Container $container)
{
$this->container = $container;
}
/**
* Establish a PDO connection based on the configuration.
*
* @param array $config
* @param string|null $name
* @return \Illuminate\Database\Connection
*/
public function make(array $config, $name = null)
{
$config = $this->parseConfig($config, $name);
if (isset($config['read'])) {
return $this->createReadWriteConnection($config);
}
return $this->createSingleConnection($config);
}
/**
* Parse and prepare the database configuration.
*
* @param array $config
* @param string $name
* @return array
*/
protected function parseConfig(array $config, $name)
{
return Arr::add(Arr::add($config, 'prefix', ''), 'name', $name);
}
/**
* Create a single database connection instance.
*
* @param array $config
* @return \Illuminate\Database\Connection
*/
protected function createSingleConnection(array $config)
{
$pdo = $this->createPdoResolver($config);
return $this->createConnection(
$config['driver'], $pdo, $config['database'], $config['prefix'], $config
);
}
/**
* Create a read / write database connection instance.
*
* @param array $config
* @return \Illuminate\Database\Connection
*/
protected function createReadWriteConnection(array $config)
{
$connection = $this->createSingleConnection($this->getWriteConfig($config));
return $connection->setReadPdo($this->createReadPdo($config));
}
/**
* Create a new PDO instance for reading.
*
* @param array $config
* @return \Closure
*/
protected function createReadPdo(array $config)
{
return $this->createPdoResolver($this->getReadConfig($config));
}
/**
* Get the read configuration for a read / write connection.
*
* @param array $config
* @return array
*/
protected function getReadConfig(array $config)
{
return $this->mergeReadWriteConfig(
$config, $this->getReadWriteConfig($config, 'read')
);
}
/**
* Get the write configuration for a read / write connection.
*
* @param array $config
* @return array
*/
protected function getWriteConfig(array $config)
{
return $this->mergeReadWriteConfig(
$config, $this->getReadWriteConfig($config, 'write')
);
}
/**
* Get a read / write level configuration.
*
* @param array $config
* @param string $type
* @return array
*/
protected function getReadWriteConfig(array $config, $type)
{
return isset($config[$type][0])
? Arr::random($config[$type])
: $config[$type];
}
/**
* Merge a configuration for a read / write connection.
*
* @param array $config
* @param array $merge
* @return array
*/
protected function mergeReadWriteConfig(array $config, array $merge)
{
return Arr::except(array_merge($config, $merge), ['read', 'write']);
}
/**
* Create a new Closure that resolves to a PDO instance.
*
* @param array $config
* @return \Closure
*/
protected function createPdoResolver(array $config)
{
return array_key_exists('host', $config)
? $this->createPdoResolverWithHosts($config)
: $this->createPdoResolverWithoutHosts($config);
}
/**
* Create a new Closure that resolves to a PDO instance with a specific host or an array of hosts.
*
* @param array $config
* @return \Closure
*
* @throws \PDOException
*/
protected function createPdoResolverWithHosts(array $config)
{
return function () use ($config) {
foreach (Arr::shuffle($this->parseHosts($config)) as $host) {
$config['host'] = $host;
try {
return $this->createConnector($config)->connect($config);
} catch (PDOException $e) {
continue;
}
}
throw $e;
};
}
/**
* Parse the hosts configuration item into an array.
*
* @param array $config
* @return array
*
* @throws \InvalidArgumentException
*/
protected function parseHosts(array $config)
{
$hosts = Arr::wrap($config['host']);
if (empty($hosts)) {
throw new InvalidArgumentException('Database hosts array is empty.');
}
return $hosts;
}
/**
* Create a new Closure that resolves to a PDO instance where there is no configured host.
*
* @param array $config
* @return \Closure
*/
protected function createPdoResolverWithoutHosts(array $config)
{
return fn () => $this->createConnector($config)->connect($config);
}
/**
* Create a connector instance based on the configuration.
*
* @param array $config
* @return \Illuminate\Database\Connectors\ConnectorInterface
*
* @throws \InvalidArgumentException
*/
public function createConnector(array $config)
{
if (! isset($config['driver'])) {
throw new InvalidArgumentException('A driver must be specified.');
}
if ($this->container->bound($key = "db.connector.{$config['driver']}")) {
return $this->container->make($key);
}
return match ($config['driver']) {
'mysql' => new MySqlConnector,
'mariadb' => new MariaDbConnector,
'pgsql' => new PostgresConnector,
'sqlite' => new SQLiteConnector,
'sqlsrv' => new SqlServerConnector,
default => throw new InvalidArgumentException("Unsupported driver [{$config['driver']}]."),
};
}
/**
* Create a new connection instance.
*
* @param string $driver
* @param \PDO|\Closure $connection
* @param string $database
* @param string $prefix
* @param array $config
* @return \Illuminate\Database\Connection
*
* @throws \InvalidArgumentException
*/
protected function createConnection($driver, $connection, $database, $prefix = '', array $config = [])
{
if ($resolver = Connection::getResolver($driver)) {
return $resolver($connection, $database, $prefix, $config);
}
return match ($driver) {
'mysql' => new MySqlConnection($connection, $database, $prefix, $config),
'mariadb' => new MariaDbConnection($connection, $database, $prefix, $config),
'pgsql' => new PostgresConnection($connection, $database, $prefix, $config),
'sqlite' => new SQLiteConnection($connection, $database, $prefix, $config),
'sqlsrv' => new SqlServerConnection($connection, $database, $prefix, $config),
default => throw new InvalidArgumentException("Unsupported driver [{$driver}]."),
};
}
}
@@ -0,0 +1,122 @@
<?php
namespace Illuminate\Database\Connectors;
use Exception;
use Illuminate\Database\DetectsLostConnections;
use PDO;
use Throwable;
class Connector
{
use DetectsLostConnections;
/**
* The default PDO connection options.
*
* @var array
*/
protected $options = [
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false,
];
/**
* Create a new PDO connection.
*
* @param string $dsn
* @param array $config
* @param array $options
* @return \PDO
*
* @throws \Exception
*/
public function createConnection($dsn, array $config, array $options)
{
[$username, $password] = [
$config['username'] ?? null, $config['password'] ?? null,
];
try {
return $this->createPdoConnection(
$dsn, $username, $password, $options
);
} catch (Exception $e) {
return $this->tryAgainIfCausedByLostConnection(
$e, $dsn, $username, $password, $options
);
}
}
/**
* Create a new PDO connection instance.
*
* @param string $dsn
* @param string $username
* @param string $password
* @param array $options
* @return \PDO
*/
protected function createPdoConnection($dsn, $username, $password, $options)
{
return new PDO($dsn, $username, $password, $options);
}
/**
* Handle an exception that occurred during connect execution.
*
* @param \Throwable $e
* @param string $dsn
* @param string $username
* @param string $password
* @param array $options
* @return \PDO
*
* @throws \Throwable
*/
protected function tryAgainIfCausedByLostConnection(Throwable $e, $dsn, $username, $password, $options)
{
if ($this->causedByLostConnection($e)) {
return $this->createPdoConnection($dsn, $username, $password, $options);
}
throw $e;
}
/**
* Get the PDO options based on the configuration.
*
* @param array $config
* @return array
*/
public function getOptions(array $config)
{
$options = $config['options'] ?? [];
return array_diff_key($this->options, $options) + $options;
}
/**
* Get the default PDO connection options.
*
* @return array
*/
public function getDefaultOptions()
{
return $this->options;
}
/**
* Set the default PDO connection options.
*
* @param array $options
* @return void
*/
public function setDefaultOptions(array $options)
{
$this->options = $options;
}
}
@@ -0,0 +1,14 @@
<?php
namespace Illuminate\Database\Connectors;
interface ConnectorInterface
{
/**
* Establish a database connection.
*
* @param array $config
* @return \PDO
*/
public function connect(array $config);
}
@@ -0,0 +1,32 @@
<?php
namespace Illuminate\Database\Connectors;
use PDO;
class MariaDbConnector extends MySqlConnector
{
/**
* Get the sql_mode value.
*
* @param \PDO $connection
* @param array $config
* @return string|null
*/
protected function getSqlMode(PDO $connection, array $config)
{
if (isset($config['modes'])) {
return implode(',', $config['modes']);
}
if (! isset($config['strict'])) {
return null;
}
if (! $config['strict']) {
return 'NO_ENGINE_SUBSTITUTION';
}
return 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
}
}
@@ -0,0 +1,154 @@
<?php
namespace Illuminate\Database\Connectors;
use PDO;
class MySqlConnector extends Connector implements ConnectorInterface
{
/**
* Establish a database connection.
*
* @param array $config
* @return \PDO
*/
public function connect(array $config)
{
$dsn = $this->getDsn($config);
$options = $this->getOptions($config);
// We need to grab the PDO options that should be used while making the brand
// new connection instance. The PDO options control various aspects of the
// connection's behavior, and some might be specified by the developers.
$connection = $this->createConnection($dsn, $config, $options);
if (! empty($config['database'])) {
$connection->exec("use `{$config['database']}`;");
}
$this->configureConnection($connection, $config);
return $connection;
}
/**
* Create a DSN string from a configuration.
*
* Chooses socket or host/port based on the 'unix_socket' config value.
*
* @param array $config
* @return string
*/
protected function getDsn(array $config)
{
return $this->hasSocket($config)
? $this->getSocketDsn($config)
: $this->getHostDsn($config);
}
/**
* Determine if the given configuration array has a UNIX socket value.
*
* @param array $config
* @return bool
*/
protected function hasSocket(array $config)
{
return isset($config['unix_socket']) && ! empty($config['unix_socket']);
}
/**
* Get the DSN string for a socket configuration.
*
* @param array $config
* @return string
*/
protected function getSocketDsn(array $config)
{
return "mysql:unix_socket={$config['unix_socket']};dbname={$config['database']}";
}
/**
* Get the DSN string for a host / port configuration.
*
* @param array $config
* @return string
*/
protected function getHostDsn(array $config)
{
extract($config, EXTR_SKIP);
return isset($port)
? "mysql:host={$host};port={$port};dbname={$database}"
: "mysql:host={$host};dbname={$database}";
}
/**
* Configure the given PDO connection.
*
* @param \PDO $connection
* @param array $config
* @return void
*/
protected function configureConnection(PDO $connection, array $config)
{
if (isset($config['isolation_level'])) {
$connection->exec(sprintf('SET SESSION TRANSACTION ISOLATION LEVEL %s;', $config['isolation_level']));
}
$statements = [];
if (isset($config['charset'])) {
if (isset($config['collation'])) {
$statements[] = sprintf("NAMES '%s' COLLATE '%s'", $config['charset'], $config['collation']);
} else {
$statements[] = sprintf("NAMES '%s'", $config['charset']);
}
}
if (isset($config['timezone'])) {
$statements[] = sprintf("time_zone='%s'", $config['timezone']);
}
$sqlMode = $this->getSqlMode($connection, $config);
if ($sqlMode !== null) {
$statements[] = sprintf("SESSION sql_mode='%s'", $sqlMode);
}
if ($statements !== []) {
$connection->exec(sprintf('SET %s;', implode(', ', $statements)));
}
}
/**
* Get the sql_mode value.
*
* @param \PDO $connection
* @param array $config
* @return string|null
*/
protected function getSqlMode(PDO $connection, array $config)
{
if (isset($config['modes'])) {
return implode(',', $config['modes']);
}
if (! isset($config['strict'])) {
return null;
}
if (! $config['strict']) {
return 'NO_ENGINE_SUBSTITUTION';
}
$version = $config['version'] ?? $connection->getAttribute(PDO::ATTR_SERVER_VERSION);
if (version_compare($version, '8.0.11') >= 0) {
return 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
}
return 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';
}
}
@@ -0,0 +1,189 @@
<?php
namespace Illuminate\Database\Connectors;
use Illuminate\Database\Concerns\ParsesSearchPath;
use PDO;
class PostgresConnector extends Connector implements ConnectorInterface
{
use ParsesSearchPath;
/**
* The default PDO connection options.
*
* @var array
*/
protected $options = [
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
];
/**
* Establish a database connection.
*
* @param array $config
* @return \PDO
*/
public function connect(array $config)
{
// First we'll create the basic DSN and connection instance connecting to the
// using the configuration option specified by the developer. We will also
// set the default character set on the connections to UTF-8 by default.
$connection = $this->createConnection(
$this->getDsn($config), $config, $this->getOptions($config)
);
$this->configureIsolationLevel($connection, $config);
// Next, we will check to see if a timezone has been specified in this config
// and if it has we will issue a statement to modify the timezone with the
// database. Setting this DB timezone is an optional configuration item.
$this->configureTimezone($connection, $config);
$this->configureSearchPath($connection, $config);
$this->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 timezone on the connection.
*
* @param \PDO $connection
* @param array $config
* @return void
*/
protected function configureTimezone($connection, array $config)
{
if (isset($config['timezone'])) {
$timezone = $config['timezone'];
$connection->prepare("set time zone '{$timezone}'")->execute();
}
}
/**
* Set the "search_path" on the database connection.
*
* @param \PDO $connection
* @param array $config
* @return void
*/
protected function configureSearchPath($connection, $config)
{
if (isset($config['search_path']) || isset($config['schema'])) {
$searchPath = $this->quoteSearchPath(
$this->parseSearchPath($config['search_path'] ?? $config['schema'])
);
$connection->prepare("set search_path to {$searchPath}")->execute();
}
}
/**
* Format the search path for the DSN.
*
* @param array $searchPath
* @return string
*/
protected function quoteSearchPath($searchPath)
{
return count($searchPath) === 1 ? '"'.$searchPath[0].'"' : '"'.implode('", "', $searchPath).'"';
}
/**
* Create a DSN string from a configuration.
*
* @param array $config
* @return string
*/
protected function getDsn(array $config)
{
// First we will create the basic DSN setup as well as the port if it is in
// in the configuration options. This will give us the basic DSN we will
// need to establish the PDO connections and return them back for use.
extract($config, EXTR_SKIP);
$host = isset($host) ? "host={$host};" : '';
// Sometimes - users may need to connect to a database that has a different
// name than the database used for "information_schema" queries. This is
// typically the case if using "pgbouncer" type software when pooling.
$database = $connect_via_database ?? $database;
$port = $connect_via_port ?? $port ?? null;
$dsn = "pgsql:{$host}dbname='{$database}'";
// If a port was specified, we will add it to this Postgres DSN connections
// format. Once we have done that we are ready to return this connection
// string back out for usage, as this has been fully constructed here.
if (! is_null($port)) {
$dsn .= ";port={$port}";
}
if (isset($charset)) {
$dsn .= ";client_encoding='{$charset}'";
}
// Postgres allows an application_name to be set by the user and this name is
// used to when monitoring the application with pg_stat_activity. So we'll
// determine if the option has been specified and run a statement if so.
if (isset($application_name)) {
$dsn .= ";application_name='".str_replace("'", "\'", $application_name)."'";
}
return $this->addSslOptions($dsn, $config);
}
/**
* Add the SSL options to the DSN.
*
* @param string $dsn
* @param array $config
* @return string
*/
protected function addSslOptions($dsn, array $config)
{
foreach (['sslmode', 'sslcert', 'sslkey', 'sslrootcert'] as $option) {
if (isset($config[$option])) {
$dsn .= ";{$option}={$config[$option]}";
}
}
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();
}
}
@@ -0,0 +1,39 @@
<?php
namespace Illuminate\Database\Connectors;
use Illuminate\Database\SQLiteDatabaseDoesNotExistException;
class SQLiteConnector extends Connector implements ConnectorInterface
{
/**
* Establish a database connection.
*
* @param array $config
* @return \PDO
*
* @throws \Illuminate\Database\SQLiteDatabaseDoesNotExistException
*/
public function connect(array $config)
{
$options = $this->getOptions($config);
// 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:') {
return $this->createConnection('sqlite::memory:', $config, $options);
}
$path = realpath($config['database']);
// Here we'll verify that the SQLite database exists before going any further
// as the developer probably wants to know if the database exists and this
// SQLite driver will not throw any exception if it does not by default.
if ($path === false) {
throw new SQLiteDatabaseDoesNotExistException($config['database']);
}
return $this->createConnection("sqlite:{$path}", $config, $options);
}
}
@@ -0,0 +1,233 @@
<?php
namespace Illuminate\Database\Connectors;
use Illuminate\Support\Arr;
use PDO;
class SqlServerConnector extends Connector implements ConnectorInterface
{
/**
* The PDO connection options.
*
* @var array
*/
protected $options = [
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
];
/**
* Establish a database connection.
*
* @param array $config
* @return \PDO
*/
public function connect(array $config)
{
$options = $this->getOptions($config);
$connection = $this->createConnection($this->getDsn($config), $config, $options);
$this->configureIsolationLevel($connection, $config);
return $connection;
}
/**
* Set the connection transaction isolation level.
*
* https://learn.microsoft.com/en-us/sql/t-sql/statements/set-transaction-isolation-level-transact-sql
*
* @param \PDO $connection
* @param array $config
* @return void
*/
protected function configureIsolationLevel($connection, array $config)
{
if (! isset($config['isolation_level'])) {
return;
}
$connection->prepare(
"SET TRANSACTION ISOLATION LEVEL {$config['isolation_level']}"
)->execute();
}
/**
* Create a DSN string from a configuration.
*
* @param array $config
* @return string
*/
protected function getDsn(array $config)
{
// First we will create the basic DSN setup as well as the port if it is in
// in the configuration options. This will give us the basic DSN we will
// need to establish the PDO connections and return them back for use.
if ($this->prefersOdbc($config)) {
return $this->getOdbcDsn($config);
}
if (in_array('sqlsrv', $this->getAvailableDrivers())) {
return $this->getSqlSrvDsn($config);
} else {
return $this->getDblibDsn($config);
}
}
/**
* Determine if the database configuration prefers ODBC.
*
* @param array $config
* @return bool
*/
protected function prefersOdbc(array $config)
{
return in_array('odbc', $this->getAvailableDrivers()) &&
($config['odbc'] ?? null) === true;
}
/**
* Get the DSN string for a DbLib connection.
*
* @param array $config
* @return string
*/
protected function getDblibDsn(array $config)
{
return $this->buildConnectString('dblib', array_merge([
'host' => $this->buildHostString($config, ':'),
'dbname' => $config['database'],
], Arr::only($config, ['appname', 'charset', 'version'])));
}
/**
* Get the DSN string for an ODBC connection.
*
* @param array $config
* @return string
*/
protected function getOdbcDsn(array $config)
{
return isset($config['odbc_datasource_name'])
? 'odbc:'.$config['odbc_datasource_name'] : '';
}
/**
* Get the DSN string for a SqlSrv connection.
*
* @param array $config
* @return string
*/
protected function getSqlSrvDsn(array $config)
{
$arguments = [
'Server' => $this->buildHostString($config, ','),
];
if (isset($config['database'])) {
$arguments['Database'] = $config['database'];
}
if (isset($config['readonly'])) {
$arguments['ApplicationIntent'] = 'ReadOnly';
}
if (isset($config['pooling']) && $config['pooling'] === false) {
$arguments['ConnectionPooling'] = '0';
}
if (isset($config['appname'])) {
$arguments['APP'] = $config['appname'];
}
if (isset($config['encrypt'])) {
$arguments['Encrypt'] = $config['encrypt'];
}
if (isset($config['trust_server_certificate'])) {
$arguments['TrustServerCertificate'] = $config['trust_server_certificate'];
}
if (isset($config['multiple_active_result_sets']) && $config['multiple_active_result_sets'] === false) {
$arguments['MultipleActiveResultSets'] = 'false';
}
if (isset($config['transaction_isolation'])) {
$arguments['TransactionIsolation'] = $config['transaction_isolation'];
}
if (isset($config['multi_subnet_failover'])) {
$arguments['MultiSubnetFailover'] = $config['multi_subnet_failover'];
}
if (isset($config['column_encryption'])) {
$arguments['ColumnEncryption'] = $config['column_encryption'];
}
if (isset($config['key_store_authentication'])) {
$arguments['KeyStoreAuthentication'] = $config['key_store_authentication'];
}
if (isset($config['key_store_principal_id'])) {
$arguments['KeyStorePrincipalId'] = $config['key_store_principal_id'];
}
if (isset($config['key_store_secret'])) {
$arguments['KeyStoreSecret'] = $config['key_store_secret'];
}
if (isset($config['login_timeout'])) {
$arguments['LoginTimeout'] = $config['login_timeout'];
}
if (isset($config['authentication'])) {
$arguments['Authentication'] = $config['authentication'];
}
return $this->buildConnectString('sqlsrv', $arguments);
}
/**
* Build a connection string from the given arguments.
*
* @param string $driver
* @param array $arguments
* @return string
*/
protected function buildConnectString($driver, array $arguments)
{
return $driver.':'.implode(';', array_map(function ($key) use ($arguments) {
return sprintf('%s=%s', $key, $arguments[$key]);
}, array_keys($arguments)));
}
/**
* Build a host string from the given configuration.
*
* @param array $config
* @param string $separator
* @return string
*/
protected function buildHostString(array $config, $separator)
{
if (empty($config['port'])) {
return $config['host'];
}
return $config['host'].$separator.$config['port'];
}
/**
* Get the available PDO drivers.
*
* @return array
*/
protected function getAvailableDrivers()
{
return PDO::getAvailableDrivers();
}
}
@@ -0,0 +1,86 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Console\Command;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\MariaDbConnection;
use Illuminate\Database\MySqlConnection;
use Illuminate\Database\PostgresConnection;
use Illuminate\Database\SQLiteConnection;
use Illuminate\Database\SqlServerConnection;
use Illuminate\Support\Arr;
abstract class DatabaseInspectionCommand extends Command
{
/**
* Get a human-readable name for the given connection.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param string $database
* @return string
*/
protected function getConnectionName(ConnectionInterface $connection, $database)
{
return match (true) {
$connection instanceof MySqlConnection && $connection->isMaria() => 'MariaDB',
$connection instanceof MySqlConnection => 'MySQL',
$connection instanceof MariaDbConnection => 'MariaDB',
$connection instanceof PostgresConnection => 'PostgreSQL',
$connection instanceof SQLiteConnection => 'SQLite',
$connection instanceof SqlServerConnection => 'SQL Server',
default => $database,
};
}
/**
* 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']);
}
/**
* Remove the table prefix from a table name, if it exists.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param string $table
* @return string
*/
protected function withoutTablePrefix(ConnectionInterface $connection, string $table)
{
$prefix = $connection->getTablePrefix();
return str_starts_with($table, $prefix)
? substr($table, strlen($prefix))
: $table;
}
}
@@ -0,0 +1,242 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Console\Command;
use Illuminate\Support\ConfigurationUrlParser;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Process\Process;
use UnexpectedValueException;
#[AsCommand(name: 'db')]
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',
'mariadb' => '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 MariaDB CLI.
*
* @param array $connection
* @return array
*/
protected function getMariaDbArguments(array $connection)
{
return $this->getMysqlArguments($connection);
}
/**
* Get the arguments for the Postgres CLI.
*
* @param array $connection
* @return array
*/
protected function getPgsqlArguments(array $connection)
{
return [$connection['database']];
}
/**
* Get the arguments for the SQLite CLI.
*
* @param array $connection
* @return array
*/
protected function getSqliteArguments(array $connection)
{
return [$connection['database']];
}
/**
* Get the arguments for the SQL Server CLI.
*
* @param array $connection
* @return array
*/
protected function getSqlsrvArguments(array $connection)
{
return array_merge(...$this->getOptionalArguments([
'database' => ['-d', $connection['database']],
'username' => ['-U', $connection['username']],
'password' => ['-P', $connection['password']],
'host' => ['-S', 'tcp:'.$connection['host']
.($connection['port'] ? ','.$connection['port'] : ''), ],
'trust_server_certificate' => ['-C'],
], $connection));
}
/**
* Get the environment variables for the Postgres CLI.
*
* @param array $connection
* @return array|null
*/
protected function getPgsqlEnvironment(array $connection)
{
return array_merge(...$this->getOptionalArguments([
'username' => ['PGUSER' => $connection['username']],
'host' => ['PGHOST' => $connection['host']],
'port' => ['PGPORT' => $connection['port']],
'password' => ['PGPASSWORD' => $connection['password']],
], $connection));
}
/**
* Get the optional arguments based on the connection configuration.
*
* @param array $args
* @param array $connection
* @return array
*/
protected function getOptionalArguments(array $args, array $connection)
{
return array_values(array_filter($args, function ($key) use ($connection) {
return ! empty($connection[$key]);
}, ARRAY_FILTER_USE_KEY));
}
}
@@ -0,0 +1,94 @@
<?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 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 void
*/
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)
{
$migrations = Config::get('database.migrations', 'migrations');
$migrationTable = is_array($migrations) ? ($migrations['table'] ?? 'migrations') : $migrations;
return $connection->getSchemaState()
->withMigrationTable($migrationTable)
->handleOutputUsing(function ($type, $buffer) {
$this->output->write($buffer);
});
}
/**
* Get the path that the dump should be written to.
*
* @param \Illuminate\Database\Connection $connection
*/
protected function path(Connection $connection)
{
return tap($this->option('path') ?: database_path('schema/'.$connection->getName().'-schema.sql'), function ($path) {
(new Filesystem)->ensureDirectoryExists(dirname($path));
});
}
}
@@ -0,0 +1,143 @@
<?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 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'],
];
}
}
@@ -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(): array
{
return [
//
];
}
}
@@ -0,0 +1,51 @@
<?php
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Console\Command;
class BaseCommand extends Command
{
/**
* Get all of the migration paths.
*
* @return string[]
*/
protected function getMigrationPaths()
{
// Here, we will check to see if a path option has been defined. If it has we will
// use the path relative to the root of the installation folder so our database
// migrations may be run for any customized path from within the application.
if ($this->input->hasOption('path') && $this->option('path')) {
return collect($this->option('path'))->map(function ($path) {
return ! $this->usingRealPath()
? $this->laravel->basePath().'/'.$path
: $path;
})->all();
}
return array_merge(
$this->migrator->paths(), [$this->getMigrationPath()]
);
}
/**
* Determine if the given path(s) are pre-resolved "real" paths.
*
* @return bool
*/
protected function usingRealPath()
{
return $this->input->hasOption('realpath') && $this->option('realpath');
}
/**
* Get the path to the migration directory.
*
* @return string
*/
protected function getMigrationPath()
{
return $this->laravel->databasePath().DIRECTORY_SEPARATOR.'migrations';
}
}
@@ -0,0 +1,149 @@
<?php
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Console\Prohibitable;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Events\DatabaseRefreshed;
use Illuminate\Database\Migrations\Migrator;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'migrate:fresh')]
class FreshCommand extends Command
{
use ConfirmableTrait, Prohibitable;
/**
* 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';
/**
* The migrator instance.
*
* @var \Illuminate\Database\Migrations\Migrator
*/
protected $migrator;
/**
* Create a new fresh command instance.
*
* @param \Illuminate\Database\Migrations\Migrator $migrator
* @return void
*/
public function __construct(Migrator $migrator)
{
parent::__construct();
$this->migrator = $migrator;
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if ($this->isProhibited() ||
! $this->confirmToProceed()) {
return Command::FAILURE;
}
$database = $this->input->getOption('database');
$this->migrator->usingConnection($database, function () use ($database) {
if ($this->migrator->repositoryExists()) {
$this->newLine();
$this->components->task('Dropping all tables', fn () => $this->callSilent('db:wipe', array_filter([
'--database' => $database,
'--drop-views' => $this->option('drop-views'),
'--drop-types' => $this->option('drop-types'),
'--force' => true,
])) == 0);
}
});
$this->newLine();
$this->call('migrate', array_filter([
'--database' => $database,
'--path' => $this->input->getOption('path'),
'--realpath' => $this->input->getOption('realpath'),
'--schema-path' => $this->input->getOption('schema-path'),
'--force' => true,
'--step' => $this->option('step'),
]));
if ($this->laravel->bound(Dispatcher::class)) {
$this->laravel[Dispatcher::class]->dispatch(
new DatabaseRefreshed($database, $this->needsSeeding())
);
}
if ($this->needsSeeding()) {
$this->runSeeder($database);
}
return 0;
}
/**
* Determine if the developer has requested database seeding.
*
* @return bool
*/
protected function needsSeeding()
{
return $this->option('seed') || $this->option('seeder');
}
/**
* Run the database seeder command.
*
* @param string $database
* @return void
*/
protected function runSeeder($database)
{
$this->call('db:seed', array_filter([
'--database' => $database,
'--class' => $this->option('seeder') ?: 'Database\\Seeders\\DatabaseSeeder',
'--force' => true,
]));
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'],
['drop-views', null, InputOption::VALUE_NONE, 'Drop all tables and views'],
['drop-types', null, InputOption::VALUE_NONE, 'Drop all tables and types (Postgres only)'],
['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'],
['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to the migrations files to be executed'],
['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'],
['schema-path', null, InputOption::VALUE_OPTIONAL, 'The path to a schema dump file'],
['seed', null, InputOption::VALUE_NONE, 'Indicates if the seed task should be re-run'],
['seeder', null, InputOption::VALUE_OPTIONAL, 'The class name of the root seeder'],
['step', null, InputOption::VALUE_NONE, 'Force the migrations to be run so they can be rolled back individually'],
];
}
}
@@ -0,0 +1,72 @@
<?php
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Console\Command;
use Illuminate\Database\Migrations\MigrationRepositoryInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'migrate:install')]
class InstallCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'migrate:install';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create the migration repository';
/**
* The repository instance.
*
* @var \Illuminate\Database\Migrations\MigrationRepositoryInterface
*/
protected $repository;
/**
* Create a new migration install command instance.
*
* @param \Illuminate\Database\Migrations\MigrationRepositoryInterface $repository
* @return void
*/
public function __construct(MigrationRepositoryInterface $repository)
{
parent::__construct();
$this->repository = $repository;
}
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$this->repository->setSource($this->input->getOption('database'));
$this->repository->createRepository();
$this->components->info('Migration table created successfully.');
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'],
];
}
}
@@ -0,0 +1,315 @@
<?php
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 RuntimeException;
use Symfony\Component\Console\Attribute\AsCommand;
use Throwable;
use function Laravel\Prompts\confirm;
#[AsCommand(name: 'migrate')]
class MigrateCommand extends BaseCommand implements Isolatable
{
use ConfirmableTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'migrate {--database= : The database connection to use}
{--force : Force the operation to run when in production}
{--path=* : The path(s) to the migrations files to be executed}
{--realpath : Indicate any provided migration file paths are pre-resolved absolute paths}
{--schema-path= : The path to a schema dump file}
{--pretend : Dump the SQL queries that would be run}
{--seed : Indicates if the seed task should be re-run}
{--seeder= : The class name of the root seeder}
{--step : Force the migrations to be run so they can be rolled back individually}
{--graceful : Return a successful exit code even if an error occurs}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Run the database migrations';
/**
* The migrator instance.
*
* @var \Illuminate\Database\Migrations\Migrator
*/
protected $migrator;
/**
* The event dispatcher instance.
*
* @var \Illuminate\Contracts\Events\Dispatcher
*/
protected $dispatcher;
/**
* Create a new migration command instance.
*
* @param \Illuminate\Database\Migrations\Migrator $migrator
* @param \Illuminate\Contracts\Events\Dispatcher $dispatcher
* @return void
*/
public function __construct(Migrator $migrator, Dispatcher $dispatcher)
{
parent::__construct();
$this->migrator = $migrator;
$this->dispatcher = $dispatcher;
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if (! $this->confirmToProceed()) {
return 1;
}
try {
$this->runMigrations();
} catch (Throwable $e) {
if ($this->option('graceful')) {
$this->components->warn($e->getMessage());
return 0;
}
throw $e;
}
return 0;
}
/**
* Run the pending migrations.
*
* @return void
*/
protected function runMigrations()
{
$this->migrator->usingConnection($this->option('database'), function () {
$this->prepareDatabase();
// Next, we will check to see if a path option has been defined. If it has
// we will use the path relative to the root of this installation folder
// so that migrations may be run for any path within the applications.
$this->migrator->setOutput($this->output)
->run($this->getMigrationPaths(), [
'pretend' => $this->option('pretend'),
'step' => $this->option('step'),
]);
// Finally, if the "seed" option has been given, we will re-run the database
// seed task to re-populate the database, which is convenient when adding
// a migration and a seed at the same time, as it is only this command.
if ($this->option('seed') && ! $this->option('pretend')) {
$this->call('db:seed', [
'--class' => $this->option('seeder') ?: 'Database\\Seeders\\DatabaseSeeder',
'--force' => true,
]);
}
});
}
/**
* Prepare the migration database for running.
*
* @return void
*/
protected function prepareDatabase()
{
if (! $this->repositoryExists()) {
$this->components->info('Preparing database.');
$this->components->task('Creating migration table', function () {
return $this->callSilent('migrate:install', array_filter([
'--database' => $this->option('database'),
])) == 0;
});
$this->newLine();
}
if (! $this->migrator->hasRunAnyMigrations() && ! $this->option('pretend')) {
$this->loadSchemaState();
}
}
/**
* Determine if the migrator repository exists.
*
* @return bool
*/
protected function repositoryExists()
{
return retry(2, fn () => $this->migrator->repositoryExists(), 0, function ($e) {
try {
if ($e->getPrevious() instanceof SQLiteDatabaseDoesNotExistException) {
return $this->createMissingSqliteDatabase($e->getPrevious()->path);
}
$connection = $this->migrator->resolveConnection($this->option('database'));
if (
$e->getPrevious() instanceof PDOException &&
$e->getPrevious()->getCode() === 1049 &&
in_array($connection->getDriverName(), ['mysql', 'mariadb'])) {
return $this->createMissingMysqlDatabase($connection);
}
return false;
} catch (Throwable) {
return false;
}
});
}
/**
* Create a missing SQLite database.
*
* @param string $path
* @return bool
*
* @throws \RuntimeException
*/
protected function createMissingSqliteDatabase($path)
{
if ($this->option('force')) {
return touch($path);
}
if ($this->option('no-interaction')) {
return false;
}
$this->components->warn('The SQLite database configured for this application does not exist: '.$path);
if (! confirm('Would you like to create it?', default: true)) {
$this->components->info('Operation cancelled. No database was created.');
throw new RuntimeException('Database was not created. Aborting migration.');
}
return touch($path);
}
/**
* Create a missing MySQL database.
*
* @return bool
*
* @throws \RuntimeException
*/
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 (! confirm('Would you like to create it?', default: true)) {
$this->components->info('Operation cancelled. No database was created.');
throw new RuntimeException('Database was not created. Aborting migration.');
}
}
try {
$this->laravel['config']->set("database.connections.{$connection->getName()}.database", 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');
}
}
@@ -0,0 +1,146 @@
<?php
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use Illuminate\Database\Migrations\MigrationCreator;
use Illuminate\Support\Composer;
use Illuminate\Support\Str;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'make:migration')]
class MigrateMakeCommand extends BaseCommand implements PromptsForMissingInput
{
/**
* The console command signature.
*
* @var string
*/
protected $signature = 'make:migration {name : The name of the migration}
{--create= : The table to be created}
{--table= : The table to migrate}
{--path= : The location where the migration file should be created}
{--realpath : Indicate any provided migration file paths are pre-resolved absolute paths}
{--fullpath : Output the full path of the migration (Deprecated)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new migration file';
/**
* The migration creator instance.
*
* @var \Illuminate\Database\Migrations\MigrationCreator
*/
protected $creator;
/**
* The Composer instance.
*
* @var \Illuminate\Support\Composer
*
* @deprecated Will be removed in a future Laravel version.
*/
protected $composer;
/**
* Create a new migration install command instance.
*
* @param \Illuminate\Database\Migrations\MigrationCreator $creator
* @param \Illuminate\Support\Composer $composer
* @return void
*/
public function __construct(MigrationCreator $creator, Composer $composer)
{
parent::__construct();
$this->creator = $creator;
$this->composer = $composer;
}
/**
* Execute the console command.
*
* @return void
*/
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 = Str::snake(trim($this->input->getArgument('name')));
$table = $this->input->getOption('table');
$create = $this->input->getOption('create') ?: false;
// If no table was given as an option but a create option is given then we
// will use the "create" option as the table name. This allows the devs
// to pass a table name into this option as a short-cut for creating.
if (! $table && is_string($create)) {
$table = $create;
$create = true;
}
// Next, we will attempt to guess the table name if this the migration has
// "create" in the name. This will allow us to provide a convenient way
// of creating migrations that create new tables for the application.
if (! $table) {
[$table, $create] = TableGuesser::guess($name);
}
// Now we are ready to write the migration out to disk. Once we've written
// the migration out, we will dump-autoload for the entire framework to
// make sure that the migrations are registered by the class loaders.
$this->writeMigration($name, $table, $create);
}
/**
* Write the migration file to disk.
*
* @param string $name
* @param string $table
* @param bool $create
* @return void
*/
protected function writeMigration($name, $table, $create)
{
$file = $this->creator->create(
$name, $this->getMigrationPath(), $table, $create
);
$this->components->info(sprintf('Migration [%s] created successfully.', $file));
}
/**
* Get migration path (either specified by '--path' option or default location).
*
* @return string
*/
protected function getMigrationPath()
{
if (! is_null($targetPath = $this->input->getOption('path'))) {
return ! $this->usingRealPath()
? $this->laravel->basePath().'/'.$targetPath
: $targetPath;
}
return parent::getMigrationPath();
}
/**
* Prompt for missing input arguments using the returned questions.
*
* @return array
*/
protected function promptForMissingArgumentsUsing()
{
return [
'name' => ['What should the migration be named?', 'E.g. create_flights_table'],
];
}
}
@@ -0,0 +1,163 @@
<?php
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Console\Prohibitable;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Events\DatabaseRefreshed;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'migrate:refresh')]
class RefreshCommand extends Command
{
use ConfirmableTrait, Prohibitable;
/**
* The console command name.
*
* @var string
*/
protected $name = 'migrate:refresh';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Reset and re-run all migrations';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if ($this->isProhibited() ||
! $this->confirmToProceed()) {
return 1;
}
// Next we'll gather some of the options so that we can have the right options
// to pass to the commands. This includes options such as which database to
// use and the path to use for the migration. Then we'll run the command.
$database = $this->input->getOption('database');
$path = $this->input->getOption('path');
// If the "step" option is specified it means we only want to rollback a small
// number of migrations before migrating again. For example, the user might
// only rollback and remigrate the latest four migrations instead of all.
$step = $this->input->getOption('step') ?: 0;
if ($step > 0) {
$this->runRollback($database, $path, $step);
} else {
$this->runReset($database, $path);
}
// The refresh command is essentially just a brief aggregate of a few other of
// the migration commands and just provides a convenient wrapper to execute
// them in succession. We'll also see if we need to re-seed the database.
$this->call('migrate', array_filter([
'--database' => $database,
'--path' => $path,
'--realpath' => $this->input->getOption('realpath'),
'--force' => true,
]));
if ($this->laravel->bound(Dispatcher::class)) {
$this->laravel[Dispatcher::class]->dispatch(
new DatabaseRefreshed($database, $this->needsSeeding())
);
}
if ($this->needsSeeding()) {
$this->runSeeder($database);
}
return 0;
}
/**
* Run the rollback command.
*
* @param string $database
* @param string $path
* @param int $step
* @return void
*/
protected function runRollback($database, $path, $step)
{
$this->call('migrate:rollback', array_filter([
'--database' => $database,
'--path' => $path,
'--realpath' => $this->input->getOption('realpath'),
'--step' => $step,
'--force' => true,
]));
}
/**
* Run the reset command.
*
* @param string $database
* @param string $path
* @return void
*/
protected function runReset($database, $path)
{
$this->call('migrate:reset', array_filter([
'--database' => $database,
'--path' => $path,
'--realpath' => $this->input->getOption('realpath'),
'--force' => true,
]));
}
/**
* 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'],
['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'],
];
}
}
@@ -0,0 +1,95 @@
<?php
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Console\Prohibitable;
use Illuminate\Database\Migrations\Migrator;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'migrate:reset')]
class ResetCommand extends BaseCommand
{
use ConfirmableTrait, Prohibitable;
/**
* The console command name.
*
* @var string
*/
protected $name = 'migrate:reset';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Rollback all database migrations';
/**
* The migrator instance.
*
* @var \Illuminate\Database\Migrations\Migrator
*/
protected $migrator;
/**
* Create a new migration rollback command instance.
*
* @param \Illuminate\Database\Migrations\Migrator $migrator
* @return void
*/
public function __construct(Migrator $migrator)
{
parent::__construct();
$this->migrator = $migrator;
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if ($this->isProhibited() ||
! $this->confirmToProceed()) {
return 1;
}
return $this->migrator->usingConnection($this->option('database'), function () {
// First, we'll make sure that the migration table actually exists before we
// start trying to rollback and re-run all of the migrations. If it's not
// present we'll just bail out with an info message for the developers.
if (! $this->migrator->repositoryExists()) {
return $this->components->warn('Migration table not found.');
}
$this->migrator->setOutput($this->output)->reset(
$this->getMigrationPaths(), $this->option('pretend')
);
});
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'],
['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'],
['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to the migrations files to be executed'],
['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'],
['pretend', null, InputOption::VALUE_NONE, 'Dump the SQL queries that would be run'],
];
}
}
@@ -0,0 +1,90 @@
<?php
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Database\Migrations\Migrator;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand('migrate:rollback')]
class RollbackCommand extends BaseCommand
{
use ConfirmableTrait;
/**
* The console command name.
*
* @var string
*/
protected $name = 'migrate:rollback';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Rollback the last database migration';
/**
* The migrator instance.
*
* @var \Illuminate\Database\Migrations\Migrator
*/
protected $migrator;
/**
* Create a new migration rollback command instance.
*
* @param \Illuminate\Database\Migrations\Migrator $migrator
* @return void
*/
public function __construct(Migrator $migrator)
{
parent::__construct();
$this->migrator = $migrator;
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if (! $this->confirmToProceed()) {
return 1;
}
$this->migrator->usingConnection($this->option('database'), function () {
$this->migrator->setOutput($this->output)->rollback(
$this->getMigrationPaths(), [
'pretend' => $this->option('pretend'),
'step' => (int) $this->option('step'),
'batch' => (int) $this->option('batch'),
]
);
});
return 0;
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'],
['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'],
['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to the migrations files to be executed'],
['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'],
['pretend', null, InputOption::VALUE_NONE, 'Dump the SQL queries that would be run'],
['step', null, InputOption::VALUE_OPTIONAL, 'The number of migrations to be reverted'],
['batch', null, InputOption::VALUE_REQUIRED, 'The batch of migrations (identified by their batch number) to be reverted'],
];
}
}
@@ -0,0 +1,142 @@
<?php
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Database\Migrations\Migrator;
use Illuminate\Support\Collection;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'migrate:status')]
class StatusCommand extends BaseCommand
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'migrate:status';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Show the status of each migration';
/**
* The migrator instance.
*
* @var \Illuminate\Database\Migrations\Migrator
*/
protected $migrator;
/**
* Create a new migration rollback command instance.
*
* @param \Illuminate\Database\Migrations\Migrator $migrator
* @return void
*/
public function __construct(Migrator $migrator)
{
parent::__construct();
$this->migrator = $migrator;
}
/**
* Execute the console command.
*
* @return int|null
*/
public function handle()
{
return $this->migrator->usingConnection($this->option('database'), function () {
if (! $this->migrator->repositoryExists()) {
$this->components->error('Migration table not found.');
return 1;
}
$ran = $this->migrator->getRepository()->getRan();
$batches = $this->migrator->getRepository()->getMigrationBatches();
$migrations = $this->getStatusFor($ran, $batches)
->when($this->option('pending') !== false, fn ($collection) => $collection->filter(function ($migration) {
return str($migration[1])->contains('Pending');
}));
if (count($migrations) > 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();
} elseif ($this->option('pending') !== false) {
$this->components->info('No pending migrations');
} else {
$this->components->info('No migrations found');
}
if ($this->option('pending') && $migrations->some(fn ($m) => str($m[1])->contains('Pending'))) {
return $this->option('pending');
}
});
}
/**
* Get the status for the given run migrations.
*
* @param array $ran
* @param array $batches
* @return \Illuminate\Support\Collection
*/
protected function getStatusFor(array $ran, array $batches)
{
return Collection::make($this->getAllMigrationFiles())
->map(function ($migration) use ($ran, $batches) {
$migrationName = $this->migrator->getMigrationName($migration);
$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];
});
}
/**
* Get an array of all of the migration files.
*
* @return array
*/
protected function getAllMigrationFiles()
{
return $this->migrator->getMigrationFiles($this->getMigrationPaths());
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'],
['pending', null, InputOption::VALUE_OPTIONAL, 'Only list pending migrations', false],
['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to the migrations files to use'],
['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'],
];
}
}
@@ -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];
}
}
}
}
@@ -0,0 +1,138 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\Events\DatabaseBusy;
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 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
*/
public function __construct(ConnectionResolverInterface $connection, Dispatcher $events)
{
parent::__construct();
$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']
)
);
});
}
}
@@ -0,0 +1,201 @@
<?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\ModelPruningFinished;
use Illuminate\Database\Events\ModelPruningStarting;
use Illuminate\Database\Events\ModelsPruned;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Finder\Finder;
#[AsCommand(name: 'model:prune')]
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}
{--path=* : Absolute path(s) to directories where models are located}
{--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");
});
$events->dispatch(new ModelPruningStarting($models->all()));
$models->each(function ($model) {
$this->pruneModel($model);
});
$events->dispatch(new ModelPruningFinished($models->all()));
$events->forget(ModelsPruned::class);
}
/**
* Prune the given model.
*
* @param string $model
* @return void
*/
protected function pruneModel(string $model)
{
$instance = new $model;
$chunkSize = property_exists($instance, 'prunableChunkSize')
? $instance->prunableChunkSize
: $this->option('chunk');
$total = $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(Finder::create()->in($this->getPath())->files()->name('*.php'))
->map(function ($model) {
$namespace = $this->laravel->getNamespace();
return $namespace.str_replace(
['/', '.php'],
['\\', ''],
Str::after($model->getRealPath(), realpath(app_path()).DIRECTORY_SEPARATOR)
);
})->when(! empty($except), function ($models) use ($except) {
return $models->reject(function ($model) use ($except) {
return in_array($model, $except);
});
})->filter(function ($model) {
return class_exists($model);
})->filter(function ($model) {
return $this->isPrunable($model);
})->values();
}
/**
* Get the path where models are located.
*
* @return string[]|string
*/
protected function getPath()
{
if (! empty($path = $this->option('path'))) {
return collect($path)->map(function ($path) {
return base_path($path);
})->all();
}
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.");
}
}
}
@@ -0,0 +1,140 @@
<?php
namespace Illuminate\Database\Console\Seeds;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
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;
/**
* The console command name.
*
* @var string
*/
protected $name = 'db:seed';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Seed the database with records';
/**
* The connection resolver instance.
*
* @var \Illuminate\Database\ConnectionResolverInterface
*/
protected $resolver;
/**
* Create a new database seed command instance.
*
* @param \Illuminate\Database\ConnectionResolverInterface $resolver
* @return void
*/
public function __construct(Resolver $resolver)
{
parent::__construct();
$this->resolver = $resolver;
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if (! $this->confirmToProceed()) {
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;
}
/**
* Get a seeder instance from the container.
*
* @return \Illuminate\Database\Seeder
*/
protected function getSeeder()
{
$class = $this->input->getArgument('class') ?? $this->input->getOption('class');
if (! str_contains($class, '\\')) {
$class = 'Database\\Seeders\\'.$class;
}
if ($class === 'Database\\Seeders\\DatabaseSeeder' &&
! class_exists($class)) {
$class = 'DatabaseSeeder';
}
return $this->laravel->make($class)
->setContainer($this->laravel)
->setCommand($this);
}
/**
* Get the name of the database connection to use.
*
* @return string
*/
protected function getDatabase()
{
$database = $this->input->getOption('database');
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.
*
* @return array
*/
protected function getOptions()
{
return [
['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'],
];
}
}
@@ -0,0 +1,92 @@
<?php
namespace Illuminate\Database\Console\Seeds;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'make:seeder')]
class SeederMakeCommand extends GeneratorCommand
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'make:seeder';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new seeder class';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Seeder';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
parent::handle();
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub()
{
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;
}
/**
* Get the destination class path.
*
* @param string $name
* @return string
*/
protected function getPath($name)
{
$name = str_replace('\\', '/', Str::replaceFirst($this->rootNamespace(), '', $name));
if (is_dir($this->laravel->databasePath().'/seeds')) {
return $this->laravel->databasePath().'/seeds/'.$name.'.php';
}
return $this->laravel->databasePath().'/seeders/'.$name.'.php';
}
/**
* Get the root namespace for the class.
*
* @return string
*/
protected function rootNamespace()
{
return 'Database\Seeders\\';
}
}
@@ -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);
}
}
@@ -0,0 +1,17 @@
<?php
namespace {{ namespace }};
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class {{ class }} extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}
@@ -0,0 +1,238 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\Schema\Builder;
use Illuminate\Support\Arr;
use Illuminate\Support\Number;
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 </>}
{--types : Show the user defined types}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Display information about the given database';
/**
* Execute the console command.
*
* @param \Illuminate\Database\ConnectionResolverInterface $connections
* @return int
*/
public function handle(ConnectionResolverInterface $connections)
{
$connection = $connections->connection($database = $this->input->getOption('database'));
$schema = $connection->getSchemaBuilder();
$data = [
'platform' => [
'config' => $this->getConfigFromDatabase($database),
'name' => $this->getConnectionName($connection, $database),
'version' => $connection->getServerVersion(),
'open_connections' => $this->getConnectionCount($connection),
],
'tables' => $this->tables($connection, $schema),
];
if ($this->option('views')) {
$data['views'] = $this->views($connection, $schema);
}
if ($this->option('types')) {
$data['types'] = $this->types($connection, $schema);
}
$this->display($data);
return 0;
}
/**
* Get information regarding the tables within the database.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Database\Schema\Builder $schema
* @return \Illuminate\Support\Collection
*/
protected function tables(ConnectionInterface $connection, Builder $schema)
{
return collect($schema->getTables())->map(fn ($table) => [
'table' => $table['name'],
'schema' => $table['schema'],
'size' => $table['size'],
'rows' => $this->option('counts') ? $connection->table($table['name'])->count() : null,
'engine' => $table['engine'],
'collation' => $table['collation'],
'comment' => $table['comment'],
]);
}
/**
* Get information regarding the views within the database.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Database\Schema\Builder $schema
* @return \Illuminate\Support\Collection
*/
protected function views(ConnectionInterface $connection, Builder $schema)
{
return collect($schema->getViews())
->reject(fn ($view) => str($view['name'])->startsWith(['pg_catalog', 'information_schema', 'spt_']))
->map(fn ($view) => [
'view' => $view['name'],
'schema' => $view['schema'],
'rows' => $connection->table($view->getName())->count(),
]);
}
/**
* Get information regarding the user-defined types within the database.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Database\Schema\Builder $schema
* @return \Illuminate\Support\Collection
*/
protected function types(ConnectionInterface $connection, Builder $schema)
{
return collect($schema->getTypes())
->map(fn ($type) => [
'name' => $type['name'],
'schema' => $type['schema'],
'type' => $type['type'],
'category' => $type['category'],
]);
}
/**
* Render the database information.
*
* @param array $data
* @return void
*/
protected function display(array $data)
{
$this->option('json') ? $this->displayJson($data) : $this->displayForCli($data);
}
/**
* Render the database information as JSON.
*
* @param array $data
* @return void
*/
protected function displayJson(array $data)
{
$this->output->writeln(json_encode($data));
}
/**
* Render the database information formatted for the CLI.
*
* @param array $data
* @return void
*/
protected function displayForCli(array $data)
{
$platform = $data['platform'];
$tables = $data['tables'];
$views = $data['views'] ?? null;
$types = $data['types'] ?? null;
$this->newLine();
$this->components->twoColumnDetail('<fg=green;options=bold>'.$platform['name'].'</>', $platform['version']);
$this->components->twoColumnDetail('Database', Arr::get($platform['config'], 'database'));
$this->components->twoColumnDetail('Host', Arr::get($platform['config'], 'host'));
$this->components->twoColumnDetail('Port', Arr::get($platform['config'], 'port'));
$this->components->twoColumnDetail('Username', Arr::get($platform['config'], 'username'));
$this->components->twoColumnDetail('URL', Arr::get($platform['config'], 'url'));
$this->components->twoColumnDetail('Open Connections', $platform['open_connections']);
$this->components->twoColumnDetail('Tables', $tables->count());
if ($tableSizeSum = $tables->sum('size')) {
$this->components->twoColumnDetail('Total Size', Number::fileSize($tableSizeSum, 2));
}
$this->newLine();
if ($tables->isNotEmpty()) {
$hasSchema = ! is_null($tables->first()['schema']);
$this->components->twoColumnDetail(
($hasSchema ? '<fg=green;options=bold>Schema</> <fg=gray;options=bold>/</> ' : '').'<fg=green;options=bold>Table</>',
'Size'.($this->option('counts') ? ' <fg=gray;options=bold>/</> <fg=yellow;options=bold>Rows</>' : '')
);
$tables->each(function ($table) {
if ($tableSize = $table['size']) {
$tableSize = Number::fileSize($tableSize, 2);
}
$this->components->twoColumnDetail(
($table['schema'] ? $table['schema'].' <fg=gray;options=bold>/</> ' : '').$table['table'].($this->output->isVerbose() ? ' <fg=gray>'.$table['engine'].'</>' : null),
($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()) {
$hasSchema = ! is_null($views->first()['schema']);
$this->components->twoColumnDetail(
($hasSchema ? '<fg=green;options=bold>Schema</> <fg=gray;options=bold>/</> ' : '').'<fg=green;options=bold>View</>',
'<fg=green;options=bold>Rows</>'
);
$views->each(fn ($view) => $this->components->twoColumnDetail(
($view['schema'] ? $view['schema'].' <fg=gray;options=bold>/</> ' : '').$view['view'],
Number::format($view['rows'])
));
$this->newLine();
}
if ($types && $types->isNotEmpty()) {
$hasSchema = ! is_null($types->first()['schema']);
$this->components->twoColumnDetail(
($hasSchema ? '<fg=green;options=bold>Schema</> <fg=gray;options=bold>/</> ' : '').'<fg=green;options=bold>Type</>',
'<fg=green;options=bold>Type</> <fg=gray;options=bold>/</> <fg=green;options=bold>Category</>'
);
$types->each(fn ($type) => $this->components->twoColumnDetail(
($type['schema'] ? $type['schema'].' <fg=gray;options=bold>/</> ' : '').$type['name'],
$type['type'].' <fg=gray;options=bold>/</> '.$type['category']
));
$this->newLine();
}
}
}
@@ -0,0 +1,546 @@
<?php
namespace Illuminate\Database\Console;
use BackedEnum;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
use ReflectionClass;
use ReflectionMethod;
use ReflectionNamedType;
use SplFileObject;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Output\OutputInterface;
use UnitEnum;
#[AsCommand(name: 'model:show')]
class ShowModelCommand extends DatabaseInspectionCommand
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'model:show {model}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Show information about an Eloquent model';
/**
* The console command signature.
*
* @var string
*/
protected $signature = 'model:show {model : The model to show}
{--database= : The database connection to use}
{--json : Output the model as JSON}';
/**
* The methods that can be called in a model to indicate a relation.
*
* @var array
*/
protected $relationMethods = [
'hasMany',
'hasManyThrough',
'hasOneThrough',
'belongsToMany',
'hasOne',
'belongsTo',
'morphOne',
'morphTo',
'morphMany',
'morphToMany',
'morphedByMany',
];
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$class = $this->qualifyModel($this->argument('model'));
try {
$model = $this->laravel->make($class);
$class = get_class($model);
} catch (BindingResolutionException $e) {
$this->components->error($e->getMessage());
return 1;
}
if ($this->option('database')) {
$model->setConnection($this->option('database'));
}
$this->display(
$class,
$model->getConnection()->getName(),
$model->getConnection()->getTablePrefix().$model->getTable(),
$this->getPolicy($model),
$this->getAttributes($model),
$this->getRelations($model),
$this->getEvents($model),
$this->getObservers($model),
);
return 0;
}
/**
* Get the first policy associated with this model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return string
*/
protected function getPolicy($model)
{
$policy = Gate::getPolicyFor($model::class);
return $policy ? $policy::class : null;
}
/**
* Get the column attributes for the given model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Support\Collection
*/
protected function getAttributes($model)
{
$connection = $model->getConnection();
$schema = $connection->getSchemaBuilder();
$table = $model->getTable();
$columns = $schema->getColumns($table);
$indexes = $schema->getIndexes($table);
return collect($columns)
->map(fn ($column) => [
'name' => $column['name'],
'type' => $column['type'],
'increments' => $column['auto_increment'],
'nullable' => $column['nullable'],
'default' => $this->getColumnDefault($column, $model),
'unique' => $this->columnIsUnique($column['name'], $indexes),
'fillable' => $model->isFillable($column['name']),
'hidden' => $this->attributeIsHidden($column['name'], $model),
'appended' => null,
'cast' => $this->getCastType($column['name'], $model),
])
->merge($this->getVirtualAttributes($model, $columns));
}
/**
* Get the virtual (non-column) attributes for the given model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param array $columns
* @return \Illuminate\Support\Collection
*/
protected function getVirtualAttributes($model, $columns)
{
$class = new ReflectionClass($model);
return collect($class->getMethods())
->reject(
fn (ReflectionMethod $method) => $method->isStatic()
|| $method->isAbstract()
|| $method->getDeclaringClass()->getName() === Model::class
)
->mapWithKeys(function (ReflectionMethod $method) use ($model) {
if (preg_match('/^get(.+)Attribute$/', $method->getName(), $matches) === 1) {
return [Str::snake($matches[1]) => 'accessor'];
} elseif ($model->hasAttributeMutator($method->getName())) {
return [Str::snake($method->getName()) => 'attribute'];
} else {
return [];
}
})
->reject(fn ($cast, $name) => collect($columns)->contains('name', $name))
->map(fn ($cast, $name) => [
'name' => $name,
'type' => null,
'increments' => false,
'nullable' => null,
'default' => null,
'unique' => null,
'fillable' => $model->isFillable($name),
'hidden' => $this->attributeIsHidden($name, $model),
'appended' => $model->hasAppended($name),
'cast' => $cast,
])
->values();
}
/**
* Get the relations from the given model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Support\Collection
*/
protected function getRelations($model)
{
return collect(get_class_methods($model))
->map(fn ($method) => new ReflectionMethod($model, $method))
->reject(
fn (ReflectionMethod $method) => $method->isStatic()
|| $method->isAbstract()
|| $method->getDeclaringClass()->getName() === Model::class
|| $method->getNumberOfParameters() > 0
)
->filter(function (ReflectionMethod $method) {
if ($method->getReturnType() instanceof ReflectionNamedType
&& is_subclass_of($method->getReturnType()->getName(), Relation::class)) {
return true;
}
$file = new SplFileObject($method->getFileName());
$file->seek($method->getStartLine() - 1);
$code = '';
while ($file->key() < $method->getEndLine()) {
$code .= trim($file->current());
$file->next();
}
return collect($this->relationMethods)
->contains(fn ($relationMethod) => str_contains($code, '$this->'.$relationMethod.'('));
})
->map(function (ReflectionMethod $method) use ($model) {
$relation = $method->invoke($model);
if (! $relation instanceof Relation) {
return null;
}
return [
'name' => $method->getName(),
'type' => Str::afterLast(get_class($relation), '\\'),
'related' => get_class($relation->getRelated()),
];
})
->filter()
->values();
}
/**
* Get the Events that the model dispatches.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Support\Collection
*/
protected function getEvents($model)
{
return collect($model->dispatchesEvents())
->map(fn (string $class, string $event) => [
'event' => $event,
'class' => $class,
])->values();
}
/**
* Get the Observers watching this model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Support\Collection
*/
protected function getObservers($model)
{
$listeners = $this->getLaravel()->make('events')->getRawListeners();
// Get the Eloquent observers for this model...
$listeners = array_filter($listeners, function ($v, $key) use ($model) {
return Str::startsWith($key, 'eloquent.') && Str::endsWith($key, $model::class);
}, ARRAY_FILTER_USE_BOTH);
// Format listeners Eloquent verb => Observer methods...
$extractVerb = function ($key) {
preg_match('/eloquent.([a-zA-Z]+)\: /', $key, $matches);
return $matches[1] ?? '?';
};
$formatted = [];
foreach ($listeners as $key => $observerMethods) {
$formatted[] = [
'event' => $extractVerb($key),
'observer' => array_map(fn ($obs) => is_string($obs) ? $obs : 'Closure', $observerMethods),
];
}
return collect($formatted);
}
/**
* Render the model information.
*
* @param string $class
* @param string $database
* @param string $table
* @param string $policy
* @param \Illuminate\Support\Collection $attributes
* @param \Illuminate\Support\Collection $relations
* @param \Illuminate\Support\Collection $events
* @param \Illuminate\Support\Collection $observers
* @return void
*/
protected function display($class, $database, $table, $policy, $attributes, $relations, $events, $observers)
{
$this->option('json')
? $this->displayJson($class, $database, $table, $policy, $attributes, $relations, $events, $observers)
: $this->displayCli($class, $database, $table, $policy, $attributes, $relations, $events, $observers);
}
/**
* Render the model information as JSON.
*
* @param string $class
* @param string $database
* @param string $table
* @param string $policy
* @param \Illuminate\Support\Collection $attributes
* @param \Illuminate\Support\Collection $relations
* @param \Illuminate\Support\Collection $events
* @param \Illuminate\Support\Collection $observers
* @return void
*/
protected function displayJson($class, $database, $table, $policy, $attributes, $relations, $events, $observers)
{
$this->output->writeln(
collect([
'class' => $class,
'database' => $database,
'table' => $table,
'policy' => $policy,
'attributes' => $attributes,
'relations' => $relations,
'events' => $events,
'observers' => $observers,
])->toJson()
);
}
/**
* Render the model information for the CLI.
*
* @param string $class
* @param string $database
* @param string $table
* @param string $policy
* @param \Illuminate\Support\Collection $attributes
* @param \Illuminate\Support\Collection $relations
* @param \Illuminate\Support\Collection $events
* @param \Illuminate\Support\Collection $observers
* @return void
*/
protected function displayCli($class, $database, $table, $policy, $attributes, $relations, $events, $observers)
{
$this->newLine();
$this->components->twoColumnDetail('<fg=green;options=bold>'.$class.'</>');
$this->components->twoColumnDetail('Database', $database);
$this->components->twoColumnDetail('Table', $table);
if ($policy) {
$this->components->twoColumnDetail('Policy', $policy);
}
$this->newLine();
$this->components->twoColumnDetail(
'<fg=green;options=bold>Attributes</>',
'type <fg=gray>/</> <fg=yellow;options=bold>cast</>',
);
foreach ($attributes as $attribute) {
$first = trim(sprintf(
'%s %s',
$attribute['name'],
collect(['increments', 'unique', 'nullable', 'fillable', 'hidden', 'appended'])
->filter(fn ($property) => $attribute[$property])
->map(fn ($property) => sprintf('<fg=gray>%s</>', $property))
->implode('<fg=gray>,</> ')
));
$second = collect([
$attribute['type'],
$attribute['cast'] ? '<fg=yellow;options=bold>'.$attribute['cast'].'</>' : null,
])->filter()->implode(' <fg=gray>/</> ');
$this->components->twoColumnDetail($first, $second);
if ($attribute['default'] !== null) {
$this->components->bulletList(
[sprintf('default: %s', $attribute['default'])],
OutputInterface::VERBOSITY_VERBOSE
);
}
}
$this->newLine();
$this->components->twoColumnDetail('<fg=green;options=bold>Relations</>');
foreach ($relations as $relation) {
$this->components->twoColumnDetail(
sprintf('%s <fg=gray>%s</>', $relation['name'], $relation['type']),
$relation['related']
);
}
$this->newLine();
$this->components->twoColumnDetail('<fg=green;options=bold>Events</>');
if ($events->count()) {
foreach ($events as $event) {
$this->components->twoColumnDetail(
sprintf('%s', $event['event']),
sprintf('%s', $event['class']),
);
}
}
$this->newLine();
$this->components->twoColumnDetail('<fg=green;options=bold>Observers</>');
if ($observers->count()) {
foreach ($observers as $observer) {
$this->components->twoColumnDetail(
sprintf('%s', $observer['event']),
implode(', ', $observer['observer'])
);
}
}
$this->newLine();
}
/**
* Get the cast type for the given column.
*
* @param string $column
* @param \Illuminate\Database\Eloquent\Model $model
* @return string|null
*/
protected function getCastType($column, $model)
{
if ($model->hasGetMutator($column) || $model->hasSetMutator($column)) {
return 'accessor';
}
if ($model->hasAttributeMutator($column)) {
return 'attribute';
}
return $this->getCastsWithDates($model)->get($column) ?? null;
}
/**
* Get the model casts, including any date casts.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Support\Collection
*/
protected function getCastsWithDates($model)
{
return collect($model->getDates())
->filter()
->flip()
->map(fn () => 'datetime')
->merge($model->getCasts());
}
/**
* Get the default value for the given column.
*
* @param array $column
* @param \Illuminate\Database\Eloquent\Model $model
* @return mixed|null
*/
protected function getColumnDefault($column, $model)
{
$attributeDefault = $model->getAttributes()[$column['name']] ?? null;
return match (true) {
$attributeDefault instanceof BackedEnum => $attributeDefault->value,
$attributeDefault instanceof UnitEnum => $attributeDefault->name,
default => $attributeDefault ?? $column['default'],
};
}
/**
* Determine if the given attribute is hidden.
*
* @param string $attribute
* @param \Illuminate\Database\Eloquent\Model $model
* @return bool
*/
protected function attributeIsHidden($attribute, $model)
{
if (count($model->getHidden()) > 0) {
return in_array($attribute, $model->getHidden());
}
if (count($model->getVisible()) > 0) {
return ! in_array($attribute, $model->getVisible());
}
return false;
}
/**
* Determine if the given attribute is unique.
*
* @param string $column
* @param array $indexes
* @return bool
*/
protected function columnIsUnique($column, $indexes)
{
return collect($indexes)->contains(
fn ($index) => count($index['columns']) === 1 && $index['columns'][0] === $column && $index['unique']
);
}
/**
* Qualify the given model class base name.
*
* @param string $model
* @return string
*
* @see \Illuminate\Console\GeneratorCommand
*/
protected function qualifyModel(string $model)
{
if (str_contains($model, '\\') && class_exists($model)) {
return $model;
}
$model = ltrim($model, '\\/');
$model = str_replace('/', '\\', $model);
$rootNamespace = $this->laravel->getNamespace();
if (Str::startsWith($model, $rootNamespace)) {
return $model;
}
return is_dir(app_path('Models'))
? $rootNamespace.'Models\\'.$model
: $rootNamespace.$model;
}
}
@@ -0,0 +1,248 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\Schema\Builder;
use Illuminate\Support\Arr;
use Illuminate\Support\Number;
use Symfony\Component\Console\Attribute\AsCommand;
use function Laravel\Prompts\select;
#[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)
{
$connection = $connections->connection($this->input->getOption('database'));
$schema = $connection->getSchemaBuilder();
$tables = $schema->getTables();
$tableName = $this->argument('table') ?: select(
'Which table would you like to inspect?',
array_column($tables, 'name')
);
$table = Arr::first($tables, fn ($table) => $table['name'] === $tableName);
if (! $table) {
$this->components->warn("Table [{$tableName}] doesn't exist.");
return 1;
}
$tableName = $this->withoutTablePrefix($connection, $table['name']);
$columns = $this->columns($schema, $tableName);
$indexes = $this->indexes($schema, $tableName);
$foreignKeys = $this->foreignKeys($schema, $tableName);
$data = [
'table' => [
'name' => $table['name'],
'columns' => count($columns),
'size' => $table['size'],
],
'columns' => $columns,
'indexes' => $indexes,
'foreign_keys' => $foreignKeys,
];
$this->display($data);
return 0;
}
/**
* Get the information regarding the table's columns.
*
* @param \Illuminate\Database\Schema\Builder $schema
* @param string $table
* @return \Illuminate\Support\Collection
*/
protected function columns(Builder $schema, string $table)
{
return collect($schema->getColumns($table))->map(fn ($column) => [
'column' => $column['name'],
'attributes' => $this->getAttributesForColumn($column),
'default' => $column['default'],
'type' => $column['type'],
]);
}
/**
* Get the attributes for a table column.
*
* @param array $column
* @return \Illuminate\Support\Collection
*/
protected function getAttributesForColumn($column)
{
return collect([
$column['type_name'],
$column['auto_increment'] ? 'autoincrement' : null,
$column['nullable'] ? 'nullable' : null,
$column['collation'],
])->filter();
}
/**
* Get the information regarding the table's indexes.
*
* @param \Illuminate\Database\Schema\Builder $schema
* @param string $table
* @return \Illuminate\Support\Collection
*/
protected function indexes(Builder $schema, string $table)
{
return collect($schema->getIndexes($table))->map(fn ($index) => [
'name' => $index['name'],
'columns' => collect($index['columns']),
'attributes' => $this->getAttributesForIndex($index),
]);
}
/**
* Get the attributes for a table index.
*
* @param array $index
* @return \Illuminate\Support\Collection
*/
protected function getAttributesForIndex($index)
{
return collect([
$index['type'],
count($index['columns']) > 1 ? 'compound' : null,
$index['unique'] && ! $index['primary'] ? 'unique' : null,
$index['primary'] ? 'primary' : null,
])->filter();
}
/**
* Get the information regarding the table's foreign keys.
*
* @param \Illuminate\Database\Schema\Builder $schema
* @param string $table
* @return \Illuminate\Support\Collection
*/
protected function foreignKeys(Builder $schema, string $table)
{
return collect($schema->getForeignKeys($table))->map(fn ($foreignKey) => [
'name' => $foreignKey['name'],
'columns' => collect($foreignKey['columns']),
'foreign_schema' => $foreignKey['foreign_schema'],
'foreign_table' => $foreignKey['foreign_table'],
'foreign_columns' => collect($foreignKey['foreign_columns']),
'on_update' => $foreignKey['on_update'],
'on_delete' => $foreignKey['on_delete'],
]);
}
/**
* Render the table information.
*
* @param array $data
* @return void
*/
protected function display(array $data)
{
$this->option('json') ? $this->displayJson($data) : $this->displayForCli($data);
}
/**
* Render the table information as JSON.
*
* @param array $data
* @return void
*/
protected function displayJson(array $data)
{
$this->output->writeln(json_encode($data));
}
/**
* Render the table information formatted for the CLI.
*
* @param array $data
* @return void
*/
protected function displayForCli(array $data)
{
[$table, $columns, $indexes, $foreignKeys] = [
$data['table'], $data['columns'], $data['indexes'], $data['foreign_keys'],
];
$this->newLine();
$this->components->twoColumnDetail('<fg=green;options=bold>'.$table['name'].'</>');
$this->components->twoColumnDetail('Columns', $table['columns']);
if ($size = $table['size']) {
$this->components->twoColumnDetail('Size', Number::fileSize($size, 2));
}
$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(', ').'</>',
(! is_null($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['columns']->implode(', ').' references '.$foreignKey['foreign_columns']->implode(', ').' on '.$foreignKey['foreign_table'].'</>',
$foreignKey['on_update'].' / '.$foreignKey['on_delete'],
);
});
$this->newLine();
}
}
}
@@ -0,0 +1,116 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Console\Prohibitable;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'db:wipe')]
class WipeCommand extends Command
{
use ConfirmableTrait, Prohibitable;
/**
* The console command name.
*
* @var string
*/
protected $name = '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->isProhibited() ||
! $this->confirmToProceed()) {
return Command::FAILURE;
}
$database = $this->input->getOption('database');
if ($this->option('drop-views')) {
$this->dropAllViews($database);
$this->components->info('Dropped all views successfully.');
}
$this->dropAllTables($database);
$this->components->info('Dropped all tables successfully.');
if ($this->option('drop-types')) {
$this->dropAllTypes($database);
$this->components->info('Dropped all types successfully.');
}
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'],
];
}
}
@@ -0,0 +1,458 @@
<?php
namespace Illuminate\Database;
use Illuminate\Database\Connectors\ConnectionFactory;
use Illuminate\Database\Events\ConnectionEstablished;
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\Contracts\Foundation\Application
*/
protected $app;
/**
* The database connection factory instance.
*
* @var \Illuminate\Database\Connectors\ConnectionFactory
*/
protected $factory;
/**
* The active connection instances.
*
* @var array<string, \Illuminate\Database\Connection>
*/
protected $connections = [];
/**
* The custom connection resolvers.
*
* @var array<string, callable>
*/
protected $extensions = [];
/**
* The callback to be executed to reconnect to a database.
*
* @var callable
*/
protected $reconnector;
/**
* Create a new database manager instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @param \Illuminate\Database\Connectors\ConnectionFactory $factory
* @return void
*/
public function __construct($app, ConnectionFactory $factory)
{
$this->app = $app;
$this->factory = $factory;
$this->reconnector = function ($connection) {
$this->reconnect($connection->getNameWithReadWriteType());
};
}
/**
* Get a database connection instance.
*
* @param string|null $name
* @return \Illuminate\Database\Connection
*/
public function connection($name = null)
{
$name = $name ?: $this->getDefaultConnection();
[$database, $type] = $this->parseConnectionName($name);
// If we haven't created this connection, we'll create it based on the config
// provided in the application. Once we've created the connections we will
// set the "fetch mode" for PDO which determines the query return types.
if (! isset($this->connections[$name])) {
$this->connections[$name] = $this->configure(
$this->makeConnection($database), $type
);
$this->dispatchConnectionEstablishedEvent($this->connections[$name]);
}
return $this->connections[$name];
}
/**
* Get a database connection instance from the given configuration.
*
* @param string $name
* @param array $config
* @param bool $force
* @return \Illuminate\Database\ConnectionInterface
*/
public function connectUsing(string $name, array $config, bool $force = false)
{
if ($force) {
$this->purge($name);
}
if (isset($this->connections[$name])) {
throw new RuntimeException("Cannot establish connection [$name] because another connection with that name already exists.");
}
$connection = $this->configure(
$this->factory->make($config, $name), null
);
$this->dispatchConnectionEstablishedEvent($connection);
return tap($connection, fn ($connection) => $this->connections[$name] = $connection);
}
/**
* Parse the connection into an array of the name and read / write type.
*
* @param string $name
* @return array
*/
protected function parseConnectionName($name)
{
$name = $name ?: $this->getDefaultConnection();
return Str::endsWith($name, ['::read', '::write'])
? explode('::', $name, 2) : [$name, null];
}
/**
* Make the database connection instance.
*
* @param string $name
* @return \Illuminate\Database\Connection
*/
protected function makeConnection($name)
{
$config = $this->configuration($name);
// First we will check by the connection name to see if an extension has been
// registered specifically for that connection. If it has we will call the
// Closure and pass it the config allowing it to resolve the connection.
if (isset($this->extensions[$name])) {
return call_user_func($this->extensions[$name], $config, $name);
}
// Next we will check to see if an extension has been registered for a driver
// and will call the Closure if so, which allows us to have a more generic
// resolver for the drivers themselves which applies to all connections.
if (isset($this->extensions[$driver = $config['driver']])) {
return call_user_func($this->extensions[$driver], $config, $name);
}
return $this->factory->make($config, $name);
}
/**
* Get the configuration for a connection.
*
* @param string $name
* @return array
*
* @throws \InvalidArgumentException
*/
protected function configuration($name)
{
$name = $name ?: $this->getDefaultConnection();
// To get the database connection configuration, we will just pull each of the
// connection configurations and get the configurations for the given name.
// If the configuration doesn't exist, we'll throw an exception and bail.
$connections = $this->app['config']['database.connections'];
if (is_null($config = Arr::get($connections, $name))) {
throw new InvalidArgumentException("Database connection [{$name}] not configured.");
}
return (new ConfigurationUrlParser)
->parseConfiguration($config);
}
/**
* Prepare the database connection instance.
*
* @param \Illuminate\Database\Connection $connection
* @param string $type
* @return \Illuminate\Database\Connection
*/
protected function configure(Connection $connection, $type)
{
$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
// used by the application. Once we're finished we'll return it back out.
if ($this->app->bound('events')) {
$connection->setEventDispatcher($this->app['events']);
}
if ($this->app->bound('db.transactions')) {
$connection->setTransactionManager($this->app['db.transactions']);
}
// Here we'll set a reconnector callback. This reconnector can be any callable
// so we will set a Closure to reconnect from this manager with the name of
// the connection, which will allow us to reconnect from the connections.
$connection->setReconnector($this->reconnector);
return $connection;
}
/**
* Dispatch the ConnectionEstablished event if the event dispatcher is available.
*
* @param \Illuminate\Database\Connection $connection
* @return void
*/
protected function dispatchConnectionEstablishedEvent(Connection $connection)
{
if (! $this->app->bound('events')) {
return;
}
$this->app['events']->dispatch(
new ConnectionEstablished($connection)
);
}
/**
* Prepare the read / write mode for database connection instance.
*
* @param \Illuminate\Database\Connection $connection
* @param string|null $type
* @return \Illuminate\Database\Connection
*/
protected function setPdoForType(Connection $connection, $type = null)
{
if ($type === 'read') {
$connection->setPdo($connection->getReadPdo());
} elseif ($type === 'write') {
$connection->setReadPdo($connection->getPdo());
}
return $connection;
}
/**
* Disconnect from the given database and remove from local cache.
*
* @param string|null $name
* @return void
*/
public function purge($name = null)
{
$name = $name ?: $this->getDefaultConnection();
$this->disconnect($name);
unset($this->connections[$name]);
}
/**
* Disconnect from the given database.
*
* @param string|null $name
* @return void
*/
public function disconnect($name = null)
{
if (isset($this->connections[$name = $name ?: $this->getDefaultConnection()])) {
$this->connections[$name]->disconnect();
}
}
/**
* Reconnect to the given database.
*
* @param string|null $name
* @return \Illuminate\Database\Connection
*/
public function reconnect($name = null)
{
$this->disconnect($name = $name ?: $this->getDefaultConnection());
if (! isset($this->connections[$name])) {
return $this->connection($name);
}
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.
*
* @param string $name
* @return \Illuminate\Database\Connection
*/
protected function refreshPdoConnections($name)
{
[$database, $type] = $this->parseConnectionName($name);
$fresh = $this->configure(
$this->makeConnection($database), $type
);
return $this->connections[$name]
->setPdo($fresh->getRawPdo())
->setReadPdo($fresh->getRawReadPdo());
}
/**
* Get the default connection name.
*
* @return string
*/
public function getDefaultConnection()
{
return $this->app['config']['database.default'];
}
/**
* Set the default connection name.
*
* @param string $name
* @return void
*/
public function setDefaultConnection($name)
{
$this->app['config']['database.default'] = $name;
}
/**
* Get all of the supported drivers.
*
* @return string[]
*/
public function supportedDrivers()
{
return ['mysql', 'mariadb', 'pgsql', 'sqlite', 'sqlsrv'];
}
/**
* Get all of the drivers that are actually available.
*
* @return string[]
*/
public function availableDrivers()
{
return array_intersect(
$this->supportedDrivers(),
str_replace('dblib', 'sqlsrv', PDO::getAvailableDrivers())
);
}
/**
* Register an extension connection resolver.
*
* @param string $name
* @param callable $resolver
* @return void
*/
public function extend($name, callable $resolver)
{
$this->extensions[$name] = $resolver;
}
/**
* Remove an extension connection resolver.
*
* @param string $name
* @return void
*/
public function forgetExtension($name)
{
unset($this->extensions[$name]);
}
/**
* Return all of the created connections.
*
* @return array<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
* @return mixed
*/
public function __call($method, $parameters)
{
if (static::hasMacro($method)) {
return $this->macroCall($method, $parameters);
}
return $this->connection()->$method(...$parameters);
}
}
@@ -0,0 +1,113 @@
<?php
namespace Illuminate\Database;
use Faker\Factory as FakerFactory;
use Faker\Generator as FakerGenerator;
use Illuminate\Contracts\Queue\EntityResolver;
use Illuminate\Database\Connectors\ConnectionFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\QueueEntityResolver;
use Illuminate\Support\ServiceProvider;
class DatabaseServiceProvider extends ServiceProvider
{
/**
* The array of resolved Faker instances.
*
* @var array
*/
protected static $fakers = [];
/**
* Bootstrap the application events.
*
* @return void
*/
public function boot()
{
Model::setConnectionResolver($this->app['db']);
Model::setEventDispatcher($this->app['events']);
}
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
Model::clearBootedModels();
$this->registerConnectionServices();
$this->registerFakerGenerator();
$this->registerQueueableEntityResolver();
}
/**
* Register the primary database bindings.
*
* @return void
*/
protected function registerConnectionServices()
{
// The connection factory is used to create the actual connection instances on
// the database. We will inject the factory into the manager so that it may
// make the connections while they are actually needed and not of before.
$this->app->singleton('db.factory', function ($app) {
return new ConnectionFactory($app);
});
// The database manager is used to resolve various connections, since multiple
// connections might be managed. It also implements the connection resolver
// interface which may be used by other components requiring connections.
$this->app->singleton('db', function ($app) {
return new DatabaseManager($app, $app['db.factory']);
});
$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;
});
}
/**
* Register the Faker Generator instance in the container.
*
* @return void
*/
protected function registerFakerGenerator()
{
$this->app->singleton(FakerGenerator::class, function ($app, $parameters) {
$locale = $parameters['locale'] ?? $app['config']->get('app.faker_locale', 'en_US');
if (! isset(static::$fakers[$locale])) {
static::$fakers[$locale] = FakerFactory::create($locale);
}
static::$fakers[$locale]->unique(true);
return static::$fakers[$locale];
});
}
/**
* Register the queueable entity resolver implementation.
*
* @return void
*/
protected function registerQueueableEntityResolver()
{
$this->app->singleton(EntityResolver::class, function () {
return new QueueEntityResolver;
});
}
}
@@ -0,0 +1,82 @@
<?php
namespace Illuminate\Database;
class DatabaseTransactionRecord
{
/**
* The name of the database connection.
*
* @var string
*/
public $connection;
/**
* The transaction level.
*
* @var int
*/
public $level;
/**
* The parent instance of this transaction.
*
* @var \Illuminate\Database\DatabaseTransactionRecord
*/
public $parent;
/**
* 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
* @param \Illuminate\Database\DatabaseTransactionRecord|null $parent
* @return void
*/
public function __construct($connection, $level, ?DatabaseTransactionRecord $parent = null)
{
$this->connection = $connection;
$this->level = $level;
$this->parent = $parent;
}
/**
* Register a callback to be executed after committing.
*
* @param callable $callback
* @return void
*/
public function addCallback($callback)
{
$this->callbacks[] = $callback;
}
/**
* 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;
}
}
@@ -0,0 +1,248 @@
<?php
namespace Illuminate\Database;
use Illuminate\Support\Collection;
class DatabaseTransactionsManager
{
/**
* All of the committed transactions.
*
* @var \Illuminate\Support\Collection<int, \Illuminate\Database\DatabaseTransactionRecord>
*/
protected $committedTransactions;
/**
* All of the pending transactions.
*
* @var \Illuminate\Support\Collection<int, \Illuminate\Database\DatabaseTransactionRecord>
*/
protected $pendingTransactions;
/**
* The current transaction.
*
* @var array
*/
protected $currentTransaction = [];
/**
* Create a new database transactions manager instance.
*
* @return void
*/
public function __construct()
{
$this->committedTransactions = new Collection;
$this->pendingTransactions = new Collection;
}
/**
* Start a new database transaction.
*
* @param string $connection
* @param int $level
* @return void
*/
public function begin($connection, $level)
{
$this->pendingTransactions->push(
$newTransaction = new DatabaseTransactionRecord(
$connection,
$level,
$this->currentTransaction[$connection] ?? null
)
);
$this->currentTransaction[$connection] = $newTransaction;
}
/**
* Commit the root database transaction and execute callbacks.
*
* @param string $connection
* @param int $levelBeingCommitted
* @param int $newTransactionLevel
* @return array
*/
public function commit($connection, $levelBeingCommitted, $newTransactionLevel)
{
$this->stageTransactions($connection, $levelBeingCommitted);
if (isset($this->currentTransaction[$connection])) {
$this->currentTransaction[$connection] = $this->currentTransaction[$connection]->parent;
}
if (! $this->afterCommitCallbacksShouldBeExecuted($newTransactionLevel) &&
$newTransactionLevel !== 0) {
return [];
}
// This method is only called when the root database transaction is committed so there
// shouldn't be any pending transactions, but going to clear them here anyways just
// in case. This method could be refactored to receive a level in the future too.
$this->pendingTransactions = $this->pendingTransactions->reject(
fn ($transaction) => $transaction->connection === $connection &&
$transaction->level >= $levelBeingCommitted
)->values();
[$forThisConnection, $forOtherConnections] = $this->committedTransactions->partition(
fn ($transaction) => $transaction->connection == $connection
);
$this->committedTransactions = $forOtherConnections->values();
$forThisConnection->map->executeCallbacks();
return $forThisConnection;
}
/**
* Move relevant pending transactions to a committed state.
*
* @param string $connection
* @param int $levelBeingCommitted
* @return void
*/
public function stageTransactions($connection, $levelBeingCommitted)
{
$this->committedTransactions = $this->committedTransactions->merge(
$this->pendingTransactions->filter(
fn ($transaction) => $transaction->connection === $connection &&
$transaction->level >= $levelBeingCommitted
)
);
$this->pendingTransactions = $this->pendingTransactions->reject(
fn ($transaction) => $transaction->connection === $connection &&
$transaction->level >= $levelBeingCommitted
);
}
/**
* Rollback the active database transaction.
*
* @param string $connection
* @param int $newTransactionLevel
* @return void
*/
public function rollback($connection, $newTransactionLevel)
{
if ($newTransactionLevel === 0) {
$this->removeAllTransactionsForConnection($connection);
} else {
$this->pendingTransactions = $this->pendingTransactions->reject(
fn ($transaction) => $transaction->connection == $connection &&
$transaction->level > $newTransactionLevel
)->values();
if ($this->currentTransaction) {
do {
$this->removeCommittedTransactionsThatAreChildrenOf($this->currentTransaction[$connection]);
$this->currentTransaction[$connection] = $this->currentTransaction[$connection]->parent;
} while (
isset($this->currentTransaction[$connection]) &&
$this->currentTransaction[$connection]->level > $newTransactionLevel
);
}
}
}
/**
* Remove all pending, completed, and current transactions for the given connection name.
*
* @param string $connection
* @return void
*/
protected function removeAllTransactionsForConnection($connection)
{
$this->currentTransaction[$connection] = null;
$this->pendingTransactions = $this->pendingTransactions->reject(
fn ($transaction) => $transaction->connection == $connection
)->values();
$this->committedTransactions = $this->committedTransactions->reject(
fn ($transaction) => $transaction->connection == $connection
)->values();
}
/**
* Remove all transactions that are children of the given transaction.
*
* @param \Illuminate\Database\DatabaseTransactionRecord $transaction
* @return void
*/
protected function removeCommittedTransactionsThatAreChildrenOf(DatabaseTransactionRecord $transaction)
{
[$removedTransactions, $this->committedTransactions] = $this->committedTransactions->partition(
fn ($committed) => $committed->connection == $transaction->connection &&
$committed->parent === $transaction
);
// There may be multiple deeply nested transactions that have already committed that we
// also need to remove. We will recurse down the children of all removed transaction
// instances until there are no more deeply nested child transactions for removal.
$removedTransactions->each(
fn ($transaction) => $this->removeCommittedTransactionsThatAreChildrenOf($transaction)
);
}
/**
* Register a transaction callback.
*
* @param callable $callback
* @return void
*/
public function addCallback($callback)
{
if ($current = $this->callbackApplicableTransactions()->last()) {
return $current->addCallback($callback);
}
$callback();
}
/**
* Get the transactions that are applicable to callbacks.
*
* @return \Illuminate\Support\Collection<int, \Illuminate\Database\DatabaseTransactionRecord>
*/
public function callbackApplicableTransactions()
{
return $this->pendingTransactions;
}
/**
* Determine if after commit callbacks should be executed for the given transaction level.
*
* @param int $level
* @return bool
*/
public function afterCommitCallbacksShouldBeExecuted($level)
{
return $level === 0;
}
/**
* Get all of the pending transactions.
*
* @return \Illuminate\Support\Collection
*/
public function getPendingTransactions()
{
return $this->pendingTransactions;
}
/**
* Get all of the committed transactions.
*
* @return \Illuminate\Support\Collection
*/
public function getCommittedTransactions()
{
return $this->committedTransactions;
}
}
@@ -0,0 +1,10 @@
<?php
namespace Illuminate\Database;
use PDOException;
class DeadlockException extends PDOException
{
//
}
@@ -0,0 +1,37 @@
<?php
namespace Illuminate\Database;
use Illuminate\Support\Str;
use PDOException;
use Throwable;
trait DetectsConcurrencyErrors
{
/**
* Determine if the given exception was caused by a concurrency error such as a deadlock or serialization failure.
*
* @param \Throwable $e
* @return bool
*/
protected function causedByConcurrencyError(Throwable $e)
{
if ($e instanceof PDOException && ($e->getCode() === 40001 || $e->getCode() === '40001')) {
return true;
}
$message = $e->getMessage();
return Str::contains($message, [
'Deadlock found when trying to get lock',
'deadlock detected',
'The database file is locked',
'database is locked',
'database table is locked',
'A table in the database is locked',
'has been chosen as the deadlock victim',
'Lock wait timeout exceeded; try restarting transaction',
'WSREP detected deadlock/conflict and aborted the transaction. Try restarting the transaction',
]);
}
}
@@ -0,0 +1,81 @@
<?php
namespace Illuminate\Database;
use Illuminate\Support\Str;
use Throwable;
trait DetectsLostConnections
{
/**
* Determine if the given exception was caused by a lost connection.
*
* @param \Throwable $e
* @return bool
*/
protected function causedByLostConnection(Throwable $e)
{
$message = $e->getMessage();
return Str::contains($message, [
'server has gone away',
'Server has gone away',
'no connection to the server',
'Lost connection',
'is dead or not enabled',
'Error while sending',
'decryption failed or bad record mac',
'server closed the connection unexpectedly',
'SSL connection has been closed unexpectedly',
'Error writing data to the connection',
'Resource deadlock avoided',
'Transaction() on null',
'child connection forced to terminate due to client_idle_limit',
'query_wait_timeout',
'reset by peer',
'Physical connection is not usable',
'TCP Provider: Error code 0x68',
'ORA-03114',
'Packets out of order. Expected',
'Adaptive Server connection failed',
'Communication link failure',
'connection is no longer usable',
'Login timeout expired',
'SQLSTATE[HY000] [2002] Connection refused',
'running with the --read-only option so it cannot execute this statement',
'The connection is broken and recovery is not possible. The connection is marked by the client driver as unrecoverable. No attempt was made to restore the connection.',
'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Try again',
'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Name or service not known',
'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo for',
'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: EOF detected',
'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.',
'Unknown $curl_error_code: 77',
'SSL: Handshake timed out',
'SQLSTATE[08006] [7] SSL error: sslv3 alert unexpected message',
'SQLSTATE[08006] [7] unrecognized SSL error code:',
'SQLSTATE[HY000] [2002] No connection could be made because the target machine actively refused it',
'SQLSTATE[HY000] [2002] A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond',
'SQLSTATE[HY000] [2002] Network is unreachable',
'SQLSTATE[HY000] [2002] The requested address is not valid in its context',
'SQLSTATE[HY000] [2002] A socket operation was attempted to an unreachable network',
'SQLSTATE[HY000]: General error: 3989',
'went away',
'No such file or directory',
'server is shutting down',
'failed to connect to',
]);
}
}
@@ -0,0 +1,19 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class ObservedBy
{
/**
* Create a new attribute instance.
*
* @param array|string $classes
* @return void
*/
public function __construct(array|string $classes)
{
}
}
@@ -0,0 +1,19 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class ScopedBy
{
/**
* Create a new attribute instance.
*
* @param array|string $classes
* @return void
*/
public function __construct(array|string $classes)
{
}
}
@@ -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;
}
}
@@ -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(string $event)
{
return new BroadcastableModelEventOccurred($this, $event);
}
/**
* Get the channels that model events should broadcast on.
*
* @param string $event
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn($event)
{
return [$this];
}
/**
* Get the queue connection that should be used to broadcast model events.
*
* @return string|null
*/
public function broadcastConnection()
{
//
}
/**
* Get the queue that should be used to broadcast model events.
*
* @return string|null
*/
public function broadcastQueue()
{
//
}
/**
* Determine if the model event broadcast queued job should be dispatched after all transactions are committed.
*
* @return bool
*/
public function broadcastAfterCommit()
{
return false;
}
}
@@ -0,0 +1,18 @@
<?php
namespace Illuminate\Database\Eloquent;
trait BroadcastsEventsAfterCommit
{
use BroadcastsEvents;
/**
* Determine if the model event broadcast queued job should be dispatched after all transactions are committed.
*
* @return bool
*/
public function broadcastAfterCommit()
{
return true;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,46 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use ArrayObject as BaseArrayObject;
use Illuminate\Contracts\Support\Arrayable;
use JsonSerializable;
/**
* @template TKey of array-key
* @template TItem
*
* @extends \ArrayObject<TKey, TItem>
*/
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();
}
}
@@ -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 \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Database\Eloquent\Casts\ArrayObject<array-key, mixed>, iterable>
*/
public static function castUsing(array $arguments)
{
return new class implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
if (! isset($attributes[$key])) {
return;
}
$data = Json::decode($attributes[$key]);
return is_array($data) ? new ArrayObject($data, ArrayObject::ARRAY_AS_PROPS) : null;
}
public function set($model, $key, $value, $attributes)
{
return [$key => Json::encode($value)];
}
public function serialize($model, string $key, $value, array $attributes)
{
return $value->getArrayCopy();
}
};
}
}
@@ -0,0 +1,60 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Collection;
use InvalidArgumentException;
class AsCollection implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @param array $arguments
* @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection<array-key, mixed>, iterable>
*/
public static function castUsing(array $arguments)
{
return new class($arguments) implements CastsAttributes
{
public function __construct(protected array $arguments)
{
}
public function get($model, $key, $value, $attributes)
{
if (! isset($attributes[$key])) {
return;
}
$data = Json::decode($attributes[$key]);
$collectionClass = $this->arguments[0] ?? Collection::class;
if (! is_a($collectionClass, Collection::class, true)) {
throw new InvalidArgumentException('The provided class must extend ['.Collection::class.'].');
}
return is_array($data) ? new $collectionClass($data) : null;
}
public function set($model, $key, $value, $attributes)
{
return [$key => Json::encode($value)];
}
};
}
/**
* Specify the collection for the cast.
*
* @param class-string $class
* @return string
*/
public static function using($class)
{
return static::class.':'.$class;
}
}
@@ -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 \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Database\Eloquent\Casts\ArrayObject<array-key, mixed>, iterable>
*/
public static function castUsing(array $arguments)
{
return new class implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
if (isset($attributes[$key])) {
return new ArrayObject(Json::decode(Crypt::decryptString($attributes[$key])));
}
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;
}
};
}
}
@@ -0,0 +1,63 @@
<?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;
use InvalidArgumentException;
class AsEncryptedCollection implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @param array $arguments
* @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection<array-key, mixed>, iterable>
*/
public static function castUsing(array $arguments)
{
return new class($arguments) implements CastsAttributes
{
public function __construct(protected array $arguments)
{
}
public function get($model, $key, $value, $attributes)
{
$collectionClass = $this->arguments[0] ?? Collection::class;
if (! is_a($collectionClass, Collection::class, true)) {
throw new InvalidArgumentException('The provided class must extend ['.Collection::class.'].');
}
if (isset($attributes[$key])) {
return new $collectionClass(Json::decode(Crypt::decryptString($attributes[$key])));
}
return null;
}
public function set($model, $key, $value, $attributes)
{
if (! is_null($value)) {
return [$key => Crypt::encryptString(Json::encode($value))];
}
return null;
}
};
}
/**
* Specify the collection for the cast.
*
* @param class-string $class
* @return string
*/
public static function using($class)
{
return static::class.':'.$class;
}
}
@@ -0,0 +1,95 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use BackedEnum;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Collection;
class AsEnumArrayObject implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @template TEnum
*
* @param array{class-string<TEnum>} $arguments
* @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Database\Eloquent\Casts\ArrayObject<array-key, TEnum>, iterable<TEnum>>
*/
public static function castUsing(array $arguments)
{
return new class($arguments) implements CastsAttributes
{
protected $arguments;
public function __construct(array $arguments)
{
$this->arguments = $arguments;
}
public function get($model, $key, $value, $attributes)
{
if (! isset($attributes[$key])) {
return;
}
$data = Json::decode($attributes[$key]);
if (! is_array($data)) {
return;
}
$enumClass = $this->arguments[0];
return new ArrayObject((new Collection($data))->map(function ($value) use ($enumClass) {
return is_subclass_of($enumClass, BackedEnum::class)
? $enumClass::from($value)
: constant($enumClass.'::'.$value);
})->toArray());
}
public function set($model, $key, $value, $attributes)
{
if ($value === null) {
return [$key => null];
}
$storable = [];
foreach ($value as $enum) {
$storable[] = $this->getStorableEnumValue($enum);
}
return [$key => Json::encode($storable)];
}
public function serialize($model, string $key, $value, array $attributes)
{
return (new Collection($value->getArrayCopy()))->map(function ($enum) {
return $this->getStorableEnumValue($enum);
})->toArray();
}
protected function getStorableEnumValue($enum)
{
if (is_string($enum) || is_int($enum)) {
return $enum;
}
return $enum instanceof BackedEnum ? $enum->value : $enum->name;
}
};
}
/**
* Specify the Enum for the cast.
*
* @param class-string $class
* @return string
*/
public static function of($class)
{
return static::class.':'.$class;
}
}
@@ -0,0 +1,91 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use BackedEnum;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Collection;
class AsEnumCollection implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @template TEnum of \UnitEnum|\BackedEnum
*
* @param array{class-string<TEnum>} $arguments
* @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection<array-key, TEnum>, iterable<TEnum>>
*/
public static function castUsing(array $arguments)
{
return new class($arguments) implements CastsAttributes
{
protected $arguments;
public function __construct(array $arguments)
{
$this->arguments = $arguments;
}
public function get($model, $key, $value, $attributes)
{
if (! isset($attributes[$key])) {
return;
}
$data = Json::decode($attributes[$key]);
if (! is_array($data)) {
return;
}
$enumClass = $this->arguments[0];
return (new Collection($data))->map(function ($value) use ($enumClass) {
return is_subclass_of($enumClass, BackedEnum::class)
? $enumClass::from($value)
: constant($enumClass.'::'.$value);
});
}
public function set($model, $key, $value, $attributes)
{
$value = $value !== null
? Json::encode((new Collection($value))->map(function ($enum) {
return $this->getStorableEnumValue($enum);
})->jsonSerialize())
: null;
return [$key => $value];
}
public function serialize($model, string $key, $value, array $attributes)
{
return (new Collection($value))->map(function ($enum) {
return $this->getStorableEnumValue($enum);
})->toArray();
}
protected function getStorableEnumValue($enum)
{
if (is_string($enum) || is_int($enum)) {
return $enum;
}
return $enum instanceof BackedEnum ? $enum->value : $enum->name;
}
};
}
/**
* Specify the Enum for the cast.
*
* @param class-string $class
* @return string
*/
public static function of($class)
{
return static::class.':'.$class;
}
}
@@ -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 \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Stringable, string|\Stringable>
*/
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;
}
};
}
}
@@ -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;
}
}
@@ -0,0 +1,54 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
class Json
{
/**
* The custom JSON encoder.
*
* @var callable|null
*/
protected static $encoder;
/**
* The custom JSON decode.
*
* @var callable|null
*/
protected static $decoder;
/**
* Encode the given value.
*/
public static function encode(mixed $value): mixed
{
return isset(static::$encoder) ? (static::$encoder)($value) : json_encode($value);
}
/**
* Decode the given value.
*/
public static function decode(mixed $value, ?bool $associative = true): mixed
{
return isset(static::$decoder)
? (static::$decoder)($value, $associative)
: json_decode($value, $associative);
}
/**
* Encode all values using the given callable.
*/
public static function encodeUsing(?callable $encoder): void
{
static::$encoder = $encoder;
}
/**
* Decode all values using the given callable.
*/
public static function decodeUsing(?callable $decoder): void
{
static::$decoder = $decoder;
}
}
@@ -0,0 +1,789 @@
<?php
namespace Illuminate\Database\Eloquent;
use Illuminate\Contracts\Queue\QueueableCollection;
use Illuminate\Contracts\Queue\QueueableEntity;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
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
{
use InteractsWithDictionary;
/**
* Find a model in the collection by key.
*
* @template TFindDefault
*
* @param mixed $key
* @param TFindDefault $default
* @return static<TKey, TModel>|TModel|TFindDefault
*/
public function find($key, $default = null)
{
if ($key instanceof Model) {
$key = $key->getKey();
}
if ($key instanceof Arrayable) {
$key = $key->toArray();
}
if (is_array($key)) {
if ($this->isEmpty()) {
return new static;
}
return $this->whereIn($this->first()->getKeyName(), $key);
}
return Arr::first($this->items, fn ($model) => $model->getKey() == $key, $default);
}
/**
* Load a set of relationships onto the collection.
*
* @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder<TModel>): mixed)|string>|string $relations
* @return $this
*/
public function load($relations)
{
if ($this->isNotEmpty()) {
if (is_string($relations)) {
$relations = func_get_args();
}
$query = $this->first()->newQueryWithoutRelationships()->with($relations);
$this->items = $query->eagerLoadRelations($this->items);
}
return $this;
}
/**
* Load a set of aggregations over relationship's column onto the collection.
*
* @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder<TModel>): mixed)|string>|string $relations
* @param string $column
* @param string|null $function
* @return $this
*/
public function loadAggregate($relations, $column, $function = null)
{
if ($this->isEmpty()) {
return $this;
}
$models = $this->first()->newModelQuery()
->whereKey($this->modelKeys())
->select($this->first()->getKeyName())
->withAggregate($relations, $column, $function)
->get()
->keyBy($this->first()->getKeyName());
$attributes = Arr::except(
array_keys($models->first()->getAttributes()),
$models->first()->getKeyName()
);
$this->each(function ($model) use ($models, $attributes) {
$extraAttributes = Arr::only($models->get($model->getKey())->getAttributes(), $attributes);
$model->forceFill($extraAttributes)
->syncOriginalAttributes($attributes)
->mergeCasts($models->get($model->getKey())->getCasts());
});
return $this;
}
/**
* Load a set of relationship counts onto the collection.
*
* @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder<TModel>): 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<TModel>): 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<TModel>): 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<TModel>): 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<TModel>): 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<TModel>): 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<TModel>): 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<int, TModel> $models
* @param array $path
* @return void
*/
protected function loadMissingRelation(self $models, array $path)
{
$relation = array_shift($path);
$name = explode(':', key($relation))[0];
if (is_string(reset($relation))) {
$relation = reset($relation);
}
$models->filter(fn ($model) => ! is_null($model) && ! $model->relationLoaded($name))->load($relation);
if (empty($path)) {
return;
}
$models = $models->pluck($name)->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<TModel>): mixed)|string> $relations
* @return $this
*/
public function loadMorph($relation, $relations)
{
$this->pluck($relation)
->filter()
->groupBy(fn ($model) => get_class($model))
->each(fn ($models, $className) => static::make($models)->load($relations[$className] ?? []));
return $this;
}
/**
* Load a set of relationship counts onto the mixed relationship collection.
*
* @param string $relation
* @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder<TModel>): mixed)|string> $relations
* @return $this
*/
public function loadMorphCount($relation, $relations)
{
$this->pluck($relation)
->filter()
->groupBy(fn ($model) => get_class($model))
->each(fn ($models, $className) => static::make($models)->loadCount($relations[$className] ?? []));
return $this;
}
/**
* Determine if a key exists in the collection.
*
* @param (callable(TModel, TKey): bool)|TModel|string|int $key
* @param mixed $operator
* @param mixed $value
* @return bool
*/
public function contains($key, $operator = null, $value = null)
{
if (func_num_args() > 1 || $this->useAsCallable($key)) {
return parent::contains(...func_get_args());
}
if ($key instanceof Model) {
return parent::contains(fn ($model) => $model->is($key));
}
return parent::contains(fn ($model) => $model->getKey() == $key);
}
/**
* Get the array of primary keys.
*
* @return array<int, array-key>
*/
public function modelKeys()
{
return array_map(fn ($model) => $model->getKey(), $this->items);
}
/**
* Merge the collection with the given items.
*
* @param iterable<array-key, TModel> $items
* @return static
*/
public function merge($items)
{
$dictionary = $this->getDictionary();
foreach ($items as $item) {
$dictionary[$this->getDictionaryKey($item->getKey())] = $item;
}
return new static(array_values($dictionary));
}
/**
* Run a map over each of the items.
*
* @template TMapValue
*
* @param callable(TModel, TKey): TMapValue $callback
* @return \Illuminate\Support\Collection<TKey, TMapValue>|static<TKey, TMapValue>
*/
public function map(callable $callback)
{
$result = parent::map($callback);
return $result->contains(fn ($item) => ! $item instanceof Model) ? $result->toBase() : $result;
}
/**
* Run an associative map over each of the items.
*
* The callback should return an associative array with a single key / value pair.
*
* @template TMapWithKeysKey of array-key
* @template TMapWithKeysValue
*
* @param callable(TModel, TKey): array<TMapWithKeysKey, TMapWithKeysValue> $callback
* @return \Illuminate\Support\Collection<TMapWithKeysKey, TMapWithKeysValue>|static<TMapWithKeysKey, TMapWithKeysValue>
*/
public function mapWithKeys(callable $callback)
{
$result = parent::mapWithKeys($callback);
return $result->contains(fn ($item) => ! $item instanceof Model) ? $result->toBase() : $result;
}
/**
* Reload a fresh model instance from the database for all the entities.
*
* @param array<array-key, string>|string $with
* @return static
*/
public function fresh($with = [])
{
if ($this->isEmpty()) {
return new static;
}
$model = $this->first();
$freshModels = $model->newQueryWithoutScopes()
->with(is_string($with) ? func_get_args() : $with)
->whereIn($model->getKeyName(), $this->modelKeys())
->get()
->getDictionary();
return $this->filter(fn ($model) => $model->exists && isset($freshModels[$model->getKey()]))
->map(fn ($model) => $freshModels[$model->getKey()]);
}
/**
* Diff the collection with the given items.
*
* @param iterable<array-key, TModel> $items
* @return static
*/
public function diff($items)
{
$diff = new static;
$dictionary = $this->getDictionary($items);
foreach ($this->items as $item) {
if (! isset($dictionary[$this->getDictionaryKey($item->getKey())])) {
$diff->add($item);
}
}
return $diff;
}
/**
* Intersect the collection with the given 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) {
if (isset($dictionary[$this->getDictionaryKey($item->getKey())])) {
$intersect->add($item);
}
}
return $intersect;
}
/**
* Return only unique items from the collection.
*
* @param (callable(TModel, TKey): mixed)|string|null $key
* @param bool $strict
* @return static<int, TModel>
*/
public function unique($key = null, $strict = false)
{
if (! is_null($key)) {
return parent::unique($key, $strict);
}
return new static(array_values($this->getDictionary()));
}
/**
* Returns only the models from the collection with the specified keys.
*
* @param array<array-key, mixed>|null $keys
* @return static<int, TModel>
*/
public function only($keys)
{
if (is_null($keys)) {
return new static($this->items);
}
$dictionary = Arr::only($this->getDictionary(), array_map($this->getDictionaryKey(...), (array) $keys));
return new static(array_values($dictionary));
}
/**
* Returns all models in the collection except the models with specified keys.
*
* @param array<array-key, mixed>|null $keys
* @return static<int, TModel>
*/
public function except($keys)
{
if (is_null($keys)) {
return new static($this->items);
}
$dictionary = Arr::except($this->getDictionary(), array_map($this->getDictionaryKey(...), (array) $keys));
return new static(array_values($dictionary));
}
/**
* Make the given, typically visible, attributes hidden across the entire collection.
*
* @param array<array-key, string>|string $attributes
* @return $this
*/
public function makeHidden($attributes)
{
return $this->each->makeHidden($attributes);
}
/**
* Make the given, typically hidden, attributes visible across the entire collection.
*
* @param array<array-key, string>|string $attributes
* @return $this
*/
public function makeVisible($attributes)
{
return $this->each->makeVisible($attributes);
}
/**
* Set the visible attributes across the entire collection.
*
* @param array<int, string> $visible
* @return $this
*/
public function setVisible($visible)
{
return $this->each->setVisible($visible);
}
/**
* Set the hidden attributes across the entire collection.
*
* @param array<int, string> $hidden
* @return $this
*/
public function setHidden($hidden)
{
return $this->each->setHidden($hidden);
}
/**
* 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 iterable<array-key, TModel>|null $items
* @return array<array-key, TModel>
*/
public function getDictionary($items = null)
{
$items = is_null($items) ? $this->items : $items;
$dictionary = [];
foreach ($items as $value) {
$dictionary[$this->getDictionaryKey($value->getKey())] = $value;
}
return $dictionary;
}
/**
* The following methods are intercepted to always return base collections.
*/
/**
* Count the number of items in the collection by a field or using a callback.
*
* @param (callable(TModel, TKey): array-key)|string|null $countBy
* @return \Illuminate\Support\Collection<array-key, int>
*/
public function countBy($countBy = null)
{
return $this->toBase()->countBy($countBy);
}
/**
* Collapse the collection of items into a single array.
*
* @return \Illuminate\Support\Collection<int, mixed>
*/
public function collapse()
{
return $this->toBase()->collapse();
}
/**
* Get a flattened array of the items in the collection.
*
* @param int $depth
* @return \Illuminate\Support\Collection<int, mixed>
*/
public function flatten($depth = INF)
{
return $this->toBase()->flatten($depth);
}
/**
* Flip the items in the collection.
*
* @return \Illuminate\Support\Collection<TModel, TKey>
*/
public function flip()
{
return $this->toBase()->flip();
}
/**
* Get the keys of the collection items.
*
* @return \Illuminate\Support\Collection<int, TKey>
*/
public function keys()
{
return $this->toBase()->keys();
}
/**
* 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 an array with the values of a given key.
*
* @param string|array<array-key, string>|null $value
* @param string|null $key
* @return \Illuminate\Support\Collection<array-key, mixed>
*/
public function pluck($value, $key = null)
{
return $this->toBase()->pluck($value, $key);
}
/**
* Zip the collection together with one or more arrays.
*
* @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 $this->toBase()->zip(...func_get_args());
}
/**
* Get the comparison function to detect duplicates.
*
* @param bool $strict
* @return callable(TModel, TModel): bool
*/
protected function duplicateComparator($strict)
{
return fn ($a, $b) => $a->is($b);
}
/**
* Get the type of the entities being queued.
*
* @return string|null
*
* @throws \LogicException
*/
public function getQueueableClass()
{
if ($this->isEmpty()) {
return;
}
$class = $this->getQueueableModelClass($this->first());
$this->each(function ($model) use ($class) {
if ($this->getQueueableModelClass($model) !== $class) {
throw new LogicException('Queueing collections with multiple model types is not supported.');
}
});
return $class;
}
/**
* Get the queueable class name for the given model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return string
*/
protected function getQueueableModelClass($model)
{
return method_exists($model, 'getQueueableClassName')
? $model->getQueueableClassName()
: get_class($model);
}
/**
* Get the identifiers for all of the entities.
*
* @return array<int, mixed>
*/
public function getQueueableIds()
{
if ($this->isEmpty()) {
return [];
}
return $this->first() instanceof QueueableEntity
? $this->map->getQueueableId()->all()
: $this->modelKeys();
}
/**
* Get the relationships of the entities being queued.
*
* @return array<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<TModel>
*
* @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(fn ($model) => ! $model instanceof $class)->isNotEmpty()) {
throw new LogicException('Unable to create query for collection with mixed types.');
}
return $model->newModelQuery()->whereKey($this->modelKeys());
}
}
@@ -0,0 +1,255 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
trait GuardsAttributes
{
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [];
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['*'];
/**
* Indicates if all mass assignment is enabled.
*
* @var bool
*/
protected static $unguarded = false;
/**
* The actual columns that exist on the database and can be guarded.
*
* @var array<string>
*/
protected static $guardableColumns = [];
/**
* Get the fillable attributes for the model.
*
* @return array<string>
*/
public function getFillable()
{
return $this->fillable;
}
/**
* Set the fillable attributes for the model.
*
* @param array<string> $fillable
* @return $this
*/
public function fillable(array $fillable)
{
$this->fillable = $fillable;
return $this;
}
/**
* Merge new fillable attributes with existing fillable attributes on the model.
*
* @param array<string> $fillable
* @return $this
*/
public function mergeFillable(array $fillable)
{
$this->fillable = array_values(array_unique(array_merge($this->fillable, $fillable)));
return $this;
}
/**
* Get the guarded attributes for the model.
*
* @return array<string>
*/
public function getGuarded()
{
return $this->guarded === false
? []
: $this->guarded;
}
/**
* Set the guarded attributes for the model.
*
* @param array<string> $guarded
* @return $this
*/
public function guard(array $guarded)
{
$this->guarded = $guarded;
return $this;
}
/**
* Merge new guarded attributes with existing guarded attributes on the model.
*
* @param array<string> $guarded
* @return $this
*/
public function mergeGuarded(array $guarded)
{
$this->guarded = array_values(array_unique(array_merge($this->guarded, $guarded)));
return $this;
}
/**
* Disable all mass assignable restrictions.
*
* @param bool $state
* @return void
*/
public static function unguard($state = true)
{
static::$unguarded = $state;
}
/**
* Enable the mass assignment restrictions.
*
* @return void
*/
public static function reguard()
{
static::$unguarded = false;
}
/**
* Determine if the current state is "unguarded".
*
* @return bool
*/
public static function isUnguarded()
{
return static::$unguarded;
}
/**
* Run the given callable while being unguarded.
*
* @param callable $callback
* @return mixed
*/
public static function unguarded(callable $callback)
{
if (static::$unguarded) {
return $callback();
}
static::unguard();
try {
return $callback();
} finally {
static::reguard();
}
}
/**
* Determine if the given attribute may be mass assigned.
*
* @param string $key
* @return bool
*/
public function isFillable($key)
{
if (static::$unguarded) {
return true;
}
// If the key is in the "fillable" array, we can of course assume that it's
// a fillable attribute. Otherwise, we will check the guarded array when
// we need to determine if the attribute is black-listed on the model.
if (in_array($key, $this->getFillable())) {
return true;
}
// If the attribute is explicitly listed in the "guarded" array then we can
// return false immediately. This means this attribute is definitely not
// fillable and there is no point in going any further in this method.
if ($this->isGuarded($key)) {
return false;
}
return empty($this->getFillable()) &&
! str_contains($key, '.') &&
! str_starts_with($key, '_');
}
/**
* Determine if the given key is guarded.
*
* @param string $key
* @return bool
*/
public function isGuarded($key)
{
if (empty($this->getGuarded())) {
return false;
}
return $this->getGuarded() == ['*'] ||
! empty(preg_grep('/^'.preg_quote($key, '/').'$/i', $this->getGuarded())) ||
! $this->isGuardableColumn($key);
}
/**
* Determine if the given column is a valid, guardable column.
*
* @param string $key
* @return bool
*/
protected function isGuardableColumn($key)
{
if (! isset(static::$guardableColumns[get_class($this)])) {
$columns = $this->getConnection()
->getSchemaBuilder()
->getColumnListing($this->getTable());
if (empty($columns)) {
return true;
}
static::$guardableColumns[get_class($this)] = $columns;
}
return in_array($key, static::$guardableColumns[get_class($this)]);
}
/**
* Determine if the model is totally guarded.
*
* @return bool
*/
public function totallyGuarded()
{
return count($this->getFillable()) === 0 && $this->getGuarded() == ['*'];
}
/**
* Get the fillable attributes of a given array.
*
* @param array $attributes
* @return array
*/
protected function fillableFromArray(array $attributes)
{
if (count($this->getFillable()) > 0 && ! static::$unguarded) {
return array_intersect_key($attributes, array_flip($this->getFillable()));
}
return $attributes;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,452 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Events\NullDispatcher;
use Illuminate\Support\Arr;
use InvalidArgumentException;
use ReflectionClass;
trait HasEvents
{
/**
* The event map for the model.
*
* Allows for object-based events for native Eloquent events.
*
* @var array
*/
protected $dispatchesEvents = [];
/**
* User exposed observable events.
*
* These are extra user-defined events observers may subscribe to.
*
* @var array
*/
protected $observables = [];
/**
* Boot the has event trait for a model.
*
* @return void
*/
public static function bootHasEvents()
{
static::observe(static::resolveObserveAttributes());
}
/**
* Resolve the observe class names from the attributes.
*
* @return array
*/
public static function resolveObserveAttributes()
{
$reflectionClass = new ReflectionClass(static::class);
return collect($reflectionClass->getAttributes(ObservedBy::class))
->map(fn ($attribute) => $attribute->getArguments())
->flatten()
->all();
}
/**
* Register observers with the model.
*
* @param object|array|string $classes
* @return void
*
* @throws \RuntimeException
*/
public static function observe($classes)
{
$instance = new static;
foreach (Arr::wrap($classes) as $class) {
$instance->registerObserver($class);
}
}
/**
* Register a single observer with the model.
*
* @param object|string $class
* @return void
*
* @throws \RuntimeException
*/
protected function registerObserver($class)
{
$className = $this->resolveObserverClassName($class);
// When registering a model observer, we will spin through the possible events
// and determine if this observer has that method. If it does, we will hook
// it into the model's event system, making it convenient to watch these.
foreach ($this->getObservableEvents() as $event) {
if (method_exists($class, $event)) {
static::registerModelEvent($event, $className.'@'.$event);
}
}
}
/**
* Resolve the observer's class name from an object or string.
*
* @param object|string $class
* @return string
*
* @throws \InvalidArgumentException
*/
private function resolveObserverClassName($class)
{
if (is_object($class)) {
return get_class($class);
}
if (class_exists($class)) {
return $class;
}
throw new InvalidArgumentException('Unable to find observer: '.$class);
}
/**
* Get the observable event names.
*
* @return array
*/
public function getObservableEvents()
{
return array_merge(
[
'retrieved', 'creating', 'created', 'updating', 'updated',
'saving', 'saved', 'restoring', 'restored', 'replicating',
'deleting', 'deleted', 'forceDeleting', 'forceDeleted',
],
$this->observables
);
}
/**
* Set the observable event names.
*
* @param array $observables
* @return $this
*/
public function setObservableEvents(array $observables)
{
$this->observables = $observables;
return $this;
}
/**
* Add an observable event name.
*
* @param array|mixed $observables
* @return void
*/
public function addObservableEvents($observables)
{
$this->observables = array_unique(array_merge(
$this->observables, is_array($observables) ? $observables : func_get_args()
));
}
/**
* Remove an observable event name.
*
* @param array|mixed $observables
* @return void
*/
public function removeObservableEvents($observables)
{
$this->observables = array_diff(
$this->observables, is_array($observables) ? $observables : func_get_args()
);
}
/**
* Register a model event with the dispatcher.
*
* @param string $event
* @param \Illuminate\Events\QueuedClosure|\Closure|string|array $callback
* @return void
*/
protected static function registerModelEvent($event, $callback)
{
if (isset(static::$dispatcher)) {
$name = static::class;
static::$dispatcher->listen("eloquent.{$event}: {$name}", $callback);
}
}
/**
* Fire the given event for the model.
*
* @param string $event
* @param bool $halt
* @return mixed
*/
protected function fireModelEvent($event, $halt = true)
{
if (! isset(static::$dispatcher)) {
return true;
}
// First, we will get the proper method to call on the event dispatcher, and then we
// will attempt to fire a custom, object based event for the given event. If that
// returns a result we can return that result, or we'll call the string events.
$method = $halt ? 'until' : 'dispatch';
$result = $this->filterModelEventResults(
$this->fireCustomModelEvent($event, $method)
);
if ($result === false) {
return false;
}
return ! empty($result) ? $result : static::$dispatcher->{$method}(
"eloquent.{$event}: ".static::class, $this
);
}
/**
* Fire a custom model event for the given event.
*
* @param string $event
* @param string $method
* @return mixed|null
*/
protected function fireCustomModelEvent($event, $method)
{
if (! isset($this->dispatchesEvents[$event])) {
return;
}
$result = static::$dispatcher->$method(new $this->dispatchesEvents[$event]($this));
if (! is_null($result)) {
return $result;
}
}
/**
* Filter the model event results.
*
* @param mixed $result
* @return mixed
*/
protected function filterModelEventResults($result)
{
if (is_array($result)) {
$result = array_filter($result, function ($response) {
return ! is_null($response);
});
}
return $result;
}
/**
* Register a retrieved model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|\Closure|string|array $callback
* @return void
*/
public static function retrieved($callback)
{
static::registerModelEvent('retrieved', $callback);
}
/**
* Register a saving model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|\Closure|string|array $callback
* @return void
*/
public static function saving($callback)
{
static::registerModelEvent('saving', $callback);
}
/**
* Register a saved model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|\Closure|string|array $callback
* @return void
*/
public static function saved($callback)
{
static::registerModelEvent('saved', $callback);
}
/**
* Register an updating model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|\Closure|string|array $callback
* @return void
*/
public static function updating($callback)
{
static::registerModelEvent('updating', $callback);
}
/**
* Register an updated model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|\Closure|string|array $callback
* @return void
*/
public static function updated($callback)
{
static::registerModelEvent('updated', $callback);
}
/**
* Register a creating model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|\Closure|string|array $callback
* @return void
*/
public static function creating($callback)
{
static::registerModelEvent('creating', $callback);
}
/**
* Register a created model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|\Closure|string|array $callback
* @return void
*/
public static function created($callback)
{
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 \Illuminate\Events\QueuedClosure|\Closure|string|array $callback
* @return void
*/
public static function deleting($callback)
{
static::registerModelEvent('deleting', $callback);
}
/**
* Register a deleted model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|\Closure|string|array $callback
* @return void
*/
public static function deleted($callback)
{
static::registerModelEvent('deleted', $callback);
}
/**
* Remove all the event listeners for the model.
*
* @return void
*/
public static function flushEventListeners()
{
if (! isset(static::$dispatcher)) {
return;
}
$instance = new static;
foreach ($instance->getObservableEvents() as $event) {
static::$dispatcher->forget("eloquent.{$event}: ".static::class);
}
foreach (array_values($instance->dispatchesEvents) as $event) {
static::$dispatcher->forget($event);
}
}
/**
* Get the event map for the model.
*
* @return array
*/
public function dispatchesEvents()
{
return $this->dispatchesEvents;
}
/**
* Get the event dispatcher instance.
*
* @return \Illuminate\Contracts\Events\Dispatcher
*/
public static function getEventDispatcher()
{
return static::$dispatcher;
}
/**
* Set the event dispatcher instance.
*
* @param \Illuminate\Contracts\Events\Dispatcher $dispatcher
* @return void
*/
public static function setEventDispatcher(Dispatcher $dispatcher)
{
static::$dispatcher = $dispatcher;
}
/**
* Unset the event dispatcher for models.
*
* @return void
*/
public static function unsetEventDispatcher()
{
static::$dispatcher = null;
}
/**
* Execute a callback without firing any model events for any model type.
*
* @param callable $callback
* @return mixed
*/
public static function withoutEvents(callable $callback)
{
$dispatcher = static::getEventDispatcher();
if ($dispatcher) {
static::setEventDispatcher(new NullDispatcher($dispatcher));
}
try {
return $callback();
} finally {
if ($dispatcher) {
static::setEventDispatcher($dispatcher);
}
}
}
}
@@ -0,0 +1,138 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Closure;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Support\Arr;
use InvalidArgumentException;
use ReflectionClass;
trait HasGlobalScopes
{
/**
* Boot the has global scopes trait for a model.
*
* @return void
*/
public static function bootHasGlobalScopes()
{
static::addGlobalScopes(static::resolveGlobalScopeAttributes());
}
/**
* Resolve the global scope class names from the attributes.
*
* @return array
*/
public static function resolveGlobalScopeAttributes()
{
$reflectionClass = new ReflectionClass(static::class);
return collect($reflectionClass->getAttributes(ScopedBy::class))
->map(fn ($attribute) => $attribute->getArguments())
->flatten()
->all();
}
/**
* Register a new global scope on the model.
*
* @param \Illuminate\Database\Eloquent\Scope|\Closure|string $scope
* @param \Illuminate\Database\Eloquent\Scope|\Closure|null $implementation
* @return mixed
*
* @throws \InvalidArgumentException
*/
public static function addGlobalScope($scope, $implementation = null)
{
if (is_string($scope) && ($implementation instanceof Closure || $implementation instanceof Scope)) {
return static::$globalScopes[static::class][$scope] = $implementation;
} elseif ($scope instanceof Closure) {
return static::$globalScopes[static::class][spl_object_hash($scope)] = $scope;
} elseif ($scope instanceof Scope) {
return static::$globalScopes[static::class][get_class($scope)] = $scope;
} elseif (is_string($scope) && class_exists($scope) && is_subclass_of($scope, Scope::class)) {
return static::$globalScopes[static::class][$scope] = new $scope;
}
throw new InvalidArgumentException('Global scope must be an instance of Closure or Scope or be a class name of a class extending '.Scope::class);
}
/**
* Register multiple global scopes on the model.
*
* @param array $scopes
* @return void
*/
public static function addGlobalScopes(array $scopes)
{
foreach ($scopes as $key => $scope) {
if (is_string($key)) {
static::addGlobalScope($key, $scope);
} else {
static::addGlobalScope($scope);
}
}
}
/**
* Determine if a model has a global scope.
*
* @param \Illuminate\Database\Eloquent\Scope|string $scope
* @return bool
*/
public static function hasGlobalScope($scope)
{
return ! is_null(static::getGlobalScope($scope));
}
/**
* Get a global scope registered with the model.
*
* @param \Illuminate\Database\Eloquent\Scope|string $scope
* @return \Illuminate\Database\Eloquent\Scope|\Closure|null
*/
public static function getGlobalScope($scope)
{
if (is_string($scope)) {
return Arr::get(static::$globalScopes, static::class.'.'.$scope);
}
return Arr::get(
static::$globalScopes, static::class.'.'.get_class($scope)
);
}
/**
* Get all of the global scopes that are currently registered.
*
* @return array
*/
public static function getAllGlobalScopes()
{
return static::$globalScopes;
}
/**
* Set the current global scopes.
*
* @param array $scopes
* @return void
*/
public static function setAllGlobalScopes($scopes)
{
static::$globalScopes = $scopes;
}
/**
* Get the global scopes for this class instance.
*
* @return array
*/
public function getGlobalScopes()
{
return Arr::get(static::$globalScopes, static::class, []);
}
}
@@ -0,0 +1,989 @@
<?php
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\PendingHasThroughRelationship;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\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;
trait HasRelationships
{
/**
* The loaded relationships for the model.
*
* @var array
*/
protected $relations = [];
/**
* The relationships that should be touched on save.
*
* @var array
*/
protected $touches = [];
/**
* The many to many relationship methods.
*
* @var string[]
*/
public static $manyMethods = [
'belongsToMany', 'morphToMany', 'morphedByMany',
];
/**
* 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.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
*
* @param class-string<TRelatedModel> $related
* @param string|null $foreignKey
* @param string|null $localKey
* @return \Illuminate\Database\Eloquent\Relations\HasOne<TRelatedModel, $this>
*/
public function hasOne($related, $foreignKey = null, $localKey = null)
{
$instance = $this->newRelatedInstance($related);
$foreignKey = $foreignKey ?: $this->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
return $this->newHasOne($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);
}
/**
* Instantiate a new HasOne relationship.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param TDeclaringModel $parent
* @param string $foreignKey
* @param string $localKey
* @return \Illuminate\Database\Eloquent\Relations\HasOne<TRelatedModel, TDeclaringModel>
*/
protected function newHasOne(Builder $query, Model $parent, $foreignKey, $localKey)
{
return new HasOne($query, $parent, $foreignKey, $localKey);
}
/**
* Define a has-one-through relationship.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
*
* @param class-string<TRelatedModel> $related
* @param class-string<TIntermediateModel> $through
* @param string|null $firstKey
* @param string|null $secondKey
* @param string|null $localKey
* @param string|null $secondLocalKey
* @return \Illuminate\Database\Eloquent\Relations\HasOneThrough<TRelatedModel, TIntermediateModel, $this>
*/
public function hasOneThrough($related, $through, $firstKey = null, $secondKey = null, $localKey = null, $secondLocalKey = null)
{
$through = $this->newRelatedThroughInstance($through);
$firstKey = $firstKey ?: $this->getForeignKey();
$secondKey = $secondKey ?: $through->getForeignKey();
return $this->newHasOneThrough(
$this->newRelatedInstance($related)->newQuery(), $this, $through,
$firstKey, $secondKey, $localKey ?: $this->getKeyName(),
$secondLocalKey ?: $through->getKeyName()
);
}
/**
* Instantiate a new HasOneThrough relationship.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param TDeclaringModel $farParent
* @param TIntermediateModel $throughParent
* @param string $firstKey
* @param string $secondKey
* @param string $localKey
* @param string $secondLocalKey
* @return \Illuminate\Database\Eloquent\Relations\HasOneThrough<TRelatedModel, TIntermediateModel, TDeclaringModel>
*/
protected function newHasOneThrough(Builder $query, Model $farParent, Model $throughParent, $firstKey, $secondKey, $localKey, $secondLocalKey)
{
return new HasOneThrough($query, $farParent, $throughParent, $firstKey, $secondKey, $localKey, $secondLocalKey);
}
/**
* Define a polymorphic one-to-one relationship.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
*
* @param class-string<TRelatedModel> $related
* @param string $name
* @param string|null $type
* @param string|null $id
* @param string|null $localKey
* @return \Illuminate\Database\Eloquent\Relations\MorphOne<TRelatedModel, $this>
*/
public function morphOne($related, $name, $type = null, $id = null, $localKey = null)
{
$instance = $this->newRelatedInstance($related);
[$type, $id] = $this->getMorphs($name, $type, $id);
$table = $instance->getTable();
$localKey = $localKey ?: $this->getKeyName();
return $this->newMorphOne($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $localKey);
}
/**
* Instantiate a new MorphOne relationship.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param TDeclaringModel $parent
* @param string $type
* @param string $id
* @param string $localKey
* @return \Illuminate\Database\Eloquent\Relations\MorphOne<TRelatedModel, TDeclaringModel>
*/
protected function newMorphOne(Builder $query, Model $parent, $type, $id, $localKey)
{
return new MorphOne($query, $parent, $type, $id, $localKey);
}
/**
* Define an inverse one-to-one or many relationship.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
*
* @param class-string<TRelatedModel> $related
* @param string|null $foreignKey
* @param string|null $ownerKey
* @param string|null $relation
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<TRelatedModel, $this>
*/
public function belongsTo($related, $foreignKey = null, $ownerKey = null, $relation = null)
{
// If no relation name was given, we will use this debug backtrace to extract
// the calling method's name and use that as the relationship name as most
// of the time this will be what we desire to use for the relationships.
if (is_null($relation)) {
$relation = $this->guessBelongsToRelation();
}
$instance = $this->newRelatedInstance($related);
// If no foreign key was supplied, we can use a backtrace to guess the proper
// foreign key name by using the name of the relationship function, which
// when combined with an "_id" should conventionally match the columns.
if (is_null($foreignKey)) {
$foreignKey = Str::snake($relation).'_'.$instance->getKeyName();
}
// Once we have the foreign key names we'll just create a new Eloquent query
// for the related models and return the relationship instance which will
// actually be responsible for retrieving and hydrating every relation.
$ownerKey = $ownerKey ?: $instance->getKeyName();
return $this->newBelongsTo(
$instance->newQuery(), $this, $foreignKey, $ownerKey, $relation
);
}
/**
* Instantiate a new BelongsTo relationship.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param TDeclaringModel $child
* @param string $foreignKey
* @param string $ownerKey
* @param string $relation
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<TRelatedModel, TDeclaringModel>
*/
protected function newBelongsTo(Builder $query, Model $child, $foreignKey, $ownerKey, $relation)
{
return new BelongsTo($query, $child, $foreignKey, $ownerKey, $relation);
}
/**
* Define a polymorphic, inverse one-to-one or many relationship.
*
* @param string|null $name
* @param string|null $type
* @param string|null $id
* @param string|null $ownerKey
* @return \Illuminate\Database\Eloquent\Relations\MorphTo<\Illuminate\Database\Eloquent\Model, $this>
*/
public function morphTo($name = null, $type = null, $id = null, $ownerKey = null)
{
// If no name is provided, we will use the backtrace to get the function name
// since that is most likely the name of the polymorphic interface. We can
// use that to get both the class and foreign key that will be utilized.
$name = $name ?: $this->guessBelongsToRelation();
[$type, $id] = $this->getMorphs(
Str::snake($name), $type, $id
);
// If the type value is null it is probably safe to assume we're eager loading
// the relationship. In this case we'll just pass in a dummy query where we
// need to remove any eager loads that may already be defined on a model.
return is_null($class = $this->getAttributeFromArray($type)) || $class === ''
? $this->morphEagerTo($name, $type, $id, $ownerKey)
: $this->morphInstanceTo($class, $name, $type, $id, $ownerKey);
}
/**
* Define a polymorphic, inverse one-to-one or many relationship.
*
* @param string $name
* @param string $type
* @param string $id
* @param string $ownerKey
* @return \Illuminate\Database\Eloquent\Relations\MorphTo<\Illuminate\Database\Eloquent\Model, $this>
*/
protected function morphEagerTo($name, $type, $id, $ownerKey)
{
return $this->newMorphTo(
$this->newQuery()->setEagerLoads([]), $this, $id, $ownerKey, $type, $name
);
}
/**
* Define a polymorphic, inverse one-to-one or many relationship.
*
* @param string $target
* @param string $name
* @param string $type
* @param string $id
* @param string $ownerKey
* @return \Illuminate\Database\Eloquent\Relations\MorphTo<\Illuminate\Database\Eloquent\Model, $this>
*/
protected function morphInstanceTo($target, $name, $type, $id, $ownerKey)
{
$instance = $this->newRelatedInstance(
static::getActualClassNameForMorph($target)
);
return $this->newMorphTo(
$instance->newQuery(), $this, $id, $ownerKey ?? $instance->getKeyName(), $type, $name
);
}
/**
* Instantiate a new MorphTo relationship.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param TDeclaringModel $parent
* @param string $foreignKey
* @param string $ownerKey
* @param string $type
* @param string $relation
* @return \Illuminate\Database\Eloquent\Relations\MorphTo<TRelatedModel, TDeclaringModel>
*/
protected function newMorphTo(Builder $query, Model $parent, $foreignKey, $ownerKey, $type, $relation)
{
return new MorphTo($query, $parent, $foreignKey, $ownerKey, $type, $relation);
}
/**
* Retrieve the actual class name for a given morph class.
*
* @param string $class
* @return string
*/
public static function getActualClassNameForMorph($class)
{
return Arr::get(Relation::morphMap() ?: [], $class, $class);
}
/**
* Guess the "belongs to" relationship name.
*
* @return string
*/
protected function guessBelongsToRelation()
{
[$one, $two, $caller] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
return $caller['function'];
}
/**
* Create a pending has-many-through or has-one-through relationship.
*
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
*
* @param string|\Illuminate\Database\Eloquent\Relations\HasMany<TIntermediateModel, covariant $this>|\Illuminate\Database\Eloquent\Relations\HasOne<TIntermediateModel, covariant $this> $relationship
* @return (
* $relationship is string
* ? \Illuminate\Database\Eloquent\PendingHasThroughRelationship<\Illuminate\Database\Eloquent\Model, $this>
* : \Illuminate\Database\Eloquent\PendingHasThroughRelationship<TIntermediateModel, $this>
* )
*/
public function through($relationship)
{
if (is_string($relationship)) {
$relationship = $this->{$relationship}();
}
return new PendingHasThroughRelationship($this, $relationship);
}
/**
* Define a one-to-many relationship.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
*
* @param class-string<TRelatedModel> $related
* @param string|null $foreignKey
* @param string|null $localKey
* @return \Illuminate\Database\Eloquent\Relations\HasMany<TRelatedModel, $this>
*/
public function hasMany($related, $foreignKey = null, $localKey = null)
{
$instance = $this->newRelatedInstance($related);
$foreignKey = $foreignKey ?: $this->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
return $this->newHasMany(
$instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey
);
}
/**
* Instantiate a new HasMany relationship.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param TDeclaringModel $parent
* @param string $foreignKey
* @param string $localKey
* @return \Illuminate\Database\Eloquent\Relations\HasMany<TRelatedModel, TDeclaringModel>
*/
protected function newHasMany(Builder $query, Model $parent, $foreignKey, $localKey)
{
return new HasMany($query, $parent, $foreignKey, $localKey);
}
/**
* Define a has-many-through relationship.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
*
* @param class-string<TRelatedModel> $related
* @param class-string<TIntermediateModel> $through
* @param string|null $firstKey
* @param string|null $secondKey
* @param string|null $localKey
* @param string|null $secondLocalKey
* @return \Illuminate\Database\Eloquent\Relations\HasManyThrough<TRelatedModel, TIntermediateModel, $this>
*/
public function hasManyThrough($related, $through, $firstKey = null, $secondKey = null, $localKey = null, $secondLocalKey = null)
{
$through = $this->newRelatedThroughInstance($through);
$firstKey = $firstKey ?: $this->getForeignKey();
$secondKey = $secondKey ?: $through->getForeignKey();
return $this->newHasManyThrough(
$this->newRelatedInstance($related)->newQuery(),
$this,
$through,
$firstKey,
$secondKey,
$localKey ?: $this->getKeyName(),
$secondLocalKey ?: $through->getKeyName()
);
}
/**
* Instantiate a new HasManyThrough relationship.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param TDeclaringModel $farParent
* @param TIntermediateModel $throughParent
* @param string $firstKey
* @param string $secondKey
* @param string $localKey
* @param string $secondLocalKey
* @return \Illuminate\Database\Eloquent\Relations\HasManyThrough<TRelatedModel, TIntermediateModel, TDeclaringModel>
*/
protected function newHasManyThrough(Builder $query, Model $farParent, Model $throughParent, $firstKey, $secondKey, $localKey, $secondLocalKey)
{
return new HasManyThrough($query, $farParent, $throughParent, $firstKey, $secondKey, $localKey, $secondLocalKey);
}
/**
* Define a polymorphic one-to-many relationship.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
*
* @param class-string<TRelatedModel> $related
* @param string $name
* @param string|null $type
* @param string|null $id
* @param string|null $localKey
* @return \Illuminate\Database\Eloquent\Relations\MorphMany<TRelatedModel, $this>
*/
public function morphMany($related, $name, $type = null, $id = null, $localKey = null)
{
$instance = $this->newRelatedInstance($related);
// Here we will gather up the morph type and ID for the relationship so that we
// can properly query the intermediate table of a relation. Finally, we will
// get the table and create the relationship instances for the developers.
[$type, $id] = $this->getMorphs($name, $type, $id);
$table = $instance->getTable();
$localKey = $localKey ?: $this->getKeyName();
return $this->newMorphMany($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $localKey);
}
/**
* Instantiate a new MorphMany relationship.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param TDeclaringModel $parent
* @param string $type
* @param string $id
* @param string $localKey
* @return \Illuminate\Database\Eloquent\Relations\MorphMany<TRelatedModel, TDeclaringModel>
*/
protected function newMorphMany(Builder $query, Model $parent, $type, $id, $localKey)
{
return new MorphMany($query, $parent, $type, $id, $localKey);
}
/**
* Define a many-to-many relationship.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
*
* @param class-string<TRelatedModel> $related
* @param string|class-string<\Illuminate\Database\Eloquent\Model>|null $table
* @param string|null $foreignPivotKey
* @param string|null $relatedPivotKey
* @param string|null $parentKey
* @param string|null $relatedKey
* @param string|null $relation
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<TRelatedModel, $this>
*/
public function belongsToMany($related, $table = null, $foreignPivotKey = null, $relatedPivotKey = null,
$parentKey = null, $relatedKey = null, $relation = null)
{
// If no relationship name was passed, we will pull backtraces to get the
// name of the calling function. We will use that function name as the
// title of this relation since that is a great convention to apply.
if (is_null($relation)) {
$relation = $this->guessBelongsToManyRelation();
}
// First, we'll need to determine the foreign key and "other key" for the
// relationship. Once we have determined the keys we'll make the query
// instances as well as the relationship instances we need for this.
$instance = $this->newRelatedInstance($related);
$foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey();
$relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey();
// If no table name was provided, we can guess it by concatenating the two
// models using underscores in alphabetical order. The two model names
// are transformed to snake case from their default CamelCase also.
if (is_null($table)) {
$table = $this->joiningTable($related, $instance);
}
return $this->newBelongsToMany(
$instance->newQuery(), $this, $table, $foreignPivotKey,
$relatedPivotKey, $parentKey ?: $this->getKeyName(),
$relatedKey ?: $instance->getKeyName(), $relation
);
}
/**
* Instantiate a new BelongsToMany relationship.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param TDeclaringModel $parent
* @param string|class-string<\Illuminate\Database\Eloquent\Model> $table
* @param string $foreignPivotKey
* @param string $relatedPivotKey
* @param string $parentKey
* @param string $relatedKey
* @param string|null $relationName
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<TRelatedModel, TDeclaringModel>
*/
protected function newBelongsToMany(Builder $query, Model $parent, $table, $foreignPivotKey, $relatedPivotKey,
$parentKey, $relatedKey, $relationName = null)
{
return new BelongsToMany($query, $parent, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $relationName);
}
/**
* Define a polymorphic many-to-many relationship.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
*
* @param class-string<TRelatedModel> $related
* @param string $name
* @param string|null $table
* @param string|null $foreignPivotKey
* @param string|null $relatedPivotKey
* @param string|null $parentKey
* @param string|null $relatedKey
* @param string|null $relation
* @param bool $inverse
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany<TRelatedModel, $this>
*/
public function morphToMany($related, $name, $table = null, $foreignPivotKey = null,
$relatedPivotKey = null, $parentKey = null,
$relatedKey = null, $relation = null, $inverse = false)
{
$relation = $relation ?: $this->guessBelongsToManyRelation();
// First, we will need to determine the foreign key and "other key" for the
// relationship. Once we have determined the keys we will make the query
// instances, as well as the relationship instances we need for these.
$instance = $this->newRelatedInstance($related);
$foreignPivotKey = $foreignPivotKey ?: $name.'_id';
$relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey();
// Now we're ready to create a new query builder for the related model and
// the relationship instances for this relation. This relation will set
// appropriate query constraints then entirely manage the hydrations.
if (! $table) {
$words = preg_split('/(_)/u', $name, -1, PREG_SPLIT_DELIM_CAPTURE);
$lastWord = array_pop($words);
$table = implode('', $words).Str::plural($lastWord);
}
return $this->newMorphToMany(
$instance->newQuery(), $this, $name, $table,
$foreignPivotKey, $relatedPivotKey, $parentKey ?: $this->getKeyName(),
$relatedKey ?: $instance->getKeyName(), $relation, $inverse
);
}
/**
* Instantiate a new MorphToMany relationship.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param TDeclaringModel $parent
* @param string $name
* @param string $table
* @param string $foreignPivotKey
* @param string $relatedPivotKey
* @param string $parentKey
* @param string $relatedKey
* @param string|null $relationName
* @param bool $inverse
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany<TRelatedModel, TDeclaringModel>
*/
protected function newMorphToMany(Builder $query, Model $parent, $name, $table, $foreignPivotKey,
$relatedPivotKey, $parentKey, $relatedKey,
$relationName = null, $inverse = false)
{
return new MorphToMany($query, $parent, $name, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey,
$relationName, $inverse);
}
/**
* Define a polymorphic, inverse many-to-many relationship.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
*
* @param class-string<TRelatedModel> $related
* @param string $name
* @param string|null $table
* @param string|null $foreignPivotKey
* @param string|null $relatedPivotKey
* @param string|null $parentKey
* @param string|null $relatedKey
* @param string|null $relation
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany<TRelatedModel, $this>
*/
public function morphedByMany($related, $name, $table = null, $foreignPivotKey = null,
$relatedPivotKey = null, $parentKey = null, $relatedKey = null, $relation = null)
{
$foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey();
// For the inverse of the polymorphic many-to-many relations, we will change
// the way we determine the foreign and other keys, as it is the opposite
// of the morph-to-many method since we're figuring out these inverses.
$relatedPivotKey = $relatedPivotKey ?: $name.'_id';
return $this->morphToMany(
$related, $name, $table, $foreignPivotKey,
$relatedPivotKey, $parentKey, $relatedKey, $relation, true
);
}
/**
* Get the relationship name of the belongsToMany relationship.
*
* @return string|null
*/
protected function guessBelongsToManyRelation()
{
$caller = Arr::first(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), function ($trace) {
return ! in_array(
$trace['function'],
array_merge(static::$manyMethods, ['guessBelongsToManyRelation'])
);
});
return ! is_null($caller) ? $caller['function'] : null;
}
/**
* Get the joining table name for a many-to-many relation.
*
* @param string $related
* @param \Illuminate\Database\Eloquent\Model|null $instance
* @return string
*/
public function joiningTable($related, $instance = null)
{
// The joining table name, by convention, is simply the snake cased models
// sorted alphabetically and concatenated with an underscore, so we can
// just sort the models and join them together to get the table name.
$segments = [
$instance ? $instance->joiningTableSegment()
: Str::snake(class_basename($related)),
$this->joiningTableSegment(),
];
// Now that we have the model names in an array we can just sort them and
// use the implode function to join them together with an underscores,
// which is typically used by convention within the database system.
sort($segments);
return strtolower(implode('_', $segments));
}
/**
* Get this model's half of the intermediate table name for belongsToMany relationships.
*
* @return string
*/
public function joiningTableSegment()
{
return Str::snake(class_basename($this));
}
/**
* Determine if the model touches a given relation.
*
* @param string $relation
* @return bool
*/
public function touches($relation)
{
return in_array($relation, $this->getTouchedRelations());
}
/**
* Touch the owning relations of the model.
*
* @return void
*/
public function touchOwners()
{
foreach ($this->getTouchedRelations() as $relation) {
$this->$relation()->touch();
if ($this->$relation instanceof self) {
$this->$relation->fireModelEvent('saved', false);
$this->$relation->touchOwners();
} elseif ($this->$relation instanceof Collection) {
$this->$relation->each->touchOwners();
}
}
}
/**
* Get the polymorphic relationship columns.
*
* @param string $name
* @param string $type
* @param string $id
* @return array
*/
protected function getMorphs($name, $type, $id)
{
return [$type ?: $name.'_type', $id ?: $name.'_id'];
}
/**
* Get the class name for polymorphic relations.
*
* @return string
*/
public function getMorphClass()
{
$morphMap = Relation::morphMap();
if (! empty($morphMap) && in_array(static::class, $morphMap)) {
return array_search(static::class, $morphMap, true);
}
if (static::class === Pivot::class) {
return static::class;
}
if (Relation::requiresMorphMap()) {
throw new ClassMorphViolationException($this);
}
return static::class;
}
/**
* Create a new model instance for a related model.
*
* @param string $class
* @return mixed
*/
protected function newRelatedInstance($class)
{
return tap(new $class, function ($instance) {
if (! $instance->getConnectionName()) {
$instance->setConnection($this->connection);
}
});
}
/**
* 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.
*
* @return array
*/
public function getRelations()
{
return $this->relations;
}
/**
* Get a specified relationship.
*
* @param string $relation
* @return mixed
*/
public function getRelation($relation)
{
return $this->relations[$relation];
}
/**
* Determine if the given relation is loaded.
*
* @param string $key
* @return bool
*/
public function relationLoaded($key)
{
return array_key_exists($key, $this->relations);
}
/**
* Set the given relationship on the model.
*
* @param string $relation
* @param mixed $value
* @return $this
*/
public function setRelation($relation, $value)
{
$this->relations[$relation] = $value;
return $this;
}
/**
* Unset a loaded relationship.
*
* @param string $relation
* @return $this
*/
public function unsetRelation($relation)
{
unset($this->relations[$relation]);
return $this;
}
/**
* Set the entire relations array on the model.
*
* @param array $relations
* @return $this
*/
public function setRelations(array $relations)
{
$this->relations = $relations;
return $this;
}
/**
* Duplicate the instance and unset all the loaded relations.
*
* @return $this
*/
public function withoutRelations()
{
$model = clone $this;
return $model->unsetRelations();
}
/**
* Unset all the loaded relations for the instance.
*
* @return $this
*/
public function unsetRelations()
{
$this->relations = [];
return $this;
}
/**
* Get the relationships that are touched on save.
*
* @return array
*/
public function getTouchedRelations()
{
return $this->touches;
}
/**
* Set the relationships that are touched on save.
*
* @param array $touches
* @return $this
*/
public function setTouchedRelations(array $touches)
{
$this->touches = $touches;
return $this;
}
}
@@ -0,0 +1,224 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Illuminate\Support\Facades\Date;
trait HasTimestamps
{
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
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($attribute = null)
{
if ($attribute) {
$this->$attribute = $this->freshTimestamp();
return $this->save();
}
if (! $this->usesTimestamps()) {
return false;
}
$this->updateTimestamps();
return $this->save();
}
/**
* Update the model's update timestamp without raising any events.
*
* @param string|null $attribute
* @return bool
*/
public function touchQuietly($attribute = null)
{
return static::withoutEvents(fn () => $this->touch($attribute));
}
/**
* Update the creation and update timestamps.
*
* @return $this
*/
public function updateTimestamps()
{
$time = $this->freshTimestamp();
$updatedAtColumn = $this->getUpdatedAtColumn();
if (! is_null($updatedAtColumn) && ! $this->isDirty($updatedAtColumn)) {
$this->setUpdatedAt($time);
}
$createdAtColumn = $this->getCreatedAtColumn();
if (! $this->exists && ! is_null($createdAtColumn) && ! $this->isDirty($createdAtColumn)) {
$this->setCreatedAt($time);
}
return $this;
}
/**
* Set the value of the "created at" attribute.
*
* @param mixed $value
* @return $this
*/
public function setCreatedAt($value)
{
$this->{$this->getCreatedAtColumn()} = $value;
return $this;
}
/**
* Set the value of the "updated at" attribute.
*
* @param mixed $value
* @return $this
*/
public function setUpdatedAt($value)
{
$this->{$this->getUpdatedAtColumn()} = $value;
return $this;
}
/**
* Get a fresh timestamp for the model.
*
* @return \Illuminate\Support\Carbon
*/
public function freshTimestamp()
{
return Date::now();
}
/**
* Get a fresh timestamp for the model.
*
* @return string
*/
public function freshTimestampString()
{
return $this->fromDateTime($this->freshTimestamp());
}
/**
* Determine if the model uses timestamps.
*
* @return bool
*/
public function usesTimestamps()
{
return $this->timestamps && ! static::isIgnoringTimestamps($this::class);
}
/**
* Get the name of the "created at" column.
*
* @return string|null
*/
public function getCreatedAtColumn()
{
return static::CREATED_AT;
}
/**
* Get the name of the "updated at" column.
*
* @return string|null
*/
public function getUpdatedAtColumn()
{
return static::UPDATED_AT;
}
/**
* Get the fully qualified "created at" column.
*
* @return string|null
*/
public function getQualifiedCreatedAtColumn()
{
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;
}
}
@@ -0,0 +1,90 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Str;
trait HasUlids
{
/**
* Initialize the trait.
*
* @return void
*/
public function initializeHasUlids()
{
$this->usesUniqueIds = true;
}
/**
* Get the columns that should receive a unique identifier.
*
* @return array
*/
public function uniqueIds()
{
return [$this->getKeyName()];
}
/**
* 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\Contracts\Database\Eloquent\Builder
*
* @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 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;
}
}
@@ -0,0 +1,57 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
trait HasUniqueIds
{
/**
* Indicates if the model uses unique ids.
*
* @var bool
*/
public $usesUniqueIds = false;
/**
* Determine if the model uses unique ids.
*
* @return bool
*/
public function usesUniqueIds()
{
return $this->usesUniqueIds;
}
/**
* Generate unique keys for the model.
*
* @return void
*/
public function setUniqueIds()
{
foreach ($this->uniqueIds() as $column) {
if (empty($this->{$column})) {
$this->{$column} = $this->newUniqueId();
}
}
}
/**
* Generate a new key for the model.
*
* @return string
*/
public function newUniqueId()
{
return null;
}
/**
* Get the columns that should receive a unique identifier.
*
* @return array
*/
public function uniqueIds()
{
return [];
}
}
@@ -0,0 +1,90 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Str;
trait HasUuids
{
/**
* Initialize the trait.
*
* @return void
*/
public function initializeHasUuids()
{
$this->usesUniqueIds = true;
}
/**
* Get the columns that should receive a unique identifier.
*
* @return array
*/
public function uniqueIds()
{
return [$this->getKeyName()];
}
/**
* Generate a new UUID for the model.
*
* @return string
*/
public function newUniqueId()
{
return (string) Str::orderedUuid();
}
/**
* Retrieve the model for a bound value.
*
* @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Relations\Relation<*, *, *> $query
* @param mixed $value
* @param string|null $field
* @return \Illuminate\Contracts\Database\Eloquent\Builder
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function resolveRouteBindingQuery($query, $value, $field = null)
{
if ($field && in_array($field, $this->uniqueIds()) && ! 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;
}
}
@@ -0,0 +1,20 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Illuminate\Support\Str;
trait HasVersion7Uuids
{
use HasUuids;
/**
* Generate a new UUID (version 7) for the model.
*
* @return string
*/
public function newUniqueId()
{
return (string) Str::uuid7();
}
}
@@ -0,0 +1,124 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
trait HidesAttributes
{
/**
* The attributes that should be hidden for serialization.
*
* @var array<string>
*/
protected $hidden = [];
/**
* The attributes that should be visible in serialization.
*
* @var array<string>
*/
protected $visible = [];
/**
* Get the hidden attributes for the model.
*
* @return array<string>
*/
public function getHidden()
{
return $this->hidden;
}
/**
* Set the hidden attributes for the model.
*
* @param array<string> $hidden
* @return $this
*/
public function setHidden(array $hidden)
{
$this->hidden = $hidden;
return $this;
}
/**
* Get the visible attributes for the model.
*
* @return array<string>
*/
public function getVisible()
{
return $this->visible;
}
/**
* Set the visible attributes for the model.
*
* @param array<string> $visible
* @return $this
*/
public function setVisible(array $visible)
{
$this->visible = $visible;
return $this;
}
/**
* Make the given, typically hidden, attributes visible.
*
* @param array<string>|string|null $attributes
* @return $this
*/
public function makeVisible($attributes)
{
$attributes = is_array($attributes) ? $attributes : func_get_args();
$this->hidden = array_diff($this->hidden, $attributes);
if (! empty($this->visible)) {
$this->visible = array_values(array_unique(array_merge($this->visible, $attributes)));
}
return $this;
}
/**
* Make the given, typically hidden, attributes visible if the given truth test passes.
*
* @param bool|\Closure $condition
* @param array<string>|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>|string|null $attributes
* @return $this
*/
public function makeHidden($attributes)
{
$this->hidden = array_values(array_unique(array_merge(
$this->hidden, is_array($attributes) ? $attributes : func_get_args()
)));
return $this;
}
/**
* Make the given, typically visible, attributes hidden if the given truth test passes.
*
* @param bool|\Closure $condition
* @param array<string>|string|null $attributes
* @return $this
*/
public function makeHiddenIf($condition, $attributes)
{
return value($condition, $this) ? $this->makeHidden($attributes) : $this;
}
}
@@ -0,0 +1,895 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use BadMethodCallException;
use Closure;
use Illuminate\Database\Eloquent\Builder;
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;
/** @mixin \Illuminate\Database\Eloquent\Builder */
trait QueriesRelationships
{
/**
* Add a relationship count / exists condition to the query.
*
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
* @param string $operator
* @param int $count
* @param string $boolean
* @param \Closure|null $callback
* @return $this
*
* @throws \RuntimeException
*/
public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', ?Closure $callback = null)
{
if (is_string($relation)) {
if (str_contains($relation, '.')) {
return $this->hasNested($relation, $operator, $count, $boolean, $callback);
}
$relation = $this->getRelationWithoutConstraints($relation);
}
if ($relation instanceof MorphTo) {
return $this->hasMorph($relation, ['*'], $operator, $count, $boolean, $callback);
}
// If we only need to check for the existence of the relation, then we can optimize
// the subquery to only run a "where exists" clause instead of this full "count"
// clause. This will make these queries run much faster compared with a count.
$method = $this->canUseExistsForExistenceCheck($operator, $count)
? 'getRelationExistenceQuery'
: 'getRelationExistenceCountQuery';
$hasQuery = $relation->{$method}(
$relation->getRelated()->newQueryWithoutRelationships(), $this
);
// Next we will call any given callback as an "anonymous" scope so they can get the
// proper logical grouping of the where clauses if needed by this Eloquent query
// builder. Then, we will be ready to finalize and return this query instance.
if ($callback) {
$hasQuery->callScope($callback);
}
return $this->addHasWhere(
$hasQuery, $relation, $operator, $count, $boolean
);
}
/**
* Add nested relationship count / exists conditions to the query.
*
* Sets up recursive call to whereHas until we finish the nested relation.
*
* @param string $relations
* @param string $operator
* @param int $count
* @param string $boolean
* @param \Closure|null $callback
* @return $this
*/
protected function hasNested($relations, $operator = '>=', $count = 1, $boolean = 'and', $callback = null)
{
$relations = explode('.', $relations);
$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.
count($relations) > 1
? $q->whereHas(array_shift($relations), $closure)
: $q->has(array_shift($relations), $operator, $count, 'and', $callback);
};
return $this->has(array_shift($relations), $doesntHave ? '<' : '>=', 1, $boolean, $closure);
}
/**
* Add a relationship count / exists condition to the query with an "or".
*
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
* @param string $operator
* @param int $count
* @return $this
*/
public function orHas($relation, $operator = '>=', $count = 1)
{
return $this->has($relation, $operator, $count, 'or');
}
/**
* Add a relationship count / exists condition to the query.
*
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
* @param string $boolean
* @param \Closure|null $callback
* @return $this
*/
public function doesntHave($relation, $boolean = 'and', ?Closure $callback = null)
{
return $this->has($relation, '<', 1, $boolean, $callback);
}
/**
* Add a relationship count / exists condition to the query with an "or".
*
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
* @return $this
*/
public function orDoesntHave($relation)
{
return $this->doesntHave($relation, 'or');
}
/**
* Add a relationship count / exists condition to the query with where clauses.
*
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
* @param \Closure|null $callback
* @param string $operator
* @param int $count
* @return $this
*/
public function whereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1)
{
return $this->has($relation, $operator, $count, 'and', $callback);
}
/**
* Add a relationship count / exists condition to the query with where clauses.
*
* Also load the relationship with same condition.
*
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
* @param \Closure|null $callback
* @param string $operator
* @param int $count
* @return $this
*/
public function withWhereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1)
{
return $this->whereHas(Str::before($relation, ':'), $callback, $operator, $count)
->with($callback ? [$relation => fn ($query) => $callback($query)] : $relation);
}
/**
* Add a relationship count / exists condition to the query with where clauses and an "or".
*
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
* @param \Closure|null $callback
* @param string $operator
* @param int $count
* @return $this
*/
public function orWhereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1)
{
return $this->has($relation, $operator, $count, 'or', $callback);
}
/**
* Add a relationship count / exists condition to the query with where clauses.
*
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
* @param \Closure|null $callback
* @return $this
*/
public function whereDoesntHave($relation, ?Closure $callback = null)
{
return $this->doesntHave($relation, 'and', $callback);
}
/**
* Add a relationship count / exists condition to the query with where clauses and an "or".
*
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
* @param \Closure|null $callback
* @return $this
*/
public function orWhereDoesntHave($relation, ?Closure $callback = null)
{
return $this->doesntHave($relation, 'or', $callback);
}
/**
* Add a polymorphic relationship count / exists condition to the query.
*
* @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 $this
*/
public function hasMorph($relation, $types, $operator = '>=', $count = 1, $boolean = 'and', ?Closure $callback = null)
{
if (is_string($relation)) {
$relation = $this->getRelationWithoutConstraints($relation);
}
$types = (array) $types;
if ($types === ['*']) {
$types = $this->model->newModelQuery()->distinct()->pluck($relation->getMorphType())->filter()->all();
}
if (empty($types)) {
return $this->where(new Expression('0'), $operator, $count, $boolean);
}
foreach ($types as &$type) {
$type = Relation::getMorphedModel($type) ?? $type;
}
return $this->where(function ($query) use ($relation, $callback, $operator, $count, $types) {
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.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, TDeclaringModel> $relation
* @param class-string<TRelatedModel> $type
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<TRelatedModel, TDeclaringModel>
*/
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 $this
*/
public function orHasMorph($relation, $types, $operator = '>=', $count = 1)
{
return $this->hasMorph($relation, $types, $operator, $count, 'or');
}
/**
* Add a polymorphic relationship count / exists condition to the query.
*
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
* @param string|array $types
* @param string $boolean
* @param \Closure|null $callback
* @return $this
*/
public function doesntHaveMorph($relation, $types, $boolean = 'and', ?Closure $callback = null)
{
return $this->hasMorph($relation, $types, '<', 1, $boolean, $callback);
}
/**
* Add a polymorphic relationship count / exists condition to the query with an "or".
*
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
* @param string|array $types
* @return $this
*/
public function orDoesntHaveMorph($relation, $types)
{
return $this->doesntHaveMorph($relation, $types, 'or');
}
/**
* Add a polymorphic relationship count / exists condition to the query with where clauses.
*
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
* @param string|array $types
* @param \Closure|null $callback
* @param string $operator
* @param int $count
* @return $this
*/
public function whereHasMorph($relation, $types, ?Closure $callback = null, $operator = '>=', $count = 1)
{
return $this->hasMorph($relation, $types, $operator, $count, 'and', $callback);
}
/**
* Add a polymorphic relationship count / exists condition to the query with where clauses and an "or".
*
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
* @param string|array $types
* @param \Closure|null $callback
* @param string $operator
* @param int $count
* @return $this
*/
public function orWhereHasMorph($relation, $types, ?Closure $callback = null, $operator = '>=', $count = 1)
{
return $this->hasMorph($relation, $types, $operator, $count, 'or', $callback);
}
/**
* Add a polymorphic relationship count / exists condition to the query with where clauses.
*
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
* @param string|array $types
* @param \Closure|null $callback
* @return $this
*/
public function whereDoesntHaveMorph($relation, $types, ?Closure $callback = null)
{
return $this->doesntHaveMorph($relation, $types, 'and', $callback);
}
/**
* Add a polymorphic relationship count / exists condition to the query with where clauses and an "or".
*
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
* @param string|array $types
* @param \Closure|null $callback
* @return $this
*/
public function orWhereDoesntHaveMorph($relation, $types, ?Closure $callback = null)
{
return $this->doesntHaveMorph($relation, $types, 'or', $callback);
}
/**
* Add a basic where clause to a relationship query.
*
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
* @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column
* @param mixed $operator
* @param mixed $value
* @return $this
*/
public function 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 \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>|string $relation
* @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column
* @param mixed $operator
* @param mixed $value
* @return $this
*/
public function 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\Contracts\Database\Query\Expression $column
* @param mixed $operator
* @param mixed $value
* @return $this
*/
public function whereMorphRelation($relation, $types, $column, $operator = null, $value = null)
{
return $this->whereHasMorph($relation, $types, function ($query) use ($column, $operator, $value) {
$query->where($column, $operator, $value);
});
}
/**
* Add a polymorphic relationship condition to the query with an "or where" clause.
*
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
* @param string|array $types
* @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column
* @param mixed $operator
* @param mixed $value
* @return $this
*/
public function orWhereMorphRelation($relation, $types, $column, $operator = null, $value = null)
{
return $this->orWhereHasMorph($relation, $types, function ($query) use ($column, $operator, $value) {
$query->where($column, $operator, $value);
});
}
/**
* Add a morph-to relationship condition to the query.
*
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
* @param \Illuminate\Database\Eloquent\Model|string|null $model
* @return $this
*/
public function whereMorphedTo($relation, $model, $boolean = 'and')
{
if (is_string($relation)) {
$relation = $this->getRelationWithoutConstraints($relation);
}
if (is_null($model)) {
return $this->whereNull($relation->qualifyColumn($relation->getMorphType()), $boolean);
}
if (is_string($model)) {
$morphMap = Relation::morphMap();
if (! empty($morphMap) && in_array($model, $morphMap)) {
$model = array_search($model, $morphMap, true);
}
return $this->where($relation->qualifyColumn($relation->getMorphType()), $model, null, $boolean);
}
return $this->where(function ($query) use ($relation, $model) {
$query->where($relation->qualifyColumn($relation->getMorphType()), $model->getMorphClass())
->where($relation->qualifyColumn($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 $this
*/
public function whereNotMorphedTo($relation, $model, $boolean = 'and')
{
if (is_string($relation)) {
$relation = $this->getRelationWithoutConstraints($relation);
}
if (is_string($model)) {
$morphMap = Relation::morphMap();
if (! empty($morphMap) && in_array($model, $morphMap)) {
$model = array_search($model, $morphMap, true);
}
return $this->whereNot($relation->qualifyColumn($relation->getMorphType()), '<=>', $model, $boolean);
}
return $this->whereNot(function ($query) use ($relation, $model) {
$query->where($relation->qualifyColumn($relation->getMorphType()), '<=>', $model->getMorphClass())
->where($relation->qualifyColumn($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|null $model
* @return $this
*/
public function orWhereMorphedTo($relation, $model)
{
return $this->whereMorphedTo($relation, $model, 'or');
}
/**
* Add a not morph-to relationship condition to the query with an "or where" clause.
*
* @param \Illuminate\Database\Eloquent\Relations\MorphTo<*, *>|string $relation
* @param \Illuminate\Database\Eloquent\Model|string $model
* @return $this
*/
public function orWhereNotMorphedTo($relation, $model)
{
return $this->whereNotMorphedTo($relation, $model, 'or');
}
/**
* Add a "belongs to" relationship where clause to the query.
*
* @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection<int, \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) {
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 \Illuminate\Contracts\Database\Query\Expression|string $column
* @param string $function
* @return $this
*/
public function withAggregate($relations, $column, $function = null)
{
if (empty($relations)) {
return $this;
}
if (is_null($this->query->columns)) {
$this->query->select([$this->query->from.'.*']);
}
$relations = is_array($relations) ? $relations : [$relations];
foreach ($this->parseWithRelations($relations) as $name => $constraints) {
// First we will determine if the name has been aliased using an "as" clause on the name
// and if it has we will extract the actual relationship name and the desired name of
// the resulting column. This allows multiple aggregates on the same relationships.
$segments = explode(' ', $name);
unset($alias);
if (count($segments) === 3 && Str::lower($segments[1]) === 'as') {
[$name, $alias] = [$segments[0], $segments[2]];
}
$relation = $this->getRelationWithoutConstraints($name);
if ($function) {
if ($this->getQuery()->getGrammar()->isExpression($column)) {
$aggregateColumn = $this->getQuery()->getGrammar()->getValue($column);
} else {
$hashedColumn = $this->getRelationHashedColumn($column, $relation);
$aggregateColumn = $this->getQuery()->getGrammar()->wrap(
$column === '*' ? $column : $relation->getRelated()->qualifyColumn($hashedColumn)
);
}
$expression = $function === 'exists' ? $aggregateColumn : sprintf('%s(%s)', $function, $aggregateColumn);
} else {
$expression = $this->getQuery()->getGrammar()->getValue($column);
}
// Here, we will grab the relationship sub-query and prepare to add it to the main query
// as a sub-select. First, we'll get the "has" query and use that to get the relation
// sub-query. We'll format this relationship name and append this column if needed.
$query = $relation->getRelationExistenceQuery(
$relation->getRelated()->newQuery(), $this, new Expression($expression)
)->setBindings([], 'select');
$query->callScope($constraints);
$query = $query->mergeConstraintsFrom($relation->getQuery())->toBase();
// If the query contains certain elements like orderings / more than one column selected
// then we will remove those elements from the query so that it will execute properly
// when given to the database. Otherwise, we may receive SQL errors or poor syntax.
$query->orders = null;
$query->setBindings([], 'order');
if (count($query->columns) > 1) {
$query->columns = [$query->columns[0]];
$query->bindings['select'] = [];
}
// Finally, we will make the proper column alias to the query and run this sub-select on
// the query builder. Then, we will return the builder instance back to the developer
// for further constraint chaining that needs to take place on the query as needed.
$alias ??= Str::snake(
preg_replace('/[^[:alnum:][:space:]_]/u', '', "$name $function {$this->getQuery()->getGrammar()->getValue($column)}")
);
if ($function === 'exists') {
$this->selectRaw(
sprintf('exists(%s) as %s', $query->toSql(), $this->getQuery()->grammar->wrap($alias)),
$query->getBindings()
)->withCasts([$alias => 'bool']);
} else {
$this->selectSub(
$function ? $query : $query->limit(1),
$alias
);
}
}
return $this;
}
/**
* Get the relation hashed column name for the given column and relation.
*
* @param string $column
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *> $relation
* @return string
*/
protected function getRelationHashedColumn($column, $relation)
{
if (str_contains($column, '.')) {
return $column;
}
return $this->getQuery()->from === $relation->getQuery()->getQuery()->from
? "{$relation->getRelationCountHash(false)}.$column"
: $column;
}
/**
* Add subselect queries to count the relations.
*
* @param mixed $relations
* @return $this
*/
public function withCount($relations)
{
return $this->withAggregate(is_array($relations) ? $relations : func_get_args(), '*', 'count');
}
/**
* Add subselect queries to include the max of the relation's column.
*
* @param string|array $relation
* @param \Illuminate\Contracts\Database\Query\Expression|string $column
* @return $this
*/
public function withMax($relation, $column)
{
return $this->withAggregate($relation, $column, 'max');
}
/**
* Add subselect queries to include the min of the relation's column.
*
* @param string|array $relation
* @param \Illuminate\Contracts\Database\Query\Expression|string $column
* @return $this
*/
public function withMin($relation, $column)
{
return $this->withAggregate($relation, $column, 'min');
}
/**
* Add subselect queries to include the sum of the relation's column.
*
* @param string|array $relation
* @param \Illuminate\Contracts\Database\Query\Expression|string $column
* @return $this
*/
public function withSum($relation, $column)
{
return $this->withAggregate($relation, $column, 'sum');
}
/**
* Add subselect queries to include the average of the relation's column.
*
* @param string|array $relation
* @param \Illuminate\Contracts\Database\Query\Expression|string $column
* @return $this
*/
public function withAvg($relation, $column)
{
return $this->withAggregate($relation, $column, 'avg');
}
/**
* Add subselect queries to include the existence of related models.
*
* @param string|array $relation
* @return $this
*/
public function withExists($relation)
{
return $this->withAggregate($relation, '*', 'exists');
}
/**
* Add the "has" condition where clause to the query.
*
* @param \Illuminate\Database\Eloquent\Builder<*> $hasQuery
* @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *> $relation
* @param string $operator
* @param int $count
* @param string $boolean
* @return $this
*/
protected function addHasWhere(Builder $hasQuery, Relation $relation, $operator, $count, $boolean)
{
$hasQuery->mergeConstraintsFrom($relation->getQuery());
return $this->canUseExistsForExistenceCheck($operator, $count)
? $this->addWhereExistsQuery($hasQuery->toBase(), $boolean, $operator === '<' && $count === 1)
: $this->addWhereCountQuery($hasQuery->toBase(), $operator, $count, $boolean);
}
/**
* Merge the where constraints from another query to the current query.
*
* @param \Illuminate\Database\Eloquent\Builder<*> $from
* @return $this
*/
public function mergeConstraintsFrom(Builder $from)
{
$whereBindings = $from->getQuery()->getRawBindings()['where'] ?? [];
$wheres = $from->getQuery()->from !== $this->getQuery()->from
? $this->requalifyWhereTables(
$from->getQuery()->wheres,
$from->getQuery()->grammar->getValue($from->getQuery()->from),
$this->getModel()->getTable()
) : $from->getQuery()->wheres;
// Here we have some other query that we want to merge the where constraints from. We will
// copy over any where constraints on the query as well as remove any global scopes the
// query might have removed. Then we will return ourselves with the finished merging.
return $this->withoutGlobalScopes(
$from->removedScopes()
)->mergeWheres(
$wheres, $whereBindings
);
}
/**
* Updates the table name for any columns with a new qualified name.
*
* @param array $wheres
* @param string $from
* @param string $to
* @return array
*/
protected function requalifyWhereTables(array $wheres, string $from, string $to): array
{
return 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 string $operator
* @param int $count
* @param string $boolean
* @return $this
*/
protected function addWhereCountQuery(QueryBuilder $query, $operator = '>=', $count = 1, $boolean = 'and')
{
$this->query->addBinding($query->getBindings(), 'where');
return $this->where(
new Expression('('.$query->toSql().')'),
$operator,
is_numeric($count) ? new Expression($count) : $count,
$boolean
);
}
/**
* Get the "has relation" base query instance.
*
* @param string $relation
* @return \Illuminate\Database\Eloquent\Relations\Relation<*, *, *>
*/
protected function getRelationWithoutConstraints($relation)
{
return Relation::noConstraints(function () use ($relation) {
return $this->getModel()->{$relation}();
});
}
/**
* Check if we can run an "exists" query to optimize performance.
*
* @param string $operator
* @param int $count
* @return bool
*/
protected function canUseExistsForExistenceCheck($operator, $count)
{
return ($operator === '>=' || $operator === '<') && $count === 1;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -0,0 +1,940 @@
<?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<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
*/
public static $namespace = 'Database\\Factories\\';
/**
* The default model name resolver.
*
* @var callable(self): class-string<TModel>
*/
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 static
*/
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 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 TModel
*/
public function createOneQuietly($attributes = [])
{
return $this->count(null)->createQuietly($attributes);
}
/**
* Create a collection of models and persist them to the database.
*
* @param int|null|iterable<int, array<string, mixed>> $records
* @return \Illuminate\Database\Eloquent\Collection<int, TModel>
*/
public function createMany(int|iterable|null $records = null)
{
$records ??= ($this->count ?? 1);
$this->count = null;
if (is_numeric($records)) {
$records = array_fill(0, $records, []);
}
return new EloquentCollection(
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 int|null|iterable<int, array<string, mixed>> $records
* @return \Illuminate\Database\Eloquent\Collection<int, TModel>
*/
public function createManyQuietly(int|iterable|null $records = null)
{
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, TModel>|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, TModel>|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, TModel>|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<int, \Illuminate\Database\Eloquent\Model> $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 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, TModel>|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())?->getKey()
?? $attribute->recycle($this->recycle)->create()->getKey();
} elseif ($attribute instanceof Model) {
$attribute = $attribute->getKey();
}
return $attribute;
})
->map(function ($attribute, $key) use (&$definition, $evaluateRelations) {
if (is_callable($attribute) && ! is_string($attribute) && ! is_array($attribute)) {
$attribute = $attribute($definition);
}
$attribute = $evaluateRelations($attribute);
$definition[$key] = $attribute;
return $attribute;
})
->all();
}
/**
* Add a new state transformation to the model definition.
*
* @param (callable(array<string, mixed>, TModel|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 mixed ...$sequence
* @return static
*/
public function sequence(...$sequence)
{
return $this->state(new Sequence(...$sequence));
}
/**
* Add a new sequenced state transformation to the model definition and update the pending creation count to the size of the sequence.
*
* @param array ...$sequence
* @return static
*/
public function forEachSequence(...$sequence)
{
return $this->state(new Sequence(...$sequence))->count(count($sequence));
}
/**
* Add a new cross joined sequenced state transformation to the model definition.
*
* @param array ...$sequence
* @return static
*/
public function crossJoinSequence(...$sequence)
{
return $this->state(new CrossJoinSequence(...$sequence));
}
/**
* Define a child relationship for the model.
*
* @param \Illuminate\Database\Eloquent\Factories\Factory $factory
* @param string|null $relationship
* @return static
*/
public function has(self $factory, $relationship = null)
{
return $this->newInstance([
'has' => $this->has->concat([new Relationship(
$factory, $relationship ?? $this->guessRelationship($factory->modelName())
)]),
]);
}
/**
* Attempt to guess the relationship name for a "has" relationship.
*
* @param string $related
* @return string
*/
protected function guessRelationship(string $related)
{
$guess = Str::camel(Str::plural(class_basename($related)));
return method_exists($this->modelName(), $guess) ? $guess : Str::singular($guess);
}
/**
* Define an attached relationship for the model.
*
* @param \Illuminate\Database\Eloquent\Factories\Factory|\Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $factory
* @param (callable(): array<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.
*
* @template TClass of \Illuminate\Database\Eloquent\Model
*
* @param class-string<TClass> $modelClassName
* @return TClass|null
*/
public function getRandomRecycledModel($modelClassName)
{
return $this->recycle->get($modelClassName)?->random();
}
/**
* Add a new "after making" callback to the model definition.
*
* @param \Closure(TModel): mixed $callback
* @return static
*/
public function afterMaking(Closure $callback)
{
return $this->newInstance(['afterMaking' => $this->afterMaking->concat([$callback])]);
}
/**
* Add a new "after creating" callback to the model definition.
*
* @param \Closure(TModel): 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 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<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<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.
*
* @template TClass of \Illuminate\Database\Eloquent\Model
*
* @param class-string<TClass> $modelName
* @return \Illuminate\Database\Eloquent\Factories\Factory<TClass>
*/
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.
*
* @template TClass of \Illuminate\Database\Eloquent\Model
*
* @param class-string<TClass> $modelName
* @return class-string<\Illuminate\Database\Eloquent\Factories\Factory<TClass>>
*/
public static function resolveFactoryName(string $modelName)
{
$resolver = static::$factoryNameResolver ?? function (string $modelName) {
$appNamespace = static::appNamespace();
$modelName = Str::startsWith($modelName, $appNamespace.'Models\\')
? Str::after($modelName, $appNamespace.'Models\\')
: Str::after($modelName, $appNamespace);
return static::$namespace.$modelName.'Factory';
};
return $resolver($modelName);
}
/**
* Get the application namespace for the application.
*
* @return string
*/
protected static function appNamespace()
{
try {
return Container::getInstance()
->make(Application::class)
->getNamespace();
} catch (Throwable) {
return 'App\\';
}
}
/**
* 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
);
}
}
}
@@ -0,0 +1,39 @@
<?php
namespace Illuminate\Database\Eloquent\Factories;
/**
* @template TFactory of \Illuminate\Database\Eloquent\Factories\Factory
*/
trait HasFactory
{
/**
* Get a new factory instance for the model.
*
* @param (callable(array<string, mixed>, static|null): array<string, mixed>)|array<string, mixed>|int|null $count
* @param (callable(array<string, mixed>, static|null): array<string, mixed>)|array<string, mixed> $state
* @return TFactory
*/
public static function factory($count = null, $state = [])
{
$factory = static::newFactory() ?? Factory::factoryForModel(static::class);
return $factory
->count(is_numeric($count) ? $count : null)
->state(is_callable($count) || is_array($count) ? $count : $state);
}
/**
* Create a new factory instance for the model.
*
* @return TFactory|null
*/
protected static function newFactory()
{
if (isset(static::$factory)) {
return static::$factory::new();
}
return null;
}
}
@@ -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;
}
}
@@ -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 mixed ...$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;
});
}
}
@@ -0,0 +1,124 @@
<?php
namespace Illuminate\Database\Eloquent;
/**
* @template TBuilder of \Illuminate\Database\Eloquent\Builder
*/
trait HasBuilder
{
/**
* Begin querying the model.
*
* @return TBuilder
*/
public static function query()
{
return parent::query();
}
/**
* Create a new Eloquent query builder for the model.
*
* @param \Illuminate\Database\Query\Builder $query
* @return TBuilder
*/
public function newEloquentBuilder($query)
{
return parent::newEloquentBuilder($query);
}
/**
* Get a new query builder for the model's table.
*
* @return TBuilder
*/
public function newQuery()
{
return parent::newQuery();
}
/**
* Get a new query builder that doesn't have any global scopes or eager loading.
*
* @return TBuilder
*/
public function newModelQuery()
{
return parent::newModelQuery();
}
/**
* Get a new query builder with no relationships loaded.
*
* @return TBuilder
*/
public function newQueryWithoutRelationships()
{
return parent::newQueryWithoutRelationships();
}
/**
* Get a new query builder that doesn't have any global scopes.
*
* @return TBuilder
*/
public function newQueryWithoutScopes()
{
return parent::newQueryWithoutScopes();
}
/**
* Get a new query instance without a given scope.
*
* @param \Illuminate\Database\Eloquent\Scope|string $scope
* @return TBuilder
*/
public function newQueryWithoutScope($scope)
{
return parent::newQueryWithoutScope($scope);
}
/**
* Get a new query to restore one or more models by their queueable IDs.
*
* @param array|int $ids
* @return TBuilder
*/
public function newQueryForRestoration($ids)
{
return parent::newQueryForRestoration($ids);
}
/**
* Begin querying the model on a given connection.
*
* @param string|null $connection
* @return TBuilder
*/
public static function on($connection = null)
{
return parent::on($connection);
}
/**
* Begin querying the model on the write connection.
*
* @return TBuilder
*/
public static function onWriteConnection()
{
return parent::onWriteConnection();
}
/**
* Begin querying a model with eager loading.
*
* @param array|string $relations
* @return TBuilder
*/
public static function with($relations)
{
return parent::with($relations);
}
}
@@ -0,0 +1,20 @@
<?php
namespace Illuminate\Database\Eloquent;
/**
* @template TCollection of \Illuminate\Database\Eloquent\Collection
*/
trait HasCollection
{
/**
* Create a new Eloquent Collection instance.
*
* @param array<array-key, \Illuminate\Database\Eloquent\Model> $models
* @return TCollection
*/
public function newCollection(array $models = [])
{
return new static::$collectionClass($models);
}
}
@@ -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);
});
}
}
@@ -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 void
*/
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;
}
}
@@ -0,0 +1,49 @@
<?php
namespace Illuminate\Database\Eloquent;
use RuntimeException;
class JsonEncodingException extends RuntimeException
{
/**
* Create a new JSON encoding exception for the model.
*
* @param mixed $model
* @param string $message
* @return static
*/
public static function forModel($model, $message)
{
return new static('Error encoding model ['.get_class($model).'] with ID ['.$model->getKey().'] to JSON: '.$message);
}
/**
* Create a new JSON encoding exception for the resource.
*
* @param \Illuminate\Http\Resources\Json\JsonResource $resource
* @param string $message
* @return static
*/
public static function forResource($resource, $message)
{
$model = $resource->resource;
return new static('Error encoding resource ['.get_class($resource).'] with model ['.get_class($model).'] with ID ['.$model->getKey().'] to JSON: '.$message);
}
/**
* Create a new JSON encoding exception for an attribute.
*
* @param mixed $model
* @param mixed $key
* @param string $message
* @return static
*/
public static function forAttribute($model, $key, $message)
{
$class = get_class($model);
return new static("Unable to encode attribute [{$key}] for model [{$class}] to JSON: {$message}.");
}
}
@@ -0,0 +1,10 @@
<?php
namespace Illuminate\Database\Eloquent;
use RuntimeException;
class MassAssignmentException extends RuntimeException
{
//
}
@@ -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<static>
*/
public function prunable()
{
throw new LogicException('Please implement the prunable method on your model.');
}
}
@@ -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
@@ -0,0 +1,69 @@
<?php
namespace Illuminate\Database\Eloquent;
use Illuminate\Database\RecordsNotFoundException;
use Illuminate\Support\Arr;
/**
* @template TModel of \Illuminate\Database\Eloquent\Model
*/
class ModelNotFoundException extends RecordsNotFoundException
{
/**
* Name of the affected Eloquent model.
*
* @var class-string<TModel>
*/
protected $model;
/**
* The affected model IDs.
*
* @var array<int, int|string>
*/
protected $ids;
/**
* Set the affected Eloquent model and instance 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 = Arr::wrap($ids);
$this->message = "No query results for model [{$model}]";
if (count($this->ids) > 0) {
$this->message .= ' '.implode(', ', $this->ids);
} else {
$this->message .= '.';
}
return $this;
}
/**
* Get the affected Eloquent model.
*
* @return class-string<TModel>
*/
public function getModel()
{
return $this->model;
}
/**
* Get the affected Eloquent model IDs.
*
* @return array<int, int|string>
*/
public function getIds()
{
return $this->ids;
}
}
@@ -0,0 +1,111 @@
<?php
namespace Illuminate\Database\Eloquent;
use BadMethodCallException;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphOneOrMany;
use Illuminate\Support\Str;
/**
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*/
class PendingHasThroughRelationship
{
/**
* The root model that the relationship exists on.
*
* @var TDeclaringModel
*/
protected $rootModel;
/**
* The local relationship.
*
* @var \Illuminate\Database\Eloquent\Relations\HasMany<TIntermediateModel, TDeclaringModel>|\Illuminate\Database\Eloquent\Relations\HasOne<TIntermediateModel, TDeclaringModel>
*/
protected $localRelationship;
/**
* Create a pending has-many-through or has-one-through relationship.
*
* @param TDeclaringModel $rootModel
* @param \Illuminate\Database\Eloquent\Relations\HasMany<TIntermediateModel, TDeclaringModel>|\Illuminate\Database\Eloquent\Relations\HasOne<TIntermediateModel, TDeclaringModel> $localRelationship
*/
public function __construct($rootModel, $localRelationship)
{
$this->rootModel = $rootModel;
$this->localRelationship = $localRelationship;
}
/**
* Define the distant relationship that this model has.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
*
* @param string|(callable(TIntermediateModel): (\Illuminate\Database\Eloquent\Relations\HasOne<TRelatedModel, TIntermediateModel>|\Illuminate\Database\Eloquent\Relations\HasMany<TRelatedModel, TIntermediateModel>|\Illuminate\Database\Eloquent\Relations\MorphOneOrMany<TRelatedModel, TIntermediateModel>)) $callback
* @return (
* $callback is string
* ? \Illuminate\Database\Eloquent\Relations\HasManyThrough<\Illuminate\Database\Eloquent\Model, TIntermediateModel, TDeclaringModel>|\Illuminate\Database\Eloquent\Relations\HasOneThrough<\Illuminate\Database\Eloquent\Model, TIntermediateModel, TDeclaringModel>
* : (
* $callback is callable(TIntermediateModel): \Illuminate\Database\Eloquent\Relations\HasOne<TRelatedModel, TIntermediateModel>
* ? \Illuminate\Database\Eloquent\Relations\HasOneThrough<TRelatedModel, TIntermediateModel, TDeclaringModel>
* : \Illuminate\Database\Eloquent\Relations\HasManyThrough<TRelatedModel, TIntermediateModel, TDeclaringModel>
* )
* )
*/
public function has($callback)
{
if (is_string($callback)) {
$callback = fn () => $this->localRelationship->getRelated()->{$callback}();
}
$distantRelation = $callback($this->localRelationship->getRelated());
if ($distantRelation instanceof HasMany) {
$returnedRelation = $this->rootModel->hasManyThrough(
$distantRelation->getRelated()::class,
$this->localRelationship->getRelated()::class,
$this->localRelationship->getForeignKeyName(),
$distantRelation->getForeignKeyName(),
$this->localRelationship->getLocalKeyName(),
$distantRelation->getLocalKeyName(),
);
} else {
$returnedRelation = $this->rootModel->hasOneThrough(
$distantRelation->getRelated()::class,
$this->localRelationship->getRelated()::class,
$this->localRelationship->getForeignKeyName(),
$distantRelation->getForeignKeyName(),
$this->localRelationship->getLocalKeyName(),
$distantRelation->getLocalKeyName(),
);
}
if ($this->localRelationship instanceof MorphOneOrMany) {
$returnedRelation->where($this->localRelationship->getQualifiedMorphType(), $this->localRelationship->getMorphClass());
}
return $returnedRelation;
}
/**
* Handle dynamic method calls into the model.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
if (Str::startsWith($method, 'has')) {
return $this->has(Str::of($method)->after('has')->lcfirst()->toString());
}
throw new BadMethodCallException(sprintf(
'Call to undefined method %s::%s()', static::class, $method
));
}
}
@@ -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<static>
*/
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()
{
//
}
}

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