Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions src/boost/docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

<a name="introduction"></a>
Expand Down Expand Up @@ -104,6 +107,39 @@ class AppServiceProvider extends ServiceProvider
}
```

<a name="merging-configuration"></a>
#### 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<int, string>
*/
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.

<a name="the-boot-method"></a>
### The Boot Method

Expand Down Expand Up @@ -150,6 +186,23 @@ public function boot(ResponseFactory $response): void
}
```

<a name="conditionally-loading-providers"></a>
### 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.

<a name="registering-providers"></a>
## Registering Providers

Expand All @@ -174,6 +227,25 @@ return [
];
```

<a name="provider-priority"></a>
### 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.

<a name="deferred-providers"></a>
## Deferred Providers

Expand Down
8 changes: 8 additions & 0 deletions src/foundation/src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/support/src/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
79 changes: 79 additions & 0 deletions tests/Foundation/FoundationApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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
{
}
Expand Down
47 changes: 46 additions & 1 deletion tests/Support/SupportServiceProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@
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
{
protected Application $app;

protected function setUp(): void
{
parent::setUp();

$this->app = $app = m::mock(Application::class)->makePartial();

$config = new ConfigRepository([
Expand All @@ -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();
Expand Down Expand Up @@ -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
Expand Down