diff --git a/README.md b/README.md index b28d84d..d498678 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,128 @@ +[eng](README.md) / [pt-br](README_PT.md) # Peroxide/Container -A simple Dependency Injection container based on PSR-11 for APIs, with zero dependencies and low functionalities. \ No newline at end of file +A straightforward Dependency Injection container, designed for use with APIs, adhering to the PSR-11 standard. It boasts minimal functionality and operates independently, free from external dependencies. + +## Our filosophy +We are passionate about working with components that are as clean and simple as possible. **Peroxide\Container** is a fusion of inspiration drawn from libraries such as *Laminas\ServiceManager*, *Pimple*, and with a touch of *PHPCI*. + +The great advantage is that we have no external dependencies. All configuration is achieved through PHP code using array configuration files. All you need to do is ensure that your framework supports PSR-11, set up the configuration, and you're ready to begin your coding journey. +## How to use it +### Instaling +```bash +composer require peroxide/container +``` +--- + +## Starting your journey +**Peroxide\Container** is fully compliant with PSR-11, and it provides the following methods: + +```php +# From PSR-11 +public function get(string $id): object; +public function has(string $id): bool; + +# From our interface SetDependency +public function set(string $id, callable $factory): void; +public function setInvokableClass(string $id, string $invocableClass): void; +``` + +### Create you configuration as *array* +```php + fn() => new YourDependencyName(), + YourDependency::class => YourDependencyFactoryClass::class, + + // should be invokable class + ConcreteClass::class => new ConcreteClassFactory(), + + // Or passing as reference string + ConcreteClass::class => ConcreteClassFactory::class +]; + +$container = new Container($config); + +// how to get dependencies +$container->get(YourDependencyName::class); +$container->get(YourDependency::class); +$container->get(ConcreteClass::class); +``` +### Creating your Factory Class +```php +use Psr\Container\ContainerInterface; +use Peroxide\DependencyInjection\Interfaces\ContainerFactory; + +class ConcreteClassFactory implements ContainerFactory +{ + public function __invoke(ContainerInterface $container): ConcreteClass + { + // config your dependency injection here + // you can compose your dependency + // return new ParentDependency($container->get(DependencyChild::class)); + return new ConcreteClass(); + } +} +``` +It is also possible to set dependencies separately, after obtaining your container instance: +```php +use Peroxide\DependencyInjection\Container; + +$container = new Container(); + +$container->set(DependencyPath::class, fn() => new DependencyInstance()); +``` + +If the dependency doesn't exist, it will be created; otherwise, it will be replaced by the new factory. +## More configurations +To handle dependency injection within the container, you can easily use ```arrow function``` to compose your dependencies. +```php +$container = new Container([ + // Dependency parent with dependency child + + // all dependencies should be involved by a Closure(function() or fn()) + Dependency::class => fn() => new Dependency(), + + ParentDependency::class => function($container) { + return new ParentDependency( + $container->get(Dependency::class) + ); + } + // or simply + ParentDependency::class => fn($c) => new ParentDependency($c->get(Dependency::class)) +]); +``` +You can also compose your configuration using the spread operator, as shown in the example: +```php +use Peroxide\DependencyInjection\Container; +# on 'dependencies.php' config file +$config1 = [ ... ]; +$config2 = [ ... ]; +return [...$config1, ...$config2]; + +// ------------------- + +$config = require __DIR__ . '/dependencies.php'; + +$container = new Container($config); +``` +## How to deal with Singleton? +Just use the Singleton invocable class, here's an example: +```php +use Peroxide\DependencyInjection\Container; +use Peroxide\DependencyInjection\Invokables\Singleton; + +$container = new Container([ + // Dependency parent with dependency child + Dependency::class => new Singleton(fn() => new Dependency()), + ParentDependency::class => new Singleton( + fn($container) => new ParentDependency($container->get(Dependency::class)) + ) +]); +``` +The ```Peroxide\DependencyInjection\Invokables\Singleton``` class serves as a wrapper to indicate to our container that we want this class to not create a new instance every time it is retrieved. + +## Why can't I config parameters on container? +We believe that storing configuration values in the dependency container is unnecessary. Instead, each service should be configured using external environment data. By doing so, you can centralize your project's configuration. diff --git a/README_PT.md b/README_PT.md new file mode 100644 index 0000000..f165b87 --- /dev/null +++ b/README_PT.md @@ -0,0 +1,126 @@ +[eng](README.md) / [pt-br](README_PT.md) +# Peroxide/Container + +Um contêiner de Injeção de Dependência direto, projetado para ser usado com APIs, aderindo ao padrão PSR-11. Ele oferece funcionalidade mínima e opera de forma independente, sem depender de recursos externos. +## Nossa filosofia +Somos apaixonados por trabalhar com componentes o mais limpos e simples possível. **Peroxide/Container** é uma fusão de inspiração proveniente de bibliotecas como *Laminas\ServiceManager*, *Pimple*, e um toque de *PHPCI*. + +A grande vantagem é que não temos dependências externas. Toda a configuração é feita por meio de código PHP usando arquivos de configuração em forma de arrays. Tudo o que você precisa fazer é garantir que seu Framework suporte a PSR-11, configure a biblioteca e você estará pronto para começar sua jornada de codificação. + +## Como usar +### Instalando +```bash +composer require peroxide/container +``` +--- + +## Iniciando sua jornada +**Peroxide\Container** está totalmente em conformidade com a PSR-11 e oferece os seguintes métodos: + +```php +# From PSR-11 +public function get(string $id): object; +public function has(string $id): bool; + +# From our interface SetDependency +public function set(string $id, callable $factory): void; +public function setInvokableClass(string $id, string $invocableClass): void; +``` + +### Crie sua configuração com arrays +```php + fn() => new YourDependencyName(), + YourDependency::class => YourDependencyFactoryClass::class, + + // deve ser uma classe invocável + ConcreteClass::class => new ConcreteClassFactory(), + + // Ou passe como uma referencia em string seu factory + ConcreteClass::class => ConcreteClassFactory::class +]; + +$container = new Container($config); + +// como resgatar sua dependência pronta +$container->get(YourDependencyName::class); +$container->get(YourDependency::class); +$container->get(ConcreteClass::class); +``` +### Criando sua classe Factory +```php +use Psr\Container\ContainerInterface; +use Peroxide\DependencyInjection\Interfaces\ContainerFactory; + +class ConcreteClassFactory implements ContainerFactory +{ + public function __invoke(ContainerInterface $container): ConcreteClass + { + // configure sua injeção de dependência aqui + // você pode compor sua dependência + // retorne new ParentDependency($container->get(DependencyChild::class)); + return new ConcreteClass(); + } +} +``` +Também é possível definir dependências separadamente, após obter a instância do seu contêiner: +```php +use Peroxide\DependencyInjection\Container; + +$container = new Container(); + +$container->set(DependencyPath::class, fn() => new DependencyInstance()); +``` + +Se a dependência não existir, ela será criada; caso contrário, será substituída pela atual definição. +## Mais configurações +Para lidar com injeção de dependência dentro do contêiner, você pode facilmente usar uma ```arrow function``` para compor suas dependências. +```php +$container = new Container([ + // todas as dependências devem ser envolvidas por uma Closure (função ou fn()) + Dependency::class => fn() => new Dependency(), + + ParentDependency::class => function($container) { + return new ParentDependency( + $container->get(Dependency::class) + ); + } + // ou simplesmente + ParentDependency::class => fn($c) => new ParentDependency($c->get(Dependency::class)) +]); +``` +Você também pode compor sua configuração usando o operador de expansão, como mostrado no exemplo: +```php +use Peroxide\DependencyInjection\Container; +# no arquivo de configuração 'dependencies.php' +$config1 = [ ... ]; +$config2 = [ ... ]; +return [...$config1, ...$config2]; + +// ------------------- +# em index.php +$config = require __DIR__ . '/dependencies.php'; + +$container = new Container($config); +``` +## Como lidar com Singleton? +Basta usar a classe ```Singleton```, aqui está um exemplo: +```php +use Peroxide\DependencyInjection\Container; +use Peroxide\DependencyInjection\Invokables\Singleton; + +$container = new Container([ + // Dependência pai com dependência filha + Dependency::class => new Singleton(fn() => new Dependency()), + ParentDependency::class => new Singleton( + fn($container) => new ParentDependency($container->get(Dependency::class)) + ) +]); +``` +A classe ```Peroxide\DependencyInjection\Invokables\Singleton``` atua como um invólucro para indicar ao nosso contêiner que desejamos que esta classe não crie uma nova instância toda vez que for solicitada. + +## Por que não posso configurar parâmetros no contêiner? +Acreditamos que não é necessário armazenar valores de configuração no contêiner de dependência. Em vez disso, cada serviço deve ser configurado usando dados de ambiente externos (por exemplo .env). Fazendo isso, você centraliza a configuração do seu projeto. \ No newline at end of file diff --git a/composer.json b/composer.json index 1868855..7d5e6df 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,11 @@ "require-dev": { "phpunit/phpunit": "^10.3" }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, "scripts": { "tests": "phpunit" }, diff --git a/src/Container.php b/src/Container.php index ec10c87..6232c27 100644 --- a/src/Container.php +++ b/src/Container.php @@ -31,11 +31,13 @@ public function setInvokableClass(string $id, string $invocableClass): void if (false === class_exists($invocableClass)) { throw new NotFoundException("Class '$id' isn't in project autoload"); } + $invocableObject = new $invocableClass($this); if (true === is_callable($invocableObject)) { $this->dependencies[$id] = $invocableObject; return; } + throw new NotInvokableClassException("Class '$id' has not a '__invoke' method."); } diff --git a/src/Invokables/Singleton.php b/src/Invokables/Singleton.php new file mode 100644 index 0000000..581015e --- /dev/null +++ b/src/Invokables/Singleton.php @@ -0,0 +1,48 @@ + + */ + protected array $singletonObjects = []; + + /** + * @var Closure $factoryAction + */ + protected Closure $factoryAction; + + public function __construct( + callable $factory, + protected string $dependencyId = '' + ) { + $this->dependencyId = uniqid(); + $this->factoryAction = $factory; + } + + public function has(string $dependencyId): bool + { + return isset($this->singletonObjects[$dependencyId]); + } + + protected function store(string $dependencyName, callable $factoryAction, ContainerInterface $container): void + { + $this->singletonObjects[$dependencyName] = $factoryAction($container); + } + + public function __invoke(ContainerInterface $container): object + { + if ($this->has($this->dependencyId)) { + return $this->singletonObjects[$this->dependencyId]; + } + $this->store($this->dependencyId, $this->factoryAction, $container); + return $this->singletonObjects[$this->dependencyId]; + } +} \ No newline at end of file diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 19e510e..214465b 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -1,8 +1,11 @@ ConcreteClassFactory::class, + ConcreteClass::class => ConcreteClassFactory::class, ConcreteClass2::class => new ConcreteClassFactory(), - ConcreteClass3::class => fn() => new ConcreteClass(), + ConcreteClass3::class => fn() => new ConcreteClass() ]); $concreteClass = $container->get(ConcreteClass::class); $this->assertInstanceOf(ConcreteClass::class, $concreteClass); + + $concreteClass2 = $container->get(ConcreteClass2::class); + $this->assertInstanceOf(ConcreteClass::class, $concreteClass2); + + $concreteClass3 = $container->get(ConcreteClass3::class); + $this->assertInstanceOf(ConcreteClass::class, $concreteClass3); } public function testContainerShouldThrowTypeError() @@ -80,4 +91,76 @@ public function testContainerShouldThrowClassNotInProject() ConcreteClass3::class => InexistentClass::class, ]); } + + public function testContainerShouldInjectInnerDependencies() + { + $container = new Container([ + // Dependency parent with dependency child + Dependency::class => fn() => new Dependency(), + ParentDependency::class => fn($c) => new ParentDependency($c->get(Dependency::class)) + ]); + + $dependencyParent = $container->get(ParentDependency::class); + $dependency = $dependencyParent->getInnerDependency(); + + $this->assertInstanceOf(Dependency::class, $dependency); + } + + public function testContainerShouldReturnSingletonObject() + { + $container = new Container([ + // Dependency parent with dependency child + Dependency::class => new Singleton(fn() => new Dependency()), + ParentDependency::class => new Singleton( + fn($container) => new ParentDependency($container->get(Dependency::class)) + ) + ]); + + $dependency1 = $container->get(Dependency::class); + $dependency2 = $container->get(Dependency::class); + + $this->assertSame($dependency1, $dependency2); + + $containeredDependency1 = $container->get(ParentDependency::class); + $containeredDependency2 = $container->get(ParentDependency::class); + + $this->assertSame($containeredDependency1, $containeredDependency2); + } + + public function testContainerShouldReturnSingletonInsideFactoryCallable() + { + $container = new Container([ + // Dependency parent with dependency child + Dependency::class => new Singleton(fn() => new Dependency()), + AnotherDependency::class => function($container) { + $dep1 = $container->get(Dependency::class); + $dep2 = $container->get(Dependency::class); + $this->assertSame($dep1, $dep2); + return $dep2; + } + ]); + + $container->get(AnotherDependency::class); + } + + public function testContainerShouldReturnSingletonStateChange() + { + $container = new Container([ + // Dependency parent with dependency child + Dependency::class => new Singleton(fn() => new Dependency()), + AnotherDependency::class => function($container) { + $dep1 = $container->get(Dependency::class); + $dep1->testProp = 123; + return $dep1; + } + ]); + + $container->get(Dependency::class); + + $changedStateDependency = $container->get(AnotherDependency::class); + + $dependency = $container->get(Dependency::class); + + $this->assertEquals(123, $dependency->testProp); + } } diff --git a/tests/SingletonTest.php b/tests/SingletonTest.php new file mode 100644 index 0000000..ed06d39 --- /dev/null +++ b/tests/SingletonTest.php @@ -0,0 +1,45 @@ + new Dependency()); + $dependency1 = $singleton(new Container()); + $this->assertInstanceOf(Dependency::class, $dependency1); + } + + public function testDependencyShouldBeUniqueOnSecondCallTheSameInstance() + { + $singleton = new Singleton(fn() => new Dependency()); + + $dependency1 = $singleton(new Container()); + $dependency2 = $singleton(new Container()); + + $this->assertSame($dependency1, $dependency2); + } + + public function testDependencyShouldBeUniqueOnThreeCallsTheSameInstance() + { + $singleton = new Singleton(fn() => new Dependency()); + + $container = new Container(); + + $dependency1 = $singleton($container); + $dependency2 = $singleton($container); + $dependency3 = $singleton($container); + + $this->assertSame($dependency1, $dependency2); + $this->assertSame($dependency1, $dependency3); + $this->assertSame($dependency2, $dependency3); + } +} \ No newline at end of file diff --git a/tests/TestDependencies/ConcreteClass.php b/tests/TestDependencies/ConcreteClass.php index c3b1bfb..1a63a4a 100644 --- a/tests/TestDependencies/ConcreteClass.php +++ b/tests/TestDependencies/ConcreteClass.php @@ -1,4 +1,7 @@ dependency; + } +} \ No newline at end of file