diff --git a/src/boost/docs/providers.md b/src/boost/docs/providers.md
index cd27d9005..0b10d399c 100644
--- a/src/boost/docs/providers.md
+++ b/src/boost/docs/providers.md
@@ -3,8 +3,11 @@
- [Introduction](#introduction)
- [Writing Service Providers](#writing-service-providers)
- [The Register Method](#the-register-method)
+ - [Merging Configuration](#merging-configuration)
- [The Boot Method](#the-boot-method)
+ - [Conditionally Loading Providers](#conditionally-loading-providers)
- [Registering Providers](#registering-providers)
+ - [Provider Priority](#provider-priority)
- [Deferred Providers](#deferred-providers)
@@ -104,6 +107,39 @@ class AppServiceProvider extends ServiceProvider
}
```
+
+#### Merging Configuration
+
+Package service providers may merge their default configuration into the application's configuration using the `mergeConfigFrom` method. This is typically done within the `register` method:
+
+```php
+/**
+ * Register any application services.
+ */
+public function register(): void
+{
+ $this->mergeConfigFrom(
+ __DIR__.'/../config/riak.php', 'riak'
+ );
+}
+```
+
+By default, configuration is merged at the top level. If your configuration contains arrays that should allow applications to add new entries without replacing the entire array, override the `mergeableOptions` method:
+
+```php
+/**
+ * Get the options within the configuration that should be merged.
+ *
+ * @return array
+ */
+protected function mergeableOptions(string $name): array
+{
+ return $name === 'riak' ? ['connections'] : [];
+}
+```
+
+In this example, the application's `riak.connections` entries will be merged with the package's default connections. Entries with the same key will still be replaced by the application's configuration.
+
### The Boot Method
@@ -150,6 +186,23 @@ public function boot(ResponseFactory $response): void
}
```
+
+### Conditionally Loading Providers
+
+You may prevent a service provider from being registered or booted by overriding the `isEnabled` method. This is useful for packages that are installed in many applications but should only load in some of them:
+
+```php
+/**
+ * Determine whether this provider should be registered and booted.
+ */
+public function isEnabled(): bool
+{
+ return (bool) config('modules.riak.enabled');
+}
+```
+
+When this method returns `false`, the provider's `register` and `boot` methods will not be called, its `bindings` and `singletons` properties will not be processed, and the provider will not be marked as loaded.
+
## Registering Providers
@@ -174,6 +227,25 @@ return [
];
```
+
+### Provider Priority
+
+Auto-discovered package providers may define a `priority` property to control their registration order relative to other discovered package providers. Providers with a higher priority are registered first:
+
+```php
+use Hypervel\Support\ServiceProvider;
+
+class RiakServiceProvider extends ServiceProvider
+{
+ /**
+ * The registration priority for this provider.
+ */
+ public int $priority = 10;
+}
+```
+
+Provider priority only applies to auto-discovered package providers. Framework providers are always registered before discovered package providers, and application providers are registered after them.
+
## Deferred Providers
diff --git a/src/foundation/src/Application.php b/src/foundation/src/Application.php
index f095b2d98..5cbbb2e66 100644
--- a/src/foundation/src/Application.php
+++ b/src/foundation/src/Application.php
@@ -950,6 +950,14 @@ public function register(ServiceProvider|string $provider, bool $force = false):
$provider = $this->resolveProvider($provider);
}
+ // Hypervel-specific: providers may opt out of registration via isEnabled()
+ // returning false (e.g. gated on runtime config/env). A disabled provider
+ // is instantiated but never registered or booted, its bindings/singletons
+ // properties are skipped, and it is not tracked as a registered provider.
+ if (! $provider->isEnabled()) {
+ return $provider;
+ }
+
$provider->register();
// If there are bindings / singletons set as properties on the provider we
diff --git a/src/support/src/ServiceProvider.php b/src/support/src/ServiceProvider.php
index 90b9a5abb..26bc042e5 100644
--- a/src/support/src/ServiceProvider.php
+++ b/src/support/src/ServiceProvider.php
@@ -82,6 +82,20 @@ public function __construct(
) {
}
+ /**
+ * Determine whether this provider should be registered and booted.
+ *
+ * Hypervel-specific extension (not in Laravel). Override on a subclass to
+ * gate registration on runtime config / env / feature flags. When this
+ * returns false the provider is instantiated but neither register() nor
+ * boot() is called, its bindings/singletons properties are not processed,
+ * and it is not tracked in the application's provider list.
+ */
+ public function isEnabled(): bool
+ {
+ return true;
+ }
+
/**
* Register any application services.
*/
diff --git a/tests/Foundation/FoundationApplicationTest.php b/tests/Foundation/FoundationApplicationTest.php
index 8c185b57b..e2b586a8e 100644
--- a/tests/Foundation/FoundationApplicationTest.php
+++ b/tests/Foundation/FoundationApplicationTest.php
@@ -14,6 +14,7 @@
use Hypervel\Support\ServiceProvider;
use Hypervel\Tests\TestCase;
use Mockery as m;
+use RuntimeException;
use stdClass;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -63,6 +64,7 @@ public function testServiceProvidersAreCorrectlyRegistered()
{
$provider = m::mock(ApplicationBasicServiceProviderStub::class);
$class = get_class($provider);
+ $provider->shouldReceive('isEnabled')->andReturn(true);
$provider->shouldReceive('register')->once();
$app = new Application;
$app->register($provider);
@@ -114,6 +116,7 @@ public function testServiceProvidersAreCorrectlyRegisteredWhenRegisterMethodIsNo
{
$provider = m::mock(ServiceProvider::class);
$class = get_class($provider);
+ $provider->shouldReceive('isEnabled')->andReturn(true);
$provider->shouldReceive('register')->once();
$app = new Application;
$app->register($provider);
@@ -125,6 +128,7 @@ public function testServiceProvidersCouldBeLoaded()
{
$provider = m::mock(ServiceProvider::class);
$class = get_class($provider);
+ $provider->shouldReceive('isEnabled')->andReturn(true);
$provider->shouldReceive('register')->once();
$app = new Application;
$app->register($provider);
@@ -133,6 +137,63 @@ public function testServiceProvidersCouldBeLoaded()
$this->assertFalse($app->providerIsLoaded(ApplicationBasicServiceProviderStub::class));
}
+ public function testDisabledServiceProviderIsNotRegisteredOrTracked()
+ {
+ $app = new Application;
+ $app->register($provider = new ApplicationDisabledServiceProviderStub($app));
+
+ $this->assertArrayNotHasKey(get_class($provider), $app->getLoadedProviders());
+ $this->assertFalse($app->providerIsLoaded(get_class($provider)));
+ $this->assertNull($app->getProvider(get_class($provider)));
+ $this->assertSame([], $app->getProviders(get_class($provider)));
+ }
+
+ public function testDisabledServiceProviderBindingsArrayIsSkipped()
+ {
+ $app = new Application;
+ $app->register(new class($app) extends ServiceProvider {
+ public $bindings = [
+ AbstractClass::class => ConcreteClass::class,
+ ];
+
+ public function isEnabled(): bool
+ {
+ return false;
+ }
+ });
+
+ $this->assertFalse($app->bound(AbstractClass::class));
+ }
+
+ public function testDisabledServiceProviderSingletonsArrayIsSkipped()
+ {
+ $app = new Application;
+ $app->register(new class($app) extends ServiceProvider {
+ public $singletons = [
+ AbstractClass::class => ConcreteClass::class,
+ ];
+
+ public function isEnabled(): bool
+ {
+ return false;
+ }
+ });
+
+ $this->assertFalse($app->bound(AbstractClass::class));
+ }
+
+ public function testDisabledServiceProviderIsNotBootedWhenAppAlreadyBooted()
+ {
+ $app = new Application;
+ $app->boot();
+
+ // boot() throws if called — passing the late-register branch without
+ // the isEnabled() check would call bootProvider() and trigger it.
+ $app->register(new ApplicationDisabledServiceProviderStub($app));
+
+ $this->assertTrue($app->isBooted());
+ }
+
public function testEnvironment()
{
$app = new Application;
@@ -669,6 +730,24 @@ public function register(): void
}
}
+class ApplicationDisabledServiceProviderStub extends ServiceProvider
+{
+ public function isEnabled(): bool
+ {
+ return false;
+ }
+
+ public function register(): void
+ {
+ throw new RuntimeException('register() must not be called on a disabled provider');
+ }
+
+ public function boot(): void
+ {
+ throw new RuntimeException('boot() must not be called on a disabled provider');
+ }
+}
+
abstract class AbstractClass
{
}
diff --git a/tests/Support/SupportServiceProviderTest.php b/tests/Support/SupportServiceProviderTest.php
index 83dcb224c..4f5b9c849 100644
--- a/tests/Support/SupportServiceProviderTest.php
+++ b/tests/Support/SupportServiceProviderTest.php
@@ -7,8 +7,8 @@
use Hypervel\Config\Repository as ConfigRepository;
use Hypervel\Foundation\Application;
use Hypervel\Support\ServiceProvider;
+use Hypervel\Tests\TestCase;
use Mockery as m;
-use PHPUnit\Framework\TestCase;
class SupportServiceProviderTest extends TestCase
{
@@ -16,6 +16,8 @@ class SupportServiceProviderTest extends TestCase
protected function setUp(): void
{
+ parent::setUp();
+
$this->app = $app = m::mock(Application::class)->makePartial();
$config = new ConfigRepository([
@@ -29,6 +31,33 @@ protected function setUp(): void
$two->boot();
}
+ public function testIsEnabledReturnsTrueByDefault()
+ {
+ $provider = new ServiceProviderForTestingOne($this->app);
+
+ $this->assertTrue($provider->isEnabled());
+ }
+
+ public function testIsEnabledCanBeOverriddenToReturnFalse()
+ {
+ $provider = new ServiceProviderForTestingDisabled($this->app);
+
+ $this->assertFalse($provider->isEnabled());
+ }
+
+ public function testIsEnabledCanReadFromContainerAndConfig()
+ {
+ $config = new ConfigRepository(['package' => ['enabled' => true]]);
+ $app = m::mock(Application::class)->makePartial();
+ $app->shouldReceive('make')->with('config')->andReturn($config);
+
+ $provider = new ServiceProviderForTestingConditionallyEnabled($app);
+ $this->assertTrue($provider->isEnabled());
+
+ $config->set('package.enabled', false);
+ $this->assertFalse($provider->isEnabled());
+ }
+
public function testPublishableServiceProviders()
{
$toPublish = ServiceProvider::publishableProviders();
@@ -476,6 +505,22 @@ public function boot()
}
}
+class ServiceProviderForTestingDisabled extends ServiceProvider
+{
+ public function isEnabled(): bool
+ {
+ return false;
+ }
+}
+
+class ServiceProviderForTestingConditionallyEnabled extends ServiceProvider
+{
+ public function isEnabled(): bool
+ {
+ return (bool) $this->app->make('config')->get('package.enabled', false);
+ }
+}
+
class ServiceProviderForTestingFlat extends ServiceProvider
{
public function register(): void