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
183 changes: 179 additions & 4 deletions src/Phaseolies/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Phaseolies\Auth\ActorManager;
use Phaseolies\Support\Router;
use Phaseolies\Providers\GhostableProvider;
use Phaseolies\Providers\ServiceProvider;
use Phaseolies\Http\DispatchResult;
use Phaseolies\Http\Response;
Expand Down Expand Up @@ -127,13 +128,55 @@ class Application extends Container
*/
protected $serviceProviders = [];

/**
* The queued ghost providers keyed by class name.
*
* @var array<class-string<ServiceProvider>, ServiceProvider>
*/
protected array $ghostProviders = [];

/**
* Map of service identifiers to ghost provider classes.
*
* @var array<string, class-string<ServiceProvider>>
*/
protected array $ghostServices = [];

/**
* Tracks ghost providers that have already been loaded.
*
* @var array<class-string<ServiceProvider>, true>
*/
protected array $loadedGhostProviders = [];

/**
* Tracks ghost providers currently being loaded.
*
* @var array<class-string<ServiceProvider>, true>
*/
protected array $loadingGhostProviders = [];

/**
* Indicates if the providers has been booted
*
* @var bool
*/
protected $providersBooted = false;

/**
* Indicates if the application is currently booting eager providers.
*
* @var bool
*/
protected bool $bootingProviders = false;

/**
* Tracks providers whose boot method has already run.
*
* @var array<class-string<ServiceProvider>, true>
*/
protected array $bootedProviderClasses = [];

/**
* @var Router
*/
Expand Down Expand Up @@ -304,13 +347,64 @@ protected function registerProviders(array $providers = []): void
{
foreach ($providers as $provider) {
$providerInstance = new $provider($this);

if ($providerInstance instanceof ServiceProvider) {
$providerInstance->register();
$this->serviceProviders[] = $providerInstance;
if ($this->shouldQueueGhostProvider($providerInstance)) {
$this->queueGhostProvider($providerInstance);
continue;
}

$this->registerProviderInstance($providerInstance);
}
}
}

/**
* Determine if the provider should be queued as a ghost provider
*
* @param ServiceProvider $providerInstance
* @return bool
*/
protected function shouldQueueGhostProvider(ServiceProvider $providerInstance): bool
{
return !$this->runningInConsole() && $providerInstance instanceof GhostableProvider;
}

/**
* Register and track an eager provider instance.
*
* @param ServiceProvider $providerInstance
* @return void
*/
protected function registerProviderInstance(ServiceProvider $providerInstance): void
{
$providerInstance->register();

$this->serviceProviders[] = $providerInstance;
}

/**
* Queue a ghost provider until one of its services is requested.
*
* @param ServiceProvider $providerInstance
* @return void
*/
protected function queueGhostProvider(ServiceProvider $providerInstance): void
{
/** @var GhostableProvider $providerInstance */
$providerClass = $providerInstance::class;

$this->ghostProviders[$providerClass] = $providerInstance;

foreach ($providerInstance->ghosts() as $ghost) {
if (!is_string($ghost) || $ghost === '') {
continue;
}

$this->ghostServices[$ghost] = $providerClass;
}
}

/**
* Boots core service providers.
*
Expand All @@ -334,8 +428,14 @@ protected function bootCoreProviders(): self
*/
protected function bootProviders(): void
{
foreach ($this->serviceProviders as $providerInstance) {
$providerInstance->boot();
$this->bootingProviders = true;

try {
foreach ($this->serviceProviders as $providerInstance) {
$this->bootProviderInstance($providerInstance);
}
} finally {
$this->bootingProviders = false;
}

$this->bootstrap();
Expand Down Expand Up @@ -730,6 +830,81 @@ public function getProvider(string $provider): ?ServiceProvider
return null;
}

/**
* Determine if the application has a binding or queued ghost for the given service.
*
* @param string $key
* @return bool
*/
public function has(string $key): bool
{
return parent::has($key) || isset($this->ghostServices[$key]);
}

/**
* Load a queued ghost provider when one of its services is requested.
*
* @param string $abstract
* @return bool
*/
public function loadGhostProvider(string $abstract): bool
{
$providerClass = $this->ghostServices[$abstract] ?? null;

if ($providerClass === null) {
return false;
}

if (isset($this->loadedGhostProviders[$providerClass]) || isset($this->loadingGhostProviders[$providerClass])) {
return true;
}

$providerInstance = $this->ghostProviders[$providerClass] ?? null;

if (!$providerInstance instanceof ServiceProvider) {
return false;
}

$this->loadingGhostProviders[$providerClass] = true;

foreach (($providerInstance instanceof GhostableProvider ? $providerInstance->ghosts() : []) as $ghost) {
unset($this->ghostServices[$ghost]);
}

try {
$this->registerProviderInstance($providerInstance);

if ($this->providersBooted || $this->bootingProviders) {
$this->bootProviderInstance($providerInstance);
}

$this->loadedGhostProviders[$providerClass] = true;

return true;
} finally {
unset($this->loadingGhostProviders[$providerClass], $this->ghostProviders[$providerClass]);
}
}

/**
* Boot a provider instance once.
*
* @param ServiceProvider $providerInstance
* @return void
*/
protected function bootProviderInstance(ServiceProvider $providerInstance): void
{
$providerClass = $providerInstance::class;

if (isset($this->bootedProviderClasses[$providerClass])) {
return;
}

$providerInstance->boot();

$this->bootedProviderClasses[$providerClass] = true;
}

/**
* Determine if the application is in the given environment.
*
Expand Down
8 changes: 8 additions & 0 deletions src/Phaseolies/DI/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,14 @@ public function get(string $abstract, array $parameters = []): mixed
throw new \RuntimeException("Circular dependency detected while resolving [{$abstract}]");
}

if (
!isset(self::$bindings[$abstract]) &&
!array_key_exists($abstract, self::$instances) &&
method_exists($this, 'loadGhostProvider')
) {
$this->loadGhostProvider($abstract);
}

$this->resolving[$abstract] = true;

try {
Expand Down
18 changes: 17 additions & 1 deletion src/Phaseolies/Providers/CacheServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
use Symfony\Component\Cache\Adapter\ApcuAdapter;
use Psr\SimpleCache\CacheInterface;
use Phaseolies\Providers\ServiceProvider;
use Phaseolies\Providers\GhostableProvider;
use Phaseolies\Cache\CacheStore;
use Phaseolies\Cache\IncrementableCacheInterface;

class CacheServiceProvider extends ServiceProvider
class CacheServiceProvider extends ServiceProvider implements GhostableProvider
{
/**
* @var \Closure[] Custom adapter factories
Expand Down Expand Up @@ -158,4 +159,19 @@ public function boot()
{
//
}

/**
* Get the services that should ghost-load this provider.
*
* @return array<int, string>
*/
public function ghosts(): array
{
return [
CacheStore::class,
IncrementableCacheInterface::class,
CacheInterface::class,
'cache',
];
}
}
13 changes: 13 additions & 0 deletions src/Phaseolies/Providers/GhostableProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Phaseolies\Providers;

interface GhostableProvider
{
/**
* Get the service identifiers that should trigger loading this provider.
*
* @return array<int, string>
*/
public function ghosts(): array;
}
16 changes: 15 additions & 1 deletion src/Phaseolies/Providers/LanguageServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
use Phaseolies\Translation\FileLoader;
use Phaseolies\Translation\Translator;
use Phaseolies\Providers\ServiceProvider;
use Phaseolies\Providers\GhostableProvider;

class LanguageServiceProvider extends ServiceProvider
class LanguageServiceProvider extends ServiceProvider implements GhostableProvider
{
/**
* Register the service provider.
Expand Down Expand Up @@ -55,4 +56,17 @@ public function boot()
{
Lang::setFacadeApplication($this->app);
}

/**
* Get the services that should ghost-load this provider.
*
* @return array<int, string>
*/
public function ghosts(): array
{
return [
'translation.loader',
'translator',
];
}
}
15 changes: 14 additions & 1 deletion src/Phaseolies/Providers/RateLimiterServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
namespace Phaseolies\Providers;

use Phaseolies\Providers\ServiceProvider;
use Phaseolies\Providers\GhostableProvider;
use Phaseolies\Cache\IncrementableCacheInterface;
use Phaseolies\Cache\RateLimiter;

class RateLimiterServiceProvider extends ServiceProvider
class RateLimiterServiceProvider extends ServiceProvider implements GhostableProvider
{
/**
* Register the service provider.
Expand All @@ -29,4 +30,16 @@ public function boot()
{
//
}

/**
* Get the services that should ghost-load this provider.
*
* @return array<int, string>
*/
public function ghosts(): array
{
return [
RateLimiter::class,
];
}
}
Loading
Loading