diff --git a/src/FreeDSx/Ldap/Container.php b/src/FreeDSx/Ldap/Container.php index e90f0c1b..064bb43b 100644 --- a/src/FreeDSx/Ldap/Container.php +++ b/src/FreeDSx/Ldap/Container.php @@ -17,6 +17,7 @@ use FreeDSx\Ldap\Container\ConnectionGraphContainerProvider; use FreeDSx\Ldap\Container\ContainerProviderInterface; use FreeDSx\Ldap\Container\CoreServerContainerProvider; +use FreeDSx\Ldap\Container\HandlerContainerProvider; use FreeDSx\Ldap\Container\PasswordPolicyContainerProvider; use FreeDSx\Ldap\Exception\RuntimeException; use FreeDSx\Ldap\Protocol\ServerAuthorization; @@ -47,7 +48,7 @@ class Container /** * @param array $instances */ - public function __construct(array $instances) + private function __construct(array $instances) { foreach ($instances as $className => $instance) { $this->instances[$className] = $instance; @@ -63,6 +64,47 @@ public function __construct(array $instances) } } + /** + * @param array $sharedInstances services carried into this generation (e.g. test overrides). + */ + public static function forClient( + ClientOptions $options, + ?LdapClient $client = null, + array $sharedInstances = [], + ): self { + $instances = [ClientOptions::class => $options] + $sharedInstances; + + if ($client !== null) { + $instances[LdapClient::class] = $client; + } + + return new self($instances); + } + + /** + * @param array $sharedInstances services carried into this generation (e.g. across a reload). + */ + public static function forServer( + ServerOptions $options, + array $sharedInstances = [], + ): self { + return new self([ServerOptions::class => $options] + $sharedInstances); + } + + /** + * @param array $sharedInstances services carried into this generation (e.g. across a reload). + */ + public static function forProxy( + ServerOptions $options, + ProxyOptions $proxyOptions, + array $sharedInstances = [], + ): self { + return new self([ + ServerOptions::class => $options, + ProxyOptions::class => $proxyOptions, + ] + $sharedInstances); + } + /** * @template T of object * @param class-string $className @@ -122,6 +164,7 @@ private function providers(): array $providers[] = new CoreServerContainerProvider(); $providers[] = new PasswordPolicyContainerProvider(); $providers[] = new ConnectionGraphContainerProvider(); + $providers[] = new HandlerContainerProvider(); } return $providers; diff --git a/src/FreeDSx/Ldap/Container/CoreServerContainerProvider.php b/src/FreeDSx/Ldap/Container/CoreServerContainerProvider.php index cb4a4a2d..fe40df47 100644 --- a/src/FreeDSx/Ldap/Container/CoreServerContainerProvider.php +++ b/src/FreeDSx/Ldap/Container/CoreServerContainerProvider.php @@ -559,26 +559,32 @@ private function makeProtocolFactoryProvider(Container $container): Closure $inMemoryMetrics, $operationRollup, ): ServerProtocolFactoryInterface { - $instances = [ - ServerOptions::class => $options, + $sharedInstances = [ MetricsRecorderInterface::class => $metricsRecorder, MetricsSnapshotProvider::class => $metricsSnapshots, InMemoryMetricsRecorder::class => $inMemoryMetrics, ]; if ($backend !== null) { - $instances[WritableStorageBackend::class] = $backend; - } - - if ($proxyOptions !== null) { - $instances[ProxyOptions::class] = $proxyOptions; + $sharedInstances[WritableStorageBackend::class] = $backend; } if ($operationRollup !== null) { - $instances[OperationRollupCoordinator::class] = $operationRollup; + $sharedInstances[OperationRollupCoordinator::class] = $operationRollup; } - return (new Container($instances))->get(ServerProtocolFactoryInterface::class); + $container = $proxyOptions !== null + ? Container::forProxy( + $options, + $proxyOptions, + $sharedInstances, + ) + : Container::forServer( + $options, + $sharedInstances, + ); + + return $container->get(ServerProtocolFactoryInterface::class); }; } } diff --git a/src/FreeDSx/Ldap/Container/HandlerContainerProvider.php b/src/FreeDSx/Ldap/Container/HandlerContainerProvider.php new file mode 100644 index 00000000..372a96db --- /dev/null +++ b/src/FreeDSx/Ldap/Container/HandlerContainerProvider.php @@ -0,0 +1,281 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FreeDSx\Ldap\Container; + +use FreeDSx\Ldap\Container; +use FreeDSx\Ldap\Protocol\Factory\HandlerContext; +use FreeDSx\Ldap\Protocol\Factory\HandlerId; +use FreeDSx\Ldap\Protocol\Factory\ProtocolHandlerFactoryMap; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerAbandonHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerCancelHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerDispatchHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerMonitorHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerPagingHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerPasswordModifyHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerPasswordPolicyForwardHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerProtocolHandlerInterface; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerRootDseHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerSearchHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerStartTlsHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerSubschemaHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerSyncHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerUnbindHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerUnsupportedExtendedHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerWhoAmIHandler; +use FreeDSx\Ldap\Server\Backend\Auth\PasswordHashService; +use FreeDSx\Ldap\Server\Backend\LdapBackendInterface; +use FreeDSx\Ldap\Server\Backend\Storage\Journal\ChangeJournalInterface; +use FreeDSx\Ldap\Server\Backend\Storage\Journal\Read\ChangeStream; +use FreeDSx\Ldap\Server\Backend\Storage\WritableStorageBackend; +use FreeDSx\Ldap\Server\Backend\Write\WritableLdapBackendInterface; +use FreeDSx\Ldap\Server\Backend\Write\WriteOperationDispatcher; +use FreeDSx\Ldap\Server\Clock\Sleeper\SleeperInterface; +use FreeDSx\Ldap\Server\HandlerFactoryInterface; +use FreeDSx\Ldap\Server\Metrics\MetricsSnapshotProvider; +use FreeDSx\Ldap\Server\PasswordModify\PasswordModifyService; +use FreeDSx\Ldap\Server\PasswordModify\PasswordModifyTargetResolver; +use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyComponentFactory; +use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyEngine; +use FreeDSx\Ldap\Server\SearchLimits; +use FreeDSx\Ldap\ServerOptions; +use FreeDSx\Ldap\Sync\Provider\SyncPersistStreamer; +use FreeDSx\Ldap\Sync\Provider\SyncResultProjector; + +/** + * Registers the per-route handler factories; each finishes construction from a per-connection HandlerContext. + * + * @author Chad Sikorra + */ +final class HandlerContainerProvider implements ContainerProviderInterface +{ + public function factories(): array + { + return [ + ProtocolHandlerFactoryMap::class => $this->makeFactoryMap(...), + ]; + } + + private function makeFactoryMap(Container $container): ProtocolHandlerFactoryMap + { + return new ProtocolHandlerFactoryMap([ + HandlerId::Abandon->value => static fn(): ServerProtocolHandlerInterface + => new ServerAbandonHandler(), + HandlerId::Cancel->value => static fn(HandlerContext $context): ServerProtocolHandlerInterface + => new ServerCancelHandler($context->queue), + HandlerId::WhoAmI->value => static fn(HandlerContext $context): ServerProtocolHandlerInterface + => new ServerWhoAmIHandler($context->queue), + HandlerId::PasswordModify->value => fn(HandlerContext $context): ServerProtocolHandlerInterface + => $this->makePasswordModifyHandler($container, $context), + HandlerId::PasswordPolicyForward->value => fn(HandlerContext $context): ServerProtocolHandlerInterface + => $this->makeForwardHandler($container, $context), + HandlerId::StartTls->value => static fn(HandlerContext $context): ServerProtocolHandlerInterface + => new ServerStartTlsHandler( + options: $container->get(ServerOptions::class), + queue: $context->queue, + eventLogger: $context->eventLogger, + ), + HandlerId::UnsupportedExtended->value => static fn(HandlerContext $context): ServerProtocolHandlerInterface + => new ServerUnsupportedExtendedHandler($context->queue), + HandlerId::RootDse->value => fn(HandlerContext $context): ServerProtocolHandlerInterface + => $this->makeRootDseHandler($container, $context), + HandlerId::Subschema->value => static fn(HandlerContext $context): ServerProtocolHandlerInterface + => new ServerSubschemaHandler( + options: $container->get(ServerOptions::class), + queue: $context->queue, + ), + HandlerId::Monitor->value => static fn(HandlerContext $context): ServerProtocolHandlerInterface + => new ServerMonitorHandler( + options: $container->get(ServerOptions::class), + queue: $context->queue, + snapshots: $container->get(MetricsSnapshotProvider::class), + ), + HandlerId::Paging->value => fn(HandlerContext $context, ?SearchLimits $limits): ServerProtocolHandlerInterface + => $this->makePagingHandler($container, $context, $limits), + HandlerId::Sync->value => fn(HandlerContext $context, ?SearchLimits $limits): ServerProtocolHandlerInterface + => $this->makeSyncHandler($container, $context, $limits), + HandlerId::Search->value => fn(HandlerContext $context, ?SearchLimits $limits): ServerProtocolHandlerInterface + => $this->makeSearchHandler($container, $context, $limits), + HandlerId::Unbind->value => static fn(HandlerContext $context): ServerProtocolHandlerInterface + => new ServerUnbindHandler($context->queue), + HandlerId::Dispatch->value => fn(HandlerContext $context): ServerProtocolHandlerInterface + => $this->makeDispatchHandler($container, $context), + ]); + } + + private function makePasswordModifyHandler( + Container $container, + HandlerContext $context, + ): ServerPasswordModifyHandler { + $policyComponentFactory = $container->get(PasswordPolicyComponentFactory::class); + + return new ServerPasswordModifyHandler( + queue: $context->queue, + service: new PasswordModifyService( + targetResolver: $container->get(PasswordModifyTargetResolver::class), + accessControl: $container->get(ServerOptions::class)->getAccessControl(), + writeDispatcher: $container->get(WriteOperationDispatcher::class), + hashService: $container->get(PasswordHashService::class), + changeGuard: $policyComponentFactory->makeChangeGuard( + $context->eventLogger, + $context->passwordPolicyContext, + ), + passwordPolicyContext: $context->passwordPolicyContext, + ), + ); + } + + private function makeForwardHandler( + Container $container, + HandlerContext $context, + ): ServerProtocolHandlerInterface { + $backend = $container->get(HandlerFactoryInterface::class)->makeBackend(); + if (!$backend instanceof WritableLdapBackendInterface) { + return new ServerUnsupportedExtendedHandler($context->queue); + } + + return new ServerPasswordPolicyForwardHandler( + queue: $context->queue, + backend: $backend, + policyResolver: $container->get(PasswordPolicyComponentFactory::class)->makeResolver(), + engine: $container->get(PasswordPolicyEngine::class), + ); + } + + private function makeRootDseHandler( + Container $container, + HandlerContext $context, + ): ServerRootDseHandler { + $handlerFactory = $container->get(HandlerFactoryInterface::class); + $backend = $handlerFactory->makeBackend(); + + return new ServerRootDseHandler( + options: $container->get(ServerOptions::class), + queue: $context->queue, + backend: $backend, + rootDseHandler: $handlerFactory->makeRootDseHandler(), + supportsSync: $this->syncJournalFor($container, $backend) !== null, + ); + } + + private function makeSyncHandler( + Container $container, + HandlerContext $context, + ?SearchLimits $searchLimits, + ): ServerSyncHandler { + $options = $container->get(ServerOptions::class); + $backend = $container->get(HandlerFactoryInterface::class)->makeBackend(); + $journal = $this->syncJournalFor($container, $backend); + $projector = new SyncResultProjector( + accessControl: $options->getAccessControl(), + filterEvaluator: $options->getFilterEvaluator(), + eventLogger: $context->eventLogger, + ); + + $stream = null; + $streamer = null; + $persistSupported = false; + + if ($journal !== null) { + $stream = new ChangeStream($journal); + $streamer = new SyncPersistStreamer( + queue: $context->queue, + backend: $backend, + projector: $projector, + stream: $stream, + sleeper: $container->get(SleeperInterface::class), + ); + // Persist can only deliver writes made on other connections: a single process (Swoole) + // shares them in memory, otherwise the journal itself must be cross-process. + $persistSupported = $options->getUseSwooleRunner() + || $journal->sharesAcrossProcesses(); + } + + return new ServerSyncHandler( + queue: $context->queue, + backend: $backend, + projector: $projector, + limits: $searchLimits ?? $options->makeSearchLimits(), + changeStream: $stream, + persistStreamer: $streamer, + persistSupported: $persistSupported, + ); + } + + private function makeDispatchHandler( + Container $container, + HandlerContext $context, + ): ServerDispatchHandler { + $handlerFactory = $container->get(HandlerFactoryInterface::class); + $policyWriteHandler = $container->get(PasswordPolicyComponentFactory::class)->makeWriteHandler( + $context->eventLogger, + $context->passwordPolicyContext, + ); + + return new ServerDispatchHandler( + queue: $context->queue, + backend: $handlerFactory->makeBackend(), + writeDispatcher: $policyWriteHandler !== null + ? $handlerFactory->makeWriteDispatcher($policyWriteHandler) + : $container->get(WriteOperationDispatcher::class), + accessControl: $container->get(ServerOptions::class)->getAccessControl(), + schema: $container->get(ServerOptions::class)->getSchema(), + ); + } + + private function makeSearchHandler( + Container $container, + HandlerContext $context, + ?SearchLimits $searchLimits, + ): ServerSearchHandler { + $options = $container->get(ServerOptions::class); + + return new ServerSearchHandler( + queue: $context->queue, + backend: $container->get(HandlerFactoryInterface::class)->makeBackend(), + filterEvaluator: $options->getFilterEvaluator(), + accessControl: $options->getAccessControl(), + schema: $options->getSchema(), + limits: $searchLimits ?? $options->makeSearchLimits(), + ); + } + + private function makePagingHandler( + Container $container, + HandlerContext $context, + ?SearchLimits $searchLimits, + ): ServerPagingHandler { + $options = $container->get(ServerOptions::class); + + return new ServerPagingHandler( + queue: $context->queue, + backend: $container->get(HandlerFactoryInterface::class)->makeBackend(), + filterEvaluator: $options->getFilterEvaluator(), + accessControl: $options->getAccessControl(), + requestHistory: $context->requestHistory, + schema: $options->getSchema(), + limits: $searchLimits ?? $options->makeSearchLimits(), + ); + } + + private function syncJournalFor( + Container $container, + LdapBackendInterface $backend, + ): ?ChangeJournalInterface { + if (!$container->get(ServerOptions::class)->isSyncEnabled() || !$backend instanceof WritableStorageBackend) { + return null; + } + + return $backend->changeJournal(); + } +} diff --git a/src/FreeDSx/Ldap/LdapClient.php b/src/FreeDSx/Ldap/LdapClient.php index 03f406e4..9cb4d4d6 100644 --- a/src/FreeDSx/Ldap/LdapClient.php +++ b/src/FreeDSx/Ldap/LdapClient.php @@ -68,10 +68,10 @@ public function __construct( private ClientOptions $options = new ClientOptions(), ?Container $container = null, ) { - $this->container = $container ?? new Container([ - ClientOptions::class => $this->options, - LdapClient::class => $this, - ]); + $this->container = $container ?? Container::forClient( + $this->options, + $this, + ); } /** diff --git a/src/FreeDSx/Ldap/LdapServer.php b/src/FreeDSx/Ldap/LdapServer.php index b13cf14b..07e82991 100644 --- a/src/FreeDSx/Ldap/LdapServer.php +++ b/src/FreeDSx/Ldap/LdapServer.php @@ -53,9 +53,7 @@ public function __construct( private readonly ServerOptions $options = new ServerOptions(), ?Container $container = null, ) { - $this->container = $container ?? new Container([ - ServerOptions::class => $this->options, - ]); + $this->container = $container ?? Container::forServer($this->options); } /** @@ -185,10 +183,10 @@ public static function makeProxy( ): LdapServer { return new LdapServer( $serverOptions, - new Container([ - ServerOptions::class => $serverOptions, - ProxyOptions::class => $proxyOptions, - ]), + Container::forProxy( + $serverOptions, + $proxyOptions, + ), ); } diff --git a/src/FreeDSx/Ldap/Protocol/Factory/HandlerContext.php b/src/FreeDSx/Ldap/Protocol/Factory/HandlerContext.php new file mode 100644 index 00000000..d20a4e81 --- /dev/null +++ b/src/FreeDSx/Ldap/Protocol/Factory/HandlerContext.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FreeDSx\Ldap\Protocol\Factory; + +use FreeDSx\Ldap\Protocol\Queue\ServerQueue; +use FreeDSx\Ldap\Server\Logging\EventLogger; +use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyContext; +use FreeDSx\Ldap\Server\RequestHistory; + +/** + * The per-connection roots a handler factory needs to finish constructing a handler. + * + * @internal + * @author Chad Sikorra + */ +final readonly class HandlerContext +{ + public function __construct( + public ServerQueue $queue, + public EventLogger $eventLogger, + public RequestHistory $requestHistory, + public ?PasswordPolicyContext $passwordPolicyContext = null, + ) {} +} diff --git a/src/FreeDSx/Ldap/Protocol/Factory/ProtocolHandlerFactoryMap.php b/src/FreeDSx/Ldap/Protocol/Factory/ProtocolHandlerFactoryMap.php new file mode 100644 index 00000000..e68068f2 --- /dev/null +++ b/src/FreeDSx/Ldap/Protocol/Factory/ProtocolHandlerFactoryMap.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FreeDSx\Ldap\Protocol\Factory; + +use FreeDSx\Ldap\Exception\RuntimeException; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerProtocolHandlerInterface; +use FreeDSx\Ldap\Server\SearchLimits; + +use function sprintf; + +/** + * A per-route handler factory keyed by HandlerId value; each factory finishes construction from a HandlerContext. + * + * @internal + * @author Chad Sikorra + */ +final readonly class ProtocolHandlerFactoryMap +{ + /** + * @param array $factories keyed by HandlerId value. + */ + public function __construct(private array $factories) {} + + /** + * @throws RuntimeException when the route has no registered factory. + */ + public function make( + HandlerId $handlerId, + HandlerContext $context, + ?SearchLimits $searchLimits = null, + ): ServerProtocolHandlerInterface { + $factory = $this->factories[$handlerId->value] + ?? throw new RuntimeException(sprintf( + 'No handler factory is registered for the "%s" route.', + $handlerId->value, + )); + + return $factory( + $context, + $searchLimits, + ); + } +} diff --git a/src/FreeDSx/Ldap/Protocol/Factory/ProtocolHandlerProvider.php b/src/FreeDSx/Ldap/Protocol/Factory/ProtocolHandlerProvider.php index ef84a944..416fc6eb 100644 --- a/src/FreeDSx/Ldap/Protocol/Factory/ProtocolHandlerProvider.php +++ b/src/FreeDSx/Ldap/Protocol/Factory/ProtocolHandlerProvider.php @@ -15,35 +15,11 @@ use FreeDSx\Ldap\Control\ControlBag; use FreeDSx\Ldap\Operation\Request\RequestInterface; -use FreeDSx\Ldap\Protocol\Queue\ServerQueue; -use FreeDSx\Ldap\Protocol\ServerProtocolHandler; use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerProtocolHandlerInterface; -use FreeDSx\Ldap\Server\Backend\Auth\PasswordHashService; -use FreeDSx\Ldap\Server\Backend\LdapBackendInterface; -use FreeDSx\Ldap\Server\Backend\Storage\Journal\ChangeJournalInterface; -use FreeDSx\Ldap\Server\Backend\Storage\Journal\Read\ChangeStream; -use FreeDSx\Ldap\Server\Backend\Storage\WritableStorageBackend; -use FreeDSx\Ldap\Server\Backend\Write\WritableLdapBackendInterface; -use FreeDSx\Ldap\Server\Backend\Write\WriteOperationDispatcher; -use FreeDSx\Ldap\Server\Clock\Sleeper\BlockingSleeper; -use FreeDSx\Ldap\Server\Clock\Sleeper\CoroutineSleeper; -use FreeDSx\Ldap\Server\HandlerFactoryInterface; -use FreeDSx\Ldap\Server\Logging\EventLogger; -use FreeDSx\Ldap\Server\Metrics\MetricsSnapshotProvider; -use FreeDSx\Ldap\Server\Metrics\Recorder\InMemoryMetricsRecorder; -use FreeDSx\Ldap\Server\PasswordModify\PasswordModifyService; -use FreeDSx\Ldap\Server\PasswordModify\PasswordModifyTargetResolver; -use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyComponentFactory; -use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyContext; -use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyEngine; -use FreeDSx\Ldap\Server\RequestHistory; use FreeDSx\Ldap\Server\SearchLimits; -use FreeDSx\Ldap\ServerOptions; -use FreeDSx\Ldap\Sync\Provider\SyncPersistStreamer; -use FreeDSx\Ldap\Sync\Provider\SyncResultProjector; /** - * Builds the per-request protocol handler, wiring per-connection state to shared services. + * Routes a request to its protocol handler; construction is delegated to the per-route factory map. * * @internal * @author Chad Sikorra @@ -52,18 +28,8 @@ { public function __construct( private HandlerRouteResolverInterface $routeResolver, - private HandlerFactoryInterface $handlerFactory, - private ServerOptions $options, - private PasswordModifyTargetResolver $targetResolver, - private PasswordHashService $hashService, - private WriteOperationDispatcher $writeDispatcher, - private PasswordPolicyComponentFactory $policyComponentFactory, - private PasswordPolicyEngine $passwordPolicyEngine, - private ServerQueue $queue, - private EventLogger $eventLogger, - private RequestHistory $requestHistory, - private ?PasswordPolicyContext $passwordPolicyContext = null, - private MetricsSnapshotProvider $metricsSnapshots = new InMemoryMetricsRecorder(), + private ProtocolHandlerFactoryMap $factories, + private HandlerContext $context, ) {} public function get( @@ -71,197 +37,13 @@ public function get( ControlBag $controls, ?SearchLimits $searchLimits = null, ): ServerProtocolHandlerInterface { - return match ($this->routeResolver->routeIdFor($request, $controls)) { - HandlerId::Abandon => new ServerProtocolHandler\ServerAbandonHandler(), - HandlerId::Cancel => new ServerProtocolHandler\ServerCancelHandler($this->queue), - HandlerId::WhoAmI => new ServerProtocolHandler\ServerWhoAmIHandler($this->queue), - HandlerId::PasswordModify => $this->getPasswordModifyHandler(), - HandlerId::PasswordPolicyForward => $this->getPasswordPolicyForwardHandler(), - HandlerId::StartTls => $this->getStartTlsHandler(), - HandlerId::UnsupportedExtended => new ServerProtocolHandler\ServerUnsupportedExtendedHandler($this->queue), - HandlerId::RootDse => $this->getRootDseHandler(), - HandlerId::Subschema => $this->getSubschemaHandler(), - HandlerId::Monitor => $this->getMonitorHandler(), - HandlerId::Paging => $this->getPagingHandler($searchLimits), - HandlerId::Sync => $this->getSyncHandler($searchLimits), - HandlerId::Search => $this->getSearchHandler($searchLimits), - HandlerId::Unbind => new ServerProtocolHandler\ServerUnbindHandler($this->queue), - HandlerId::Dispatch => $this->getDispatchHandler(), - }; - } - - private function getPasswordModifyHandler(): ServerProtocolHandler\ServerPasswordModifyHandler - { - return new ServerProtocolHandler\ServerPasswordModifyHandler( - queue: $this->queue, - service: new PasswordModifyService( - targetResolver: $this->targetResolver, - accessControl: $this->options->getAccessControl(), - writeDispatcher: $this->writeDispatcher, - hashService: $this->hashService, - changeGuard: $this->policyComponentFactory->makeChangeGuard( - $this->eventLogger, - $this->passwordPolicyContext, - ), - passwordPolicyContext: $this->passwordPolicyContext, + return $this->factories->make( + $this->routeResolver->routeIdFor( + $request, + $controls, ), - ); - } - - private function getPasswordPolicyForwardHandler(): ServerProtocolHandler\ServerProtocolHandlerInterface - { - $backend = $this->handlerFactory->makeBackend(); - if (!$backend instanceof WritableLdapBackendInterface) { - return new ServerProtocolHandler\ServerUnsupportedExtendedHandler($this->queue); - } - - return new ServerProtocolHandler\ServerPasswordPolicyForwardHandler( - queue: $this->queue, - backend: $backend, - policyResolver: $this->policyComponentFactory->makeResolver(), - engine: $this->passwordPolicyEngine, - ); - } - - private function getStartTlsHandler(): ServerProtocolHandler\ServerStartTlsHandler - { - return new ServerProtocolHandler\ServerStartTlsHandler( - options: $this->options, - queue: $this->queue, - eventLogger: $this->eventLogger, - ); - } - - private function getSubschemaHandler(): ServerProtocolHandler\ServerSubschemaHandler - { - return new ServerProtocolHandler\ServerSubschemaHandler( - options: $this->options, - queue: $this->queue, - ); - } - - private function getMonitorHandler(): ServerProtocolHandler\ServerMonitorHandler - { - return new ServerProtocolHandler\ServerMonitorHandler( - options: $this->options, - queue: $this->queue, - snapshots: $this->metricsSnapshots, - ); - } - - private function getSearchHandler(?SearchLimits $searchLimits): ServerProtocolHandler\ServerSearchHandler - { - return new ServerProtocolHandler\ServerSearchHandler( - queue: $this->queue, - backend: $this->handlerFactory->makeBackend(), - filterEvaluator: $this->options->getFilterEvaluator(), - accessControl: $this->options->getAccessControl(), - schema: $this->options->getSchema(), - limits: $searchLimits ?? $this->options->makeSearchLimits(), - ); - } - - private function getSyncHandler(?SearchLimits $searchLimits): ServerProtocolHandler\ServerSyncHandler - { - $backend = $this->handlerFactory->makeBackend(); - $journal = $this->syncJournalFor($backend); - $projector = new SyncResultProjector( - accessControl: $this->options->getAccessControl(), - filterEvaluator: $this->options->getFilterEvaluator(), - eventLogger: $this->eventLogger, - ); - - $stream = null; - $streamer = null; - $persistSupported = false; - - if ($journal !== null) { - $stream = new ChangeStream($journal); - $streamer = new SyncPersistStreamer( - queue: $this->queue, - backend: $backend, - projector: $projector, - stream: $stream, - sleeper: $this->options->getUseSwooleRunner() - ? new CoroutineSleeper() - : new BlockingSleeper(), - ); - // Persist can only deliver writes made on other connections: a single process (Swoole) - // shares them in memory, otherwise the journal itself must be cross-process. - $persistSupported = $this->options->getUseSwooleRunner() - || $journal->sharesAcrossProcesses(); - } - - return new ServerProtocolHandler\ServerSyncHandler( - queue: $this->queue, - backend: $backend, - projector: $projector, - limits: $searchLimits ?? $this->options->makeSearchLimits(), - changeStream: $stream, - persistStreamer: $streamer, - persistSupported: $persistSupported, - ); - } - - private function changeStreamFor(LdapBackendInterface $backend): ?ChangeStream - { - $journal = $this->syncJournalFor($backend); - - return $journal !== null - ? new ChangeStream($journal) - : null; - } - - private function syncJournalFor(LdapBackendInterface $backend): ?ChangeJournalInterface - { - if (!$this->options->isSyncEnabled() || !$backend instanceof WritableStorageBackend) { - return null; - } - - return $backend->changeJournal(); - } - - private function getDispatchHandler(): ServerProtocolHandler\ServerDispatchHandler - { - $policyWriteHandler = $this->policyComponentFactory->makeWriteHandler( - $this->eventLogger, - $this->passwordPolicyContext, - ); - - return new ServerProtocolHandler\ServerDispatchHandler( - queue: $this->queue, - backend: $this->handlerFactory->makeBackend(), - writeDispatcher: $policyWriteHandler !== null - ? $this->handlerFactory->makeWriteDispatcher($policyWriteHandler) - : $this->writeDispatcher, - accessControl: $this->options->getAccessControl(), - schema: $this->options->getSchema(), - ); - } - - private function getRootDseHandler(): ServerProtocolHandler\ServerRootDseHandler - { - $backend = $this->handlerFactory->makeBackend(); - - return new ServerProtocolHandler\ServerRootDseHandler( - options: $this->options, - queue: $this->queue, - backend: $backend, - rootDseHandler: $this->handlerFactory->makeRootDseHandler(), - supportsSync: $this->changeStreamFor($backend) !== null, - ); - } - - private function getPagingHandler(?SearchLimits $searchLimits): ServerProtocolHandler\ServerPagingHandler - { - return new ServerProtocolHandler\ServerPagingHandler( - queue: $this->queue, - backend: $this->handlerFactory->makeBackend(), - filterEvaluator: $this->options->getFilterEvaluator(), - accessControl: $this->options->getAccessControl(), - requestHistory: $this->requestHistory, - schema: $this->options->getSchema(), - limits: $searchLimits ?? $this->options->makeSearchLimits(), + $this->context, + $searchLimits, ); } } diff --git a/src/FreeDSx/Ldap/Server/ConnectionHandlerBuilder.php b/src/FreeDSx/Ldap/Server/ConnectionHandlerBuilder.php index f7fb0fae..2ae77ff5 100644 --- a/src/FreeDSx/Ldap/Server/ConnectionHandlerBuilder.php +++ b/src/FreeDSx/Ldap/Server/ConnectionHandlerBuilder.php @@ -25,6 +25,8 @@ use FreeDSx\Ldap\Protocol\Bind\Sasl\SaslExchange; use FreeDSx\Ldap\Protocol\Bind\SaslBind; use FreeDSx\Ldap\Protocol\Bind\SimpleBind; +use FreeDSx\Ldap\Protocol\Factory\HandlerContext; +use FreeDSx\Ldap\Protocol\Factory\ProtocolHandlerFactoryMap; use FreeDSx\Ldap\Protocol\Factory\ProtocolHandlerProvider; use FreeDSx\Ldap\Protocol\Factory\ProtocolHandlerProviderInterface; use FreeDSx\Ldap\Protocol\Factory\ResponseFactory; @@ -40,13 +42,11 @@ use FreeDSx\Ldap\Server\Backend\Auth\PasswordPolicyAwareAuthenticator; use FreeDSx\Ldap\Server\Backend\Auth\SaslBindPolicyEnforcer; use FreeDSx\Ldap\Server\Backend\LdapBackendInterface; -use FreeDSx\Ldap\Server\Backend\Write\WriteOperationDispatcher; use FreeDSx\Ldap\Server\Clock\Sleeper\SleeperInterface; use FreeDSx\Ldap\Server\Logging\ConnectionContext; use FreeDSx\Ldap\Server\Logging\EventLogger; use FreeDSx\Ldap\Server\Logging\OperationAuditor; use FreeDSx\Ldap\Server\Metrics\MetricsRecorderInterface; -use FreeDSx\Ldap\Server\Metrics\MetricsSnapshotProvider; use FreeDSx\Ldap\Server\Metrics\Recorder\NullMetricsRecorder; use FreeDSx\Ldap\Server\Middleware\AssertionMiddleware; use FreeDSx\Ldap\Server\Middleware\AuthorizationResolutionMiddleware; @@ -61,7 +61,6 @@ use FreeDSx\Ldap\Server\Middleware\ReadOnlyMiddleware; use FreeDSx\Ldap\Server\Middleware\RequestValidationMiddleware; use FreeDSx\Ldap\Server\Middleware\ResourceLimitMiddleware; -use FreeDSx\Ldap\Server\PasswordModify\PasswordModifyTargetResolver; use FreeDSx\Ldap\Server\PasswordPolicy\Guard\BindStrategy\PasswordPolicyBindStrategyInterface; use FreeDSx\Ldap\Server\PasswordPolicy\Guard\PasswordPolicyBindGuard; use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyComponentFactory; @@ -350,18 +349,13 @@ private function makeProtocolHandlerProvider( ): ProtocolHandlerProvider { return new ProtocolHandlerProvider( routeResolver: $this->container->get(ServerProtocolHandlerFactory::class), - handlerFactory: $this->container->get(HandlerFactoryInterface::class), - options: $this->serverOptions(), - targetResolver: $this->container->get(PasswordModifyTargetResolver::class), - hashService: $this->container->get(PasswordHashService::class), - writeDispatcher: $this->container->get(WriteOperationDispatcher::class), - policyComponentFactory: $this->container->get(PasswordPolicyComponentFactory::class), - passwordPolicyEngine: $this->container->get(PasswordPolicyEngine::class), - queue: $queue, - eventLogger: $eventLogger, - requestHistory: $requestHistory, - passwordPolicyContext: $policyContext, - metricsSnapshots: $this->container->get(MetricsSnapshotProvider::class), + factories: $this->container->get(ProtocolHandlerFactoryMap::class), + context: new HandlerContext( + queue: $queue, + eventLogger: $eventLogger, + requestHistory: $requestHistory, + passwordPolicyContext: $policyContext, + ), ); } diff --git a/tests/unit/ContainerTest.php b/tests/unit/ContainerTest.php index cc9ff40e..4b601fae 100644 --- a/tests/unit/ContainerTest.php +++ b/tests/unit/ContainerTest.php @@ -13,13 +13,13 @@ namespace Tests\Unit\FreeDSx\Ldap; -use FreeDSx\Ldap\ClientOptions; use FreeDSx\Ldap\Container; use FreeDSx\Ldap\LdapClient; use FreeDSx\Ldap\Protocol\ClientProtocolHandler; use FreeDSx\Ldap\Protocol\Factory\ClientProtocolHandlerFactory; use FreeDSx\Ldap\Protocol\Queue\ClientQueueInstantiator; use FreeDSx\Ldap\Protocol\RootDseLoader; +use FreeDSx\Ldap\Protocol\Factory\ProtocolHandlerFactoryMap; use FreeDSx\Ldap\Protocol\Queue\Response\MetricsResponseInterceptor; use FreeDSx\Ldap\Protocol\ServerAuthorization; use FreeDSx\Ldap\Protocol\ServerProtocolHandler\AssertionEvaluator; @@ -58,14 +58,7 @@ protected function setUp(): void { parent::setUp(); - $client = new LdapClient(); - $serverOptions = (new ServerOptions())->useInMemoryStorage(); - - $this->subject = new Container([ - LdapClient::class => $client, - ClientOptions::class => $client->getOptions(), - ServerOptions::class => $serverOptions, - ]); + $this->subject = Container::forServer((new ServerOptions())->useInMemoryStorage()); } public function test_it_assembles_the_backend_from_the_configured_storage(): void @@ -79,7 +72,7 @@ public function test_it_assembles_the_backend_from_the_configured_storage(): voi /** * @return array */ - public static function buildableDependenciesDataProvider(): array + public static function clientDependenciesDataProvider(): array { return [ [LdapClient::class], @@ -88,6 +81,15 @@ public static function buildableDependenciesDataProvider(): array [ClientProtocolHandlerFactory::class], [SocketPool::class], [RootDseLoader::class], + ]; + } + + /** + * @return array + */ + public static function serverDependenciesDataProvider(): array + { + return [ [ServerProtocolFactory::class], [HandlerFactoryInterface::class], [ServerAuthorization::class], @@ -105,6 +107,7 @@ public static function buildableDependenciesDataProvider(): array [OperationAuthorizationMiddleware::class], [AssertionMiddleware::class], [ResourceLimitMiddleware::class], + [ProtocolHandlerFactoryMap::class], ]; } @@ -119,8 +122,21 @@ public function test_the_sleeper_is_blocking_under_the_default_pcntl_runner(): v /** * @param class-string $class */ - #[DataProvider('buildableDependenciesDataProvider')] - public function test_it_builds_the_dependencies( + #[DataProvider('clientDependenciesDataProvider')] + public function test_it_builds_the_client_dependencies( + string $class, + ): void { + self::assertInstanceOf( + $class, + $this->clientContainer()->get($class), + ); + } + + /** + * @param class-string $class + */ + #[DataProvider('serverDependenciesDataProvider')] + public function test_it_builds_the_server_dependencies( string $class, ): void { self::assertInstanceOf( @@ -227,6 +243,16 @@ public function test_the_swoole_runner_builds_with_a_retention_sweeper(): void ); } + private function clientContainer(): Container + { + $client = new LdapClient(); + + return Container::forClient( + $client->getOptions(), + $client, + ); + } + private function journalingOptions(): ServerOptions { return (new ServerOptions()) @@ -238,8 +264,6 @@ private function journalingOptions(): ServerOptions private function containerFor(ServerOptions $options): Container { - return new Container([ - ServerOptions::class => $options->useInMemoryStorage(), - ]); + return Container::forServer($options->useInMemoryStorage()); } } diff --git a/tests/unit/LdapClientTest.php b/tests/unit/LdapClientTest.php index 17db2414..4d9daf3d 100644 --- a/tests/unit/LdapClientTest.php +++ b/tests/unit/LdapClientTest.php @@ -61,8 +61,9 @@ protected function setUp(): void ->method('isInstantiatedAndConnected') ->willReturn(false); - $this->container = new Container( - [ + $this->container = Container::forClient( + new ClientOptions(), + sharedInstances: [ ClientProtocolHandler::class => $this->mockHandler, ClientQueueInstantiator::class => $this->mockQueueInstantiator, ], diff --git a/tests/unit/Protocol/Factory/ProtocolHandlerProviderTest.php b/tests/unit/Protocol/Factory/ProtocolHandlerProviderTest.php index 53eb8381..7865d5fc 100644 --- a/tests/unit/Protocol/Factory/ProtocolHandlerProviderTest.php +++ b/tests/unit/Protocol/Factory/ProtocolHandlerProviderTest.php @@ -13,18 +13,23 @@ namespace Tests\Unit\FreeDSx\Ldap\Protocol\Factory; +use FreeDSx\Ldap\Container; use FreeDSx\Ldap\Control\ControlBag; use FreeDSx\Ldap\Control\PagingControl; use FreeDSx\Ldap\Entry\Entry; use FreeDSx\Ldap\Exception\RuntimeException; use FreeDSx\Ldap\Operation\Request\ExtendedRequest; use FreeDSx\Ldap\Operations; +use FreeDSx\Ldap\Protocol\Factory\HandlerContext; +use FreeDSx\Ldap\Protocol\Factory\HandlerId; +use FreeDSx\Ldap\Protocol\Factory\ProtocolHandlerFactoryMap; use FreeDSx\Ldap\Protocol\Factory\ProtocolHandlerProvider; use FreeDSx\Ldap\Protocol\Factory\ServerProtocolHandlerFactory; use FreeDSx\Ldap\Protocol\Queue\ServerQueue; use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerDispatchHandler; use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerPagingHandler; use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerPasswordModifyHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerProtocolHandlerInterface; use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerRootDseHandler; use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerSearchHandler; use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerStartTlsHandler; @@ -34,125 +39,29 @@ use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerWhoAmIHandler; use FreeDSx\Ldap\Search\Filter\EqualityFilter; use FreeDSx\Ldap\Server\Backend\Auth\NameResolver\DnBindNameResolver; -use FreeDSx\Ldap\Server\Backend\Auth\PasswordHashService; use FreeDSx\Ldap\Server\Backend\LdapBackendInterface; -use FreeDSx\Ldap\Server\Backend\Storage\Adapter\InMemoryStorage; -use FreeDSx\Ldap\Server\Backend\Storage\WritableStorageBackend; use FreeDSx\Ldap\Server\Backend\Write\WriteOperationDispatcher; -use FreeDSx\Ldap\Server\Clock\SystemClock; use FreeDSx\Ldap\Server\HandlerFactoryInterface; use FreeDSx\Ldap\Server\Logging\EventLogger; -use FreeDSx\Ldap\Server\PasswordModify\PasswordModifyTargetResolver; -use FreeDSx\Ldap\Server\PasswordPolicy\Constraint\PasswordChangeConstraintChain; use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicy; -use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyComponentFactory; use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyContext; -use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyEngine; use FreeDSx\Ldap\Server\RequestHistory; use FreeDSx\Ldap\ServerOptions; -use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; final class ProtocolHandlerProviderTest extends TestCase { - private ServerQueue&MockObject $mockQueue; - - private HandlerFactoryInterface&MockObject $mockHandlerFactory; - private ProtocolHandlerProvider $subject; protected function setUp(): void { - $this->mockQueue = $this->createMock(ServerQueue::class); - $this->mockHandlerFactory = $this->createMock(HandlerFactoryInterface::class); - $backend = new WritableStorageBackend(new InMemoryStorage()); - - $this->mockHandlerFactory - ->method('makeBackend') - ->willReturn($backend); - $this->mockHandlerFactory - ->method('makeWriteDispatcher') - ->willReturn(new WriteOperationDispatcher()); - $this->mockHandlerFactory - ->method('makeIdentityResolverChain') - ->willReturn(new DnBindNameResolver()); - - $options = new ServerOptions(); - $writeDispatcher = new WriteOperationDispatcher(); + $container = Container::forServer((new ServerOptions())->useInMemoryStorage()); $this->subject = new ProtocolHandlerProvider( - routeResolver: new ServerProtocolHandlerFactory($options), - handlerFactory: $this->mockHandlerFactory, - options: $options, - targetResolver: new PasswordModifyTargetResolver( - $backend, - new DnBindNameResolver(), - ), - hashService: new PasswordHashService(), - writeDispatcher: $writeDispatcher, - policyComponentFactory: new PasswordPolicyComponentFactory( - $this->mockHandlerFactory, - $options, - $writeDispatcher, - new PasswordPolicyEngine( - new SystemClock(), - new PasswordChangeConstraintChain([]), - ), - ), - passwordPolicyEngine: new PasswordPolicyEngine( - new SystemClock(), - new PasswordChangeConstraintChain([]), - ), - queue: $this->mockQueue, - eventLogger: new EventLogger(null), - requestHistory: new RequestHistory(), - passwordPolicyContext: null, - ); - } - - public function test_it_throws_when_password_policy_is_enabled_but_the_backend_is_not_writable(): void - { - $options = (new ServerOptions())->setPasswordPolicy(new PasswordPolicy()); - $handlerFactory = $this->createMock(HandlerFactoryInterface::class); - $handlerFactory - ->method('makeBackend') - ->willReturn($this->createMock(LdapBackendInterface::class)); - $writeDispatcher = new WriteOperationDispatcher(); - - $provider = new ProtocolHandlerProvider( - routeResolver: new ServerProtocolHandlerFactory($options), - handlerFactory: $handlerFactory, - options: $options, - targetResolver: new PasswordModifyTargetResolver( - $handlerFactory->makeBackend(), - new DnBindNameResolver(), - ), - hashService: new PasswordHashService(), - writeDispatcher: $writeDispatcher, - policyComponentFactory: new PasswordPolicyComponentFactory( - $handlerFactory, - $options, - $writeDispatcher, - new PasswordPolicyEngine( - new SystemClock(), - new PasswordChangeConstraintChain([]), - ), - ), - passwordPolicyEngine: new PasswordPolicyEngine( - new SystemClock(), - new PasswordChangeConstraintChain([]), - ), - queue: $this->mockQueue, - eventLogger: new EventLogger(null), - requestHistory: new RequestHistory(), - passwordPolicyContext: new PasswordPolicyContext(), - ); - - $this->expectException(RuntimeException::class); - - $provider->get( - Operations::delete('cn=foo,dc=bar'), - new ControlBag(), + routeResolver: $container->get(ServerProtocolHandlerFactory::class), + factories: $container->get(ProtocolHandlerFactoryMap::class), + context: $this->handlerContext(), ); } @@ -282,4 +191,71 @@ public function test_it_should_get_the_dispatch_handler_for_common_requests(): v $this->subject->get(Operations::rename('cn=foo', 'cn=foo'), new ControlBag()), ); } + + /** + * @return array + */ + public static function handlerIdProvider(): array + { + return array_map( + static fn(HandlerId $handlerId): array => [$handlerId], + HandlerId::cases(), + ); + } + + #[DataProvider('handlerIdProvider')] + public function test_every_route_has_a_registered_factory(HandlerId $handlerId): void + { + $container = Container::forServer((new ServerOptions())->useInMemoryStorage()); + + self::assertInstanceOf( + ServerProtocolHandlerInterface::class, + $container->get(ProtocolHandlerFactoryMap::class)->make( + $handlerId, + $this->handlerContext(), + ), + ); + } + + public function test_it_throws_when_password_policy_is_enabled_but_the_backend_is_not_writable(): void + { + $handlerFactory = $this->createMock(HandlerFactoryInterface::class); + $handlerFactory + ->method('makeBackend') + ->willReturn($this->createMock(LdapBackendInterface::class)); + $handlerFactory + ->method('makeWriteDispatcher') + ->willReturn(new WriteOperationDispatcher()); + $handlerFactory + ->method('makeIdentityResolverChain') + ->willReturn(new DnBindNameResolver()); + + $container = Container::forServer( + (new ServerOptions())->setPasswordPolicy(new PasswordPolicy()), + [HandlerFactoryInterface::class => $handlerFactory], + ); + + $provider = new ProtocolHandlerProvider( + routeResolver: $container->get(ServerProtocolHandlerFactory::class), + factories: $container->get(ProtocolHandlerFactoryMap::class), + context: $this->handlerContext(new PasswordPolicyContext()), + ); + + $this->expectException(RuntimeException::class); + + $provider->get( + Operations::delete('cn=foo,dc=bar'), + new ControlBag(), + ); + } + + private function handlerContext(?PasswordPolicyContext $passwordPolicyContext = null): HandlerContext + { + return new HandlerContext( + queue: $this->createMock(ServerQueue::class), + eventLogger: new EventLogger(null), + requestHistory: new RequestHistory(), + passwordPolicyContext: $passwordPolicyContext, + ); + } } diff --git a/tests/unit/Server/ConnectionHandlerBuilderTest.php b/tests/unit/Server/ConnectionHandlerBuilderTest.php index 09197d97..ca5c16a4 100644 --- a/tests/unit/Server/ConnectionHandlerBuilderTest.php +++ b/tests/unit/Server/ConnectionHandlerBuilderTest.php @@ -50,10 +50,7 @@ public function test_it_builds_a_handler_with_sasl_mechanisms_configured(): void private function builderFor(ServerOptions $options): ConnectionHandlerBuilderInterface { - $container = new Container([ - ServerOptions::class => $options->useInMemoryStorage(), - ]); - - return $container->get(ConnectionHandlerBuilderInterface::class); + return Container::forServer($options->useInMemoryStorage()) + ->get(ConnectionHandlerBuilderInterface::class); } }