Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions src/CallableResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

namespace Invoker;

use Interop\Container\ContainerInterface;
use Invoker\Exception\NotCallableException;

/**
* Resolves a callable from a container.
*
* @author Matthieu Napoli <matthieu@mnapoli.fr>
*/
class CallableResolver
{
/**
* @var ContainerInterface
*/
private $container;

public function __construct(ContainerInterface $container)
{
$this->container = $container;
}

/**
* Resolve the given callable into a real PHP callable.
*
* @param callable|string|array $callable
*
* @return callable Real PHP callable.
*
* @throws NotCallableException
*/
public function resolve($callable)
{
$callable = $this->resolveFromContainer($callable);

if (! is_callable($callable)) {
throw new NotCallableException(sprintf(
'%s is not a callable',
is_object($callable) ? 'Instance of ' . get_class($callable) : var_export($callable, true)
));
}

return $callable;
}

/**
* @param callable|string|array $callable
* @return callable
* @throws NotCallableException
*/
private function resolveFromContainer($callable)
{
$isStaticCallToNonStaticMethod = false;

// If it's already a callable there is nothing to do
if (is_callable($callable)) {
$isStaticCallToNonStaticMethod = $this->isStaticCallToNonStaticMethod($callable);
if (! $isStaticCallToNonStaticMethod) {
return $callable;
}
}

// The callable is a container entry name
if (is_string($callable)) {
if ($this->container->has($callable)) {
return $this->container->get($callable);
} else {
throw new NotCallableException(sprintf(
'%s is neither a callable or a valid container entry',
$callable
));
}
}

// The callable is an array whose first item is a container entry name
// e.g. ['some-container-entry', 'methodToCall']
if (is_array($callable) && is_string($callable[0])) {
if ($this->container->has($callable[0])) {
$callable[0] = $this->container->get($callable[0]);
return $callable;
} elseif ($isStaticCallToNonStaticMethod) {
throw new NotCallableException(sprintf(
'Cannot call %s::%s() because %s() is not a static method and "%s" is not a container entry',
$callable[0],
$callable[1],
$callable[1],
$callable[0]
));
} else {
throw new NotCallableException(sprintf(
'Cannot call %s on %s because it is not a class nor a valid container entry',
$callable[1],
$callable[0]
));
}
}

// Unrecognized stuff, we let it fail later
return $callable;
}

/**
* Check if the callable represents a static call to a non-static method.
*
* @param mixed $callable
* @return bool
*/
private function isStaticCallToNonStaticMethod($callable)
{
if (is_array($callable) && is_string($callable[0])) {
list($class, $method) = $callable;
$reflection = new \ReflectionMethod($class, $method);

return ! $reflection->isStatic();
}

return false;
}
}
107 changes: 21 additions & 86 deletions src/Invoker.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,36 @@ class Invoker implements InvokerInterface
*/
private $container;

/**
* @var CallableResolver|null
*/
private $callableResolver;

public function __construct(ParameterResolver $parameterResolver = null, ContainerInterface $container = null)
{
$this->parameterResolver = $parameterResolver ?: $this->createParameterResolver();
$this->container = $container;

if ($container) {
$this->callableResolver = new CallableResolver($container);
}
}

/**
* {@inheritdoc}
*/
public function call($callable, array $parameters = array())
{
if ($this->container) {
$callable = $this->resolveCallableFromContainer($callable);
if ($this->callableResolver) {
$callable = $this->callableResolver->resolve($callable);
}

if (! is_callable($callable)) {
throw new NotCallableException(sprintf(
'%s is not a callable',
is_object($callable) ? 'Instance of ' . get_class($callable) : var_export($callable, true)
));
}
$this->assertIsCallable($callable);

$callableReflection = CallableReflection::create($callable);

Expand Down Expand Up @@ -105,91 +120,11 @@ public function getContainer()
}

/**
* @param callable|string|array $callable
* @return callable
* @throws NotCallableException
* @return CallableResolver|null Returns null if no container was given in the constructor.
*/
private function resolveCallableFromContainer($callable)
public function getCallableResolver()
{
$isStaticCallToNonStaticMethod = false;

// If it's already a callable there is nothing to do
if (is_callable($callable)) {
$isStaticCallToNonStaticMethod = $this->isStaticCallToNonStaticMethod($callable);
if (! $isStaticCallToNonStaticMethod) {
return $callable;
}
}

// The callable is a container entry name
if (is_string($callable)) {
if ($this->container->has($callable)) {
return $this->container->get($callable);
} else {
throw new NotCallableException(sprintf(
'%s is neither a callable or a valid container entry',
$callable
));
}
}

// The callable is an array whose first item is a container entry name
// e.g. ['some-container-entry', 'methodToCall']
if (is_array($callable) && is_string($callable[0])) {
if ($this->container->has($callable[0])) {
$callable[0] = $this->container->get($callable[0]);
return $callable;
} elseif ($isStaticCallToNonStaticMethod) {
throw new NotCallableException(sprintf(
'Cannot call %s::%s() because %s() is not a static method and "%s" is not a container entry',
$callable[0],
$callable[1],
$callable[1],
$callable[0]
));
} else {
throw new NotCallableException(sprintf(
'Cannot call %s on %s because it is not a class nor a valid container entry',
$callable[1],
$callable[0]
));
}
}

// Unrecognized stuff, we let it fail later
return $callable;
}

/**
* @param callable $callable
* @throws NotCallableException
*/
private function assertIsCallable($callable)
{
if (! is_callable($callable)) {
throw new NotCallableException(sprintf(
'%s is not a callable',
is_object($callable) ? 'Instance of ' . get_class($callable) : var_export($callable, true)
));
}
}

/**
* Check if the callable represents a static call to a non-static method.
*
* @param mixed $callable
* @return bool
*/
private function isStaticCallToNonStaticMethod($callable)
{
if (is_array($callable) && is_string($callable[0])) {
list($class, $method) = $callable;
$reflection = new \ReflectionMethod($class, $method);

return ! $reflection->isStatic();
}

return false;
return $this->callableResolver;
}

private function assertMandatoryParametersAreResolved($parameters, ReflectionFunctionAbstract $reflection)
Expand Down
112 changes: 112 additions & 0 deletions tests/CallableResolverTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

namespace Invoker\Test;

use Invoker\CallableResolver;
use Invoker\Test\Mock\ArrayContainer;
use Invoker\Test\Mock\CallableSpy;

class CallableResolverTest extends \PHPUnit_Framework_TestCase
{
/**
* @var CallableResolver
*/
private $resolver;

/**
* @var ArrayContainer
*/
private $container;

public function setUp()
{
$this->container = new ArrayContainer;
$this->resolver = new CallableResolver($this->container);
}

/**
* @test
*/
public function resolves_function_callable()
{
$result = $this->resolver->resolve('strlen');

$this->assertSame(strlen('Hello world!'), $result('Hello world!'));
}

/**
* @test
*/
public function resolves_namespaced_function_callable()
{
$result = $this->resolver->resolve(__NAMESPACE__ . '\foo');

$this->assertEquals('bar', $result());
}

/**
* @test
*/
public function resolves_callable_from_container()
{
$callable = function () {};
$this->container->set('thing-to-call', $callable);

$this->assertSame($callable, $this->resolver->resolve('thing-to-call'));
}

/**
* @test
*/
public function resolves_invokable_class_from_container()
{
$callable = new CallableSpy;
$this->container->set('Invoker\Test\Mock\CallableSpy', $callable);

$this->assertSame($callable, $this->resolver->resolve('Invoker\Test\Mock\CallableSpy'));
}

/**
* @test
*/
public function resolves_method_call_service_from_container()
{
$fixture = new InvokerTestFixture;
$this->container->set('thing-to-call', $fixture);

$result = $this->resolver->resolve(array('thing-to-call', 'foo'));

$result();
$this->assertTrue($fixture->wasCalled);
}

/**
* @test
*/
public function resolves_method_call_class_from_container()
{
$fixture = new InvokerTestFixture;
$this->container->set('Invoker\Test\InvokerTestFixture', $fixture);

$result = $this->resolver->resolve(array('Invoker\Test\InvokerTestFixture', 'foo'));

$result();
$this->assertTrue($fixture->wasCalled);
}

/**
* @test
* @expectedException \Invoker\Exception\NotCallableException
* @expectedExceptionMessage foo is neither a callable or a valid container entry
*/
public function throws_resolving_non_callable_from_container()
{
$resolver = new CallableResolver(new ArrayContainer);
$resolver->resolve('foo');
}
}

function foo()
{
return 'bar';
}
1 change: 1 addition & 0 deletions tests/InvokerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ class InvokerTestFixture
public $wasCalled = false;
public function foo()
{
// Use this to make sure we are not called from a static context
$this->wasCalled = true;
return 'bar';
}
Expand Down