Skip to content

Commit

Permalink
Ensure binding interfaces to conrete classes works.
Browse files Browse the repository at this point in the history
  • Loading branch information
jakewhiteley-gc committed Sep 29, 2018
1 parent 14faee7 commit fc066a6
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 11 deletions.
38 changes: 34 additions & 4 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Psr\Container\ContainerInterface;
use Hodl\Exceptions\ContainerException;
use Hodl\Exceptions\NotFoundException;
use Hodl\Exceptions\ConcreteClassNotFoundException;

/**
* A simple Service container with automatic constructor resolution abilities.
Expand Down Expand Up @@ -92,11 +93,34 @@ public function addInstance($key, $object = null)
}
}

/**
* Bind a given service to an alias.
*
* @since 1.3.0
*
* @param string $key The service key to attach the alias to.
* @param string $alias The alias to attach.
*/
public function alias($key, $alias)
{
$this->storage->addAlias($key, $alias);
}

/**
* Bind a given concrete class to an interface.
*
* Alias for alias().
*
* @since 1.3.0
*
* @param string $key The concrete fully qualified class name to bind.
* @param string $interface The interface fully qualified name to bind.
*/
public function bind($key, $interface)
{
$this->storage->addAlias($key, $interface);
}

/**
* Check if a given key exists within this container, either as an object or a factory.
*
Expand Down Expand Up @@ -124,7 +148,7 @@ public function has($key)
public function get($key)
{
if (! \is_string($key) || empty($key)) {
throw new ContainerException('$key must be a string');
throw new ContainerException("$key must be a string");
}

if ($this->storage->hasStored($key)) {
Expand Down Expand Up @@ -152,7 +176,7 @@ public function get($key)
*
* @since 1.0.0
*
* @param string $key The key to remove.
* @param string $key The key to remove. Can also be an alias or bound interface.
* @return bool Whether the key and associated object were removed.
*/
public function remove(string $key)
Expand Down Expand Up @@ -233,10 +257,12 @@ public function resolveMethod($class, string $method, array $args = [])
{
if (! \is_callable([$class, $method])) {
if (\is_string($class)) {
throw new ContainerException($class . "::$method() does not exist or is not callable so could not be resolved");
$error = $class . "::$method() does not exist or is not callable so could not be resolved";
} else {
throw new ContainerException(\get_class($class) . "::$method() does not exist or is not callable so could not be resolved");
$error = \get_class($class) . "::$method() does not exist or is not callable so could not be resolved";
}

throw new ContainerException($error);
}

$reflectionMethod = new ReflectionMethod($class, $method);
Expand Down Expand Up @@ -341,6 +367,10 @@ private function resolveParams($params, $args)
continue;
}

if ($class->isInterface()) {
throw new ConcreteClassNotFoundException("$className is an interface with no bound implementation.");
}

// else the param is a class, so run $this->resolve on it
$this->addToStack($this->resolve($className, $args));
}
Expand Down
10 changes: 10 additions & 0 deletions src/Exceptions/ConcreteClassNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Hodl\Exceptions;

use Exception;
use Psr\Container\NotFoundExceptionInterface;

class ConcreteClassNotFoundException extends ContainerException implements NotFoundExceptionInterface
{
}
35 changes: 29 additions & 6 deletions src/ObjectStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class ObjectStorage
'factory' => [],
];

/**
* Map of aliases.
* @var array
*/
private $aliases = [];

/**
Expand Down Expand Up @@ -216,32 +220,51 @@ public function remove(string $key)
// if the key exists as a factory
if ($this->hasFactory($key)) {
unset($this->definitions['factory'][$key]);
$this->remove_alias_for($key);
$this->removeAliasFor($key);
return ! $this->hasFactory($key);
}

// if the key exists as an object
if ($this->hasObject($key)) {
unset($this->definitions['instance'][$key], $this->store[$key]);
$this->remove_alias_for($key);
$this->removeAliasFor($key);
return ! ($this->hasObject($key) || $this->hasStored($key));
}

// the key did not exist
return false;
}

/**
* Bind a given service to an alias.
*
* @since 1.3.0
*
* @param string $key The service key to attach the alias to.
* @param string $alias The alias to attach.
*/
public function addAlias($key, $alias)
{
$this->aliases[$alias] = $key;
}

protected function remove_alias_for($key)
/**
* Remove all aliases for a given key.
*
* The key can be the original classname, or an alias for that class.
*
* @since 1.3.0
*
* @param string $key The key to remove the aliases for.
*/
protected function removeAliasFor($key)
{
$alias = \array_search($key, $this->aliases);
$aliases = \array_keys($this->aliases, $key);

if ($alias !== false) {
unset($this->aliases[$alias]);
if (!empty($aliases)) {
foreach ($aliases as $alias) {
unset($this->aliases[$alias]);
}
}
}

Expand Down
11 changes: 11 additions & 0 deletions tests/Classes/Concrete.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Hodl\Tests\Classes;

class Concrete implements Contract
{
public function method()
{

}
}
8 changes: 8 additions & 0 deletions tests/Classes/Contract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Hodl\Tests\Classes;

interface Contract
{
public function method();
}
13 changes: 13 additions & 0 deletions tests/Classes/NeedsContract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Hodl\Tests\Classes;

class NeedsContract
{
public $contract = null;

public function __construct(Contract $contract)
{
$this->contract = $contract;
}
}
37 changes: 36 additions & 1 deletion tests/ContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@
use Hodl\Exceptions\NotFoundException;
use Hodl\Exceptions\KeyExistsException;
use Hodl\Exceptions\InvalidKeyException;
use Hodl\Exceptions\ConcreteClassNotFoundException;
use Hodl\Tests\Classes\DummyClass;
use Hodl\Tests\Classes\NoConstructor;
use Hodl\Tests\Classes\NeedsResolving;
use Hodl\Tests\Classes\Resolver;
use Hodl\Tests\Classes\Contract;
use Hodl\Tests\Classes\Concrete;
use Hodl\Tests\Classes\NeedsContract;

class ContainerTest extends \PHPUnit\Framework\TestCase
{
Expand Down Expand Up @@ -488,5 +492,36 @@ public function singletons_have_aliases_removed_upon_remove()
$hodl->remove(DummyClass::class);
$this->assertFalse($hodl->has('dummy'));
$this->assertFalse($hodl->has(DummyClass::class));
}
}

/**
* @test
*/
public function services_can_be_bound_to_interfaces()
{
$hodl = new Container();

$hodl->add(Concrete::class, function () {
return new Concrete('foo');
});

$hodl->bind(Concrete::class, Contract::class);

$this->assertTrue($hodl->get(Concrete::class) instanceof Contract);

$resolved = $hodl->resolve(NeedsContract::class);

$this->assertTrue($resolved->contract instanceof Contract);
}

/**
* @test
*/
public function ConcreteClassNotFoundException_is_thrown_when_resolving_an_unbound_interface()
{
$hodl = new Container();

$this->expectException(ConcreteClassNotFoundException::class);
$resolved = $hodl->resolve(NeedsContract::class);
}
}

0 comments on commit fc066a6

Please sign in to comment.