From ac4592608da5efc76ad9351bf534286d2a3e01af Mon Sep 17 00:00:00 2001 From: Matthew Turland Date: Sat, 11 Nov 2023 17:01:13 -0600 Subject: [PATCH] Switch from using Pimple to a custom PSR-11 container --- composer.json | 2 +- src/Container.php | 84 +++++++++++++++++++++++++++++ src/FlystreamException.php | 14 +++++ src/ServiceLocator.php | 100 +++-------------------------------- src/StreamWrapper.php | 2 +- tests/ContainerTest.php | 45 ++++++++++++++++ tests/ServiceLocatorTest.php | 30 ----------- tests/StreamWrapperTest.php | 20 ++----- 8 files changed, 157 insertions(+), 140 deletions(-) create mode 100644 src/Container.php create mode 100644 tests/ContainerTest.php diff --git a/composer.json b/composer.json index f81ad3a..a8ed947 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ "require": { "php": "^8.1", "league/flysystem": "^2.1 || ^3.0", - "pimple/pimple": "^3.4", + "psr/container": "^2.0", "psr/log": "^2.0 || ^3.0" }, "require-dev": { diff --git a/src/Container.php b/src/Container.php new file mode 100644 index 0000000..0d29833 --- /dev/null +++ b/src/Container.php @@ -0,0 +1,84 @@ +entries = [ + FilesystemRegistry::class => fn() => new FilesystemRegistry, + PassThruPathNormalizer::class => fn() => new PassThruPathNormalizer, + StripProtocolPathNormalizer::class => fn() => new StripProtocolPathNormalizer( + null, + $this->get(WhitespacePathNormalizer::class), + ), + PathNormalizer::class => fn() => $this->get(StripProtocolPathNormalizer::class), + WhitespacePathNormalizer::class => fn() => new WhitespacePathNormalizer, + PortableVisibilityConverter::class => fn() => new PortableVisibilityConverter, + VisibilityConverter::class => fn() => $this->get(PortableVisibilityConverter::class), + LockRegistryInterface::class => fn() => $this->get(LocalLockRegistry::class), + LocalLockRegistry::class => fn() => new LocalLockRegistry, + NullLogger::class => fn() => new NullLogger, + LoggerInterface::class => fn() => $this->get(NullLogger::class), + BufferInterface::class => fn() => $this->get(MemoryBuffer::class), + MemoryBuffer::class => fn() => new MemoryBuffer, + OverflowBuffer::class => fn() => new OverflowBuffer, + FileBuffer::class => fn() => new FileBuffer, + ]; + + $this->instances = []; + } + + public function get(string $id) + { + if (isset($this->instances[$id])) { + return $this->instances[$id]; + } + + if (isset($this->entries[$id])) { + return $this->instances[$id] = $this->entries[$id](); + } + + throw FlystreamException::containerEntryNotFound($id); + } + + public function has(string $id): bool + { + return isset($this->entries[$id]); + } + + public function set(string $class, string|object $instanceOrClass): void + { + $this->instances[$class] = is_string($instanceOrClass) + ? $this->get($instanceOrClass) + : $instanceOrClass; + } + + public function getIterator(): Traversable + { + foreach (array_keys($this->entries) as $id) { + yield $id => $this->get($id); + } + } +} diff --git a/src/FlystreamException.php b/src/FlystreamException.php index 039694c..e43b2ab 100644 --- a/src/FlystreamException.php +++ b/src/FlystreamException.php @@ -2,10 +2,13 @@ namespace Elazar\Flystream; +use Psr\Container\NotFoundExceptionInterface; + class FlystreamException extends \RuntimeException { public const CODE_PROTOCOL_REGISTERED = 1; public const CODE_PROTOCOL_NOT_REGISTERED = 2; + public const CODE_CONTAINER_ENTRY_NOT_FOUND = 3; public static function protocolRegistered(string $protocol): self { @@ -28,4 +31,15 @@ public static function protocolNotRegistered(string $protocol): self self::CODE_PROTOCOL_NOT_REGISTERED ); } + + public static function containerEntryNotFound(string $id): self + { + return new class( + sprintf( + 'Specified container entry not found: %s', + $id + ), + self::CODE_CONTAINER_ENTRY_NOT_FOUND + ) extends FlystreamException implements NotFoundExceptionInterface { }; + } } diff --git a/src/ServiceLocator.php b/src/ServiceLocator.php index 6f43fee..0d7e7da 100644 --- a/src/ServiceLocator.php +++ b/src/ServiceLocator.php @@ -2,17 +2,7 @@ namespace Elazar\Flystream; -use League\Flysystem\PathNormalizer; -use League\Flysystem\UnixVisibility\PortableVisibilityConverter; -use League\Flysystem\UnixVisibility\VisibilityConverter; -use League\Flysystem\WhitespacePathNormalizer; -use Pimple\Container; -use Pimple\Psr11\Container as PsrContainer; -use Pimple\ServiceProviderInterface; -use Psr\Log\LoggerInterface; -use Psr\Log\NullLogger; - -class ServiceLocator implements ServiceProviderInterface +class ServiceLocator { private static ?self $instance = null; @@ -20,99 +10,25 @@ class ServiceLocator implements ServiceProviderInterface public function __construct() { - $this->container = new Container(); - $this->register($this->container); - } - - /** - * @return void - */ - public function register(Container $c) - { - $c[FilesystemRegistry::class] = - fn () => new FilesystemRegistry(); - - $c[PassThruPathNormalizer::class] = - fn () => new PassThruPathNormalizer(); - - $c[StripProtocolPathNormalizer::class] = - fn () => new StripProtocolPathNormalizer( - null, - $c[WhitespacePathNormalizer::class] - ); - - $c[PathNormalizer::class] = - fn () => $c[StripProtocolPathNormalizer::class]; - - $c[WhitespacePathNormalizer::class] = - fn () => new WhitespacePathNormalizer(); - - $c[PortableVisibilityConverter::class] = - fn () => new PortableVisibilityConverter(); - - $c[VisibilityConverter::class] = - fn () => $c[PortableVisibilityConverter::class]; - - $c[LockRegistryInterface::class] = - fn () => $c[LocalLockRegistry::class]; - - $c[LocalLockRegistry::class] = - fn () => new LocalLockRegistry(); - - $c[NullLogger::class] = - fn () => new NullLogger(); - - $c[LoggerInterface::class] = - fn () => $c[NullLogger::class]; - - $c[BufferInterface::class] = - fn () => $c[MemoryBuffer::class]; - - $c[OverflowBuffer::class] = - fn () => new OverflowBuffer(); - - $c[FileBuffer::class] = - fn () => new FileBuffer(); - - $c[MemoryBuffer::class] = - fn () => new MemoryBuffer(); - } - - public function getContainer(): Container - { - return $this->container; - } - - public function getPsrContainer(): PsrContainer - { - return new PsrContainer($this->container); + $this->container = new Container; } - public static function getInstance(): ?self + public static function getInstance(): self { if (self::$instance === null) { - self::$instance = new self(); + self::$instance = new self; } return self::$instance; } - /** - * @return mixed - */ - public static function get(string $class) + public static function get(string $class): object { - return self::getInstance()->getContainer()[$class]; + return self::getInstance()->container->get($class); } - /** - * @param string|object $instanceOrClass - */ - public static function set(string $class, $instanceOrClass): void + public static function set(string $class, string|object $instanceOrClass): void { - $container = self::getInstance()->getContainer(); - $container[$class] = fn () => is_string($instanceOrClass) - ? $container[$instanceOrClass] - : $instanceOrClass; + self::getInstance()->container->set($class, $instanceOrClass); } public static function setInstance(self $instance): void diff --git a/src/StreamWrapper.php b/src/StreamWrapper.php index 9edcf00..d409f62 100644 --- a/src/StreamWrapper.php +++ b/src/StreamWrapper.php @@ -414,7 +414,7 @@ private function getFilesystem(string $path): FilesystemOperator private function get(string $key) { - return ServiceLocator::getInstance()->getContainer()[$key]; + return ServiceLocator::get($key); } private function getDir(string $path): Iterator diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php new file mode 100644 index 0000000..c75968c --- /dev/null +++ b/tests/ContainerTest.php @@ -0,0 +1,45 @@ + $instance) { + $actualDependencyCount++; + expect($container->has($class))->toBe(true); + expect($container->get($class))->toBe($instance); + } + expect($actualDependencyCount)->toBe($expectedDependencyCount); +}); + +it('throws an exception for an unknown key', function () { + (new Container)->get('unknown-key'); +})->throws(FlystreamException::class); + +it('can override a dependency using a class name', function () { + $container = new Container; + + $default = $container->get(BufferInterface::class); + expect($default)->toBeInstanceOf(MemoryBuffer::class); + + $container->set(BufferInterface::class, FileBuffer::class); + + $override = $container->get(BufferInterface::class); + expect($override)->toBeInstanceOf(FileBuffer::class); +}); + +it('can override a dependency using an instance', function () { + $container = new Container; + + $buffer = new FileBuffer; + $container->set(BufferInterface::class, $buffer); + + $override = $container->get(BufferInterface::class); + expect($override)->toBe($buffer); +}); diff --git a/tests/ServiceLocatorTest.php b/tests/ServiceLocatorTest.php index b8b4ec0..33ce875 100644 --- a/tests/ServiceLocatorTest.php +++ b/tests/ServiceLocatorTest.php @@ -4,8 +4,6 @@ use Elazar\Flystream\ServiceLocator; use League\Flysystem\PathNormalizer; use League\Flysystem\WhitespacePathNormalizer; -use Pimple\Container; -use Psr\Container\ContainerInterface; it('initializes an instance on initial access', function () { $locator = ServiceLocator::getInstance(); @@ -18,34 +16,6 @@ expect(ServiceLocator::getInstance())->toBe($expected); }); -it('exposes a Pimple container', function () { - $container = (new ServiceLocator())->getContainer(); - expect($container)->toBeInstanceOf(Container::class); - $keys = $container->keys(); - expect($keys)->toBeArray()->not->toBeEmpty(); - foreach ($keys as $key) { - $dependency = $container[$key]; - expect($dependency)->toBeInstanceOf($key); - } -}); - -it('exposes an equivalent PSR-11 container', function () { - $locator = new ServiceLocator(); - $container = $locator->getPsrContainer(); - expect($container)->toBeInstanceOf(ContainerInterface::class); - foreach ($locator->getContainer()->keys() as $key) { - expect($container->has($key))->toBeTrue(); - } -}); - -it('functions as a Pimple provider', function () { - $locator = new ServiceLocator(); - $actual = new Container(); - $actual->register($locator); - $expected = $locator->getContainer(); - expect($actual)->toEqualCanonicalizing($expected); -}); - it('provides a static accessor for dependencies', function () { $registry = ServiceLocator::get(FilesystemRegistry::class); expect($registry)->toBeInstanceOf(FilesystemRegistry::class); diff --git a/tests/StreamWrapperTest.php b/tests/StreamWrapperTest.php index bf83688..e6f0161 100644 --- a/tests/StreamWrapperTest.php +++ b/tests/StreamWrapperTest.php @@ -10,29 +10,17 @@ use Monolog\Logger; use Psr\Log\LoggerInterface; -function dumpLogs() -{ - $container = ServiceLocator::getInstance()->getContainer(); - echo implode('', array_map( - fn (array $record): string => $record['formatted'] . PHP_EOL, - $container[LoggerInterface::class] - ->popHandler() - ->getRecords() - )); -} - beforeEach(function () { $serviceLocator = new ServiceLocator(); ServiceLocator::setInstance($serviceLocator); - $container = $serviceLocator->getContainer(); $this->logger = new Logger(__FILE__); - $this->logger->pushHandler(new TestHandler()); - $container[LoggerInterface::class] = $this->logger; + $this->logger->pushHandler(new TestHandler); + ServiceLocator::set(LoggerInterface::class, $this->logger); - $this->registry = $container[FilesystemRegistry::class]; + $this->registry = ServiceLocator::get(FilesystemRegistry::class); - $this->filesystem = new Filesystem(new InMemoryFilesystemAdapter()); + $this->filesystem = new Filesystem(new InMemoryFilesystemAdapter); $this->registry->register('fly', $this->filesystem); });