diff --git a/src/Illuminate/Database/DetectsLostConnections.php b/src/Illuminate/Database/DetectsLostConnections.php index 191eefedc891..93be53b2fdc9 100644 --- a/src/Illuminate/Database/DetectsLostConnections.php +++ b/src/Illuminate/Database/DetectsLostConnections.php @@ -52,6 +52,8 @@ protected function causedByLostConnection(Throwable $e) 'Temporary failure in name resolution', 'SSL: Broken pipe', 'SQLSTATE[08S01]: Communication link failure', + 'SQLSTATE[08006] [7] could not connect to server: Connection refused Is the server running on host', + 'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: No route to host', ]); } } diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index ca84d06cdbc1..a8f2fb87842c 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -1608,6 +1608,16 @@ protected static function registerMixin($mixin, $replace) } } + /** + * Clone the Eloquent query builder. + * + * @return static + */ + public function clone() + { + return clone $this; + } + /** * Force a clone of the underlying query builder when cloning. * diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 459b14c73399..7d660905e393 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -1322,6 +1322,16 @@ public function getAttributes() return $this->attributes; } + /** + * Get all of the current attributes on the model for an insert operation. + * + * @return array + */ + protected function getAttributesForInsert() + { + return $this->getAttributes(); + } + /** * Set the array of model attributes. No checking is done. * diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 62fcd058e258..512d59eae10e 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -3,7 +3,6 @@ namespace Illuminate\Database\Eloquent; use ArrayAccess; -use Exception; use Illuminate\Contracts\Queue\QueueableCollection; use Illuminate\Contracts\Queue\QueueableEntity; use Illuminate\Contracts\Routing\UrlRoutable; @@ -20,6 +19,7 @@ use Illuminate\Support\Str; use Illuminate\Support\Traits\ForwardsCalls; use JsonSerializable; +use LogicException; abstract class Model implements Arrayable, ArrayAccess, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable { @@ -1011,7 +1011,7 @@ protected function performInsert(Builder $query) // If the model has an incrementing key, we can use the "insertGetId" method on // the query builder, which will give us back the final inserted ID for this // table from the database. Not all tables have to be incrementing though. - $attributes = $this->getAttributes(); + $attributes = $this->getAttributesForInsert(); if ($this->getIncrementing()) { $this->insertAndSetId($query, $attributes); @@ -1097,14 +1097,14 @@ public static function destroy($ids) * * @return bool|null * - * @throws \Exception + * @throws \LogicException */ public function delete() { $this->mergeAttributesFromClassCasts(); if (is_null($this->getKeyName())) { - throw new Exception('No primary key defined on model.'); + throw new LogicException('No primary key defined on model.'); } // If the model doesn't exist, there is nothing to delete so we'll just return diff --git a/src/Illuminate/Database/Migrations/Migrator.php b/src/Illuminate/Database/Migrations/Migrator.php index 093e41e78a43..35b4bb92e92a 100755 --- a/src/Illuminate/Database/Migrations/Migrator.php +++ b/src/Illuminate/Database/Migrations/Migrator.php @@ -13,6 +13,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; +use ReflectionClass; use Symfony\Component\Console\Output\OutputInterface; class Migrator @@ -185,9 +186,9 @@ protected function runUp($file, $batch, $pretend) // First we will resolve a "real" instance of the migration class from this // migration file name. Once we have the instances we can run the actual // command such as "up" or "down", or we can just simulate the action. - $migration = $this->resolve( - $name = $this->getMigrationName($file) - ); + $migration = $this->resolvePath($file); + + $name = $this->getMigrationName($file); if ($pretend) { return $this->pretendToRun($migration, 'up'); @@ -348,9 +349,9 @@ protected function runDown($file, $migration, $pretend) // First we will get the file name of the migration so we can resolve out an // instance of the migration. Once we get an instance we can either run a // pretend execution of the migration or we can run the real migration. - $instance = $this->resolve( - $name = $this->getMigrationName($file) - ); + $instance = $this->resolvePath($file); + + $name = $this->getMigrationName($file); $this->note("Rolling back: {$name}"); @@ -413,6 +414,12 @@ protected function pretendToRun($migration, $method) foreach ($this->getQueries($migration, $method) as $query) { $name = get_class($migration); + $reflectionClass = new ReflectionClass($migration); + + if ($reflectionClass->isAnonymous()) { + $name = $this->getMigrationName($reflectionClass->getFileName()); + } + $this->note("{$name}: {$query['query']}"); } } @@ -448,11 +455,39 @@ protected function getQueries($migration, $method) */ public function resolve($file) { - $class = Str::studly(implode('_', array_slice(explode('_', $file), 4))); + $class = $this->getMigrationClass($file); return new $class; } + /** + * Resolve a migration instance from a migration path. + * + * @param string $path + * @return object + */ + protected function resolvePath(string $path) + { + $class = $this->getMigrationClass($this->getMigrationName($path)); + + if (class_exists($class)) { + return new $class; + } + + return $this->files->getRequire($path); + } + + /** + * Generate a migration class name based on the migration file name. + * + * @param string $migrationName + * @return string + */ + protected function getMigrationClass(string $migrationName): string + { + return Str::studly(implode('_', array_slice(explode('_', $migrationName), 4))); + } + /** * Get all of the migration files in a given path. * diff --git a/src/Illuminate/Database/Query/Builder.php b/src/Illuminate/Database/Query/Builder.php index 710d0a3a568c..befc8a8cfa33 100755 --- a/src/Illuminate/Database/Query/Builder.php +++ b/src/Illuminate/Database/Query/Builder.php @@ -244,7 +244,7 @@ public function select($columns = ['*']) /** * Add a subselect expression to the query. * - * @param \Closure|\Illuminate\Database\Query\Builder|string $query + * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|string $query * @param string $as * @return $this * diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index 622659995410..e2b968ab6bd4 100755 --- a/src/Illuminate/Database/Schema/Blueprint.php +++ b/src/Illuminate/Database/Schema/Blueprint.php @@ -677,6 +677,17 @@ public function string($column, $length = null) return $this->addColumn('string', $column, compact('length')); } + /** + * Create a new tiny text column on the table. + * + * @param string $column + * @return \Illuminate\Database\Schema\ColumnDefinition + */ + public function tinyText($column) + { + return $this->addColumn('tinyText', $column); + } + /** * Create a new text column on the table. * diff --git a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index dd53b9fe9bed..37df3337b040 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -490,6 +490,17 @@ protected function typeString(Fluent $column) return "varchar({$column->length})"; } + /** + * Create the column definition for a tiny text type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeTinyText(Fluent $column) + { + return 'tinytext'; + } + /** * Create the column definition for a text type. * diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index adaf21f90e4c..fb7005b09df3 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -472,6 +472,17 @@ protected function typeString(Fluent $column) return "varchar({$column->length})"; } + /** + * Create the column definition for a tiny text type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeTinyText(Fluent $column) + { + return 'varchar(255)'; + } + /** * Create the column definition for a text type. * diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index 556d749e23b2..b7e406f578ef 100755 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -432,6 +432,17 @@ protected function typeString(Fluent $column) return 'varchar'; } + /** + * Create the column definition for a tiny text type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeTinyText(Fluent $column) + { + return 'text'; + } + /** * Create the column definition for a text type. * diff --git a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php index c3fc442e2368..b147628ec2f3 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -417,6 +417,17 @@ protected function typeString(Fluent $column) return "nvarchar({$column->length})"; } + /** + * Create the column definition for a tiny text type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeTinyText(Fluent $column) + { + return 'nvarchar(255)'; + } + /** * Create the column definition for a text type. * diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index f7d1716456d0..b9a9eecbbca9 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -33,7 +33,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '8.36.2'; + const VERSION = '8.37.0'; /** * The base path for the Laravel installation. diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithViews.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithViews.php index 574009a68f95..faa6c64a367e 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithViews.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithViews.php @@ -2,6 +2,7 @@ namespace Illuminate\Foundation\Testing\Concerns; +use Closure; use Illuminate\Support\Facades\View as ViewFacade; use Illuminate\Support\MessageBag; use Illuminate\Support\Str; @@ -58,6 +59,10 @@ protected function component(string $componentClass, array $data = []) $view = $component->resolveView(); + if ($view instanceof Closure) { + $view = $view($data); + } + return $view instanceof View ? new TestView($view->with($component->data())) : new TestView(view($view, $component->data())); diff --git a/src/Illuminate/Http/Client/Factory.php b/src/Illuminate/Http/Client/Factory.php index 4db3e1fa98a0..6fc069d390b7 100644 --- a/src/Illuminate/Http/Client/Factory.php +++ b/src/Illuminate/Http/Client/Factory.php @@ -34,6 +34,8 @@ * @method \Illuminate\Http\Client\PendingRequest withoutVerifying() * @method \Illuminate\Http\Client\PendingRequest dump() * @method \Illuminate\Http\Client\PendingRequest dd() + * @method \Illuminate\Http\Client\PendingRequest async() + * @method \Illuminate\Http\Client\Pool pool() * @method \Illuminate\Http\Client\Response delete(string $url, array $data = []) * @method \Illuminate\Http\Client\Response get(string $url, array $query = []) * @method \Illuminate\Http\Client\Response head(string $url, array $query = []) diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 5801bc0b70f0..f0790c5a54a3 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -5,10 +5,13 @@ use GuzzleHttp\Client; use GuzzleHttp\Cookie\CookieJar; use GuzzleHttp\Exception\ConnectException; +use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Exception\TransferException; use GuzzleHttp\HandlerStack; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Illuminate\Support\Traits\Macroable; +use Psr\Http\Message\MessageInterface; use Symfony\Component\VarDumper\VarDumper; class PendingRequest @@ -22,6 +25,13 @@ class PendingRequest */ protected $factory; + /** + * The Guzzle client instance. + * + * @var \GuzzleHttp\Client + */ + protected $client; + /** * The base URL for the request. * @@ -106,6 +116,20 @@ class PendingRequest */ protected $middleware; + /** + * Whether the requests should be asynchronous. + * + * @var bool + */ + protected $async = false; + + /** + * The pending request promise. + * + * @var \GuzzleHttp\Promise\PromiseInterface + */ + protected $promise; + /** * Create a new HTTP Client instance. * @@ -571,6 +595,27 @@ public function delete($url, $data = []) ]); } + /** + * Send a pool of asynchronous requests concurrently. + * + * @param callable $callback + * @return array + */ + public function pool(callable $callback) + { + $results = []; + + $requests = tap(new Pool($this->factory), $callback)->getRequests(); + + foreach ($requests as $key => $item) { + $results[$key] = $item instanceof static ? $item->getPromise()->wait() : $item->wait(); + } + + ksort($results); + + return $results; + } + /** * Send the request to the given URL. * @@ -601,18 +646,14 @@ public function send(string $method, string $url, array $options = []) [$this->pendingBody, $this->pendingFiles] = [null, []]; + if ($this->async) { + return $this->makePromise($method, $url, $options); + } + return retry($this->tries ?? 1, function () use ($method, $url, $options) { try { - $laravelData = $this->parseRequestData($method, $url, $options); - - return tap(new Response($this->buildClient()->request($method, $url, $this->mergeOptions([ - 'laravel_data' => $laravelData, - 'on_stats' => function ($transferStats) { - $this->transferStats = $transferStats; - }, - ], $options))), function ($response) { - $response->cookies = $this->cookies; - $response->transferStats = $this->transferStats; + return tap(new Response($this->sendRequest($method, $url, $options)), function ($response) { + $this->populateResponse($response); if ($this->tries > 1 && ! $response->successful()) { $response->throw(); @@ -637,6 +678,49 @@ protected function parseMultipartBodyFormat(array $data) })->values()->all(); } + /** + * Send an asynchronous request to the given URL. + * + * @param string $method + * @param string $url + * @param array $options + * @return \GuzzleHttp\Promise\PromiseInterface + */ + protected function makePromise(string $method, string $url, array $options = []) + { + return $this->promise = $this->sendRequest($method, $url, $options) + ->then(function (MessageInterface $message) { + return $this->populateResponse(new Response($message)); + }) + ->otherwise(function (TransferException $e) { + return $e instanceof RequestException ? $this->populateResponse(new Response($e->getResponse())) : $e; + }); + } + + /** + * Send a request either synchronously or asynchronously. + * + * @param string $method + * @param string $url + * @param array $options + * @return \Psr\Http\Message\MessageInterface|\GuzzleHttp\Promise\PromiseInterface + * + * @throws \Exception + */ + protected function sendRequest(string $method, string $url, array $options = []) + { + $clientMethod = $this->async ? 'requestAsync' : 'request'; + + $laravelData = $this->parseRequestData($method, $url, $options); + + return $this->buildClient()->$clientMethod($method, $url, $this->mergeOptions([ + 'laravel_data' => $laravelData, + 'on_stats' => function ($transferStats) { + $this->transferStats = $transferStats; + }, + ], $options)); + } + /** * Get the request data as an array so that we can attach it to the request for convenient assertions. * @@ -664,6 +748,21 @@ protected function parseRequestData($method, $url, array $options) return $laravelData; } + /** + * Populate the given response with additional data. + * + * @param \Illuminate\Http\Client\Response $response + * @return \Illuminate\Http\Client\Response + */ + protected function populateResponse(Response $response) + { + $response->cookies = $this->cookies; + + $response->transferStats = $this->transferStats; + + return $response; + } + /** * Build the Guzzle client. * @@ -671,7 +770,7 @@ protected function parseRequestData($method, $url, array $options) */ public function buildClient() { - return new Client([ + return $this->client = $this->client ?: new Client([ 'handler' => $this->buildHandlerStack(), 'cookies' => true, ]); @@ -826,4 +925,40 @@ public function stub($callback) return $this; } + + /** + * Toggle asynchronicity in requests. + * + * @param bool $async + * @return $this + */ + public function async(bool $async = true) + { + $this->async = $async; + + return $this; + } + + /** + * Retrieve the pending request promise. + * + * @return \GuzzleHttp\Promise\PromiseInterface|null + */ + public function getPromise() + { + return $this->promise; + } + + /** + * Set the client instance. + * + * @param \GuzzleHttp\Client $client + * @return $this + */ + public function setClient(Client $client) + { + $this->client = $client; + + return $this; + } } diff --git a/src/Illuminate/Http/Client/Pool.php b/src/Illuminate/Http/Client/Pool.php new file mode 100644 index 000000000000..15002b28e92e --- /dev/null +++ b/src/Illuminate/Http/Client/Pool.php @@ -0,0 +1,83 @@ +factory = $factory ?: new Factory(); + + $this->client = $this->factory->buildClient(); + } + + /** + * Add a request to the pool with a key. + * + * @param string $key + * @return \Illuminate\Http\Client\PendingRequest + */ + public function as(string $key) + { + return $this->pool[$key] = $this->asyncRequest(); + } + + /** + * Retrieve a new async pending request. + * + * @return \Illuminate\Http\Client\PendingRequest + */ + protected function asyncRequest() + { + return $this->factory->setClient($this->client)->async(); + } + + /** + * Retrieve the requests in the pool. + * + * @return array + */ + public function getRequests() + { + return $this->pool; + } + + /** + * Add a request to the pool with a numeric index. + * + * @param string $method + * @param array $parameters + * @return \Illuminate\Http\Client\PendingRequest + */ + public function __call($method, $parameters) + { + return $this->pool[] = $this->asyncRequest()->$method(...$parameters); + } +} diff --git a/src/Illuminate/Pagination/LengthAwarePaginator.php b/src/Illuminate/Pagination/LengthAwarePaginator.php index 3e5adad0a316..d1c6cc711fb5 100644 --- a/src/Illuminate/Pagination/LengthAwarePaginator.php +++ b/src/Illuminate/Pagination/LengthAwarePaginator.php @@ -99,7 +99,7 @@ public function render($view = null, $data = []) * * @return \Illuminate\Support\Collection */ - protected function linkCollection() + public function linkCollection() { return collect($this->elements())->flatMap(function ($item) { if (! is_array($item)) { diff --git a/src/Illuminate/Queue/Console/RetryCommand.php b/src/Illuminate/Queue/Console/RetryCommand.php index 2f651b60d098..212883fecdcb 100644 --- a/src/Illuminate/Queue/Console/RetryCommand.php +++ b/src/Illuminate/Queue/Console/RetryCommand.php @@ -18,6 +18,7 @@ class RetryCommand extends Command */ protected $signature = 'queue:retry {id?* : The ID of the failed job or "all" to retry all jobs} + {--queue= : Retry all of the failed jobs for the specified queue} {--range=* : Range of job IDs (numeric) to be retried}'; /** @@ -62,6 +63,10 @@ protected function getJobIds() return Arr::pluck($this->laravel['queue.failer']->all(), 'id'); } + if ($queue = $this->option('queue')) { + return $this->getJobIdsByQueue($queue); + } + if ($ranges = (array) $this->option('range')) { $ids = array_merge($ids, $this->getJobIdsByRanges($ranges)); } @@ -69,6 +74,26 @@ protected function getJobIds() return array_values(array_filter(array_unique($ids))); } + /** + * Get the job IDs by queue, if applicable. + * + * @param string $queue + * @return array + */ + protected function getJobIdsByQueue($queue) + { + $ids = collect($this->laravel['queue.failer']->all()) + ->where('queue', $queue) + ->pluck('id') + ->toArray(); + + if (count($ids) === 0) { + $this->error("Unable to find failed jobs for queue [{$queue}]."); + } + + return $ids; + } + /** * Get the job IDs ranges, if applicable. * diff --git a/src/Illuminate/Session/Store.php b/src/Illuminate/Session/Store.php index ff2a3b966a20..151e8b6330b5 100755 --- a/src/Illuminate/Session/Store.php +++ b/src/Illuminate/Session/Store.php @@ -193,6 +193,17 @@ public function exists($key) }); } + /** + * Determine if the given key is missing from the session data. + * + * @param string|array $key + * @return bool + */ + public function missing($key) + { + return ! $this->exists($key); + } + /** * Checks if a key is present and not null. * diff --git a/src/Illuminate/Support/Facades/Http.php b/src/Illuminate/Support/Facades/Http.php index 426d574789c5..51763b6caef0 100644 --- a/src/Illuminate/Support/Facades/Http.php +++ b/src/Illuminate/Support/Facades/Http.php @@ -32,6 +32,8 @@ * @method static \Illuminate\Http\Client\PendingRequest withoutVerifying() * @method static \Illuminate\Http\Client\PendingRequest dump() * @method static \Illuminate\Http\Client\PendingRequest dd() + * @method static \Illuminate\Http\Client\PendingRequest async() + * @method static \Illuminate\Http\Client\Pool pool() * @method static \Illuminate\Http\Client\Response delete(string $url, array $data = []) * @method static \Illuminate\Http\Client\Response get(string $url, array $query = []) * @method static \Illuminate\Http\Client\Response head(string $url, array $query = []) diff --git a/src/Illuminate/View/Engines/EngineResolver.php b/src/Illuminate/View/Engines/EngineResolver.php index d0edb7367df6..6a5b80026342 100755 --- a/src/Illuminate/View/Engines/EngineResolver.php +++ b/src/Illuminate/View/Engines/EngineResolver.php @@ -57,4 +57,15 @@ public function resolve($engine) throw new InvalidArgumentException("Engine [{$engine}] not found."); } + + /** + * Remove a resolved engine. + * + * @param string $engine + * @return void + */ + public function forget($engine) + { + unset($this->resolved[$engine]); + } } diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 0809f8e0fe98..682545ff9b98 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -1497,6 +1497,18 @@ public function testWithCastsMethod() $builder->withCasts(['foo' => 'bar']); } + public function testClone() + { + $query = new BaseBuilder(m::mock(ConnectionInterface::class), new Grammar, m::mock(Processor::class)); + $builder = new Builder($query); + $builder->select('*')->from('users'); + $clone = $builder->clone()->where('email', 'foo'); + + $this->assertNotSame($builder, $clone); + $this->assertSame('select * from "users"', $builder->toSql()); + $this->assertSame('select * from "users" where "email" = ?', $clone->toSql()); + } + protected function mockConnectionForModel($model, $database) { $grammarClass = 'Illuminate\Database\Query\Grammars\\'.$database.'Grammar'; diff --git a/tests/Database/DatabaseSchemaBlueprintTest.php b/tests/Database/DatabaseSchemaBlueprintTest.php index 94303415e8f4..5247df35c3ba 100755 --- a/tests/Database/DatabaseSchemaBlueprintTest.php +++ b/tests/Database/DatabaseSchemaBlueprintTest.php @@ -291,4 +291,62 @@ public function testGenerateRelationshipColumnWithUuidModel() 'alter table `posts` add `eloquent_model_uuid_stub_id` char(36) not null', ], $blueprint->toSql($connection, new MySqlGrammar)); } + + public function testTinyTextColumn() + { + $base = new Blueprint('posts', function ($table) { + $table->tinyText('note'); + }); + + $connection = m::mock(Connection::class); + + $blueprint = clone $base; + $this->assertEquals([ + 'alter table `posts` add `note` tinytext not null', + ], $blueprint->toSql($connection, new MySqlGrammar)); + + $blueprint = clone $base; + $this->assertEquals([ + 'alter table "posts" add column "note" text not null', + ], $blueprint->toSql($connection, new SQLiteGrammar)); + + $blueprint = clone $base; + $this->assertEquals([ + 'alter table "posts" add column "note" varchar(255) not null', + ], $blueprint->toSql($connection, new PostgresGrammar)); + + $blueprint = clone $base; + $this->assertEquals([ + 'alter table "posts" add "note" nvarchar(255) not null', + ], $blueprint->toSql($connection, new SqlServerGrammar)); + } + + public function testTinyTextNullableColumn() + { + $base = new Blueprint('posts', function ($table) { + $table->tinyText('note')->nullable(); + }); + + $connection = m::mock(Connection::class); + + $blueprint = clone $base; + $this->assertEquals([ + 'alter table `posts` add `note` tinytext null', + ], $blueprint->toSql($connection, new MySqlGrammar)); + + $blueprint = clone $base; + $this->assertEquals([ + 'alter table "posts" add column "note" text', + ], $blueprint->toSql($connection, new SQLiteGrammar)); + + $blueprint = clone $base; + $this->assertEquals([ + 'alter table "posts" add column "note" varchar(255) null', + ], $blueprint->toSql($connection, new PostgresGrammar)); + + $blueprint = clone $base; + $this->assertEquals([ + 'alter table "posts" add "note" nvarchar(255) null', + ], $blueprint->toSql($connection, new SqlServerGrammar)); + } } diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index 12d7a1806c21..84709fa3ba66 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -2,8 +2,11 @@ namespace Illuminate\Tests\Http; +use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Psr7\Response as Psr7Response; use Illuminate\Http\Client\Factory; +use Illuminate\Http\Client\PendingRequest; +use Illuminate\Http\Client\Pool; use Illuminate\Http\Client\Request; use Illuminate\Http\Client\RequestException; use Illuminate\Http\Client\Response; @@ -833,4 +836,70 @@ public function testResponseSequenceIsMacroable() $this->assertSame('yes!', $this->factory->fakeSequence()->customMethod()); } + + public function testRequestsCanBeAsync() + { + $request = new PendingRequest($this->factory); + + $promise = $request->async()->get('http://foo.com'); + + $this->assertInstanceOf(PromiseInterface::class, $promise); + + $this->assertSame($promise, $request->getPromise()); + } + + public function testClientCanBeSet() + { + $client = $this->factory->buildClient(); + + $request = new PendingRequest($this->factory); + + $this->assertNotSame($client, $request->buildClient()); + + $request->setClient($client); + + $this->assertSame($client, $request->buildClient()); + } + + public function testMultipleRequestsAreSentInThePool() + { + $this->factory->fake([ + '200.com' => $this->factory::response('', 200), + '400.com' => $this->factory::response('', 400), + '500.com' => $this->factory::response('', 500), + ]); + + $responses = $this->factory->pool(function (Pool $pool) { + return [ + $pool->get('200.com'), + $pool->get('400.com'), + $pool->get('500.com'), + ]; + }); + + $this->assertSame(200, $responses[0]->status()); + $this->assertSame(400, $responses[1]->status()); + $this->assertSame(500, $responses[2]->status()); + } + + public function testMultipleRequestsAreSentInThePoolWithKeys() + { + $this->factory->fake([ + '200.com' => $this->factory::response('', 200), + '400.com' => $this->factory::response('', 400), + '500.com' => $this->factory::response('', 500), + ]); + + $responses = $this->factory->pool(function (Pool $pool) { + return [ + $pool->as('test200')->get('200.com'), + $pool->as('test400')->get('400.com'), + $pool->as('test500')->get('500.com'), + ]; + }); + + $this->assertSame(200, $responses['test200']->status()); + $this->assertSame(400, $responses['test400']->status()); + $this->assertSame(500, $responses['test500']->status()); + } } diff --git a/tests/Integration/Migration/MigratorTest.php b/tests/Integration/Migration/MigratorTest.php index 50ee6f3cbd6b..08dd9c97862f 100644 --- a/tests/Integration/Migration/MigratorTest.php +++ b/tests/Integration/Migration/MigratorTest.php @@ -2,11 +2,29 @@ namespace Illuminate\Tests\Integration\Migration; +use Illuminate\Support\Facades\DB; +use Mockery; +use Mockery\Mock; use Orchestra\Testbench\TestCase; -use PDOException; +use Symfony\Component\Console\Output\OutputInterface; class MigratorTest extends TestCase { + /** + * @var Mock + */ + private $output; + + protected function setUp(): void + { + parent::setUp(); + + $this->output = Mockery::mock(OutputInterface::class); + $this->subject = $this->app->make('migrator'); + $this->subject->setOutput($this->output); + $this->subject->getRepository()->createRepository(); + } + protected function getEnvironmentSetUp($app) { $app['config']->set('app.debug', 'true'); @@ -19,25 +37,55 @@ protected function getEnvironmentSetUp($app) ]); } - public function testDontDisplayOutputWhenOutputObjectIsNotAvailable() + public function testMigrate() + { + $this->expectOutput('Migrating: 2014_10_12_000000_create_people_table'); + $this->expectOutput(Mockery::pattern('#Migrated: 2014_10_12_000000_create_people_table (.*)#')); + $this->expectOutput('Migrating: 2015_10_04_000000_modify_people_table'); + $this->expectOutput(Mockery::pattern('#Migrated: 2015_10_04_000000_modify_people_table (.*)#')); + $this->expectOutput('Migrating: 2016_10_04_000000_modify_people_table'); + $this->expectOutput(Mockery::pattern('#Migrated: 2016_10_04_000000_modify_people_table (.*)#')); + + $this->subject->run([__DIR__.'/fixtures']); + + self::assertTrue(DB::getSchemaBuilder()->hasTable('people')); + self::assertTrue(DB::getSchemaBuilder()->hasColumn('people', 'first_name')); + self::assertTrue(DB::getSchemaBuilder()->hasColumn('people', 'last_name')); + } + + public function testRollback() { - $migrator = $this->app->make('migrator'); + $this->getConnection()->statement('CREATE TABLE people(id INT, first_name VARCHAR, last_name VARCHAR);'); + $this->subject->getRepository()->log('2014_10_12_000000_create_people_table', 1); + $this->subject->getRepository()->log('2015_10_04_000000_modify_people_table', 1); + $this->subject->getRepository()->log('2016_10_04_000000_modify_people_table', 1); - $migrator->getRepository()->createRepository(); + $this->expectOutput('Rolling back: 2016_10_04_000000_modify_people_table'); + $this->expectOutput(Mockery::pattern('#Rolled back: 2016_10_04_000000_modify_people_table (.*)#')); + $this->expectOutput('Rolling back: 2015_10_04_000000_modify_people_table'); + $this->expectOutput(Mockery::pattern('#Rolled back: 2015_10_04_000000_modify_people_table (.*)#')); + $this->expectOutput('Rolling back: 2014_10_12_000000_create_people_table'); + $this->expectOutput(Mockery::pattern('#Rolled back: 2014_10_12_000000_create_people_table (.*)#')); - $migrator->run([__DIR__.'/fixtures']); + $this->subject->rollback([__DIR__.'/fixtures']); - $this->assertTrue($this->tableExists('people')); + self::assertFalse(DB::getSchemaBuilder()->hasTable('people')); } - private function tableExists($table): bool + public function testPretendMigrate() { - try { - $this->app->make('db')->select("SELECT COUNT(*) FROM $table"); - } catch (PDOException $e) { - return false; - } + $this->expectOutput('CreatePeopleTable: create table "people" ("id" integer not null primary key autoincrement, "name" varchar not null, "email" varchar not null, "password" varchar not null, "remember_token" varchar, "created_at" datetime, "updated_at" datetime)'); + $this->expectOutput('CreatePeopleTable: create unique index "people_email_unique" on "people" ("email")'); + $this->expectOutput('2015_10_04_000000_modify_people_table: alter table "people" add column "first_name" varchar'); + $this->expectOutput('2016_10_04_000000_modify_people_table: alter table "people" add column "last_name" varchar'); + + $this->subject->run([__DIR__.'/fixtures'], ['pretend' => true]); - return true; + self::assertFalse(DB::getSchemaBuilder()->hasTable('people')); + } + + private function expectOutput($argument): void + { + $this->output->shouldReceive('writeln')->once()->with($argument); } } diff --git a/tests/Integration/Migration/fixtures/2015_10_04_000000_modify_people_table.php b/tests/Integration/Migration/fixtures/2015_10_04_000000_modify_people_table.php new file mode 100644 index 000000000000..88ac706cd12a --- /dev/null +++ b/tests/Integration/Migration/fixtures/2015_10_04_000000_modify_people_table.php @@ -0,0 +1,31 @@ +string('first_name')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('people', function (Blueprint $table) { + $table->dropColumn('first_name'); + }); + } +}; diff --git a/tests/Integration/Migration/fixtures/2016_10_04_000000_modify_people_table.php b/tests/Integration/Migration/fixtures/2016_10_04_000000_modify_people_table.php new file mode 100644 index 000000000000..6492b6d7f55a --- /dev/null +++ b/tests/Integration/Migration/fixtures/2016_10_04_000000_modify_people_table.php @@ -0,0 +1,31 @@ +string('last_name')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('people', function (Blueprint $table) { + $table->dropColumn('last_name'); + }); + } +}; diff --git a/tests/Session/SessionStoreTest.php b/tests/Session/SessionStoreTest.php index 5871cc657a85..188bdce4d89f 100644 --- a/tests/Session/SessionStoreTest.php +++ b/tests/Session/SessionStoreTest.php @@ -452,6 +452,22 @@ public function testKeyExists() $this->assertFalse($session->exists(['hulk.two'])); } + public function testKeyMissing() + { + $session = $this->getSession(); + $session->put('foo', 'bar'); + $this->assertFalse($session->missing('foo')); + $session->put('baz', null); + $session->put('hulk', ['one' => true]); + $this->assertFalse($session->has('baz')); + $this->assertFalse($session->missing('baz')); + $this->assertTrue($session->missing('bogus')); + $this->assertFalse($session->missing(['foo', 'baz'])); + $this->assertTrue($session->missing(['foo', 'baz', 'bogus'])); + $this->assertFalse($session->missing(['hulk.one'])); + $this->assertTrue($session->missing(['hulk.two'])); + } + public function testRememberMethodCallsPutAndReturnsDefault() { $session = $this->getSession();