This commit is contained in:
2026-05-01 23:40:14 +08:00
commit b8f599a617
3867 changed files with 478663 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
<?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
*/
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;
}
}
+615
View File
@@ -0,0 +1,615 @@
<?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\RecordNotFoundException;
use Illuminate\Database\RecordsNotFoundException;
use Illuminate\Pagination\Cursor;
use Illuminate\Pagination\CursorPaginator;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Conditionable;
use InvalidArgumentException;
use RuntimeException;
/**
* @template TValue
*
* @mixin \Illuminate\Database\Query\Builder
*/
trait BuildsQueries
{
use Conditionable;
/**
* Chunk the results of the query.
*
* @param int $count
* @param callable(\Illuminate\Support\Collection<int, TValue>, int): mixed $callback
* @return bool
*/
public function chunk($count, callable $callback)
{
$this->enforceOrderBy();
$skip = $this->getOffset();
$remaining = $this->getLimit();
$page = 1;
do {
$offset = (($page - 1) * $count) + (int) $skip;
$limit = is_null($remaining) ? $count : min($count, $remaining);
if ($limit == 0) {
break;
}
$results = $this->offset($offset)->limit($limit)->get();
$countResults = $results->count();
if ($countResults == 0) {
break;
}
if (! is_null($remaining)) {
$remaining = max($remaining - $countResults, 0);
}
if ($callback($results, $page) === false) {
return false;
}
unset($results);
$page++;
} while ($countResults == $count);
return true;
}
/**
* Run a map over each item while chunking.
*
* @template TReturn
*
* @param callable(TValue): TReturn $callback
* @param int $count
* @return \Illuminate\Support\Collection<int, TReturn>
*/
public function chunkMap(callable $callback, $count = 1000)
{
$collection = new Collection;
$this->chunk($count, function ($items) use ($collection, $callback) {
$items->each(function ($item) use ($collection, $callback) {
$collection->push($callback($item));
});
});
return $collection;
}
/**
* Execute a callback over each item while chunking.
*
* @param callable(TValue, int): mixed $callback
* @param int $count
* @return bool
*
* @throws \RuntimeException
*/
public function each(callable $callback, $count = 1000)
{
return $this->chunk($count, function ($results) use ($callback) {
foreach ($results as $key => $value) {
if ($callback($value, $key) === false) {
return false;
}
}
});
}
/**
* Chunk the results of a query by comparing IDs.
*
* @param int $count
* @param callable(\Illuminate\Support\Collection<int, 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;
$skip = $this->getOffset();
$remaining = $this->getLimit();
$page = 1;
do {
$clone = clone $this;
if ($skip && $page > 1) {
$clone->offset(0);
}
$limit = is_null($remaining) ? $count : min($count, $remaining);
if ($limit == 0) {
break;
}
// We'll execute the query for the given page and get the results. If there are
// no results we can just break and return from here. When there are results
// we will call the callback with the current chunk of these results here.
if ($descending) {
$results = $clone->forPageBeforeId($limit, $lastId, $column)->get();
} else {
$results = $clone->forPageAfterId($limit, $lastId, $column)->get();
}
$countResults = $results->count();
if ($countResults == 0) {
break;
}
if (! is_null($remaining)) {
$remaining = max($remaining - $countResults, 0);
}
// On each chunk result set, we will pass them to the callback and then let the
// developer take care of everything within the callback, which allows us to
// keep the memory low for spinning through large result sets for working.
if ($callback($results, $page) === false) {
return false;
}
$lastId = data_get($results->last(), $alias);
if ($lastId === null) {
throw new RuntimeException("The chunkById operation was aborted because the [{$alias}] column is not present in the query result.");
}
unset($results);
$page++;
} while ($countResults == $count);
return true;
}
/**
* Execute a callback over each item while chunking by ID.
*
* @param callable(TValue, int): mixed $callback
* @param int $count
* @param string|null $column
* @param string|null $alias
* @return bool
*/
public function eachById(callable $callback, $count = 1000, $column = null, $alias = null)
{
return $this->chunkById($count, function ($results, $page) use ($callback, $count) {
foreach ($results as $key => $value) {
if ($callback($value, (($page - 1) * $count) + $key) === false) {
return false;
}
}
}, $column, $alias);
}
/**
* Query lazily, by chunks of the given size.
*
* @param int $chunkSize
* @return \Illuminate\Support\LazyCollection<int, TValue>
*
* @throws \InvalidArgumentException
*/
public function lazy($chunkSize = 1000)
{
if ($chunkSize < 1) {
throw new InvalidArgumentException('The chunk size should be at least 1');
}
$this->enforceOrderBy();
return new LazyCollection(function () use ($chunkSize) {
$page = 1;
while (true) {
$results = $this->forPage($page++, $chunkSize)->get();
foreach ($results as $result) {
yield $result;
}
if ($results->count() < $chunkSize) {
return;
}
}
});
}
/**
* Query lazily, by chunking the results of a query by comparing IDs.
*
* @param int $chunkSize
* @param string|null $column
* @param string|null $alias
* @return \Illuminate\Support\LazyCollection<int, TValue>
*
* @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<int, TValue>
*
* @throws \InvalidArgumentException
*/
public function lazyByIdDesc($chunkSize = 1000, $column = null, $alias = null)
{
return $this->orderedLazyById($chunkSize, $column, $alias, true);
}
/**
* Query lazily, by chunking the results of a query by comparing IDs in a given order.
*
* @param int $chunkSize
* @param string|null $column
* @param string|null $alias
* @param bool $descending
* @return \Illuminate\Support\LazyCollection
*
* @throws \InvalidArgumentException
* @throws \RuntimeException
*/
protected function orderedLazyById($chunkSize = 1000, $column = null, $alias = null, $descending = false)
{
if ($chunkSize < 1) {
throw new InvalidArgumentException('The chunk size should be at least 1');
}
$column ??= $this->defaultKeyName();
$alias ??= $column;
return new LazyCollection(function () use ($chunkSize, $column, $alias, $descending) {
$lastId = null;
while (true) {
$clone = clone $this;
if ($descending) {
$results = $clone->forPageBeforeId($chunkSize, $lastId, $column)->get();
} else {
$results = $clone->forPageAfterId($chunkSize, $lastId, $column)->get();
}
foreach ($results as $result) {
yield $result;
}
if ($results->count() < $chunkSize) {
return;
}
$lastId = $results->last()->{$alias};
if ($lastId === null) {
throw new RuntimeException("The lazyById operation was aborted because the [{$alias}] column is not present in the query result.");
}
}
});
}
/**
* Execute the query and get the first result.
*
* @param array|string $columns
* @return TValue|null
*/
public function first($columns = ['*'])
{
return $this->limit(1)->get($columns)->first();
}
/**
* Execute the query and get the first result or throw an exception.
*
* @param array|string $columns
* @param string|null $message
* @return TValue
*
* @throws \Illuminate\Database\RecordNotFoundException
*/
public function firstOrFail($columns = ['*'], $message = null)
{
if (! is_null($result = $this->first($columns))) {
return $result;
}
throw new RecordNotFoundException($message ?: 'No record found for the given query.');
}
/**
* Execute the query and get the first result if it's the sole matching record.
*
* @param array|string $columns
* @return TValue
*
* @throws \Illuminate\Database\RecordsNotFoundException
* @throws \Illuminate\Database\MultipleRecordsFoundException
*/
public function sole($columns = ['*'])
{
$result = $this->limit(2)->get($columns);
$count = $result->count();
if ($count === 0) {
throw new RecordsNotFoundException;
}
if ($count > 1) {
throw new MultipleRecordsFoundException($count);
}
return $result->first();
}
/**
* Paginate the given query using a cursor paginator.
*
* @param int $perPage
* @param array|string $columns
* @param string $cursorName
* @param \Illuminate\Pagination\Cursor|string|null $cursor
* @return \Illuminate\Contracts\Pagination\CursorPaginator
*/
protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
{
if (! $cursor instanceof Cursor) {
$cursor = is_string($cursor)
? Cursor::fromEncoded($cursor)
: CursorPaginator::resolveCurrentCursor($cursorName, $cursor);
}
$orders = $this->ensureOrderForCursorPagination(! is_null($cursor) && $cursor->pointsToPreviousItems());
if (! is_null($cursor)) {
// Reset the union bindings so we can add the cursor where in the correct position...
$this->setBindings([], 'union');
$addCursorConditions = function (self $builder, $previousColumn, $originalColumn, $i) use (&$addCursorConditions, $cursor, $orders) {
$unionBuilders = $builder->getUnionBuilders();
if (! is_null($previousColumn)) {
$originalColumn ??= $this->getOriginalColumnNameForCursorPagination($this, $previousColumn);
$builder->where(
Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn,
'=',
$cursor->parameter($previousColumn)
);
$unionBuilders->each(function ($unionBuilder) use ($previousColumn, $cursor) {
$unionBuilder->where(
$this->getOriginalColumnNameForCursorPagination($unionBuilder, $previousColumn),
'=',
$cursor->parameter($previousColumn)
);
$this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
});
}
$builder->where(function (self $secondBuilder) use ($addCursorConditions, $cursor, $orders, $i, $unionBuilders) {
['column' => $column, 'direction' => $direction] = $orders[$i];
$originalColumn = $this->getOriginalColumnNameForCursorPagination($this, $column);
$secondBuilder->where(
Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn,
$direction === 'asc' ? '>' : '<',
$cursor->parameter($column)
);
if ($i < $orders->count() - 1) {
$secondBuilder->orWhere(function (self $thirdBuilder) use ($addCursorConditions, $column, $originalColumn, $i) {
$addCursorConditions($thirdBuilder, $column, $originalColumn, $i + 1);
});
}
$unionBuilders->each(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions) {
$unionWheres = $unionBuilder->getRawBindings()['where'];
$originalColumn = $this->getOriginalColumnNameForCursorPagination($unionBuilder, $column);
$unionBuilder->where(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions, $originalColumn, $unionWheres) {
$unionBuilder->where(
$originalColumn,
$direction === 'asc' ? '>' : '<',
$cursor->parameter($column)
);
if ($i < $orders->count() - 1) {
$unionBuilder->orWhere(function (self $fourthBuilder) use ($addCursorConditions, $column, $originalColumn, $i) {
$addCursorConditions($fourthBuilder, $column, $originalColumn, $i + 1);
});
}
$this->addBinding($unionWheres, 'union');
$this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
});
});
});
};
$addCursorConditions($this, null, null, 0);
}
$this->limit($perPage + 1);
return $this->cursorPaginator($this->get($columns), $perPage, $cursor, [
'path' => Paginator::resolveCurrentPath(),
'cursorName' => $cursorName,
'parameters' => $orders->pluck('column')->toArray(),
]);
}
/**
* Get the original column name of the given column, without any aliasing.
*
* @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*> $builder
* @param string $parameter
* @return string
*/
protected function getOriginalColumnNameForCursorPagination($builder, string $parameter)
{
$columns = $builder instanceof Builder ? $builder->getQuery()->getColumns() : $builder->getColumns();
if (! is_null($columns)) {
foreach ($columns as $column) {
if (($position = strripos($column, ' as ')) !== false) {
$original = substr($column, 0, $position);
$alias = substr($column, $position + 4);
if ($parameter === $alias || $builder->getGrammar()->wrap($parameter) === $alias) {
return $original;
}
}
}
}
return $parameter;
}
/**
* Create a new length-aware paginator instance.
*
* @param \Illuminate\Support\Collection $items
* @param int $total
* @param int $perPage
* @param int $currentPage
* @param array $options
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
protected function paginator($items, $total, $perPage, $currentPage, $options)
{
return Container::getInstance()->makeWith(LengthAwarePaginator::class, compact(
'items', 'total', 'perPage', 'currentPage', 'options'
));
}
/**
* Create a new simple paginator instance.
*
* @param \Illuminate\Support\Collection $items
* @param int $perPage
* @param int $currentPage
* @param array $options
* @return \Illuminate\Pagination\Paginator
*/
protected function simplePaginator($items, $perPage, $currentPage, $options)
{
return Container::getInstance()->makeWith(Paginator::class, compact(
'items', 'perPage', 'currentPage', 'options'
));
}
/**
* Create a new cursor paginator instance.
*
* @param \Illuminate\Support\Collection $items
* @param int $perPage
* @param \Illuminate\Pagination\Cursor $cursor
* @param array $options
* @return \Illuminate\Pagination\CursorPaginator
*/
protected function cursorPaginator($items, $perPage, $cursor, $options)
{
return Container::getInstance()->makeWith(CursorPaginator::class, compact(
'items', 'perPage', 'cursor', 'options'
));
}
/**
* Pass the query to a given callback and then return it.
*
* @param callable($this): mixed $callback
* @return $this
*/
public function tap($callback)
{
$callback($this);
return $this;
}
/**
* Pass the query to a given callback and return the result.
*
* @template TReturn
*
* @param (callable($this): TReturn) $callback
* @return (TReturn is null|void ? $this : TReturn)
*/
public function pipe($callback)
{
return $callback($this) ?? $this;
}
}
@@ -0,0 +1,249 @@
<?php
namespace Illuminate\Database\Concerns;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
trait BuildsWhereDateClauses
{
/**
* Add a where clause to determine if a "date" column is in the past to the query.
*
* @param array|string $columns
* @return $this
*/
public function wherePast($columns)
{
return $this->wherePastOrFuture($columns, '<', 'and');
}
/**
* Add a where clause to determine if a "date" column is in the past or now to the query.
*
* @param array|string $columns
* @return $this
*/
public function whereNowOrPast($columns)
{
return $this->wherePastOrFuture($columns, '<=', 'and');
}
/**
* Add an "or where" clause to determine if a "date" column is in the past to the query.
*
* @param array|string $columns
* @return $this
*/
public function orWherePast($columns)
{
return $this->wherePastOrFuture($columns, '<', 'or');
}
/**
* Add a where clause to determine if a "date" column is in the past or now to the query.
*
* @param array|string $columns
* @return $this
*/
public function orWhereNowOrPast($columns)
{
return $this->wherePastOrFuture($columns, '<=', 'or');
}
/**
* Add a where clause to determine if a "date" column is in the future to the query.
*
* @param array|string $columns
* @return $this
*/
public function whereFuture($columns)
{
return $this->wherePastOrFuture($columns, '>', 'and');
}
/**
* Add a where clause to determine if a "date" column is in the future or now to the query.
*
* @param array|string $columns
* @return $this
*/
public function whereNowOrFuture($columns)
{
return $this->wherePastOrFuture($columns, '>=', 'and');
}
/**
* Add an "or where" clause to determine if a "date" column is in the future to the query.
*
* @param array|string $columns
* @return $this
*/
public function orWhereFuture($columns)
{
return $this->wherePastOrFuture($columns, '>', 'or');
}
/**
* Add an "or where" clause to determine if a "date" column is in the future or now to the query.
*
* @param array|string $columns
* @return $this
*/
public function orWhereNowOrFuture($columns)
{
return $this->wherePastOrFuture($columns, '>=', 'or');
}
/**
* Add an "where" clause to determine if a "date" column is in the past or future.
*
* @param array|string $columns
* @param string $operator
* @param string $boolean
* @return $this
*/
protected function wherePastOrFuture($columns, $operator, $boolean)
{
$type = 'Basic';
$value = Carbon::now();
foreach (Arr::wrap($columns) as $column) {
$this->wheres[] = compact('type', 'column', 'boolean', 'operator', 'value');
$this->addBinding($value);
}
return $this;
}
/**
* Add a "where date" clause to determine if a "date" column is today to the query.
*
* @param array|string $columns
* @param string $boolean
* @return $this
*/
public function whereToday($columns, $boolean = 'and')
{
return $this->whereTodayBeforeOrAfter($columns, '=', $boolean);
}
/**
* Add a "where date" clause to determine if a "date" column is before today.
*
* @param array|string $columns
* @return $this
*/
public function whereBeforeToday($columns)
{
return $this->whereTodayBeforeOrAfter($columns, '<', 'and');
}
/**
* Add a "where date" clause to determine if a "date" column is today or before to the query.
*
* @param array|string $columns
* @return $this
*/
public function whereTodayOrBefore($columns)
{
return $this->whereTodayBeforeOrAfter($columns, '<=', 'and');
}
/**
* Add a "where date" clause to determine if a "date" column is after today.
*
* @param array|string $columns
* @return $this
*/
public function whereAfterToday($columns)
{
return $this->whereTodayBeforeOrAfter($columns, '>', 'and');
}
/**
* Add a "where date" clause to determine if a "date" column is today or after to the query.
*
* @param array|string $columns
* @return $this
*/
public function whereTodayOrAfter($columns)
{
return $this->whereTodayBeforeOrAfter($columns, '>=', 'and');
}
/**
* Add an "or where date" clause to determine if a "date" column is today to the query.
*
* @param array|string $columns
* @return $this
*/
public function orWhereToday($columns)
{
return $this->whereToday($columns, 'or');
}
/**
* Add an "or where date" clause to determine if a "date" column is before today.
*
* @param array|string $columns
* @return $this
*/
public function orWhereBeforeToday($columns)
{
return $this->whereTodayBeforeOrAfter($columns, '<', 'or');
}
/**
* Add an "or where date" clause to determine if a "date" column is today or before to the query.
*
* @param array|string $columns
* @return $this
*/
public function orWhereTodayOrBefore($columns)
{
return $this->whereTodayBeforeOrAfter($columns, '<=', 'or');
}
/**
* Add an "or where date" clause to determine if a "date" column is after today.
*
* @param array|string $columns
* @return $this
*/
public function orWhereAfterToday($columns)
{
return $this->whereTodayBeforeOrAfter($columns, '>', 'or');
}
/**
* Add an "or where date" clause to determine if a "date" column is today or after to the query.
*
* @param array|string $columns
* @return $this
*/
public function orWhereTodayOrAfter($columns)
{
return $this->whereTodayBeforeOrAfter($columns, '>=', 'or');
}
/**
* Add a "where date" clause to determine if a "date" column is today or after to the query.
*
* @param array|string $columns
* @param string $operator
* @param string $boolean
* @return $this
*/
protected function whereTodayBeforeOrAfter($columns, $operator, $boolean)
{
$value = Carbon::today()->format('Y-m-d');
foreach (Arr::wrap($columns) as $column) {
$this->addDateBasedWhere('Date', $column, $operator, $value, $boolean);
}
return $this;
}
}
@@ -0,0 +1,65 @@
<?php
namespace Illuminate\Database\Concerns;
use Illuminate\Support\Collection;
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 = (new Collection(explode($delimiter, $value)))
->map(fn ($segment) => $this->wrapJsonPathSegment($segment))
->join('.');
return "'$".(str_starts_with($jsonPath, '[') ? '' : '.').$jsonPath."'";
}
/**
* Wrap the given JSON path segment.
*
* @param string $segment
* @return string
*/
protected function wrapJsonPathSegment($segment)
{
if (preg_match('/(\[[^\]]+\])+$/', $segment, $parts)) {
$key = Str::beforeLast($segment, $parts[0]);
if (! empty($key)) {
return '"'.$key.'"'.$parts[0];
}
return $parts[0];
}
return '"'.$segment.'"';
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace Illuminate\Database\Concerns;
use Illuminate\Support\Collection;
trait ExplainsQueries
{
/**
* Explains the query.
*
* @return \Illuminate\Support\Collection
*/
public function explain()
{
$sql = $this->toSql();
$bindings = $this->getBindings();
$explanation = $this->getConnection()->select('EXPLAIN '.$sql, $bindings);
return new Collection($explanation);
}
}
@@ -0,0 +1,379 @@
<?php
namespace Illuminate\Database\Concerns;
use Closure;
use Illuminate\Database\DeadlockException;
use RuntimeException;
use Throwable;
/**
* @mixin \Illuminate\Database\Connection
*/
trait ManagesTransactions
{
/**
* @template TReturn of mixed
*
* Execute a Closure within a transaction.
*
* @param (\Closure(static): TReturn) $callback
* @param int $attempts
* @return TReturn
*
* @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->executeBeginTransactionStatement();
} catch (Throwable $e) {
$this->handleBeginTransactionException($e);
}
} elseif ($this->transactions >= 1 && $this->queryGrammar->supportsSavepoints()) {
$this->createSavepoint();
}
}
/**
* Create a save point within the database.
*
* @return void
*
* @throws \Throwable
*/
protected function createSavepoint()
{
$this->getPdo()->exec(
$this->queryGrammar->compileSavepoint('trans'.($this->transactions + 1))
);
}
/**
* Handle an exception from a transaction beginning.
*
* @param \Throwable $e
* @return void
*
* @throws \Throwable
*/
protected function handleBeginTransactionException(Throwable $e)
{
if ($this->causedByLostConnection($e)) {
$this->reconnect();
$this->executeBeginTransactionStatement();
} else {
throw $e;
}
}
/**
* Commit the active database transaction.
*
* @return void
*
* @throws \Throwable
*/
public function commit()
{
if ($this->transactionLevel() == 1) {
$this->fireConnectionEvent('committing');
$this->getPdo()->commit();
}
[$levelBeingCommitted, $this->transactions] = [
$this->transactions,
max(0, $this->transactions - 1),
];
$this->transactionsManager?->commit(
$this->getName(), $levelBeingCommitted, $this->transactions
);
$this->fireConnectionEvent('committed');
}
/**
* Handle an exception encountered when committing a transaction.
*
* @param \Throwable $e
* @param int $currentAttempt
* @param int $maxAttempts
* @return void
*
* @throws \Throwable
*/
protected function handleCommitTransactionException(Throwable $e, $currentAttempt, $maxAttempts)
{
$this->transactions = max(0, $this->transactions - 1);
if ($this->causedByConcurrencyError($e) && $currentAttempt < $maxAttempts) {
$pdo = $this->getPdo();
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
return;
}
if ($this->causedByLostConnection($e)) {
$this->transactions = 0;
}
throw $e;
}
/**
* Rollback the active database transaction.
*
* @param int|null $toLevel
* @return void
*
* @throws \Throwable
*/
public function rollBack($toLevel = null)
{
// We allow developers to rollback to a certain transaction level. We will verify
// that this given transaction level is valid before attempting to rollback to
// that level. If it's not we will just return out and not attempt anything.
$toLevel = is_null($toLevel)
? $this->transactions - 1
: $toLevel;
if ($toLevel < 0 || $toLevel >= $this->transactions) {
return;
}
// Next, we will actually perform this rollback within this database and fire the
// rollback event. We will also set the current transaction level to the given
// level that was passed into this method so it will be right from here out.
try {
$this->performRollBack($toLevel);
} catch (Throwable $e) {
$this->handleRollBackException($e);
}
$this->transactions = $toLevel;
$this->transactionsManager?->rollback(
$this->getName(), $this->transactions
);
$this->fireConnectionEvent('rollingBack');
}
/**
* Perform a rollback within the database.
*
* @param int $toLevel
* @return void
*
* @throws \Throwable
*/
protected function performRollBack($toLevel)
{
if ($toLevel == 0) {
$pdo = $this->getPdo();
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
} elseif ($this->queryGrammar->supportsSavepoints()) {
$this->getPdo()->exec(
$this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1))
);
}
}
/**
* Handle an exception from a rollback.
*
* @param \Throwable $e
* @return void
*
* @throws \Throwable
*/
protected function handleRollBackException(Throwable $e)
{
if ($this->causedByLostConnection($e)) {
$this->transactions = 0;
$this->transactionsManager?->rollback(
$this->getName(), $this->transactions
);
}
throw $e;
}
/**
* Get the number of active transactions.
*
* @return int
*/
public function transactionLevel()
{
return $this->transactions;
}
/**
* Execute the callback after a transaction commits.
*
* @param callable $callback
* @return void
*
* @throws \RuntimeException
*/
public function afterCommit($callback)
{
if ($this->transactionsManager) {
return $this->transactionsManager->addCallback($callback);
}
throw new RuntimeException('Transactions Manager has not been set.');
}
/**
* Execute the callback after a transaction rolls back.
*
* @param callable $callback
* @return void
*
* @throws \RuntimeException
*/
public function afterRollBack($callback)
{
if ($this->transactionsManager) {
return $this->transactionsManager->addCallbackForRollback($callback);
}
throw new RuntimeException('Transactions Manager has not been set.');
}
}
@@ -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 ?? []);
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace Illuminate\Database;
use Illuminate\Contracts\Database\ConcurrencyErrorDetector as ConcurrencyErrorDetectorContract;
use Illuminate\Support\Str;
use PDOException;
use Throwable;
class ConcurrencyErrorDetector implements ConcurrencyErrorDetectorContract
{
/**
* Determine if the given exception was caused by a concurrency error such as a deadlock or serialization failure.
*
* @param \Throwable $e
* @return bool
*/
public function causedByConcurrencyError(Throwable $e): bool
{
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',
'Record has changed since last read in table',
]);
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace Illuminate\Database;
use Illuminate\Support\ConfigurationUrlParser as BaseConfigurationUrlParser;
class ConfigurationUrlParser extends BaseConfigurationUrlParser
{
//
}
+1804
View File
File diff suppressed because it is too large Load Diff
+184
View File
@@ -0,0 +1,184 @@
<?php
namespace Illuminate\Database;
use Closure;
interface ConnectionInterface
{
/**
* Begin a fluent query against a database table.
*
* @param \Closure|\Illuminate\Database\Query\Builder|\UnitEnum|string $table
* @param string|null $as
* @return \Illuminate\Database\Query\Builder
*/
public function table($table, $as = null);
/**
* Get a new raw query expression.
*
* @param literal-string|int|float $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
* @param array $fetchUsing
* @return array
*/
public function select($query, $bindings = [], $useReadPdo = true, array $fetchUsing = []);
/**
* Run a select statement against the database and returns a generator.
*
* @param string $query
* @param array $bindings
* @param bool $useReadPdo
* @param array $fetchUsing
* @return \Generator
*/
public function cursor($query, $bindings = [], $useReadPdo = true, array $fetchUsing = []);
/**
* 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 literal-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();
}
+91
View File
@@ -0,0 +1,91 @@
<?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
*/
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;
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace Illuminate\Database;
interface ConnectionResolverInterface
{
/**
* Get a database connection instance.
*
* @param \UnitEnum|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);
}
+285
View File
@@ -0,0 +1,285 @@
<?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
*/
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))
->setReadPdoConfig($this->getReadConfig($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) {
$exception = null;
foreach (Arr::shuffle($this->parseHosts($config)) as $host) {
$config['host'] = $host;
try {
return $this->createConnector($config)->connect($config);
} catch (PDOException $e) {
$exception = $e;
}
}
if ($exception !== null) {
throw $exception;
}
};
}
/**
* 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}]."),
};
}
}
+124
View File
@@ -0,0 +1,124 @@
<?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, #[\SensitiveParameter] $password, $options)
{
return version_compare(PHP_VERSION, '8.4.0', '<')
? new PDO($dsn, $username, $password, $options)
: PDO::connect($dsn, $username, $password, $options); /** @phpstan-ignore staticMethod.notFound (PHP 8.4) */
}
/**
* 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, #[\SensitiveParameter] $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;
}
}
+14
View File
@@ -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);
}
+32
View File
@@ -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';
}
}
+154
View File
@@ -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']) &&
(! isset($config['use_db_after_connecting']) ||
$config['use_db_after_connecting'])) {
$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)
{
return isset($config['port'])
? "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']}"
: "mysql:host={$config['host']};dbname={$config['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', '>=')) {
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';
}
}
+187
View File
@@ -0,0 +1,187 @@
<?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;
}
/**
* 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 ?? null;
$port = $connect_via_port ?? $port ?? null;
$dsn = "pgsql:{$host}dbname='{$database}'";
// If a port was specified, we will add it to this Postgres DSN connections
// format. Once we have done that we are ready to return this connection
// string back out for usage, as this has been fully constructed here.
if (! is_null($port)) {
$dsn .= ";port={$port}";
}
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;
}
/**
* 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).'"';
}
/**
* Configure the synchronous_commit setting.
*
* @param \PDO $connection
* @param array $config
* @return void
*/
protected function configureSynchronousCommit($connection, array $config)
{
if (isset($config['synchronous_commit'])) {
$connection->prepare("set synchronous_commit to '{$config['synchronous_commit']}'")->execute();
}
}
}
+149
View File
@@ -0,0 +1,149 @@
<?php
namespace Illuminate\Database\Connectors;
use Illuminate\Database\SQLiteDatabaseDoesNotExistException;
class SQLiteConnector extends Connector implements ConnectorInterface
{
/**
* Establish a database connection.
*
* @param array $config
* @return \PDO
*/
public function connect(array $config)
{
$options = $this->getOptions($config);
$path = $this->parseDatabasePath($config['database']);
$connection = $this->createConnection("sqlite:{$path}", $config, $options);
$this->configurePragmas($connection, $config);
$this->configureForeignKeyConstraints($connection, $config);
$this->configureBusyTimeout($connection, $config);
$this->configureJournalMode($connection, $config);
$this->configureSynchronous($connection, $config);
return $connection;
}
/**
* Get the absolute database path.
*
* @param string $path
* @return string
*
* @throws \Illuminate\Database\SQLiteDatabaseDoesNotExistException
*/
protected function parseDatabasePath(string $path): string
{
$database = $path;
// SQLite supports "in-memory" databases that only last as long as the owning
// connection does. These are useful for tests or for short lifetime store
// querying. In-memory databases shall be anonymous (:memory:) or named.
if ($path === ':memory:' ||
str_contains($path, '?mode=memory') ||
str_contains($path, '&mode=memory')
) {
return $path;
}
$path = realpath($path) ?: realpath(base_path($path));
// Here we'll verify that the SQLite database exists before going any further
// as the developer probably wants to know if the database exists and this
// SQLite driver will not throw any exception if it does not by default.
if ($path === false) {
throw new SQLiteDatabaseDoesNotExistException($database);
}
return $path;
}
/**
* Set miscellaneous user-configured pragmas.
*
* @param \PDO $connection
* @param array $config
* @return void
*/
protected function configurePragmas($connection, array $config): void
{
if (! isset($config['pragmas'])) {
return;
}
foreach ($config['pragmas'] as $pragma => $value) {
$connection->prepare("pragma {$pragma} = {$value}")->execute();
}
}
/**
* Enable or disable foreign key constraints if configured.
*
* @param \PDO $connection
* @param array $config
* @return void
*/
protected function configureForeignKeyConstraints($connection, array $config): void
{
if (! isset($config['foreign_key_constraints'])) {
return;
}
$foreignKeys = $config['foreign_key_constraints'] ? 1 : 0;
$connection->prepare("pragma foreign_keys = {$foreignKeys}")->execute();
}
/**
* Set the busy timeout if configured.
*
* @param \PDO $connection
* @param array $config
* @return void
*/
protected function configureBusyTimeout($connection, array $config): void
{
if (! isset($config['busy_timeout'])) {
return;
}
$connection->prepare("pragma busy_timeout = {$config['busy_timeout']}")->execute();
}
/**
* Set the journal mode if configured.
*
* @param \PDO $connection
* @param array $config
* @return void
*/
protected function configureJournalMode($connection, array $config): void
{
if (! isset($config['journal_mode'])) {
return;
}
$connection->prepare("pragma journal_mode = {$config['journal_mode']}")->execute();
}
/**
* Set the synchronous mode if configured.
*
* @param \PDO $connection
* @param array $config
* @return void
*/
protected function configureSynchronous($connection, array $config): void
{
if (! isset($config['synchronous'])) {
return;
}
$connection->prepare("pragma synchronous = {$config['synchronous']}")->execute();
}
}
+234
View File
@@ -0,0 +1,234 @@
<?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,50 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Console\Command;
use Illuminate\Database\ConnectionInterface;
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
*
* @deprecated
*/
protected function getConnectionName(ConnectionInterface $connection, $database)
{
return $connection->getDriverTitle();
}
/**
* Get the number of open connections for a database.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @return int|null
*
* @deprecated
*/
protected function getConnectionCount(ConnectionInterface $connection)
{
return $connection->threadCount();
}
/**
* Get the connection configuration details for the given connection.
*
* @param string|null $database
* @return array
*/
protected function getConfigFromDatabase($database)
{
$database ??= config('database.default');
return Arr::except(config('database.connections.'.$database), ['password']);
}
}
+257
View File
@@ -0,0 +1,257 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Console\Command;
use Illuminate\Support\ConfigurationUrlParser;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Process\Exception\ProcessFailedException;
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;
}
try {
(new Process(
array_merge([$command = $this->getCommand($connection)], $this->commandArguments($connection)),
null,
$this->commandEnvironment($connection)
))->setTimeout(null)->setTty(true)->mustRun(function ($type, $buffer) {
$this->output->write($buffer);
});
} catch (ProcessFailedException $e) {
throw_unless($e->getProcess()->getExitCode() === 127, $e);
$this->error("{$command} not found in path.");
return Command::FAILURE;
}
return 0;
}
/**
* Get the database connection configuration.
*
* @return array
*
* @throws \UnexpectedValueException
*/
public function getConnection()
{
$connection = $this->laravel['config']['database.connections.'.
(($db = $this->argument('connection')) ?? $this->laravel['config']['database.default'])
];
if (empty($connection)) {
throw new UnexpectedValueException("Invalid database connection [{$db}].");
}
if (! empty($connection['url'])) {
$connection = (new ConfigurationUrlParser)->parseConfiguration($connection);
}
if ($this->option('read')) {
if (is_array($connection['read']['host'])) {
$connection['read']['host'] = $connection['read']['host'][0];
}
$connection = array_merge($connection, $connection['read']);
} elseif ($this->option('write')) {
if (is_array($connection['write']['host'])) {
$connection['write']['host'] = $connection['write']['host'][0];
}
$connection = array_merge($connection, $connection['write']);
}
return $connection;
}
/**
* Get the arguments for the database client command.
*
* @param array $connection
* @return array
*/
public function commandArguments(array $connection)
{
$driver = ucfirst($connection['driver']);
return $this->{"get{$driver}Arguments"}($connection);
}
/**
* Get the environment variables for the database client command.
*
* @param array $connection
* @return array|null
*/
public function commandEnvironment(array $connection)
{
$driver = ucfirst($connection['driver']);
if (method_exists($this, "get{$driver}Environment")) {
return $this->{"get{$driver}Environment"}($connection);
}
return null;
}
/**
* Get the database client command to run.
*
* @param array $connection
* @return string
*/
public function getCommand(array $connection)
{
return [
'mysql' => 'mysql',
'mariadb' => 'mariadb',
'pgsql' => 'psql',
'sqlite' => 'sqlite3',
'sqlsrv' => 'sqlcmd',
][$connection['driver']];
}
/**
* Get the arguments for the MySQL CLI.
*
* @param array $connection
* @return array
*/
protected function getMysqlArguments(array $connection)
{
$optionalArguments = [
'password' => '--password='.$connection['password'],
'unix_socket' => '--socket='.($connection['unix_socket'] ?? ''),
'charset' => '--default-character-set='.($connection['charset'] ?? ''),
];
if (! $connection['password']) {
unset($optionalArguments['password']);
}
return array_merge([
'--host='.$connection['host'],
'--port='.$connection['port'],
'--user='.$connection['username'],
], $this->getOptionalArguments($optionalArguments, $connection), [$connection['database']]);
}
/**
* Get the arguments for the MariaDB CLI.
*
* @param array $connection
* @return array
*/
protected function getMariaDbArguments(array $connection)
{
return $this->getMysqlArguments($connection);
}
/**
* Get the arguments for the Postgres CLI.
*
* @param array $connection
* @return array
*/
protected function getPgsqlArguments(array $connection)
{
return [$connection['database']];
}
/**
* Get the arguments for the SQLite CLI.
*
* @param array $connection
* @return array
*/
protected function getSqliteArguments(array $connection)
{
return [$connection['database']];
}
/**
* Get the arguments for the SQL Server CLI.
*
* @param array $connection
* @return array
*/
protected function getSqlsrvArguments(array $connection)
{
return array_merge(...$this->getOptionalArguments([
'database' => ['-d', $connection['database']],
'username' => ['-U', $connection['username']],
'password' => ['-P', $connection['password']],
'host' => ['-S', 'tcp:'.$connection['host']
.($connection['port'] ? ','.$connection['port'] : ''), ],
'trust_server_certificate' => ['-C'],
], $connection));
}
/**
* Get the environment variables for the Postgres CLI.
*
* @param array $connection
* @return array|null
*/
protected function getPgsqlEnvironment(array $connection)
{
return array_merge(...$this->getOptionalArguments([
'username' => ['PGUSER' => $connection['username']],
'host' => ['PGHOST' => $connection['host']],
'port' => ['PGPORT' => $connection['port']],
'password' => ['PGPASSWORD' => $connection['password']],
], $connection));
}
/**
* Get the optional arguments based on the connection configuration.
*
* @param array $args
* @param array $connection
* @return array
*/
protected function getOptionalArguments(array $args, array $connection)
{
return array_values(array_filter($args, function ($key) use ($connection) {
return ! empty($connection[$key]);
}, ARRAY_FILTER_USE_KEY));
}
}
+104
View File
@@ -0,0 +1,104 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Console\Command;
use Illuminate\Console\Prohibitable;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Connection;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\Events\MigrationsPruned;
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
{
use Prohibitable;
/**
* 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)
{
if ($this->isProhibited()) {
return Command::FAILURE;
}
$connection = $connections->connection($database = $this->input->getOption('database'));
$this->schemaState($connection)->dump(
$connection, $path = $this->path($connection)
);
$dispatcher->dispatch(new SchemaDumped($connection, $path));
$info = 'Database schema dumped';
if ($this->option('prune')) {
(new Filesystem)->deleteDirectory(
$path = database_path('migrations'), preserve: false
);
$info .= ' and pruned';
$dispatcher->dispatch(new MigrationsPruned($connection, $path));
}
$this->components->info($info.' successfully.');
}
/**
* Create a schema state instance for the given connection.
*
* @param \Illuminate\Database\Connection $connection
* @return mixed
*/
protected function schemaState(Connection $connection)
{
$migrations = Config::get('database.migrations', 'migrations');
$migrationTable = is_array($migrations) ? ($migrations['table'] ?? 'migrations') : $migrations;
return $connection->getSchemaState()
->withMigrationTable($migrationTable)
->handleOutputUsing(function ($type, $buffer) {
$this->output->write($buffer);
});
}
/**
* Get the path that the dump should be written to.
*
* @param \Illuminate\Database\Connection $connection
*/
protected function path(Connection $connection)
{
return tap($this->option('path') ?: database_path('schema/'.$connection->getName().'-schema.sql'), function ($path) {
(new Filesystem)->ensureDirectoryExists(dirname($path));
});
}
}
@@ -0,0 +1,144 @@
<?php
namespace Illuminate\Database\Console\Factories;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
use Illuminate\Support\Stringable;
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 = (new Stringable($name))->replaceFirst($this->rootNamespace(), '')->finish('Factory')->value();
return $this->laravel->databasePath().'/factories/'.str_replace('\\', '/', $name).'.php';
}
/**
* Guess the model name from the Factory name or return a default model name.
*
* @param string $name
* @return string
*/
protected function guessModelName($name)
{
if (str_ends_with($name, 'Factory')) {
$name = substr($name, 0, -7);
}
$modelName = $this->qualifyModel(Str::after($name, $this->rootNamespace()));
if (class_exists($modelName)) {
return $modelName;
}
if (is_dir(app_path('Models/'))) {
return $this->rootNamespace().'Models\Model';
}
return $this->rootNamespace().'Model';
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['model', 'm', InputOption::VALUE_OPTIONAL, 'The name of the model'],
];
}
}
@@ -0,0 +1,24 @@
<?php
namespace {{ factoryNamespace }};
use {{ namespacedModel }};
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<{{ model }}>
*/
class {{ factory }}Factory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
//
];
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
class BaseCommand extends Command
{
/**
* Get all of the migration paths.
*
* @return string[]
*/
protected function getMigrationPaths()
{
// Here, we will check to see if a path option has been defined. If it has we will
// use the path relative to the root of the installation folder so our database
// migrations may be run for any customized path from within the application.
if ($this->input->hasOption('path') && $this->option('path')) {
return (new Collection($this->option('path')))->map(function ($path) {
return ! $this->usingRealPath()
? $this->laravel->basePath().'/'.$path
: $path;
})->all();
}
return array_merge(
$this->migrator->paths(), [$this->getMigrationPath()]
);
}
/**
* Determine if the given path(s) are pre-resolved "real" paths.
*
* @return bool
*/
protected function usingRealPath()
{
return $this->input->hasOption('realpath') && $this->option('realpath');
}
/**
* Get the path to the migration directory.
*
* @return string
*/
protected function getMigrationPath()
{
return $this->laravel->databasePath().DIRECTORY_SEPARATOR.'migrations';
}
}
@@ -0,0 +1,155 @@
<?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;
use Throwable;
#[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
*/
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) {
try {
$repositoryExists = $this->migrator->repositoryExists();
} catch (Throwable) {
$repositoryExists = false;
}
if ($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'],
];
}
}
+73
View File
@@ -0,0 +1,73 @@
<?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
*/
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'));
if (! $this->repository->repositoryExists()) {
$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'],
];
}
}
+343
View File
@@ -0,0 +1,343 @@
<?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 Illuminate\Support\Str;
use PDOException;
use RuntimeException;
use Symfony\Component\Console\Attribute\AsCommand;
use Throwable;
use function Laravel\Prompts\confirm;
#[AsCommand(name: 'migrate')]
class MigrateCommand extends BaseCommand implements Isolatable
{
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
*/
public function __construct(Migrator $migrator, Dispatcher $dispatcher)
{
parent::__construct();
$this->migrator = $migrator;
$this->dispatcher = $dispatcher;
}
/**
* Execute the console command.
*
* @return int
*
* @throws \Throwable
*/
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 {
return $this->handleMissingDatabase($e->getPrevious());
} catch (Throwable) {
return false;
}
});
}
/**
* Attempt to create the database if it is missing.
*
* @param \Throwable $e
* @return bool
*/
protected function handleMissingDatabase(Throwable $e)
{
if ($e instanceof SQLiteDatabaseDoesNotExistException) {
return $this->createMissingSqliteDatabase($e->path);
}
$connection = $this->migrator->resolveConnection($this->option('database'));
if (! $e instanceof PDOException) {
return false;
}
if (($e->getCode() === 1049 && in_array($connection->getDriverName(), ['mysql', 'mariadb'])) ||
(($e->errorInfo[0] ?? null) == '08006' &&
$connection->getDriverName() == 'pgsql' &&
Str::contains($e->getMessage(), '"'.$connection->getDatabaseName().'"'))) {
return $this->createMissingMySqlOrPgsqlDatabase($connection);
}
return false;
}
/**
* Create a missing SQLite database.
*
* @param string $path
* @return bool
*
* @throws \RuntimeException
*/
protected function createMissingSqliteDatabase($path)
{
if ($this->option('force')) {
return touch($path);
}
if ($this->option('no-interaction')) {
return false;
}
$this->components->warn('The SQLite database configured for this application does not exist: '.$path);
if (! confirm('Would you like to create it?', default: true)) {
$this->components->info('Operation cancelled. No database was created.');
throw new RuntimeException('Database was not created. Aborting migration.');
}
return touch($path);
}
/**
* Create a missing MySQL or Postgres database.
*
* @param \Illuminate\Database\Connection $connection
* @return bool
*
* @throws \RuntimeException
*/
protected function createMissingMySqlOrPgsqlDatabase($connection)
{
if ($this->laravel['config']->get("database.connections.{$connection->getName()}.database") !== $connection->getDatabaseName()) {
return false;
}
if (! $this->option('force') && $this->option('no-interaction')) {
return false;
}
if (! $this->option('force') && ! $this->option('no-interaction')) {
$this->components->warn("The database '{$connection->getDatabaseName()}' does not exist on the '{$connection->getName()}' connection.");
if (! confirm('Would you like to create it?', default: true)) {
$this->components->info('Operation cancelled. No database was created.');
throw new RuntimeException('Database was not created. Aborting migration.');
}
}
try {
$this->laravel['config']->set(
"database.connections.{$connection->getName()}.database",
match ($connection->getDriverName()) {
'mysql', 'mariadb' => null,
'pgsql' => 'postgres',
},
);
$this->laravel['db']->purge();
$freshConnection = $this->migrator->resolveConnection($this->option('database'));
return tap($freshConnection->unprepared(
match ($connection->getDriverName()) {
'mysql', 'mariadb' => "CREATE DATABASE IF NOT EXISTS `{$connection->getDatabaseName()}`",
'pgsql' => 'CREATE DATABASE "'.$connection->getDatabaseName().'"',
}
), function () {
$this->laravel['db']->purge();
});
} finally {
$this->laravel['config']->set("database.connections.{$connection->getName()}.database", $connection->getDatabaseName());
}
}
/**
* Load the schema state to seed the initial database schema structure.
*
* @return void
*/
protected function loadSchemaState()
{
$connection = $this->migrator->resolveConnection($this->option('database'));
// First, we will make sure that the connection supports schema loading and that
// the schema file exists before we proceed any further. If not, we will just
// continue with the standard migration operation as normal without errors.
if ($connection instanceof SqlServerConnection ||
! is_file($path = $this->schemaPath($connection))) {
return;
}
$this->components->info('Loading stored database schemas.');
$this->components->task($path, function () use ($connection, $path) {
// Since the schema file will create the "migrations" table and reload it to its
// proper state, we need to delete it here so we don't get an error that this
// table already exists when the stored database schema file gets executed.
$this->migrator->deleteRepository();
$connection->getSchemaState()->handleOutputUsing(function ($type, $buffer) {
$this->output->write($buffer);
})->load($path);
});
$this->newLine();
// Finally, we will fire an event that this schema has been loaded so developers
// can perform any post schema load tasks that are necessary in listeners for
// this event, which may seed the database tables with some necessary data.
$this->dispatcher->dispatch(
new SchemaLoaded($connection, $path)
);
}
/**
* Get the path to the stored schema for the given connection.
*
* @param \Illuminate\Database\Connection $connection
* @return string
*/
protected function schemaPath($connection)
{
if ($this->option('schema-path')) {
return $this->option('schema-path');
}
if (file_exists($path = database_path('schema/'.$connection->getName().'-schema.dump'))) {
return $path;
}
return database_path('schema/'.$connection->getName().'-schema.sql');
}
}
@@ -0,0 +1,149 @@
<?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
*/
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
);
if (windows_os()) {
$file = str_replace('/', '\\', $file);
}
$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'],
];
}
}
+163
View File
@@ -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 Command::FAILURE;
}
// 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'],
];
}
}
+95
View File
@@ -0,0 +1,95 @@
<?php
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Console\Prohibitable;
use Illuminate\Database\Migrations\Migrator;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'migrate:reset')]
class ResetCommand extends BaseCommand
{
use ConfirmableTrait, Prohibitable;
/**
* 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
*/
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;
}
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'],
];
}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Console\Prohibitable;
use Illuminate\Database\Migrations\Migrator;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand('migrate:rollback')]
class RollbackCommand extends BaseCommand
{
use ConfirmableTrait, Prohibitable;
/**
* 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
*/
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;
}
$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 Illuminate\Support\Stringable;
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
*/
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 (new Stringable($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) => (new Stringable($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 (new Collection($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{string, bool}
*/
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];
}
}
}
}
+141
View File
@@ -0,0 +1,141 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\Events\DatabaseBusy;
use Illuminate\Support\Collection;
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 (new Collection(explode(',', $databases)))->map(function ($database) {
if (! $database) {
$database = $this->laravel['config']['database.default'];
}
$maxConnections = $this->option('max');
$connections = $this->connection->connection($database)->threadCount();
return [
'database' => $database,
'connections' => $connections,
'status' => $maxConnections && $connections >= $maxConnections ? '<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']
)
);
});
}
}
+199
View File
@@ -0,0 +1,199 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Console\Command;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Events\ModelPruningFinished;
use Illuminate\Database\Events\ModelPruningStarting;
use Illuminate\Database\Events\ModelsPruned;
use Illuminate\Support\Collection;
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 = $model::isPrunable()
? $instance->pruneAll($chunkSize)
: 0;
if ($total == 0) {
$this->components->info("No prunable [$model] records found.");
}
}
/**
* Determine the models that should be pruned.
*
* @return \Illuminate\Support\Collection
*
* @throws \InvalidArgumentException
*/
protected function models()
{
$models = $this->option('model');
$except = $this->option('except');
if ($models && $except) {
throw new InvalidArgumentException('The --models and --except options cannot be combined.');
}
if ($models) {
return (new Collection($models))
->filter(static fn (string $model) => class_exists($model))
->values();
}
return (new Collection(Finder::create()->in($this->getPath())->files()->name('*.php')))
->map(function ($model) {
$namespace = $this->laravel->getNamespace();
return $namespace.str_replace(
['/', '.php'],
['\\', ''],
Str::after($model->getRealPath(), realpath(app_path()).DIRECTORY_SEPARATOR)
);
})
->when(! empty($except), fn ($models) => $models->reject(fn ($model) => in_array($model, $except)))
->filter(fn ($model) => $this->isPrunable($model))
->values();
}
/**
* Get the path where models are located.
*
* @return string[]|string
*/
protected function getPath()
{
if (! empty($path = $this->option('path'))) {
return (new Collection($path))
->map(fn ($path) => base_path($path))
->all();
}
return app_path('Models');
}
/**
* Display how many models will be pruned.
*
* @param class-string $model
* @return void
*/
protected function pretendToPrune($model)
{
$instance = new $model;
$count = $instance->prunable()
->when($model::isSoftDeletable(), function ($query) {
$query->withTrashed();
})->count();
if ($count === 0) {
$this->components->info("No prunable [$model] records found.");
} else {
$this->components->info("{$count} [{$model}] records will be pruned.");
}
}
/**
* Determine if the given model is prunable.
*
* @param string $model
* @return bool
*/
protected function isPrunable(string $model)
{
return class_exists($model)
&& is_a($model, Model::class, true)
&& ! (new \ReflectionClass($model))->isAbstract()
&& $model::isPrunable();
}
}
+141
View File
@@ -0,0 +1,141 @@
<?php
namespace Illuminate\Database\Console\Seeds;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Console\Prohibitable;
use Illuminate\Database\ConnectionResolverInterface as Resolver;
use Illuminate\Database\Eloquent\Model;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'db:seed')]
class SeedCommand extends Command
{
use ConfirmableTrait, Prohibitable;
/**
* 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
*/
public function __construct(Resolver $resolver)
{
parent::__construct();
$this->resolver = $resolver;
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if ($this->isProhibited() ||
! $this->confirmToProceed()) {
return Command::FAILURE;
}
$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
{
//
}
}
+241
View File
@@ -0,0 +1,241 @@
<?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\Collection;
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' => $connection->getDriverTitle(),
'connection' => $connection->getName(),
'version' => $connection->getServerVersion(),
'open_connections' => $connection->threadCount(),
],
'tables' => $this->tables($connection, $schema),
];
if ($this->option('views')) {
$data['views'] = $this->views($connection, $schema);
}
if ($this->option('types')) {
$data['types'] = $this->types($connection, $schema);
}
$this->display($data);
return 0;
}
/**
* Get information regarding the tables within the database.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Database\Schema\Builder $schema
* @return \Illuminate\Support\Collection
*/
protected function tables(ConnectionInterface $connection, Builder $schema)
{
return (new Collection($schema->getTables()))->map(fn ($table) => [
'table' => $table['name'],
'schema' => $table['schema'],
'schema_qualified_name' => $table['schema_qualified_name'],
'size' => $table['size'],
'rows' => $this->option('counts')
? $connection->withoutTablePrefix(fn ($connection) => $connection->table($table['schema_qualified_name'])->count())
: null,
'engine' => $table['engine'],
'collation' => $table['collation'],
'comment' => $table['comment'],
]);
}
/**
* Get information regarding the views within the database.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Database\Schema\Builder $schema
* @return \Illuminate\Support\Collection
*/
protected function views(ConnectionInterface $connection, Builder $schema)
{
return (new Collection($schema->getViews()))
->map(fn ($view) => [
'view' => $view['name'],
'schema' => $view['schema'],
'rows' => $connection->withoutTablePrefix(fn ($connection) => $connection->table($view['schema_qualified_name'])->count()),
]);
}
/**
* Get information regarding the user-defined types within the database.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Database\Schema\Builder $schema
* @return \Illuminate\Support\Collection
*/
protected function types(ConnectionInterface $connection, Builder $schema)
{
return (new Collection($schema->getTypes()))
->map(fn ($type) => [
'name' => $type['name'],
'schema' => $type['schema'],
'type' => $type['type'],
'category' => $type['category'],
]);
}
/**
* Render the database information.
*
* @param array $data
* @return void
*/
protected function display(array $data)
{
$this->option('json') ? $this->displayJson($data) : $this->displayForCli($data);
}
/**
* Render the database information as JSON.
*
* @param array $data
* @return void
*/
protected function displayJson(array $data)
{
$this->output->writeln(json_encode($data));
}
/**
* Render the database information formatted for the CLI.
*
* @param array $data
* @return void
*/
protected function displayForCli(array $data)
{
$platform = $data['platform'];
$tables = $data['tables'];
$views = $data['views'] ?? null;
$types = $data['types'] ?? null;
$this->newLine();
$this->components->twoColumnDetail('<fg=green;options=bold>'.$platform['name'].'</>', $platform['version']);
$this->components->twoColumnDetail('Connection', $platform['connection']);
$this->components->twoColumnDetail('Database', Arr::get($platform['config'], 'database'));
$this->components->twoColumnDetail('Host', Arr::get($platform['config'], 'host'));
$this->components->twoColumnDetail('Port', Arr::get($platform['config'], 'port'));
$this->components->twoColumnDetail('Username', Arr::get($platform['config'], 'username'));
$this->components->twoColumnDetail('URL', Arr::get($platform['config'], 'url'));
$this->components->twoColumnDetail('Open Connections', $platform['open_connections']);
$this->components->twoColumnDetail('Tables', $tables->count());
if ($tableSizeSum = $tables->sum('size')) {
$this->components->twoColumnDetail('Total Size', Number::fileSize($tableSizeSum, 2));
}
$this->newLine();
if ($tables->isNotEmpty()) {
$hasSchema = ! is_null($tables->first()['schema']);
$this->components->twoColumnDetail(
($hasSchema ? '<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) {
$tableSize = is_null($table['size']) ? null : Number::fileSize($table['size'], 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();
}
}
}
+191
View File
@@ -0,0 +1,191 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Console\Concerns\FindsAvailableModels;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Database\Eloquent\ModelInfo;
use Illuminate\Database\Eloquent\ModelInspector;
use Illuminate\Support\Collection;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Output\OutputInterface;
use function Laravel\Prompts\suggest;
#[AsCommand(name: 'model:show')]
class ShowModelCommand extends DatabaseInspectionCommand implements PromptsForMissingInput
{
use FindsAvailableModels;
/**
* 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}';
/**
* Execute the console command.
*
* @return int
*/
public function handle(ModelInspector $modelInspector)
{
try {
$info = $modelInspector->inspect(
$this->argument('model'),
$this->option('database')
);
} catch (BindingResolutionException $e) {
$this->components->error($e->getMessage());
return 1;
}
$this->display($info);
return 0;
}
/**
* Render the model information.
*
* @return void
*/
protected function display(ModelInfo $modelData)
{
$this->option('json')
? $this->displayJson($modelData)
: $this->displayCli($modelData);
}
/**
* Render the model information as JSON.
*
* @return void
*/
protected function displayJson(ModelInfo $modelData)
{
$this->output->writeln(
(new Collection($modelData))->toJson()
);
}
/**
* Render the model information for the CLI.
*
* @return void
*/
protected function displayCli(ModelInfo $modelData)
{
$this->newLine();
$this->components->twoColumnDetail('<fg=green;options=bold>'.$modelData->class.'</>');
$this->components->twoColumnDetail('Database', $modelData->database);
$this->components->twoColumnDetail('Table', $modelData->table);
if ($policy = $modelData->policy ?? false) {
$this->components->twoColumnDetail('Policy', $policy);
}
$this->newLine();
$this->components->twoColumnDetail(
'<fg=green;options=bold>Attributes</>',
'type <fg=gray>/</> <fg=yellow;options=bold>cast</>',
);
foreach ($modelData->attributes as $attribute) {
$first = trim(sprintf(
'%s %s',
$attribute['name'],
(new Collection(['increments', 'unique', 'nullable', 'fillable', 'hidden', 'appended']))
->filter(fn ($property) => $attribute[$property])
->map(fn ($property) => sprintf('<fg=gray>%s</>', $property))
->implode('<fg=gray>,</> ')
));
$second = (new Collection([
$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 ($modelData->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 ($modelData->events->count()) {
foreach ($modelData->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 ($modelData->observers->count()) {
foreach ($modelData->observers as $observer) {
$this->components->twoColumnDetail(
sprintf('%s', $observer['event']),
implode(', ', $observer['observer'])
);
}
}
$this->newLine();
}
/**
* Prompt for missing input arguments using the returned questions.
*
* @return array<string, \Closure(): string>
*/
protected function promptForMissingArgumentsUsing(): array
{
return [
'model' => fn (): string => suggest('Which model would you like to show?', $this->findAvailableModels()),
];
}
}
+283
View File
@@ -0,0 +1,283 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\Schema\Builder;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Number;
use Symfony\Component\Console\Attribute\AsCommand;
use function Laravel\Prompts\search;
#[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'));
$tables = (new Collection($connection->getSchemaBuilder()->getTables()))
->keyBy('schema_qualified_name')->all();
$tableNames = (new Collection($tables))->keys();
$tableName = $this->argument('table') ?: search(
'Which table would you like to inspect?',
fn (string $query) => $tableNames
->filter(fn ($table) => str_contains(strtolower($table), strtolower($query)))
->values()
->all()
);
$table = $tables[$tableName] ?? (new Collection($tables))->when(
Arr::wrap($connection->getSchemaBuilder()->getCurrentSchemaListing()
?? $connection->getSchemaBuilder()->getCurrentSchemaName()),
fn (Collection $collection, array $currentSchemas) => $collection->sortBy(
function (array $table) use ($currentSchemas) {
$index = array_search($table['schema'], $currentSchemas);
return $index === false ? PHP_INT_MAX : $index;
}
)
)->firstWhere('name', $tableName);
if (! $table) {
$this->components->warn("Table [{$tableName}] doesn't exist.");
return 1;
}
[$columns, $indexes, $foreignKeys] = $connection->withoutTablePrefix(function ($connection) use ($table) {
$schema = $connection->getSchemaBuilder();
$tableName = $table['schema_qualified_name'];
return [
$this->columns($schema, $tableName),
$this->indexes($schema, $tableName),
$this->foreignKeys($schema, $tableName),
];
});
$data = [
'table' => [
'schema' => $table['schema'],
'name' => $table['name'],
'schema_qualified_name' => $table['schema_qualified_name'],
'columns' => count($columns),
'size' => $table['size'],
'comment' => $table['comment'],
'collation' => $table['collation'],
'engine' => $table['engine'],
],
'columns' => $columns,
'indexes' => $indexes,
'foreign_keys' => $foreignKeys,
];
$this->display($data);
return 0;
}
/**
* Get the information regarding the table's columns.
*
* @param \Illuminate\Database\Schema\Builder $schema
* @param string $table
* @return \Illuminate\Support\Collection
*/
protected function columns(Builder $schema, string $table)
{
return (new Collection($schema->getColumns($table)))->map(fn ($column) => [
'column' => $column['name'],
'attributes' => $this->getAttributesForColumn($column),
'default' => $column['default'],
'type' => $column['type'],
]);
}
/**
* Get the attributes for a table column.
*
* @param array $column
* @return \Illuminate\Support\Collection
*/
protected function getAttributesForColumn($column)
{
return (new Collection([
$column['type_name'],
$column['generation'] ? $column['generation']['type'] : null,
$column['auto_increment'] ? 'autoincrement' : null,
$column['nullable'] ? 'nullable' : null,
$column['collation'],
]))->filter();
}
/**
* Get the information regarding the table's indexes.
*
* @param \Illuminate\Database\Schema\Builder $schema
* @param string $table
* @return \Illuminate\Support\Collection
*/
protected function indexes(Builder $schema, string $table)
{
return (new Collection($schema->getIndexes($table)))->map(fn ($index) => [
'name' => $index['name'],
'columns' => new Collection($index['columns']),
'attributes' => $this->getAttributesForIndex($index),
]);
}
/**
* Get the attributes for a table index.
*
* @param array $index
* @return \Illuminate\Support\Collection
*/
protected function getAttributesForIndex($index)
{
return (new Collection([
$index['type'],
count($index['columns']) > 1 ? 'compound' : null,
$index['unique'] && ! $index['primary'] ? 'unique' : null,
$index['primary'] ? 'primary' : null,
]))->filter();
}
/**
* Get the information regarding the table's foreign keys.
*
* @param \Illuminate\Database\Schema\Builder $schema
* @param string $table
* @return \Illuminate\Support\Collection
*/
protected function foreignKeys(Builder $schema, string $table)
{
return (new Collection($schema->getForeignKeys($table)))->map(fn ($foreignKey) => [
'name' => $foreignKey['name'],
'columns' => new Collection($foreignKey['columns']),
'foreign_schema' => $foreignKey['foreign_schema'],
'foreign_table' => $foreignKey['foreign_table'],
'foreign_columns' => new Collection($foreignKey['foreign_columns']),
'on_update' => $foreignKey['on_update'],
'on_delete' => $foreignKey['on_delete'],
]);
}
/**
* Render the table information.
*
* @param array $data
* @return void
*/
protected function display(array $data)
{
$this->option('json') ? $this->displayJson($data) : $this->displayForCli($data);
}
/**
* Render the table information as JSON.
*
* @param array $data
* @return void
*/
protected function displayJson(array $data)
{
$this->output->writeln(json_encode($data));
}
/**
* Render the table information formatted for the CLI.
*
* @param array $data
* @return void
*/
protected function displayForCli(array $data)
{
[$table, $columns, $indexes, $foreignKeys] = [
$data['table'], $data['columns'], $data['indexes'], $data['foreign_keys'],
];
$this->newLine();
$this->components->twoColumnDetail('<fg=green;options=bold>'.$table['schema_qualified_name'].'</>', $table['comment'] ? '<fg=gray>'.$table['comment'].'</>' : null);
$this->components->twoColumnDetail('Columns', $table['columns']);
if (! is_null($table['size'])) {
$this->components->twoColumnDetail('Size', Number::fileSize($table['size'], 2));
}
if ($table['engine']) {
$this->components->twoColumnDetail('Engine', $table['engine']);
}
if ($table['collation']) {
$this->components->twoColumnDetail('Collation', $table['collation']);
}
$this->newLine();
if ($columns->isNotEmpty()) {
$this->components->twoColumnDetail('<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();
}
}
}
+129
View File
@@ -0,0 +1,129 @@
<?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.');
}
$this->flushDatabaseConnection($database);
return 0;
}
/**
* Drop all of the database tables.
*
* @param string $database
* @return void
*/
protected function dropAllTables($database)
{
$this->laravel['db']->connection($database)
->getSchemaBuilder()
->dropAllTables();
}
/**
* Drop all of the database views.
*
* @param string $database
* @return void
*/
protected function dropAllViews($database)
{
$this->laravel['db']->connection($database)
->getSchemaBuilder()
->dropAllViews();
}
/**
* Drop all of the database types.
*
* @param string $database
* @return void
*/
protected function dropAllTypes($database)
{
$this->laravel['db']->connection($database)
->getSchemaBuilder()
->dropAllTypes();
}
/**
* Flush the given database connection.
*
* @param string $database
* @return void
*/
protected function flushDatabaseConnection($database)
{
$this->laravel['db']->connection($database)->disconnect();
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'],
['drop-views', null, InputOption::VALUE_NONE, 'Drop all tables and views'],
['drop-types', null, InputOption::VALUE_NONE, 'Drop all tables and types (Postgres only)'],
['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'],
];
}
}
+498
View File
@@ -0,0 +1,498 @@
<?php
namespace Illuminate\Database;
use Illuminate\Database\Connectors\ConnectionFactory;
use Illuminate\Database\Events\ConnectionEstablished;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\ConfigurationUrlParser;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use InvalidArgumentException;
use PDO;
use RuntimeException;
use UnitEnum;
use function Illuminate\Support\enum_value;
/**
* @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 dynamically configured (DB::build) connection configurations.
*
* @var array<string, array>
*/
protected $dynamicConnectionConfigurations = [];
/**
* 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
*/
public function __construct($app, ConnectionFactory $factory)
{
$this->app = $app;
$this->factory = $factory;
$this->reconnector = function ($connection) {
$connection->setPdo(
$this->reconnect($connection->getNameWithReadWriteType())->getRawPdo()
);
};
}
/**
* Get a database connection instance.
*
* @param \UnitEnum|string|null $name
* @return \Illuminate\Database\Connection
*/
public function connection($name = null)
{
[$database, $type] = $this->parseConnectionName($name = enum_value($name) ?: $this->getDefaultConnection());
// 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];
}
/**
* Build a database connection instance from the given configuration.
*
* @param array $config
* @return \Illuminate\Database\ConnectionInterface
*/
public function build(array $config)
{
$config['name'] ??= static::calculateDynamicConnectionName($config);
$this->dynamicConnectionConfigurations[$config['name']] = $config;
return $this->connectUsing($config['name'], $config, true);
}
/**
* Calculate the dynamic connection name for an on-demand connection based on its configuration.
*
* @param array $config
* @return string
*/
public static function calculateDynamicConnectionName(array $config)
{
return 'dynamic_'.md5((new Collection($config))->map(function ($value, $key) {
return $key.(is_string($value) || is_int($value) ? $value : '');
})->implode(''));
}
/**
* Get a database connection instance from the given configuration.
*
* @param \UnitEnum|string $name
* @param array $config
* @param bool $force
* @return \Illuminate\Database\ConnectionInterface
*
* @throws \RuntimeException
*/
public function connectUsing(UnitEnum|string $name, array $config, bool $force = false)
{
$name = enum_value($name);
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)
{
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)
{
$connections = $this->app['config']['database.connections'];
$config = $this->dynamicConnectionConfigurations[$name] ?? Arr::get($connections, $name);
if (is_null($config)) {
throw new InvalidArgumentException("Database connection [{$name}] not configured.");
}
return (new ConfigurationUrlParser)
->parseConfiguration($config);
}
/**
* Prepare the database connection instance.
*
* @param \Illuminate\Database\Connection $connection
* @param string $type
* @return \Illuminate\Database\Connection
*/
protected function configure(Connection $connection, $type)
{
$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 \UnitEnum|string|null $name
* @return void
*/
public function purge($name = null)
{
$this->disconnect($name = enum_value($name) ?: $this->getDefaultConnection());
unset($this->connections[$name]);
}
/**
* Disconnect from the given database.
*
* @param \UnitEnum|string|null $name
* @return void
*/
public function disconnect($name = null)
{
if (isset($this->connections[$name = enum_value($name) ?: $this->getDefaultConnection()])) {
$this->connections[$name]->disconnect();
}
}
/**
* Reconnect to the given database.
*
* @param \UnitEnum|string|null $name
* @return \Illuminate\Database\Connection
*/
public function reconnect($name = null)
{
$this->disconnect($name = enum_value($name) ?: $this->getDefaultConnection());
if (! isset($this->connections[$name])) {
return $this->connection($name);
}
return tap($this->refreshPdoConnections($name), function ($connection) {
$this->dispatchConnectionEstablishedEvent($connection);
});
}
/**
* Set the default database connection for the callback execution.
*
* @param \UnitEnum|string $name
* @param callable $callback
* @return mixed
*/
public function usingConnection($name, callable $callback)
{
$previousName = $this->getDefaultConnection();
$this->setDefaultConnection($name = enum_value($name));
try {
return $callback();
} finally {
$this->setDefaultConnection($previousName);
}
}
/**
* 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);
}
}
+127
View File
@@ -0,0 +1,127 @@
<?php
namespace Illuminate\Database;
use Faker\Factory as FakerFactory;
use Faker\Generator as FakerGenerator;
use Illuminate\Contracts\Database\ConcurrencyErrorDetector as ConcurrencyErrorDetectorContract;
use Illuminate\Contracts\Database\LostConnectionDetector as LostConnectionDetectorContract;
use Illuminate\Contracts\Queue\EntityResolver;
use Illuminate\Database\Connectors\ConnectionFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\QueueEntityResolver;
use Illuminate\Support\ServiceProvider;
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 () {
return new DatabaseTransactionsManager;
});
$this->app->singleton(ConcurrencyErrorDetectorContract::class, function () {
return new ConcurrencyErrorDetector;
});
$this->app->singleton(LostConnectionDetectorContract::class, function () {
return new LostConnectionDetector;
});
}
/**
* Register the Faker Generator instance in the container.
*
* @return void
*/
protected function registerFakerGenerator()
{
if (! class_exists(FakerGenerator::class)) {
return;
}
$this->app->singleton(FakerGenerator::class, function ($app, $parameters) {
$locale = $parameters['locale'] ?? $app['config']->get('app.faker_locale', 'en_US');
if (! isset(static::$fakers[$locale])) {
static::$fakers[$locale] = FakerFactory::create($locale);
}
static::$fakers[$locale]->unique(true);
return static::$fakers[$locale];
});
}
/**
* Register the queueable entity resolver implementation.
*
* @return void
*/
protected function registerQueueableEntityResolver()
{
$this->app->singleton(EntityResolver::class, function () {
return new QueueEntityResolver;
});
}
}
+121
View File
@@ -0,0 +1,121 @@
<?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 = [];
/**
* The callbacks that should be executed after rollback.
*
* @var array
*/
protected $callbacksForRollback = [];
/**
* Create a new database transaction record instance.
*
* @param string $connection
* @param int $level
* @param \Illuminate\Database\DatabaseTransactionRecord|null $parent
*/
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;
}
/**
* Register a callback to be executed after rollback.
*
* @param callable $callback
* @return void
*/
public function addCallbackForRollback($callback)
{
$this->callbacksForRollback[] = $callback;
}
/**
* Execute all of the callbacks.
*
* @return void
*/
public function executeCallbacks()
{
foreach ($this->callbacks as $callback) {
$callback();
}
}
/**
* Execute all of the callbacks for rollback.
*
* @return void
*/
public function executeCallbacksForRollback()
{
foreach ($this->callbacksForRollback as $callback) {
$callback();
}
}
/**
* Get all of the callbacks.
*
* @return array
*/
public function getCallbacks()
{
return $this->callbacks;
}
/**
* Get all of the callbacks for rollback.
*
* @return array
*/
public function getCallbacksForRollback()
{
return $this->callbacksForRollback;
}
}
+267
View File
@@ -0,0 +1,267 @@
<?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.
*/
public function __construct()
{
$this->committedTransactions = new Collection;
$this->pendingTransactions = new Collection;
}
/**
* Start a new database transaction.
*
* @param string $connection
* @param int $level
* @return void
*/
public function begin($connection, $level)
{
$this->pendingTransactions->push(
$newTransaction = new DatabaseTransactionRecord(
$connection,
$level,
$this->currentTransaction[$connection] ?? null
)
);
$this->currentTransaction[$connection] = $newTransaction;
}
/**
* Commit the root database transaction and execute callbacks.
*
* @param string $connection
* @param int $levelBeingCommitted
* @param int $newTransactionLevel
* @return array
*/
public function commit($connection, $levelBeingCommitted, $newTransactionLevel)
{
$this->stageTransactions($connection, $levelBeingCommitted);
if (isset($this->currentTransaction[$connection])) {
$this->currentTransaction[$connection] = $this->currentTransaction[$connection]->parent;
}
if (! $this->afterCommitCallbacksShouldBeExecuted($newTransactionLevel) &&
$newTransactionLevel !== 0) {
return [];
}
// This method is only called when the root database transaction is committed so there
// shouldn't be any pending transactions, but going to clear them here anyways just
// in case. This method could be refactored to receive a level in the future too.
$this->pendingTransactions = $this->pendingTransactions->reject(
fn ($transaction) => $transaction->connection === $connection &&
$transaction->level >= $levelBeingCommitted
)->values();
[$forThisConnection, $forOtherConnections] = $this->committedTransactions->partition(
fn ($transaction) => $transaction->connection == $connection
);
$this->committedTransactions = $forOtherConnections->values();
$forThisConnection->map->executeCallbacks();
return $forThisConnection;
}
/**
* Move relevant pending transactions to a committed state.
*
* @param string $connection
* @param int $levelBeingCommitted
* @return void
*/
public function stageTransactions($connection, $levelBeingCommitted)
{
$this->committedTransactions = $this->committedTransactions->merge(
$this->pendingTransactions->filter(
fn ($transaction) => $transaction->connection === $connection &&
$transaction->level >= $levelBeingCommitted
)
);
$this->pendingTransactions = $this->pendingTransactions->reject(
fn ($transaction) => $transaction->connection === $connection &&
$transaction->level >= $levelBeingCommitted
);
}
/**
* Rollback the active database transaction.
*
* @param string $connection
* @param int $newTransactionLevel
* @return void
*/
public function rollback($connection, $newTransactionLevel)
{
if ($newTransactionLevel === 0) {
$this->removeAllTransactionsForConnection($connection);
} else {
$this->pendingTransactions = $this->pendingTransactions->reject(
fn ($transaction) => $transaction->connection == $connection &&
$transaction->level > $newTransactionLevel
)->values();
if ($this->currentTransaction) {
do {
$this->removeCommittedTransactionsThatAreChildrenOf($this->currentTransaction[$connection]);
$this->currentTransaction[$connection]->executeCallbacksForRollback();
$this->currentTransaction[$connection] = $this->currentTransaction[$connection]->parent;
} while (
isset($this->currentTransaction[$connection]) &&
$this->currentTransaction[$connection]->level > $newTransactionLevel
);
}
}
}
/**
* Remove all pending, completed, and current transactions for the given connection name.
*
* @param string $connection
* @return void
*/
protected function removeAllTransactionsForConnection($connection)
{
if ($this->currentTransaction) {
for ($currentTransaction = $this->currentTransaction[$connection]; isset($currentTransaction); $currentTransaction = $currentTransaction->parent) {
$currentTransaction->executeCallbacksForRollback();
}
}
$this->currentTransaction[$connection] = null;
$this->pendingTransactions = $this->pendingTransactions->reject(
fn ($transaction) => $transaction->connection == $connection
)->values();
$this->committedTransactions = $this->committedTransactions->reject(
fn ($transaction) => $transaction->connection == $connection
)->values();
}
/**
* Remove all transactions that are children of the given transaction.
*
* @param \Illuminate\Database\DatabaseTransactionRecord $transaction
* @return void
*/
protected function removeCommittedTransactionsThatAreChildrenOf(DatabaseTransactionRecord $transaction)
{
[$removedTransactions, $this->committedTransactions] = $this->committedTransactions->partition(
fn ($committed) => $committed->connection == $transaction->connection &&
$committed->parent === $transaction
);
// There may be multiple deeply nested transactions that have already committed that we
// also need to remove. We will recurse down the children of all removed transaction
// instances until there are no more deeply nested child transactions for removal.
$removedTransactions->each(
fn ($transaction) => $this->removeCommittedTransactionsThatAreChildrenOf($transaction)
);
}
/**
* Register a transaction callback.
*
* @param callable $callback
* @return void
*/
public function addCallback($callback)
{
if ($current = $this->callbackApplicableTransactions()->last()) {
return $current->addCallback($callback);
}
$callback();
}
/**
* Register a callback for transaction rollback.
*
* @param callable $callback
* @return void
*/
public function addCallbackForRollback($callback)
{
if ($current = $this->callbackApplicableTransactions()->last()) {
return $current->addCallbackForRollback($callback);
}
}
/**
* Get the transactions that are applicable to callbacks.
*
* @return \Illuminate\Support\Collection<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;
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace Illuminate\Database;
use PDOException;
class DeadlockException extends PDOException
{
//
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace Illuminate\Database;
use Illuminate\Container\Container;
use Illuminate\Contracts\Database\ConcurrencyErrorDetector as ConcurrencyErrorDetectorContract;
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)
{
$container = Container::getInstance();
$detector = $container->bound(ConcurrencyErrorDetectorContract::class)
? $container[ConcurrencyErrorDetectorContract::class]
: new ConcurrencyErrorDetector();
return $detector->causedByConcurrencyError($e);
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace Illuminate\Database;
use Illuminate\Container\Container;
use Illuminate\Contracts\Database\LostConnectionDetector as LostConnectionDetectorContract;
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)
{
$container = Container::getInstance();
$detector = $container->bound(LostConnectionDetectorContract::class)
? $container[LostConnectionDetectorContract::class]
: new LostConnectionDetector();
return $detector->causedByLostConnection($e);
}
}
@@ -0,0 +1,24 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class Appends
{
/**
* @var array<int, string>
*/
public array $columns;
/**
* Create a new attribute instance.
*
* @param array<int, string>|string ...$columns
*/
public function __construct(array|string ...$columns)
{
$this->columns = is_array($columns[0]) ? $columns[0] : $columns;
}
}
+11
View File
@@ -0,0 +1,11 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
class Boot
{
//
}
@@ -0,0 +1,18 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class CollectedBy
{
/**
* Create a new attribute instance.
*
* @param class-string<\Illuminate\Database\Eloquent\Collection<*, *>> $collectionClass
*/
public function __construct(public string $collectionClass)
{
}
}
@@ -0,0 +1,19 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
use UnitEnum;
#[Attribute(Attribute::TARGET_CLASS)]
class Connection
{
/**
* Create a new attribute instance.
*
* @param UnitEnum|string $name
*/
public function __construct(public UnitEnum|string $name)
{
}
}
@@ -0,0 +1,19 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class DateFormat
{
/**
* Create a new attribute instance.
*
* @param string $format
*/
public function __construct(public string $format)
{
//
}
}
@@ -0,0 +1,24 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class Fillable
{
/**
* @var array<int, string>
*/
public array $columns;
/**
* Create a new attribute instance.
*
* @param array<int, string>|string ...$columns
*/
public function __construct(array|string ...$columns)
{
$this->columns = is_array($columns[0]) ? $columns[0] : $columns;
}
}
@@ -0,0 +1,24 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class Guarded
{
/**
* @var array<int, string>
*/
public array $columns;
/**
* Create a new attribute instance.
*
* @param array<int, string>|string ...$columns
*/
public function __construct(array|string ...$columns)
{
$this->columns = is_array($columns[0]) ? $columns[0] : $columns;
}
}
@@ -0,0 +1,24 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class Hidden
{
/**
* @var array<int, string>
*/
public array $columns;
/**
* Create a new attribute instance.
*
* @param array<int, string>|string ...$columns
*/
public function __construct(array|string ...$columns)
{
$this->columns = is_array($columns[0]) ? $columns[0] : $columns;
}
}
@@ -0,0 +1,11 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
class Initialize
{
//
}
@@ -0,0 +1,18 @@
<?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
*/
public function __construct(public array|string $classes)
{
}
}
@@ -0,0 +1,16 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
class Scope
{
/**
* Create a new attribute instance.
*/
public function __construct()
{
}
}
@@ -0,0 +1,18 @@
<?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
*/
public function __construct(public array|string $classes)
{
}
}
@@ -0,0 +1,29 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class Table
{
/**
* Create a new attribute instance.
*
* @param string|null $name
* @param string|null $key
* @param string|null $keyType
* @param bool|null $incrementing
* @param bool|null $timestamps
* @param string|null $dateFormat
*/
public function __construct(
public ?string $name = null,
public ?string $key = null,
public ?string $keyType = null,
public ?bool $incrementing = null,
public ?bool $timestamps = null,
public ?string $dateFormat = null,
) {
}
}
@@ -0,0 +1,24 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class Touches
{
/**
* @var array<int, string>
*/
public array $relations;
/**
* Create a new attribute instance.
*
* @param array<int, string>|string ...$relations
*/
public function __construct(array|string ...$relations)
{
$this->relations = is_array($relations[0]) ? $relations[0] : $relations;
}
}
@@ -0,0 +1,11 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class Unguarded
{
//
}
@@ -0,0 +1,18 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class UseEloquentBuilder
{
/**
* Create a new attribute instance.
*
* @param class-string<\Illuminate\Database\Eloquent\Builder> $builderClass
*/
public function __construct(public string $builderClass)
{
}
}
@@ -0,0 +1,18 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class UseFactory
{
/**
* Create a new attribute instance.
*
* @param class-string<\Illuminate\Database\Eloquent\Factories\Factory> $factoryClass
*/
public function __construct(public string $factoryClass)
{
}
}
@@ -0,0 +1,18 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class UsePolicy
{
/**
* Create a new attribute instance.
*
* @param class-string<*> $class
*/
public function __construct(public string $class)
{
}
}
@@ -0,0 +1,18 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class UseResource
{
/**
* Create a new attribute instance.
*
* @param class-string<*> $class
*/
public function __construct(public string $class)
{
}
}
@@ -0,0 +1,18 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class UseResourceCollection
{
/**
* Create a new attribute instance.
*
* @param class-string<*> $class
*/
public function __construct(public string $class)
{
}
}
@@ -0,0 +1,24 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class Visible
{
/**
* @var array<int, string>
*/
public array $columns;
/**
* Create a new attribute instance.
*
* @param array<int, string>|string ...$columns
*/
public function __construct(array|string ...$columns)
{
$this->columns = is_array($columns[0]) ? $columns[0] : $columns;
}
}
@@ -0,0 +1,17 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class WithoutIncrementing
{
/**
* Create a new attribute instance.
*/
public function __construct()
{
//
}
}
@@ -0,0 +1,17 @@
<?php
namespace Illuminate\Database\Eloquent\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class WithoutTimestamps
{
/**
* Create a new attribute instance.
*/
public function __construct()
{
//
}
}
@@ -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;
use Illuminate\Support\Collection as BaseCollection;
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
*/
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 (new BaseCollection($channels))
->map(fn ($channel) => $channel instanceof Model ? new PrivateChannel($channel) : $channel)
->all();
}
/**
* The name the event should broadcast as.
*
* @return string
*/
public function broadcastAs()
{
$default = class_basename($this->model).ucfirst($this->event);
return method_exists($this->model, 'broadcastAs')
? ($this->model->broadcastAs($this->event) ?: $default)
: $default;
}
/**
* Get the data that should be sent with the broadcasted event.
*
* @return array|null
*/
public function broadcastWith()
{
return method_exists($this->model, 'broadcastWith')
? $this->model->broadcastWith($this->event)
: null;
}
/**
* Manually specify the channels the event should broadcast on.
*
* @param array $channels
* @return $this
*/
public function onChannels(array $channels)
{
$this->channels = $channels;
return $this;
}
/**
* Determine if the event should be broadcast synchronously.
*
* @return bool
*/
public function shouldBroadcastNow()
{
return $this->event === 'deleted' &&
! method_exists($this->model, 'bootSoftDeletes');
}
/**
* Get the event name.
*
* @return string
*/
public function event()
{
return $this->event;
}
}
+197
View File
@@ -0,0 +1,197 @@
<?php
namespace Illuminate\Database\Eloquent;
use Illuminate\Support\Arr;
trait BroadcastsEvents
{
/**
* Boot the event broadcasting trait.
*
* @return void
*/
public static function bootBroadcastsEvents()
{
static::created(function ($model) {
$model->broadcastCreated();
});
static::updated(function ($model) {
$model->broadcastUpdated();
});
if (method_exists(static::class, 'bootSoftDeletes')) {
static::softDeleted(function ($model) {
$model->broadcastTrashed();
});
static::restored(function ($model) {
$model->broadcastRestored();
});
}
static::deleted(function ($model) {
$model->broadcastDeleted();
});
}
/**
* Broadcast that the model was created.
*
* @param \Illuminate\Broadcasting\Channel|\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|array|null $channels
* @return \Illuminate\Broadcasting\PendingBroadcast
*/
public function broadcastCreated($channels = null)
{
return $this->broadcastIfBroadcastChannelsExistForEvent(
$this->newBroadcastableModelEvent('created'), 'created', $channels
);
}
/**
* Broadcast that the model was updated.
*
* @param \Illuminate\Broadcasting\Channel|\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|array|null $channels
* @return \Illuminate\Broadcasting\PendingBroadcast
*/
public function broadcastUpdated($channels = null)
{
return $this->broadcastIfBroadcastChannelsExistForEvent(
$this->newBroadcastableModelEvent('updated'), 'updated', $channels
);
}
/**
* Broadcast that the model was trashed.
*
* @param \Illuminate\Broadcasting\Channel|\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|array|null $channels
* @return \Illuminate\Broadcasting\PendingBroadcast
*/
public function broadcastTrashed($channels = null)
{
return $this->broadcastIfBroadcastChannelsExistForEvent(
$this->newBroadcastableModelEvent('trashed'), 'trashed', $channels
);
}
/**
* Broadcast that the model was restored.
*
* @param \Illuminate\Broadcasting\Channel|\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|array|null $channels
* @return \Illuminate\Broadcasting\PendingBroadcast
*/
public function broadcastRestored($channels = null)
{
return $this->broadcastIfBroadcastChannelsExistForEvent(
$this->newBroadcastableModelEvent('restored'), 'restored', $channels
);
}
/**
* Broadcast that the model was deleted.
*
* @param \Illuminate\Broadcasting\Channel|\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|array|null $channels
* @return \Illuminate\Broadcasting\PendingBroadcast
*/
public function broadcastDeleted($channels = null)
{
return $this->broadcastIfBroadcastChannelsExistForEvent(
$this->newBroadcastableModelEvent('deleted'), 'deleted', $channels
);
}
/**
* Broadcast the given event instance if channels are configured for the model event.
*
* @param mixed $instance
* @param string $event
* @param mixed $channels
* @return \Illuminate\Broadcasting\PendingBroadcast|null
*/
protected function broadcastIfBroadcastChannelsExistForEvent($instance, $event, $channels = null)
{
if (! static::$isBroadcasting) {
return;
}
if (! empty($this->broadcastOn($event)) || ! empty($channels)) {
return broadcast($instance->onChannels(Arr::wrap($channels)));
}
}
/**
* Create a new broadcastable model event event.
*
* @param string $event
* @return mixed
*/
public function newBroadcastableModelEvent($event)
{
return tap($this->newBroadcastableEvent($event), function ($event) {
$event->connection = property_exists($this, 'broadcastConnection')
? $this->broadcastConnection
: $this->broadcastConnection();
$event->queue = property_exists($this, 'broadcastQueue')
? $this->broadcastQueue
: $this->broadcastQueue();
$event->afterCommit = property_exists($this, 'broadcastAfterCommit')
? $this->broadcastAfterCommit
: $this->broadcastAfterCommit();
});
}
/**
* Create a new broadcastable model event for the model.
*
* @param string $event
* @return \Illuminate\Database\Eloquent\BroadcastableModelEventOccurred
*/
protected function newBroadcastableEvent(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;
}
}
+2368
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,47 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use ArrayObject as BaseArrayObject;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Collection;
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 new Collection($this->getArrayCopy());
}
/**
* Get the instance as an array.
*
* @return array
*/
public function toArray()
{
return $this->getArrayCopy();
}
/**
* Get the array that should be JSON serialized.
*
* @return array
*/
public function jsonSerialize(): array
{
return $this->getArrayCopy();
}
}
@@ -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();
}
};
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\BinaryCodec;
use InvalidArgumentException;
class AsBinary implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @param array{string} $arguments
* @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes
*
* @throws \InvalidArgumentException
*/
public static function castUsing(array $arguments)
{
return new class($arguments) implements CastsAttributes
{
protected string $format;
public function __construct(protected array $arguments)
{
$this->format = $this->arguments[0]
?? throw new InvalidArgumentException('The binary codec format is required.');
if (! in_array($this->format, BinaryCodec::formats(), true)) {
throw new InvalidArgumentException(sprintf(
'Unsupported binary codec format [%s]. Allowed formats are: %s.',
$this->format,
implode(', ', BinaryCodec::formats()),
));
}
}
public function get($model, $key, $value, $attributes)
{
return BinaryCodec::decode($attributes[$key] ?? null, $this->format);
}
public function set($model, $key, $value, $attributes)
{
return [$key => BinaryCodec::encode($value, $this->format)];
}
};
}
/**
* Encode / decode values as binary UUIDs.
*/
public static function uuid(): string
{
return self::class.':uuid';
}
/**
* Encode / decode values as binary ULIDs.
*/
public static function ulid(): string
{
return self::class.':ulid';
}
/**
* Encode / decode values using the given format.
*/
public static function of(string $format): string
{
return self::class.':'.$format;
}
}
@@ -0,0 +1,96 @@
<?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\Str;
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>
*
* @throws \InvalidArgumentException
*/
public static function castUsing(array $arguments)
{
return new class($arguments) implements CastsAttributes
{
public function __construct(protected array $arguments)
{
$this->arguments = array_pad(array_values($this->arguments), 2, '');
}
public function get($model, $key, $value, $attributes)
{
if (! isset($attributes[$key])) {
return;
}
$data = Json::decode($attributes[$key]);
$collectionClass = empty($this->arguments[0]) ? Collection::class : $this->arguments[0];
if (! is_a($collectionClass, Collection::class, true)) {
throw new InvalidArgumentException('The provided class must extend ['.Collection::class.'].');
}
if (! is_array($data)) {
return null;
}
$instance = new $collectionClass($data);
if (! isset($this->arguments[1]) || ! $this->arguments[1]) {
return $instance;
}
if (is_string($this->arguments[1])) {
$this->arguments[1] = Str::parseCallback($this->arguments[1]);
}
return is_callable($this->arguments[1])
? $instance->map($this->arguments[1])
: $instance->mapInto($this->arguments[1][0]);
}
public function set($model, $key, $value, $attributes)
{
return [$key => Json::encode($value)];
}
};
}
/**
* Specify the type of object each item in the collection should be mapped to.
*
* @param array{class-string, string}|class-string $map
* @return string
*/
public static function of($map)
{
return static::using('', $map);
}
/**
* Specify the collection type for the cast.
*
* @param class-string $class
* @param array{class-string, string}|class-string|null $map
* @return string
*/
public static function using($class, $map = null)
{
if (is_array($map) && is_callable($map)) {
$map = $map[0].'@'.$map[1];
}
return static::class.':'.implode(',', [$class, $map]);
}
}
@@ -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])), ArrayObject::ARRAY_AS_PROPS);
}
return null;
}
public function set($model, $key, $value, $attributes)
{
if (! is_null($value)) {
return [$key => Crypt::encryptString(Json::encode($value))];
}
return null;
}
public function serialize($model, string $key, $value, array $attributes)
{
return ! is_null($value) ? $value->getArrayCopy() : null;
}
};
}
}
@@ -0,0 +1,95 @@
<?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 Illuminate\Support\Str;
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>
*
* @throws \InvalidArgumentException
*/
public static function castUsing(array $arguments)
{
return new class($arguments) implements CastsAttributes
{
public function __construct(protected array $arguments)
{
$this->arguments = array_pad(array_values($this->arguments), 2, '');
}
public function get($model, $key, $value, $attributes)
{
$collectionClass = empty($this->arguments[0]) ? Collection::class : $this->arguments[0];
if (! is_a($collectionClass, Collection::class, true)) {
throw new InvalidArgumentException('The provided class must extend ['.Collection::class.'].');
}
if (! isset($attributes[$key])) {
return null;
}
$instance = new $collectionClass(Json::decode(Crypt::decryptString($attributes[$key])));
if (! isset($this->arguments[1]) || ! $this->arguments[1]) {
return $instance;
}
if (is_string($this->arguments[1])) {
$this->arguments[1] = Str::parseCallback($this->arguments[1]);
}
return is_callable($this->arguments[1])
? $instance->map($this->arguments[1])
: $instance->mapInto($this->arguments[1][0]);
}
public function set($model, $key, $value, $attributes)
{
if (! is_null($value)) {
return [$key => Crypt::encryptString(Json::encode($value))];
}
return null;
}
};
}
/**
* Specify the type of object each item in the collection should be mapped to.
*
* @param array{class-string, string}|class-string $map
* @return string
*/
public static function of($map)
{
return static::using('', $map);
}
/**
* Specify the collection for the cast.
*
* @param class-string $class
* @param array{class-string, string}|class-string|null $map
* @return string
*/
public static function using($class, $map = null)
{
if (is_array($map) && is_callable($map)) {
$map = $map[0].'@'.$map[1];
}
return static::class.':'.implode(',', [$class, $map]);
}
}
@@ -0,0 +1,97 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use BackedEnum;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Collection;
use function Illuminate\Support\enum_value;
class AsEnumArrayObject implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @template TEnum of \UnitEnum
*
* @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(fn ($enum) => $this->getStorableEnumValue($enum))
->toArray();
}
protected function getStorableEnumValue($enum)
{
if (is_string($enum) || is_int($enum)) {
return $enum;
}
return enum_value($enum);
}
};
}
/**
* Specify the Enum for the cast.
*
* @param class-string $class
* @return string
*/
public static function of($class)
{
return static::class.':'.$class;
}
}
@@ -0,0 +1,93 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use BackedEnum;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Collection;
use function Illuminate\Support\enum_value;
class AsEnumCollection implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @template TEnum of \UnitEnum
*
* @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(fn ($enum) => $this->getStorableEnumValue($enum))
->toArray();
}
protected function getStorableEnumValue($enum)
{
if (is_string($enum) || is_int($enum)) {
return $enum;
}
return enum_value($enum);
}
};
}
/**
* Specify the Enum for the cast.
*
* @param class-string $class
* @return string
*/
public static function of($class)
{
return static::class.':'.$class;
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Fluent;
class AsFluent 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\Fluent, string>
*/
public static function castUsing(array $arguments)
{
return new class implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return isset($value) ? new Fluent(Json::decode($value)) : null;
}
public function set($model, $key, $value, $attributes)
{
return isset($value) ? [$key => Json::encode($value)] : null;
}
};
}
}
@@ -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\HtmlString;
class AsHtmlString 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\HtmlString, string|HtmlString>
*/
public static function castUsing(array $arguments)
{
return new class implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return isset($value) ? new HtmlString($value) : null;
}
public function set($model, $key, $value, $attributes)
{
return isset($value) ? (string) $value : null;
}
};
}
}
@@ -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\Stringable;
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) ? new Stringable($value) : null;
}
public function set($model, $key, $value, $attributes)
{
return isset($value) ? (string) $value : null;
}
};
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Uri;
class AsUri 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\Uri, string|Uri>
*/
public static function castUsing(array $arguments)
{
return new class implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return isset($value) ? new Uri($value) : null;
}
public function set($model, $key, $value, $attributes)
{
return isset($value) ? (string) $value : null;
}
};
}
}
+104
View File
@@ -0,0 +1,104 @@
<?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
*/
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;
}
}
+56
View File
@@ -0,0 +1,56 @@
<?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, int $flags = 0): mixed
{
return isset(static::$encoder)
? (static::$encoder)($value, $flags)
: json_encode($value, $flags);
}
/**
* 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;
}
}
+946
View File
@@ -0,0 +1,946 @@
<?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 ($key is (\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|array<mixed>) ? static : 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);
}
/**
* Find a model in the collection by key or throw an exception.
*
* @param mixed $key
* @return TModel
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function findOrFail($key)
{
$result = $this->find($key);
if (is_array($key) && count($result) === count(array_unique($key))) {
return $result;
} elseif (! is_array($key) && ! is_null($result)) {
return $result;
}
$exception = new ModelNotFoundException;
if (! $model = head($this->items)) {
throw $exception;
}
$ids = is_array($key) ? array_diff($key, $result->modelKeys()) : $key;
$exception->setModel(get_class($model), $ids);
throw $exception;
}
/**
* Load a set of relationships onto the collection.
*
* @param array<array-key, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): 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, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): 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, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): 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, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): 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, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): 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, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): 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, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): 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, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): 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, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): mixed)|string>|string $relations
* @return $this
*/
public function loadMissing($relations)
{
if (is_string($relations)) {
$relations = func_get_args();
}
if ($this->isNotEmpty()) {
$query = $this->first()->newQueryWithoutRelationships()->with($relations);
foreach ($query->getEagerLoads() as $key => $value) {
$segments = explode('.', explode(':', $key)[0]);
if (str_contains($key, ':')) {
$segments[count($segments) - 1] .= ':'.explode(':', $key)[1];
}
$path = [];
foreach ($segments as $segment) {
$path[] = [$segment => $segment];
}
if (is_callable($value)) {
$path[count($segments) - 1][array_last($segments)] = $value;
}
$this->loadMissingRelation($this, $path);
}
}
return $this;
}
/**
* Load a relationship path for models of the given type if it is not already eager loaded.
*
* @param array<int, <string, class-string>> $tuples
* @return void
*/
public function loadMissingRelationshipChain(array $tuples)
{
[$relation, $class] = array_shift($tuples);
$this->filter(function ($model) use ($relation, $class) {
return ! is_null($model) &&
! $model->relationLoaded($relation) &&
$model::class === $class;
})->load($relation);
if (empty($tuples)) {
return;
}
$models = $this->pluck($relation)->whereNotNull();
if ($models->first() instanceof BaseCollection) {
$models = $models->collapse();
}
(new static($models))->loadMissingRelationshipChain($tuples);
}
/**
* Load a relationship path if it is not already eager loaded.
*
* @param \Illuminate\Database\Eloquent\Collection<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)->filter();
if ($models->first() instanceof BaseCollection) {
$models = $models->collapse();
}
$this->loadMissingRelation(new static($models), $path);
}
/**
* Load a set of relationships onto the mixed relationship collection.
*
* @param string $relation
* @param array<array-key, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): 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, array|(callable(\Illuminate\Database\Eloquent\Relations\Relation<*, *, *>): 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);
}
/**
* Determine if a key does not exist in the collection.
*
* @param (callable(TModel, TKey): bool)|TModel|string|int $key
* @param mixed $operator
* @param mixed $value
* @return bool
*/
public function doesntContain($key, $operator = null, $value = null)
{
return ! $this->contains(...func_get_args());
}
/**
* 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) {
$key = $this->getDictionaryKey($item->getKey());
if ($key !== null) {
$dictionary[$key] = $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) {
$key = $this->getDictionaryKey($item->getKey());
if ($key === null || ! isset($dictionary[$key])) {
$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) {
$key = $this->getDictionaryKey($item->getKey());
if ($key !== null && isset($dictionary[$key])) {
$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
*/
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
*/
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
*/
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);
}
/**
* Merge the given, typically visible, attributes hidden across the entire collection.
*
* @param array<array-key, string>|string $attributes
* @return $this
*/
public function mergeHidden($attributes)
{
return $this->each->mergeHidden($attributes);
}
/**
* Set the hidden attributes across the entire collection.
*
* @param array<int, string> $hidden
* @return $this
*/
public function setHidden($hidden)
{
return $this->each->setHidden($hidden);
}
/**
* 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);
}
/**
* Merge the given, typically hidden, attributes visible across the entire collection.
*
* @param array<array-key, string>|string $attributes
* @return $this
*/
public function mergeVisible($attributes)
{
return $this->each->mergeVisible($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);
}
/**
* 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);
}
/**
* Sets the appends on every element of the collection, overwriting the existing appends for each.
*
* @param array<array-key, mixed> $appends
* @return $this
*/
public function setAppends(array $appends)
{
return $this->each->setAppends($appends);
}
/**
* Remove appended properties from every element in the collection.
*
* @return $this
*/
public function withoutAppends()
{
return $this->setAppends([]);
}
/**
* 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) {
$key = $this->getDictionaryKey($value->getKey());
if ($key !== null) {
$dictionary[$key] = $value;
}
}
return $dictionary;
}
/**
* The following methods are intercepted to always return base collections.
*/
/**
* {@inheritDoc}
*
* @return \Illuminate\Support\Collection<array-key, int>
*/
#[\Override]
public function countBy($countBy = null)
{
return $this->toBase()->countBy($countBy);
}
/**
* {@inheritDoc}
*
* @return \Illuminate\Support\Collection<int, mixed>
*/
#[\Override]
public function collapse()
{
return $this->toBase()->collapse();
}
/**
* {@inheritDoc}
*
* @return \Illuminate\Support\Collection<int, mixed>
*/
#[\Override]
public function flatten($depth = INF)
{
return $this->toBase()->flatten($depth);
}
/**
* {@inheritDoc}
*
* @return \Illuminate\Support\Collection<TModel, TKey>
*/
#[\Override]
public function flip()
{
return $this->toBase()->flip();
}
/**
* {@inheritDoc}
*
* @return \Illuminate\Support\Collection<int, TKey>
*/
#[\Override]
public function keys()
{
return $this->toBase()->keys();
}
/**
* {@inheritDoc}
*
* @template TPadValue
*
* @return \Illuminate\Support\Collection<int, TModel|TPadValue>
*/
#[\Override]
public function pad($size, $value)
{
return $this->toBase()->pad($size, $value);
}
/**
* {@inheritDoc}
*
* @return \Illuminate\Support\Collection<int<0, 1>, static<TKey, TModel>>
*/
#[\Override]
public function partition($key, $operator = null, $value = null)
{
return parent::partition(...func_get_args())->toBase();
}
/**
* {@inheritDoc}
*
* @return \Illuminate\Support\Collection<array-key, mixed>
*/
#[\Override]
public function pluck($value, $key = null)
{
return $this->toBase()->pluck($value, $key);
}
/**
* {@inheritDoc}
*
* @template TZipValue
*
* @return \Illuminate\Support\Collection<int, \Illuminate\Support\Collection<int, TModel|TZipValue>>
*/
#[\Override]
public function zip($items)
{
return $this->toBase()->zip(...func_get_args());
}
/**
* Get the comparison function to detect duplicates.
*
* @return callable(TModel, TModel): bool
*/
protected function duplicateComparator($strict)
{
return fn ($a, $b) => $a->is($b);
}
/**
* Enable relationship autoloading for all models in this collection.
*
* @return $this
*/
public function withRelationshipAutoloading()
{
$callback = fn ($tuples) => $this->loadMissingRelationshipChain($tuples);
foreach ($this as $model) {
if (! $model->hasRelationAutoloadCallback()) {
$model->autoloadRelationsUsing($callback, $this);
}
}
return $this;
}
/**
* Get the type of the entities being queued.
*
* @return string|null
*
* @throws \LogicException
*/
public function getQueueableClass()
{
if ($this->isEmpty()) {
return;
}
$class = $this->getQueueableModelClass($this->first());
$this->each(function ($model) use ($class) {
if ($this->getQueueableModelClass($model) !== $class) {
throw new LogicException('Queueing collections with multiple model types is not supported.');
}
});
return $class;
}
/**
* Get the queueable class name for the given model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return string
*/
protected function getQueueableModelClass($model)
{
return method_exists($model, 'getQueueableClassName')
? $model->getQueueableClassName()
: get_class($model);
}
/**
* 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->reject(fn ($model) => $model instanceof $class)->isNotEmpty()) {
throw new LogicException('Unable to create query for collection with mixed types.');
}
return $model->newModelQuery()->whereKey($this->modelKeys());
}
}
@@ -0,0 +1,286 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Initialize;
use Illuminate\Database\Eloquent\Attributes\Unguarded;
trait GuardsAttributes
{
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [];
/**
* The attributes that aren't mass assignable.
*
* @var array<string>
*/
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<class-string,list<string>>
*/
protected static $guardableColumns = [];
/**
* Initialize the GuardsAttributes trait.
*
* @return void
*/
#[Initialize]
public function initializeGuardsAttributes()
{
$this->mergeFillable(static::resolveClassAttribute(Fillable::class, 'columns') ?? []);
if ($this->guarded === ['*']) {
if (static::resolveClassAttribute(Unguarded::class) !== null) {
$this->guarded = [];
} else {
$this->guarded = static::resolveClassAttribute(Guarded::class, 'columns') ?? ['*'];
}
}
}
/**
* Get the fillable attributes for the model.
*
* @return array<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 self::$unguarded === true
? []
: $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.
*
* @template TReturn
*
* @param callable(): TReturn $callback
* @return TReturn
*/
public static function unguarded(callable $callback)
{
if (static::$unguarded) {
return $callback();
}
static::unguard();
try {
return $callback();
} finally {
static::reguard();
}
}
/**
* Determine if the given attribute may be mass assigned.
*
* @param string $key
* @return bool
*/
public function isFillable($key)
{
if (static::$unguarded) {
return true;
}
// If the key is in the "fillable" array, we can of course assume that it's
// a fillable attribute. Otherwise, we will check the guarded array when
// we need to determine if the attribute is black-listed on the model.
if (in_array($key, $this->getFillable())) {
return true;
}
// If the attribute is explicitly listed in the "guarded" array then we can
// return false immediately. This means this attribute is definitely not
// fillable and there is no point in going any further in this method.
if ($this->isGuarded($key)) {
return false;
}
return empty($this->getFillable()) &&
! str_contains($key, '.') &&
! str_starts_with($key, '_');
}
/**
* Determine if the given key is guarded.
*
* @param string $key
* @return bool
*/
public function isGuarded($key)
{
if (empty($this->getGuarded())) {
return false;
}
return $this->getGuarded() == ['*'] ||
! empty(preg_grep('/^'.preg_quote($key, '/').'$/i', $this->getGuarded())) ||
! $this->isGuardableColumn($key);
}
/**
* Determine if the given column is a valid, guardable column.
*
* @param string $key
* @return bool
*/
protected function isGuardableColumn($key)
{
if ($this->hasSetMutator($key) || $this->hasAttributeSetMutator($key) || $this->isClassCastable($key)) {
return true;
}
if (! isset(static::$guardableColumns[get_class($this)])) {
$columns = $this->getConnection()
->getSchemaBuilder()
->getColumnListing($this->getTable());
if (empty($columns)) {
return true;
}
static::$guardableColumns[get_class($this)] = $columns;
}
return in_array($key, static::$guardableColumns[get_class($this)]);
}
/**
* Determine if the model is totally guarded.
*
* @return bool
*/
public function totallyGuarded()
{
return count($this->getFillable()) === 0 && $this->getGuarded() == ['*'];
}
/**
* Get the fillable attributes of a given array.
*
* @param array<string, mixed> $attributes
* @return array<string, mixed>
*/
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,461 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Events\NullDispatcher;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use InvalidArgumentException;
use ReflectionClass;
trait HasEvents
{
/**
* The event map for the model.
*
* Allows for object-based events for native Eloquent events.
*
* @var array<string, class-string>
*/
protected $dispatchesEvents = [];
/**
* User exposed observable events.
*
* These are extra user-defined events observers may subscribe to.
*
* @var string[]
*/
protected $observables = [];
/**
* Boot the has event trait for a model.
*
* @return void
*/
public static function bootHasEvents()
{
static::whenBooted(fn () => static::observe(static::resolveObserveAttributes()));
}
/**
* Resolve the observe class names from the attributes.
*
* @return array
*/
public static function resolveObserveAttributes()
{
$reflectionClass = new ReflectionClass(static::class);
$isEloquentGrandchild = is_subclass_of(static::class, Model::class)
&& get_parent_class(static::class) !== Model::class;
return (new Collection($reflectionClass->getAttributes(ObservedBy::class)))
->map(fn ($attribute) => $attribute->getArguments())
->flatten()
->when($isEloquentGrandchild, function (Collection $attributes) {
return (new Collection(get_parent_class(static::class)::resolveObserveAttributes()))
->merge($attributes);
})
->all();
}
/**
* Register observers with the model.
*
* @param object|string[]|string $classes
* @return void
*
* @throws \RuntimeException
*/
public static function observe($classes)
{
$instance = new static;
foreach (Arr::wrap($classes) as $class) {
$instance->registerObserver($class);
}
}
/**
* Register a single observer with the model.
*
* @param object|string $class
* @return void
*
* @throws \RuntimeException
*/
protected function registerObserver($class)
{
$className = $this->resolveObserverClassName($class);
// When registering a model observer, we will spin through the possible events
// and determine if this observer has that method. If it does, we will hook
// it into the model's event system, making it convenient to watch these.
foreach ($this->getObservableEvents() as $event) {
if (method_exists($class, $event)) {
static::registerModelEvent($event, $className.'@'.$event);
}
}
}
/**
* Resolve the observer's class name from an object or string.
*
* @param object|string $class
* @return class-string
*
* @throws \InvalidArgumentException
*/
private function resolveObserverClassName($class)
{
if (is_object($class)) {
return get_class($class);
}
if (class_exists($class)) {
return $class;
}
throw new InvalidArgumentException('Unable to find observer: '.$class);
}
/**
* Get the observable event names.
*
* @return string[]
*/
public function getObservableEvents()
{
return array_merge(
[
'retrieved', 'creating', 'created', 'updating', 'updated',
'saving', 'saved', 'restoring', 'restored', 'replicating',
'trashed', 'deleting', 'deleted', 'forceDeleting', 'forceDeleted',
],
$this->observables
);
}
/**
* Set the observable event names.
*
* @param string[] $observables
* @return $this
*/
public function setObservableEvents(array $observables)
{
$this->observables = $observables;
return $this;
}
/**
* Add an observable event name.
*
* @param string|string[] $observables
* @return void
*/
public function addObservableEvents($observables)
{
$this->observables = array_unique(array_merge(
$this->observables, is_array($observables) ? $observables : func_get_args()
));
}
/**
* Remove an observable event name.
*
* @param string|string[] $observables
* @return void
*/
public function removeObservableEvents($observables)
{
$this->observables = array_diff(
$this->observables, is_array($observables) ? $observables : func_get_args()
);
}
/**
* Register a model event with the dispatcher.
*
* @param string $event
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
protected static function registerModelEvent($event, $callback)
{
if (isset(static::$dispatcher)) {
$name = static::class;
static::$dispatcher->listen("eloquent.{$event}: {$name}", $callback);
}
}
/**
* Fire the given event for the model.
*
* @param string $event
* @param bool $halt
* @return mixed
*/
protected function fireModelEvent($event, $halt = true)
{
if (! isset(static::$dispatcher)) {
return true;
}
// First, we will get the proper method to call on the event dispatcher, and then we
// will attempt to fire a custom, object based event for the given event. If that
// returns a result we can return that result, or we'll call the string events.
$method = $halt ? 'until' : 'dispatch';
$result = $this->filterModelEventResults(
$this->fireCustomModelEvent($event, $method)
);
if ($result === false) {
return false;
}
return ! empty($result) ? $result : static::$dispatcher->{$method}(
"eloquent.{$event}: ".static::class, $this
);
}
/**
* Fire a custom model event for the given event.
*
* @param string $event
* @param 'until'|'dispatch' $method
* @return array|null|void
*/
protected function fireCustomModelEvent($event, $method)
{
if (! isset($this->dispatchesEvents[$event])) {
return;
}
$result = static::$dispatcher->$method(new $this->dispatchesEvents[$event]($this));
if (! is_null($result)) {
return $result;
}
}
/**
* Filter the model event results.
*
* @param mixed $result
* @return mixed
*/
protected function filterModelEventResults($result)
{
if (is_array($result)) {
$result = array_filter($result, function ($response) {
return ! is_null($response);
});
}
return $result;
}
/**
* Register a retrieved model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
public static function retrieved($callback)
{
static::registerModelEvent('retrieved', $callback);
}
/**
* Register a saving model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
public static function saving($callback)
{
static::registerModelEvent('saving', $callback);
}
/**
* Register a saved model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
public static function saved($callback)
{
static::registerModelEvent('saved', $callback);
}
/**
* Register an updating model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
public static function updating($callback)
{
static::registerModelEvent('updating', $callback);
}
/**
* Register an updated model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
public static function updated($callback)
{
static::registerModelEvent('updated', $callback);
}
/**
* Register a creating model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
public static function creating($callback)
{
static::registerModelEvent('creating', $callback);
}
/**
* Register a created model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
public static function created($callback)
{
static::registerModelEvent('created', $callback);
}
/**
* Register a replicating model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
public static function replicating($callback)
{
static::registerModelEvent('replicating', $callback);
}
/**
* Register a deleting model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
public static function deleting($callback)
{
static::registerModelEvent('deleting', $callback);
}
/**
* Register a deleted model event with the dispatcher.
*
* @param \Illuminate\Events\QueuedClosure|callable|array|class-string $callback
* @return void
*/
public static function deleted($callback)
{
static::registerModelEvent('deleted', $callback);
}
/**
* Remove all the event listeners for the model.
*
* @return void
*/
public static function flushEventListeners()
{
if (! isset(static::$dispatcher)) {
return;
}
$instance = new static;
foreach ($instance->getObservableEvents() as $event) {
static::$dispatcher->forget("eloquent.{$event}: ".static::class);
}
foreach ($instance->dispatchesEvents as $event) {
static::$dispatcher->forget($event);
}
}
/**
* Get the event map for the model.
*
* @return array
*/
public function dispatchesEvents()
{
return $this->dispatchesEvents;
}
/**
* Get the event dispatcher instance.
*
* @return \Illuminate\Contracts\Events\Dispatcher|null
*/
public static function getEventDispatcher()
{
return static::$dispatcher;
}
/**
* Set the event dispatcher instance.
*
* @param \Illuminate\Contracts\Events\Dispatcher $dispatcher
* @return void
*/
public static function setEventDispatcher(Dispatcher $dispatcher)
{
static::$dispatcher = $dispatcher;
}
/**
* Unset the event dispatcher for models.
*
* @return void
*/
public static function unsetEventDispatcher()
{
static::$dispatcher = null;
}
/**
* Execute a callback without firing any model events for any model type.
*
* @param callable $callback
* @return mixed
*/
public static function withoutEvents(callable $callback)
{
$dispatcher = static::getEventDispatcher();
if ($dispatcher) {
static::setEventDispatcher(new NullDispatcher($dispatcher));
}
try {
return $callback();
} finally {
if ($dispatcher) {
static::setEventDispatcher($dispatcher);
}
}
}
}
@@ -0,0 +1,153 @@
<?php
namespace Illuminate\Database\Eloquent\Concerns;
use Closure;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use InvalidArgumentException;
use ReflectionAttribute;
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);
$attributes = (new Collection($reflectionClass->getAttributes(ScopedBy::class, ReflectionAttribute::IS_INSTANCEOF)));
foreach ($reflectionClass->getTraits() as $trait) {
$attributes->push(...$trait->getAttributes(ScopedBy::class, ReflectionAttribute::IS_INSTANCEOF));
}
$isEloquentGrandchild = is_subclass_of(static::class, Model::class)
&& get_parent_class(static::class) !== Model::class;
return $attributes->map(fn ($attribute) => $attribute->getArguments())
->flatten()
->when($isEloquentGrandchild, function (Collection $attributes) {
return (new Collection(get_parent_class(static::class)::resolveGlobalScopeAttributes()))
->merge($attributes);
})
->all();
}
/**
* Register a new global scope on the model.
*
* @param \Illuminate\Database\Eloquent\Scope|(\Closure(\Illuminate\Database\Eloquent\Builder<static>): mixed)|string $scope
* @param \Illuminate\Database\Eloquent\Scope|(\Closure(\Illuminate\Database\Eloquent\Builder<static>): mixed)|null $implementation
* @return mixed
*
* @throws \InvalidArgumentException
*/
public static function addGlobalScope($scope, $implementation = null)
{
if (is_string($scope) && ($implementation instanceof Closure || $implementation instanceof Scope)) {
return static::$globalScopes[static::class][$scope] = $implementation;
} elseif ($scope instanceof Closure) {
return static::$globalScopes[static::class][spl_object_hash($scope)] = $scope;
} elseif ($scope instanceof Scope) {
return static::$globalScopes[static::class][get_class($scope)] = $scope;
} elseif (is_string($scope) && class_exists($scope) && is_subclass_of($scope, Scope::class)) {
return static::$globalScopes[static::class][$scope] = new $scope;
}
throw new InvalidArgumentException('Global scope must be an instance of Closure or Scope or be a class name of a class extending '.Scope::class);
}
/**
* Register multiple global scopes on the model.
*
* @param array $scopes
* @return void
*/
public static function addGlobalScopes(array $scopes)
{
foreach ($scopes as $key => $scope) {
if (is_string($key)) {
static::addGlobalScope($key, $scope);
} else {
static::addGlobalScope($scope);
}
}
}
/**
* Determine if a model has a global scope.
*
* @param \Illuminate\Database\Eloquent\Scope|string $scope
* @return bool
*/
public static function hasGlobalScope($scope)
{
return ! is_null(static::getGlobalScope($scope));
}
/**
* Get a global scope registered with the model.
*
* @param \Illuminate\Database\Eloquent\Scope|string $scope
* @return \Illuminate\Database\Eloquent\Scope|(\Closure(\Illuminate\Database\Eloquent\Builder<static>): mixed)|null
*/
public static function getGlobalScope($scope)
{
if (is_string($scope)) {
return Arr::get(static::$globalScopes, static::class.'.'.$scope);
}
return Arr::get(
static::$globalScopes, static::class.'.'.get_class($scope)
);
}
/**
* Get all of the global scopes that are currently registered.
*
* @return array
*/
public static function getAllGlobalScopes()
{
return static::$globalScopes;
}
/**
* Set the current global scopes.
*
* @param array $scopes
* @return void
*/
public static function setAllGlobalScopes($scopes)
{
static::$globalScopes = $scopes;
}
/**
* Get the global scopes for this class instance.
*
* @return array
*/
public function getGlobalScopes()
{
return Arr::get(static::$globalScopes, static::class, []);
}
}

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