diff --git a/src/Application.php b/src/Application.php index 0f41a49..ee58a77 100644 --- a/src/Application.php +++ b/src/Application.php @@ -8,6 +8,10 @@ use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; use Rareloop\Lumberjack\Http\ResponseEmitter; +use Symfony\Component\Filesystem\Filesystem; +use Rareloop\Lumberjack\Autodiscovery\AutodiscoveredPackages; +use Rareloop\Lumberjack\Autodiscovery\ManifestCache; +use Rareloop\Lumberjack\Autodiscovery\PackageManifest; class Application implements ContainerInterface { @@ -37,13 +41,30 @@ public function setBasePath(string $basePath) { $this->basePath = $basePath; + $this->bootstrapContainer(); + } + + protected function bootstrapContainer() + { $this->bindPathsInContainer(); + + $this->registerAutodiscoveryBindings(); } protected function bindPathsInContainer() { $this->bind('path.base', $this->basePath()); $this->bind('path.config', $this->configPath()); + $this->bind('path.bootstrap', $this->bootstrapPath()); + $this->bind('path.vendor', $this->vendorPath()); + } + + protected function registerAutodiscoveryBindings() + { + $this->singleton(ManifestCache::class, \DI\autowire() + ->constructorParameter('cachePath', $this->bootstrapPath('cache' . DIRECTORY_SEPARATOR . 'packages.php'))); + + $this->singleton(AutodiscoveredPackages::class, \DI\autowire()); } public function basePath() @@ -56,6 +77,16 @@ public function configPath() return $this->basePath . DIRECTORY_SEPARATOR . 'config'; } + public function vendorPath() + { + return $this->basePath . DIRECTORY_SEPARATOR . 'vendor'; + } + + public function bootstrapPath(string $path = '') + { + return $this->basePath . DIRECTORY_SEPARATOR . 'bootstrap' . ($path ? DIRECTORY_SEPARATOR . $path : ''); + } + public function bind($key, $value) { // Prevent PHP-DI from creating singletons from class binds or closure factories diff --git a/src/Autodiscovery/AutodiscoveredPackages.php b/src/Autodiscovery/AutodiscoveredPackages.php new file mode 100644 index 0000000..e0bbf06 --- /dev/null +++ b/src/Autodiscovery/AutodiscoveredPackages.php @@ -0,0 +1,37 @@ +getManifest(), 'providers', []); + } + + public function aliases(): array + { + return (array) Arr::get($this->getManifest(), 'aliases', []); + } + + protected function getManifest(): array + { + if ($this->manifest !== null) { + return $this->manifest; + } + + if ($this->cache->exists()) { + return $this->manifest = (array) $this->cache->read(); + } + + return $this->manifest = ['providers' => [], 'aliases' => []]; + } +} diff --git a/src/Autodiscovery/DiscoveryRunner.php b/src/Autodiscovery/DiscoveryRunner.php new file mode 100644 index 0000000..d2c385a --- /dev/null +++ b/src/Autodiscovery/DiscoveryRunner.php @@ -0,0 +1,72 @@ +getComposer(); + $io = $event->getIO(); + $vendorPath = $composer->getConfig()->get('vendor-dir'); + $projectPath = dirname($vendorPath); + $extra = $composer->getPackage()->getExtra(); + + try { + $themePath = $this->resolveThemeDirectory($projectPath, $extra); + + $builder = new PackageManifest($projectPath, $vendorPath); + $cachePath = $themePath . DIRECTORY_SEPARATOR . 'bootstrap' . DIRECTORY_SEPARATOR . 'cache' + . DIRECTORY_SEPARATOR . 'packages.php'; + $cache = new ManifestCache($cachePath); + + $cache->write($builder->build()); + } catch (\RuntimeException $e) { + $io->writeError( + "Lumberjack: {$e->getMessage()} Package auto-discovery won't work as expected." + ); + } + } + + /** + * Resolve the theme directory for discovery. + * + * @param string $projectPath + * @param array $extra + * @return string + * @throws \RuntimeException + */ + protected function resolveThemeDirectory(string $projectPath, array $extra): string + { + $themeDir = Arr::get($extra, 'lumberjack.theme-dir'); + + if ($themeDir) { + $path = $projectPath . DIRECTORY_SEPARATOR . $themeDir; + + if (is_dir($path)) { + return $path; + } + + throw new \RuntimeException("The configured theme directory \"{$path}\" does not exist."); + } + + $defaultPath = $projectPath . DIRECTORY_SEPARATOR . 'web' . DIRECTORY_SEPARATOR . 'app' + . DIRECTORY_SEPARATOR . 'themes' . DIRECTORY_SEPARATOR . 'lumberjack'; + + if (is_dir($defaultPath)) { + return $defaultPath; + } + + throw new \RuntimeException( + '"extra.lumberjack.theme-dir" is not set in composer.json and the default path was not found.' + ); + } +} diff --git a/src/Autodiscovery/ManifestCache.php b/src/Autodiscovery/ManifestCache.php new file mode 100644 index 0000000..c664e80 --- /dev/null +++ b/src/Autodiscovery/ManifestCache.php @@ -0,0 +1,46 @@ +cachePath); + } + + public function read(): array + { + $data = require $this->cachePath; + + return is_array($data) ? $data : []; + } + + public function write(array $manifest): void + { + $directory = dirname($this->cachePath); + + if (!is_dir($directory)) { + mkdir($directory, 0755, true); + } + + file_put_contents( + $this->cachePath, + 'exists() ? filemtime($this->cachePath) : 0; + } + + public function getPath(): string + { + return $this->cachePath; + } +} diff --git a/src/Autodiscovery/PackageManifest.php b/src/Autodiscovery/PackageManifest.php new file mode 100644 index 0000000..4b6acaa --- /dev/null +++ b/src/Autodiscovery/PackageManifest.php @@ -0,0 +1,123 @@ +getInstalledPackages(); + $ignore = $this->getPackagesToIgnore(); + + $providers = []; + $aliases = []; + + foreach ($packages as $package) { + $name = Arr::get($package, 'name'); + + if ($this->shouldIgnore($name, $ignore)) { + continue; + } + + $extra = Arr::get($package, 'extra.lumberjack', []); + + if (!is_array($extra) || empty($extra)) { + continue; + } + + $packageProviders = Arr::get($extra, 'providers', []); + $packageAliases = Arr::get($extra, 'aliases', []); + + if (is_array($packageProviders)) { + foreach ($packageProviders as $provider) { + $providers[] = $this->formatClassName($provider); + } + } + + if (is_array($packageAliases)) { + foreach ($packageAliases as $alias => $className) { + $aliases[$alias] = $this->formatClassName($className); + } + } + } + + return [ + 'providers' => array_values(array_unique($providers)), + 'aliases' => $aliases, + ]; + } + + /** + * Format a class name, stripping accidental '::class' suffixes. + * + * @param mixed $className + * @return string + */ + protected function formatClassName(mixed $className): string + { + return str_replace('::class', '', (string) $className); + } + + public function mtime(): int + { + $path = $this->vendorPath . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR . 'installed.json'; + + return file_exists($path) ? filemtime($path) : 0; + } + + protected function getInstalledPackages(): array + { + $path = $this->vendorPath . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR . 'installed.json'; + + if (!file_exists($path)) { + return []; + } + + $installed = json_decode(file_get_contents($path), true); + + if (!is_array($installed)) { + return []; + } + + $packages = $installed['packages'] ?? $installed; + + return is_array($packages) ? $packages : []; + } + + protected function getPackagesToIgnore(): array + { + $path = $this->basePath . DIRECTORY_SEPARATOR . 'composer.json'; + + if (!file_exists($path)) { + return []; + } + + $composer = json_decode(file_get_contents($path), true); + + if (!is_array($composer)) { + return []; + } + + $ignore = Arr::get($composer, 'extra.lumberjack.dont-discover', []); + + return is_array($ignore) ? $ignore : []; + } + + protected function shouldIgnore(?string $name, array $ignore): bool + { + return $name !== null && (in_array($name, $ignore) || in_array('*', $ignore)); + } +} diff --git a/src/Bootstrappers/RegisterAliases.php b/src/Bootstrappers/RegisterAliases.php index ae1eeba..6daa435 100644 --- a/src/Bootstrappers/RegisterAliases.php +++ b/src/Bootstrappers/RegisterAliases.php @@ -3,14 +3,21 @@ namespace Rareloop\Lumberjack\Bootstrappers; use Rareloop\Lumberjack\Application; +use Rareloop\Lumberjack\Autodiscovery\AutodiscoveredPackages; class RegisterAliases { public function bootstrap(Application $app) { $config = $app->get('config'); + $manifest = $app->get(AutodiscoveredPackages::class); - foreach ($config->get('app.aliases', []) as $alias => $realClassname) { + $aliases = array_merge( + $manifest->aliases(), + $config->get('app.aliases', []) + ); + + foreach ($aliases as $alias => $realClassname) { class_alias($realClassname, $alias); } } diff --git a/src/Bootstrappers/RegisterProviders.php b/src/Bootstrappers/RegisterProviders.php index b4d4c65..41c5da1 100644 --- a/src/Bootstrappers/RegisterProviders.php +++ b/src/Bootstrappers/RegisterProviders.php @@ -3,8 +3,10 @@ namespace Rareloop\Lumberjack\Bootstrappers; use Rareloop\Lumberjack\Application; +use Rareloop\Lumberjack\Autodiscovery\AutodiscoveredPackages; use Rareloop\Lumberjack\Providers\LogServiceProvider; use Rareloop\Lumberjack\Providers\IgnitionServiceProvider; +use Illuminate\Support\Collection; class RegisterProviders { @@ -14,7 +16,15 @@ public function bootstrap(Application $app) $this->registerBaseProviders($app); - $providers = $config->get('app.providers', []); + $manifest = $app->get(AutodiscoveredPackages::class); + + $providers = Collection::make($manifest->providers()) + ->concat($config->get('app.providers', [])) + ->mapWithKeys(function ($provider) { + return [ + (is_string($provider) ? $provider : $provider::class) => $provider, + ]; + }); foreach ($providers as $provider) { $app->register($provider); diff --git a/src/ComposerHooks.php b/src/ComposerHooks.php new file mode 100644 index 0000000..89d1cbe --- /dev/null +++ b/src/ComposerHooks.php @@ -0,0 +1,25 @@ +getComposer()->getConfig()->get('vendor-dir'); + + if (file_exists($vendorPath . '/autoload.php')) { + require_once $vendorPath . '/autoload.php'; + } + + (new DiscoveryRunner)($event); + } +} diff --git a/tests/Unit/ApplicationTest.php b/tests/Unit/ApplicationTest.php index c15c04d..3b4c3e6 100644 --- a/tests/Unit/ApplicationTest.php +++ b/tests/Unit/ApplicationTest.php @@ -48,6 +48,16 @@ public function config_path_is_set_in_container_when_basepath_passed_to_construc $this->assertSame('/base/path/config', $app->get('path.config')); } + /** @test */ + public function bootstrap_path_is_set_in_container_when_basepath_passed_to_constructor() + { + $app = new Application('/base/path'); + + $this->assertSame('/base/path/bootstrap', $app->bootstrapPath()); + $this->assertSame('/base/path/bootstrap', $app->get('path.bootstrap')); + $this->assertSame('/base/path/bootstrap/cache/packages.php', $app->bootstrapPath('cache/packages.php')); + } + /** @test */ public function can_bind_a_value() { diff --git a/tests/Unit/Autodiscovery/AutodiscoveredPackagesTest.php b/tests/Unit/Autodiscovery/AutodiscoveredPackagesTest.php new file mode 100644 index 0000000..d5130ae --- /dev/null +++ b/tests/Unit/Autodiscovery/AutodiscoveredPackagesTest.php @@ -0,0 +1,96 @@ +cache = Mockery::mock(ManifestCache::class); + } + + /** @test */ + public function it_reads_from_cache_if_it_exists() + { + $manifestData = ['providers' => ['Cached\Provider']]; + + $this->cache->shouldReceive('exists')->andReturn(true); + $this->cache->shouldReceive('read')->once()->andReturn($manifestData); + + $orchestrator = new AutodiscoveredPackages($this->cache); + + $this->assertEquals(['Cached\Provider'], $orchestrator->providers()); + } + + /** @test */ + public function it_can_get_aliases() + { + $manifestData = ['aliases' => ['Foo' => 'Bar']]; + + $this->cache->shouldReceive('exists')->andReturn(true); + $this->cache->shouldReceive('read')->once()->andReturn($manifestData); + + $orchestrator = new AutodiscoveredPackages($this->cache); + + $this->assertEquals(['Foo' => 'Bar'], $orchestrator->aliases()); + } + + /** @test */ + public function it_returns_empty_arrays_if_cache_missing() + { + $this->cache->shouldReceive('exists')->andReturn(false); + + $orchestrator = new AutodiscoveredPackages($this->cache); + + $this->assertEquals([], $orchestrator->providers()); + $this->assertEquals([], $orchestrator->aliases()); + } + + /** @test */ + public function manifest_is_only_loaded_once() + { + $manifestData = ['providers' => []]; + + $this->cache->shouldReceive('exists')->andReturn(true); + $this->cache->shouldReceive('read')->once()->andReturn($manifestData); + + $orchestrator = new AutodiscoveredPackages($this->cache); + + $orchestrator->providers(); + $orchestrator->providers(); // Second call should not trigger 'read' + + $this->assertEquals([], $orchestrator->providers()); + } + + /** @test */ + public function providers_always_returns_an_array_even_if_manifest_corrupted() + { + $this->cache->shouldReceive('exists')->andReturn(true); + $this->cache->shouldReceive('read')->andReturn(['providers' => null]); + + $orchestrator = new AutodiscoveredPackages($this->cache); + + $this->assertEquals([], $orchestrator->providers()); + } + + /** @test */ + public function aliases_always_returns_an_array_even_if_manifest_corrupted() + { + $this->cache->shouldReceive('exists')->andReturn(true); + $this->cache->shouldReceive('read')->andReturn(['aliases' => null]); + + $orchestrator = new AutodiscoveredPackages($this->cache); + + $this->assertEquals([], $orchestrator->aliases()); + } +} diff --git a/tests/Unit/Autodiscovery/DiscoveryRunnerTest.php b/tests/Unit/Autodiscovery/DiscoveryRunnerTest.php new file mode 100644 index 0000000..2d47e84 --- /dev/null +++ b/tests/Unit/Autodiscovery/DiscoveryRunnerTest.php @@ -0,0 +1,112 @@ +root = vfsStream::setup('root', null, [ + 'vendor' => [ + 'composer' => [ + 'installed.json' => json_encode(['packages' => []]), + ], + ], + ]); + + $this->event = Mockery::mock(Event::class); + $this->composer = Mockery::mock(Composer::class); + $this->config = Mockery::mock(Config::class); + $this->package = Mockery::mock(RootPackageInterface::class); + + $this->io = Mockery::mock(IOInterface::class); + $this->io->shouldIgnoreMissing(); + + $this->event->shouldReceive('getComposer')->andReturn($this->composer); + $this->event->shouldReceive('getIO')->andReturn($this->io); + $this->composer->shouldReceive('getConfig')->andReturn($this->config); + $this->composer->shouldReceive('getPackage')->andReturn($this->package); + $this->config->shouldReceive('get')->with('vendor-dir')->andReturn($this->root->url() . '/vendor'); + } + + /** @test */ + public function it_can_run_the_discovery_process_with_explicit_config() + { + vfsStream::newDirectory('my-app/bootstrap/cache')->at($this->root); + + $this->package->shouldReceive('getExtra')->andReturn([ + 'lumberjack' => [ + 'theme-dir' => 'my-app', + ], + ]); + $this->io->shouldNotReceive('writeError'); + + (new DiscoveryRunner)($this->event); + + $this->assertTrue($this->root->hasChild('my-app/bootstrap/cache/packages.php')); + } + + /** @test */ + public function it_can_run_the_discovery_process_using_default_bedrock_path() + { + vfsStream::newDirectory('web/app/themes/lumberjack/bootstrap/cache')->at($this->root); + + $this->package->shouldReceive('getExtra')->andReturn([]); + $this->io->shouldNotReceive('writeError'); + + (new DiscoveryRunner)($this->event); + + $this->assertTrue($this->root->hasChild('web/app/themes/lumberjack/bootstrap/cache/packages.php')); + } + + /** @test */ + public function it_emits_warning_if_no_config_and_no_bedrock_path_found() + { + $this->package->shouldReceive('getExtra')->andReturn([]); + + $this->io->shouldReceive('writeError')->once()->with(Mockery::on(function ($message) { + return str_contains($message, 'default path was not found') && str_contains($message, 'Package auto-discovery won\'t work'); + })); + + (new DiscoveryRunner)($this->event); + + $this->assertFalse($this->root->hasChild('bootstrap/cache/packages.php')); + } + + /** @test */ + public function it_emits_error_if_configured_path_is_missing() + { + $this->package->shouldReceive('getExtra')->andReturn([ + 'lumberjack' => [ + 'theme-dir' => 'custom-app', + ], + ]); + + $this->io->shouldReceive('writeError')->once()->with(Mockery::on(function ($message) { + return str_contains($message, 'configured theme directory') && str_contains($message, 'does not exist'); + })); + + (new DiscoveryRunner)($this->event); + } +} diff --git a/tests/Unit/Autodiscovery/ManifestCacheTest.php b/tests/Unit/Autodiscovery/ManifestCacheTest.php new file mode 100644 index 0000000..733ae4d --- /dev/null +++ b/tests/Unit/Autodiscovery/ManifestCacheTest.php @@ -0,0 +1,88 @@ +root = vfsStream::setup('root'); + } + + /** @test */ + public function it_can_check_if_cache_exists() + { + $cachePath = $this->root->url() . '/packages.php'; + $cache = new ManifestCache($cachePath); + + $this->assertFalse($cache->exists()); + + file_put_contents($cachePath, 'assertTrue($cache->exists()); + } + + /** @test */ + public function it_can_write_the_cache() + { + $cachePath = $this->root->url() . '/packages.php'; + $cache = new ManifestCache($cachePath); + + $manifest = ['providers' => ['Foo\Bar']]; + $cache->write($manifest); + + $this->assertTrue($this->root->hasChild('packages.php')); + $data = require $cachePath; + $this->assertEquals($manifest, $data); + } + + /** @test */ + public function it_can_read_the_cache() + { + $cachePath = $this->root->url() . '/packages.php'; + $cache = new ManifestCache($cachePath); + + $manifest = ['providers' => ['Foo\Bar']]; + file_put_contents($cachePath, 'assertEquals($manifest, $cache->read()); + } + + /** @test */ + public function it_returns_empty_array_if_cache_is_malformed() + { + $cachePath = $this->root->url() . '/packages.php'; + $cache = new ManifestCache($cachePath); + + file_put_contents($cachePath, 'assertEquals([], $cache->read()); + } + + /** @test */ + public function it_can_get_the_mtime() + { + $cachePath = $this->root->url() . '/packages.php'; + $cache = new ManifestCache($cachePath); + + $this->assertEquals(0, $cache->mtime()); + + file_put_contents($cachePath, 'assertGreaterThan(0, $cache->mtime()); + } + + /** @test */ + public function it_can_get_the_path() + { + $cachePath = '/path/to/cache.php'; + $cache = new ManifestCache($cachePath); + + $this->assertEquals($cachePath, $cache->getPath()); + } +} diff --git a/tests/Unit/Autodiscovery/PackageManifestTest.php b/tests/Unit/Autodiscovery/PackageManifestTest.php new file mode 100644 index 0000000..5e6a60f --- /dev/null +++ b/tests/Unit/Autodiscovery/PackageManifestTest.php @@ -0,0 +1,57 @@ +root = vfsStream::setup('root', null, [ + 'vendor' => [ + 'composer' => [ + 'installed.json' => json_encode([ + 'packages' => [ + [ + 'name' => 'rareloop/lumberjack-test-package', + 'extra' => [ + 'lumberjack' => [ + 'providers' => [ + 'Rareloop\Lumberjack\Validation\ValidationServiceProvider' + ], + 'aliases' => [ + 'test-foo' => 'Rareloop\Lumberjack\Validation\FormInterface' + ], + ], + ], + ], + ], + ]), + ], + ], + 'composer.json' => json_encode([]), + ]); + } + + /** @test */ + public function it_can_discover_aliases_with_hyphens() + { + $manifest = new PackageManifest( + $this->root->url(), + $this->root->url() . '/vendor' + ); + + $data = $manifest->build(); + + $this->assertContains('Rareloop\Lumberjack\Validation\ValidationServiceProvider', $data['providers']); + $this->assertArrayHasKey('test-foo', $data['aliases']); + $this->assertEquals('Rareloop\Lumberjack\Validation\FormInterface', $data['aliases']['test-foo']); + } +} diff --git a/tests/Unit/Bootstrappers/RegisterAliasesTest.php b/tests/Unit/Bootstrappers/RegisterAliasesTest.php index e4a2839..94458fc 100644 --- a/tests/Unit/Bootstrappers/RegisterAliasesTest.php +++ b/tests/Unit/Bootstrappers/RegisterAliasesTest.php @@ -7,24 +7,56 @@ use Rareloop\Lumberjack\Application; use Rareloop\Lumberjack\Bootstrappers\RegisterAliases; use Rareloop\Lumberjack\Config; +use Rareloop\Lumberjack\Autodiscovery\AutodiscoveredPackages; class RegisterAliasesTest extends TestCase { + use \Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; + /** @test */ public function calls_class_alias_on_all_alias_mappings() { $app = new Application; + + $manifest = Mockery::mock(AutodiscoveredPackages::class); + $manifest->shouldReceive('aliases')->andReturn([]); + $app->bind(AutodiscoveredPackages::class, $manifest); + + $config = new Config; + $config->set('app.aliases', [ + 'FooOne' => TestClassToAlias::class, + ]); + $app->bind('config', $config); + + $bootstrapper = new RegisterAliases; + $bootstrapper->bootstrap($app); + + $this->assertTrue(class_exists('FooOne')); + $this->assertInstanceOf(TestClassToAlias::class, new \FooOne); + } + + /** @test */ + public function user_defined_aliases_take_precedence_over_autodiscovered_ones() + { + $app = new Application; + + $manifest = Mockery::mock(AutodiscoveredPackages::class); + $manifest->shouldReceive('aliases')->andReturn([ + 'FooTwo' => 'Package\Class\Foo', + ]); + $app->bind(AutodiscoveredPackages::class, $manifest); + $config = new Config; $config->set('app.aliases', [ - 'Foo' => TestClassToAlias::class, + 'FooTwo' => TestClassToAlias::class, ]); $app->bind('config', $config); $bootstrapper = new RegisterAliases; $bootstrapper->bootstrap($app); - $this->assertTrue(class_exists('Foo')); - $this->assertInstanceOf(TestClassToAlias::class, new \Foo); + $this->assertTrue(class_exists('FooTwo')); + $this->assertInstanceOf(TestClassToAlias::class, new \FooTwo); } } diff --git a/tests/Unit/Bootstrappers/RegisterProvidersTest.php b/tests/Unit/Bootstrappers/RegisterProvidersTest.php index 912a14f..749f8ff 100644 --- a/tests/Unit/Bootstrappers/RegisterProvidersTest.php +++ b/tests/Unit/Bootstrappers/RegisterProvidersTest.php @@ -8,6 +8,7 @@ use Rareloop\Lumberjack\Bootstrappers\LoadConfiguration; use Rareloop\Lumberjack\Bootstrappers\RegisterProviders; use Rareloop\Lumberjack\Config; +use Rareloop\Lumberjack\Autodiscovery\AutodiscoveredPackages; use Rareloop\Lumberjack\Providers\ServiceProvider; class RegisterProvidersTest extends TestCase @@ -19,6 +20,10 @@ public function registers_all_providers_found_in_config() { $app = new Application; + $manifest = Mockery::mock(AutodiscoveredPackages::class); + $manifest->shouldReceive('providers')->andReturn([]); + $app->bind(AutodiscoveredPackages::class, $manifest); + $provider1 = Mockery::mock(RPTestServiceProvider1::class, [$app]); $provider1->shouldReceive('register')->once(); $provider2 = Mockery::mock(RPTestServiceProvider2::class, [$app]); @@ -36,22 +41,42 @@ public function registers_all_providers_found_in_config() } /** @test */ - public function should_not_fall_over_on_empty_config_data() + public function user_provided_instance_takes_precedence_over_autodiscovered_class_string() { $app = new Application; + // The user's specific instance they want to use + $userInstance = new RPTestServiceProvider1($app); + $userInstance->foo = 'bar'; + + // Autodiscovery finds the class name + $manifest = Mockery::mock(AutodiscoveredPackages::class); + $manifest->shouldReceive('providers')->andReturn([ + RPTestServiceProvider1::class, + ]); + $app->bind(AutodiscoveredPackages::class, $manifest); + + // User configures the specific instance $config = new Config; + $config->set('app.providers', [ + $userInstance, + ]); $app->bind('config', $config); $registerProvidersBootstrapper = new RegisterProviders; $registerProvidersBootstrapper->bootstrap($app); - $this->addToAssertionCount(1); // does not throw an exception + // Verify that the instance registered in the app is the one the user provided + $registeredProvider = $app->getProvider(RPTestServiceProvider1::class); + $this->assertSame($userInstance, $registeredProvider); + $this->assertEquals('bar', $registeredProvider->foo); } } class RPTestServiceProvider1 extends ServiceProvider { + public $foo; + public function register() {} }