diff --git a/CHANGELOG.md b/CHANGELOG.md index 432ee0620dff..584df42a691b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ # Release Notes for 11.x -## [Unreleased](https://github.com/laravel/framework/compare/v11.40.0...11.x) +## [Unreleased](https://github.com/laravel/framework/compare/v11.41.0...11.x) + +## [v11.41.0](https://github.com/laravel/framework/compare/v11.40.0...v11.41.0) - 2025-01-28 + +* [11.x] more pint rules by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/54332 +* [11.x] Allow `TestComponent` to be macroable by [@ziadoz](https://github.com/ziadoz) in https://github.com/laravel/framework/pull/54359 +* [11.x] Fix validator return fails if using different date formats by [@mrvipchien](https://github.com/mrvipchien) in https://github.com/laravel/framework/pull/54350 +* [11.x] fix the method `explodeExplicitRule` to support Customizable Date Validation by [@mrvipchien](https://github.com/mrvipchien) in https://github.com/laravel/framework/pull/54353 +* [11.x] Adds the `addPath()` method to the `Lang` facade and the `Translator` class. by [@selcukcukur](https://github.com/selcukcukur) in https://github.com/laravel/framework/pull/54347 +* Improve: add fire failed event at once by [@cesarMtorres](https://github.com/cesarMtorres) in https://github.com/laravel/framework/pull/54376 +* [11.x] feat: Create missing pgsql database when running migrations by [@mathiasgrimm](https://github.com/mathiasgrimm) in https://github.com/laravel/framework/pull/54314 +* [11.x] Proper rate limiter fix with phpredis serialization/compression enabled by [@TheLevti](https://github.com/TheLevti) in https://github.com/laravel/framework/pull/54372 +* Update Stringable Rule testcases by [@mrvipchien](https://github.com/mrvipchien) in https://github.com/laravel/framework/pull/54387 +* [11.x] Use `Date` facade for storing the password confirmation timestamp by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54383 ## [v11.40.0](https://github.com/laravel/framework/compare/v11.39.1...v11.40.0) - 2025-01-24 diff --git a/src/Illuminate/Container/Attributes/Authenticated.php b/src/Illuminate/Container/Attributes/Authenticated.php index 67cdd53cc3cf..ffbba4553719 100644 --- a/src/Illuminate/Container/Attributes/Authenticated.php +++ b/src/Illuminate/Container/Attributes/Authenticated.php @@ -25,6 +25,6 @@ public function __construct(public ?string $guard = null) */ public static function resolve(self $attribute, Container $container) { - return $container->make('auth')->guard($attribute->guard)->user(); + return call_user_func($container->make('auth')->userResolver(), $attribute->guard); } } diff --git a/src/Illuminate/Database/Console/DbCommand.php b/src/Illuminate/Database/Console/DbCommand.php index b7561e08277f..36b6db3028fc 100644 --- a/src/Illuminate/Database/Console/DbCommand.php +++ b/src/Illuminate/Database/Console/DbCommand.php @@ -5,6 +5,7 @@ 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; @@ -44,13 +45,21 @@ public function handle() return Command::FAILURE; } - (new Process( - array_merge([$this->getCommand($connection)], $this->commandArguments($connection)), - null, - $this->commandEnvironment($connection) - ))->setTimeout(null)->setTty(true)->mustRun(function ($type, $buffer) { - $this->output->write($buffer); - }); + 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; } diff --git a/src/Illuminate/Database/Migrations/Migrator.php b/src/Illuminate/Database/Migrations/Migrator.php index e3f9d14cb7ef..d9e09bf56e2a 100755 --- a/src/Illuminate/Database/Migrations/Migrator.php +++ b/src/Illuminate/Database/Migrations/Migrator.php @@ -2,6 +2,7 @@ namespace Illuminate\Database\Migrations; +use Closure; use Illuminate\Console\View\Components\BulletList; use Illuminate\Console\View\Components\Info; use Illuminate\Console\View\Components\Task; @@ -50,6 +51,13 @@ class Migrator */ protected $resolver; + /** + * The custom connection resolver callback. + * + * @var \Closure|null + */ + protected static $connectionResolverCallback; + /** * The name of the default connection. * @@ -641,7 +649,26 @@ public function setConnection($name) */ public function resolveConnection($connection) { - return $this->resolver->connection($connection ?: $this->connection); + if (static::$connectionResolverCallback) { + return call_user_func( + static::$connectionResolverCallback, + $this->resolver, + $connection ?: $this->connection + ); + } else { + return $this->resolver->connection($connection ?: $this->connection); + } + } + + /** + * Set a connection resolver callback. + * + * @param \Closure $callback + * @return void + */ + public static function resolveConnectionsUsing(Closure $callback) + { + static::$connectionResolverCallback = $callback; } /** diff --git a/src/Illuminate/Database/Query/Builder.php b/src/Illuminate/Database/Query/Builder.php index dd762d1dda88..105cf44807fc 100755 --- a/src/Illuminate/Database/Query/Builder.php +++ b/src/Illuminate/Database/Query/Builder.php @@ -3384,7 +3384,7 @@ public function pluck($column, $key = null) // given columns / key. Once we have the results, we will be able to take // the results and get the exact data that was requested for the query. $queryResult = $this->onceWithColumns( - is_null($key) ? [$column] : [$column, $key], + is_null($key) || $key === $column ? [$column] : [$column, $key], function () { return $this->processor->processSelect( $this, $this->runSelect() diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index bbb5165a119e..9429ef722bfa 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -45,7 +45,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '11.41.0'; + const VERSION = '11.41.1'; /** * The base path for the Laravel installation. @@ -223,6 +223,7 @@ public function __construct($basePath = null) $this->registerBaseBindings(); $this->registerBaseServiceProviders(); $this->registerCoreContainerAliases(); + $this->registerLaravelCloudServices(); } /** @@ -303,6 +304,28 @@ protected function registerBaseServiceProviders() $this->register(new RoutingServiceProvider($this)); } + /** + * Register any services needed for Laravel Cloud. + * + * @return void + */ + protected function registerLaravelCloudServices() + { + if (! laravel_cloud()) { + return; + } + + $this['events']->listen( + 'bootstrapping: *', + fn ($bootstrapper) => Cloud::bootstrapperBootstrapping($this, Str::after($bootstrapper, 'bootstrapping: ')) + ); + + $this['events']->listen( + 'bootstrapped: *', + fn ($bootstrapper) => Cloud::bootstrapperBootstrapped($this, Str::after($bootstrapper, 'bootstrapped: ')) + ); + } + /** * Run the given array of bootstrap classes. * diff --git a/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php b/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php index 8a887b194acf..90bc446dca4e 100644 --- a/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php +++ b/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php @@ -8,9 +8,7 @@ use Illuminate\Contracts\Foundation\Application; use Illuminate\Log\LogManager; use Illuminate\Support\Env; -use Monolog\Formatter\JsonFormatter; use Monolog\Handler\NullHandler; -use Monolog\Handler\SocketHandler; use PHPUnit\Runner\ErrorHandler; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\ErrorHandler\Error\FatalError; @@ -55,10 +53,6 @@ public function bootstrap(Application $app) if (! $app->environment('testing')) { ini_set('display_errors', 'Off'); } - - if (laravel_cloud()) { - $this->configureCloudLogging($app); - } } /** @@ -251,34 +245,6 @@ protected function fatalErrorFromPhpError(array $error, $traceOffset = null) return new FatalError($error['message'], 0, $error, $traceOffset); } - /** - * Configure the Laravel Cloud log channels. - * - * @param \Illuminate\Contracts\Foundation\Application $app - * @return void - */ - protected function configureCloudLogging(Application $app) - { - $app['config']->set('logging.channels.stderr.formatter_with', [ - 'includeStacktraces' => true, - ]); - - $app['config']->set('logging.channels.laravel-cloud-socket', [ - 'driver' => 'monolog', - 'handler' => SocketHandler::class, - 'formatter' => JsonFormatter::class, - 'formatter_with' => [ - 'includeStacktraces' => true, - ], - 'with' => [ - 'connectionString' => $_ENV['LARAVEL_CLOUD_LOG_SOCKET'] ?? - $_SERVER['LARAVEL_CLOUD_LOG_SOCKET'] ?? - 'unix:///tmp/cloud-init.sock', - 'persistent' => true, - ], - ]); - } - /** * Forward a method call to the given method if an application instance exists. * diff --git a/src/Illuminate/Foundation/Cloud.php b/src/Illuminate/Foundation/Cloud.php new file mode 100644 index 000000000000..b41ad1295f8d --- /dev/null +++ b/src/Illuminate/Foundation/Cloud.php @@ -0,0 +1,97 @@ + function () use ($app) { + static::configureUnpooledPostgresConnection($app); + static::ensureMigrationsUseUnpooledConnection($app); + }, + HandleExceptions::class => function () use ($app) { + static::configureCloudLogging($app); + }, + default => fn () => true, + })(); + } + + /** + * Adjust the database configuration for pooled Laravel Postgres. + */ + public static function configureUnpooledPostgresConnection(Application $app): void + { + $host = $app['config']->get('database.connections.pgsql.host', ''); + + if (str_contains($host, 'pg.laravel.cloud') && + str_contains($host, '-pooler')) { + $app['config']->set( + 'database.connections.pgsql-unpooled', + array_merge($app['config']->get('database.connections.pgsql'), [ + 'host' => str_replace('-pooler', '', $host), + ]) + ); + } + } + + /** + * Ensure that migrations use the unpooled Postgres connection if applicable. + */ + public static function ensureMigrationsUseUnpooledConnection(Application $app): void + { + if (! is_array($app['config']->get('database.connections.pgsql-unpooled'))) { + return; + } + + Migrator::resolveConnectionsUsing(function ($resolver, $connection) use ($app) { + $connection = $connection ?? $app['config']->get('database.default'); + + return $resolver->connection( + $connection === 'pgsql' ? 'pgsql-unpooled' : $connection + ); + }); + } + + /** + * Configure the Laravel Cloud log channels. + */ + public static function configureCloudLogging(Application $app): void + { + $app['config']->set('logging.channels.stderr.formatter_with', [ + 'includeStacktraces' => true, + ]); + + $app['config']->set('logging.channels.laravel-cloud-socket', [ + 'driver' => 'monolog', + 'handler' => SocketHandler::class, + 'formatter' => JsonFormatter::class, + 'formatter_with' => [ + 'includeStacktraces' => true, + ], + 'with' => [ + 'connectionString' => $_ENV['LARAVEL_CLOUD_LOG_SOCKET'] ?? + $_SERVER['LARAVEL_CLOUD_LOG_SOCKET'] ?? + 'unix:///tmp/cloud-init.sock', + 'persistent' => true, + ], + ]); + } +} diff --git a/src/Illuminate/Foundation/Console/DownCommand.php b/src/Illuminate/Foundation/Console/DownCommand.php index 86b2c2841375..6f4dee7c4c8e 100644 --- a/src/Illuminate/Foundation/Console/DownCommand.php +++ b/src/Illuminate/Foundation/Console/DownCommand.php @@ -42,7 +42,7 @@ class DownCommand extends Command public function handle() { try { - if ($this->laravel->maintenanceMode()->active()) { + if ($this->laravel->maintenanceMode()->active() && ! $this->getSecret()) { $this->components->info('Application is already down.'); return 0; diff --git a/src/Illuminate/Support/Fluent.php b/src/Illuminate/Support/Fluent.php index 5e8e388faf71..4622829907ff 100755 --- a/src/Illuminate/Support/Fluent.php +++ b/src/Illuminate/Support/Fluent.php @@ -6,6 +6,7 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\Jsonable; use Illuminate\Support\Traits\InteractsWithData; +use Illuminate\Support\Traits\Macroable; use JsonSerializable; /** @@ -17,7 +18,9 @@ */ class Fluent implements Arrayable, ArrayAccess, Jsonable, JsonSerializable { - use InteractsWithData; + use InteractsWithData, Macroable { + __call as macroCall; + } /** * All of the attributes set on the fluent instance. @@ -34,9 +37,7 @@ class Fluent implements Arrayable, ArrayAccess, Jsonable, JsonSerializable */ public function __construct($attributes = []) { - foreach ($attributes as $key => $value) { - $this->attributes[$key] = $value; - } + $this->fill($attributes); } /** @@ -67,6 +68,21 @@ public function set($key, $value) return $this; } + /** + * Fill the fluent instance with an array of attributes. + * + * @param iterable $attributes + * @return $this + */ + public function fill($attributes) + { + foreach ($attributes as $key => $value) { + $this->attributes[$key] = $value; + } + + return $this; + } + /** * Get an attribute from the fluent instance. * @@ -227,6 +243,10 @@ public function offsetUnset($offset): void */ public function __call($method, $parameters) { + if (static::hasMacro($method)) { + return $this->macroCall($method, $parameters); + } + $this->attributes[$method] = count($parameters) > 0 ? reset($parameters) : true; return $this; diff --git a/src/Illuminate/Support/ServiceProvider.php b/src/Illuminate/Support/ServiceProvider.php index 086cbd220fc2..3db60f9f5988 100755 --- a/src/Illuminate/Support/ServiceProvider.php +++ b/src/Illuminate/Support/ServiceProvider.php @@ -236,17 +236,17 @@ protected function loadViewComponentsAs($prefix, array $components) } /** - * Register a translation file namespace. + * Register a translation file namespace or path. * * @param string $path - * @param string $namespace + * @param string|null $namespace * @return void */ - protected function loadTranslationsFrom($path, $namespace) + protected function loadTranslationsFrom($path, $namespace = null) { - $this->callAfterResolving('translator', function ($translator) use ($path, $namespace) { - $translator->addNamespace($namespace, $path); - }); + $this->callAfterResolving('translator', fn ($translator) => is_null($namespace) + ? $translator->addPath($path) + : $translator->addNamespace($namespace, $path)); } /** diff --git a/src/Illuminate/Translation/FileLoader.php b/src/Illuminate/Translation/FileLoader.php index 65c4c4abc323..2723491e07a8 100755 --- a/src/Illuminate/Translation/FileLoader.php +++ b/src/Illuminate/Translation/FileLoader.php @@ -204,6 +204,16 @@ public function addJsonPath($path) $this->jsonPaths[] = $path; } + /** + * Get an array of all the registered paths to translation files. + * + * @return array + */ + public function paths() + { + return $this->paths; + } + /** * Get an array of all the registered paths to JSON translation files. * diff --git a/tests/Container/ContextualAttributeBindingTest.php b/tests/Container/ContextualAttributeBindingTest.php index 4d1f056f2015..8c0d7f7f9f69 100644 --- a/tests/Container/ContextualAttributeBindingTest.php +++ b/tests/Container/ContextualAttributeBindingTest.php @@ -111,6 +111,7 @@ public function testAuthedAttribute() $container = new Container; $container->singleton('auth', function () { $manager = m::mock(AuthManager::class); + $manager->shouldReceive('userResolver')->andReturn(fn ($guard = null) => $manager->guard($guard)->user()); $manager->shouldReceive('guard')->with('foo')->andReturnUsing(function () { $guard = m::mock(GuardContract::class); $guard->shouldReceive('user')->andReturn(m:mock(AuthenticatableContract::class)); diff --git a/tests/Database/DatabaseQueryBuilderTest.php b/tests/Database/DatabaseQueryBuilderTest.php index a8caf61f6016..97f127f9074a 100755 --- a/tests/Database/DatabaseQueryBuilderTest.php +++ b/tests/Database/DatabaseQueryBuilderTest.php @@ -3364,6 +3364,17 @@ public function testPluckMethodGetsCollectionOfColumnValues() $this->assertEquals([1 => 'bar', 10 => 'baz'], $results->all()); } + public function testPluckAvoidsDuplicateColumnSelection() + { + $builder = $this->getBuilder(); + $builder->getConnection()->shouldReceive('select')->once()->with('select "foo" from "users" where "id" = ?', [1], true)->andReturn([['foo' => 'bar']]); + $builder->getProcessor()->shouldReceive('processSelect')->once()->with($builder, [['foo' => 'bar']])->andReturnUsing(function ($query, $results) { + return $results; + }); + $results = $builder->from('users')->where('id', '=', 1)->pluck('foo', 'foo'); + $this->assertEquals(['bar' => 'bar'], $results->all()); + } + public function testImplode() { // Test without glue. diff --git a/tests/Integration/Foundation/CloudTest.php b/tests/Integration/Foundation/CloudTest.php new file mode 100644 index 000000000000..99ff0e75347a --- /dev/null +++ b/tests/Integration/Foundation/CloudTest.php @@ -0,0 +1,26 @@ +app['config']->set('database.connections.pgsql', [ + 'host' => 'test-pooler.pg.laravel.cloud', + 'username' => 'test-username', + 'password' => 'test-password', + ]); + + Cloud::configureUnpooledPostgresConnection($this->app); + + $this->assertEquals([ + 'host' => 'test.pg.laravel.cloud', + 'username' => 'test-username', + 'password' => 'test-password', + ], $this->app['config']->get('database.connections.pgsql-unpooled')); + } +} diff --git a/tests/Support/SupportFluentTest.php b/tests/Support/SupportFluentTest.php index 59ff5202785b..86b5358c50a5 100755 --- a/tests/Support/SupportFluentTest.php +++ b/tests/Support/SupportFluentTest.php @@ -416,6 +416,42 @@ public function testEnumsMethod() $this->assertEquals([TestBackedEnum::B], $fluent->enums('int.b', TestBackedEnum::class)); $this->assertEmpty($fluent->enums('int.doesnt_exist', TestBackedEnum::class)); } + + public function testFill() + { + $fluent = new Fluent(['name' => 'John Doe']); + + $fluent->fill([ + 'email' => 'john.doe@example.com', + 'age' => 30, + ]); + + $this->assertEquals([ + 'name' => 'John Doe', + 'email' => 'john.doe@example.com', + 'age' => 30, + ], $fluent->getAttributes()); + } + + public function testMacroable() + { + Fluent::macro('foo', function () { + return $this->fill([ + 'foo' => 'bar', + 'baz' => 'zal', + ]); + }); + + $fluent = new Fluent([ + 'bee' => 'ser', + ]); + + $this->assertSame([ + 'bee' => 'ser', + 'foo' => 'bar', + 'baz' => 'zal', + ], $fluent->foo()->all()); + } } class FluentArrayIteratorStub implements IteratorAggregate diff --git a/tests/Support/SupportServiceProviderTest.php b/tests/Support/SupportServiceProviderTest.php index 2cf1cf17b63f..9c4f825c540c 100644 --- a/tests/Support/SupportServiceProviderTest.php +++ b/tests/Support/SupportServiceProviderTest.php @@ -5,6 +5,7 @@ use Illuminate\Config\Repository as Config; use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider; +use Illuminate\Translation\Translator; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -160,6 +161,36 @@ public function testPublishesMigrations() $this->assertNotContains('source/tagged/five', ServiceProvider::publishableMigrationPaths()); } + + public function testLoadTranslationsFromWithoutNamespace() + { + $translator = m::mock(Translator::class); + $translator->shouldReceive('addPath')->once()->with(__DIR__.'/translations'); + + $this->app->shouldReceive('afterResolving')->once()->with('translator', m::on(function ($callback) use ($translator) { + $callback($translator); + + return true; + })); + + $provider = new ServiceProviderForTestingOne($this->app); + $provider->loadTranslationsFrom(__DIR__.'/translations'); + } + + public function testLoadTranslationsFromWithNamespace() + { + $translator = m::mock(Translator::class); + $translator->shouldReceive('addNamespace')->once()->with('namespace', __DIR__.'/translations'); + + $this->app->shouldReceive('afterResolving')->once()->with('translator', m::on(function ($callback) use ($translator) { + $callback($translator); + + return true; + })); + + $provider = new ServiceProviderForTestingOne($this->app); + $provider->loadTranslationsFrom(__DIR__.'/translations', 'namespace'); + } } class ServiceProviderForTestingOne extends ServiceProvider @@ -179,6 +210,13 @@ public function boot() $this->publishesMigrations(['source/tagged/three' => 'destination/tagged/three'], 'tag_three'); $this->publishesMigrations(['source/tagged/multiple_two' => 'destination/tagged/multiple_two'], ['tag_four', 'tag_five']); } + + public function loadTranslationsFrom($path, $namespace = null) + { + $this->callAfterResolving('translator', fn ($translator) => is_null($namespace) + ? $translator->addPath($path) + : $translator->addNamespace($namespace, $path)); + } } class ServiceProviderForTestingTwo extends ServiceProvider diff --git a/tests/Translation/TranslationFileLoaderTest.php b/tests/Translation/TranslationFileLoaderTest.php index e5b3479944ba..b7e74f18bfaa 100755 --- a/tests/Translation/TranslationFileLoaderTest.php +++ b/tests/Translation/TranslationFileLoaderTest.php @@ -14,6 +14,69 @@ protected function tearDown(): void m::close(); } + public function testLoadMethodLoadsTranslationsFromAddedPath() + { + $files = m::mock(Filesystem::class); + $loader = new FileLoader($files, __DIR__); + $loader->addPath(__DIR__.'/another'); + + $files->shouldReceive('exists')->once()->with(__DIR__.'/en/messages.php')->andReturn(true); + $files->shouldReceive('getRequire')->once()->with(__DIR__.'/en/messages.php')->andReturn(['foo' => 'bar']); + + $files->shouldReceive('exists')->once()->with(__DIR__.'/another/en/messages.php')->andReturn(true); + $files->shouldReceive('getRequire')->once()->with(__DIR__.'/another/en/messages.php')->andReturn(['baz' => 'backagesplash']); + + $this->assertEquals(['foo' => 'bar', 'baz' => 'backagesplash'], $loader->load('en', 'messages')); + } + + public function testLoadMethodHandlesMissingAddedPath() + { + $files = m::mock(Filesystem::class); + $loader = new FileLoader($files, __DIR__); + $loader->addPath(__DIR__.'/missing'); + + $files->shouldReceive('exists')->once()->with(__DIR__.'/en/messages.php')->andReturn(true); + $files->shouldReceive('getRequire')->once()->with(__DIR__.'/en/messages.php')->andReturn(['foo' => 'bar']); + + $files->shouldReceive('exists')->once()->with(__DIR__.'/missing/en/messages.php')->andReturn(false); + + $this->assertEquals(['foo' => 'bar'], $loader->load('en', 'messages')); + } + + public function testLoadMethodOverwritesExistingKeysFromAddedPath() + { + $files = m::mock(Filesystem::class); + $loader = new FileLoader($files, __DIR__); + $loader->addPath(__DIR__.'/another'); + + $files->shouldReceive('exists')->once()->with(__DIR__.'/en/messages.php')->andReturn(true); + $files->shouldReceive('getRequire')->once()->with(__DIR__.'/en/messages.php')->andReturn(['foo' => 'bar']); + + $files->shouldReceive('exists')->once()->with(__DIR__.'/another/en/messages.php')->andReturn(true); + $files->shouldReceive('getRequire')->once()->with(__DIR__.'/another/en/messages.php')->andReturn(['foo' => 'baz']); + + $this->assertEquals(['foo' => 'baz'], $loader->load('en', 'messages')); + } + + public function testLoadMethodLoadsTranslationsFromMultipleAddedPaths() + { + $files = m::mock(Filesystem::class); + $loader = new FileLoader($files, __DIR__); + $loader->addPath(__DIR__.'/another'); + $loader->addPath(__DIR__.'/yet-another'); + + $files->shouldReceive('exists')->once()->with(__DIR__.'/en/messages.php')->andReturn(true); + $files->shouldReceive('getRequire')->once()->with(__DIR__.'/en/messages.php')->andReturn(['foo' => 'bar']); + + $files->shouldReceive('exists')->once()->with(__DIR__.'/another/en/messages.php')->andReturn(true); + $files->shouldReceive('getRequire')->once()->with(__DIR__.'/another/en/messages.php')->andReturn(['baz' => 'backagesplash']); + + $files->shouldReceive('exists')->once()->with(__DIR__.'/yet-another/en/messages.php')->andReturn(true); + $files->shouldReceive('getRequire')->once()->with(__DIR__.'/yet-another/en/messages.php')->andReturn(['qux' => 'quux']); + + $this->assertEquals(['foo' => 'bar', 'baz' => 'backagesplash', 'qux' => 'quux'], $loader->load('en', 'messages')); + } + public function testLoadMethodWithoutNamespacesProperlyCallsLoader() { $loader = new FileLoader($files = m::mock(Filesystem::class), __DIR__); @@ -152,4 +215,14 @@ public function testAllAddedJsonPathsReturnProperly() $loader->addJsonPath($path2); $this->assertEquals([$path1, $path2], $loader->jsonPaths()); } + + public function testAllAddedPathsReturnProperly() + { + $loader = new FileLoader(m::mock(Filesystem::class), __DIR__); + $path1 = __DIR__.'/another'; + $path2 = __DIR__.'/another2'; + $loader->addPath($path1); + $loader->addPath($path2); + $this->assertEquals([$path1, $path2], array_slice($loader->paths(), 1)); + } }