Skip to content

Commit

Permalink
Switch from using Pimple to a custom PSR-11 container
Browse files Browse the repository at this point in the history
  • Loading branch information
elazar committed Nov 11, 2023
1 parent 4d84ad8 commit ac45926
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 140 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
84 changes: 84 additions & 0 deletions src/Container.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

namespace Elazar\Flystream;

use IteratorAggregate;
use League\Flysystem\PathNormalizer;
use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
use League\Flysystem\UnixVisibility\VisibilityConverter;
use League\Flysystem\WhitespacePathNormalizer;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Traversable;

class Container implements ContainerInterface, IteratorAggregate
{
/**
* @var callable[]
*/
private array $entries;

/**
* @var object[]
*/
private array $instances;

public function __construct()
{
$this->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);
}
}
}
14 changes: 14 additions & 0 deletions src/FlystreamException.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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 { };
}
}
100 changes: 8 additions & 92 deletions src/ServiceLocator.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,117 +2,33 @@

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;

private Container $container;

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
Expand Down
2 changes: 1 addition & 1 deletion src/StreamWrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions tests/ContainerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

use Elazar\Flystream\BufferInterface;
use Elazar\Flystream\Container;
use Elazar\Flystream\FileBuffer;
use Elazar\Flystream\FlystreamException;
use Elazar\Flystream\MemoryBuffer;

it('can iterate, detect, and get default entries', function () {
$container = new Container;
$expectedDependencyCount = 15;
$actualDependencyCount = 0;
foreach ($container as $class => $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);
});
30 changes: 0 additions & 30 deletions tests/ServiceLocatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand Down
20 changes: 4 additions & 16 deletions tests/StreamWrapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down

0 comments on commit ac45926

Please sign in to comment.