adds scope to the DI container

This commit is contained in:
Johannes M. Schmitt 2011-01-17 23:28:59 +01:00 committed by Fabien Potencier
parent 59a974e8f6
commit 1d5b6ed908
48 changed files with 1187 additions and 283 deletions

View File

@ -70,6 +70,8 @@ class FrameworkBundle extends Bundle
{
parent::registerExtensions($container);
$container->addScope('request');
$container->addCompilerPass(new AddSecurityVotersPass());
$container->addCompilerPass(new ConverterManagerPass());
$container->addCompilerPass(new RoutingResolverPass());

View File

@ -0,0 +1,49 @@
<?php
namespace Symfony\Bundle\FrameworkBundle;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\HttpKernel as BaseHttpKernel;
use Symfony\Component\EventDispatcher\EventDispatcher as BaseEventDispatcher;
/**
* This HttpKernel is used to manage scope changes of the DI container.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class HttpKernel extends BaseHttpKernel
{
protected $container;
public function __construct(ContainerInterface $container, BaseEventDispatcher $eventDispatcher, ControllerResolverInterface $controllerResolver)
{
parent::__construct($eventDispatcher, $controllerResolver);
$this->container = $container;
}
public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
{
$this->container->enterScope('request');
$this->container->set('request', $request, 'request');
try {
$response = parent::handle($request, $type, $catch);
if (HttpKernelInterface::MASTER_REQUEST !== $type) {
$this->container->leaveScope('request');
}
return $response;
} catch (\Exception $e) {
if (HttpKernelInterface::MASTER_REQUEST !== $type) {
$this->container->leaveScope('request');
}
throw $e;
}
}
}

View File

@ -6,7 +6,7 @@
<parameters>
<parameter key="event_dispatcher.class">Symfony\Bundle\FrameworkBundle\EventDispatcher</parameter>
<parameter key="http_kernel.class">Symfony\Component\HttpKernel\HttpKernel</parameter>
<parameter key="http_kernel.class">Symfony\Bundle\FrameworkBundle\HttpKernel</parameter>
<parameter key="response.class">Symfony\Component\HttpFoundation\Response</parameter>
<parameter key="error_handler.class">Symfony\Component\HttpKernel\Debug\ErrorHandler</parameter>
<parameter key="error_handler.level">null</parameter>
@ -22,6 +22,7 @@
</service>
<service id="http_kernel" class="%http_kernel.class%">
<argument type="service" id="service_container" />
<argument type="service" id="event_dispatcher" />
<argument type="service" id="controller_resolver" />
</service>
@ -31,9 +32,9 @@
your front controller (app.php) so that it passes an instance of
YourRequestClass to the Kernel.
-->
<service id="request" factory-service="http_kernel" factory-method="getRequest" shared="false" />
<service id="request" scope="request" />
<service id="response" class="%response.class%" shared="false">
<service id="response" class="%response.class%" scope="prototype">
<call method="setCharset">
<argument>%kernel.charset%</argument>
</call>

View File

@ -70,19 +70,19 @@
<service id="templating.helper.assets" class="%templating.helper.assets.class%">
<tag name="templating.helper" alias="assets" />
<argument type="service" id="request" />
<argument type="service" id="request" strict="false" />
<argument>%templating.assets.base_urls%</argument>
<argument>%templating.assets.version%</argument>
</service>
<service id="templating.helper.request" class="%templating.helper.request.class%">
<tag name="templating.helper" alias="request" />
<argument type="service" id="request" />
<argument type="service" id="request" strict="false" />
</service>
<service id="templating.helper.session" class="%templating.helper.session.class%">
<tag name="templating.helper" alias="session" />
<argument type="service" id="request" />
<argument type="service" id="request" strict="false" />
</service>
<service id="templating.helper.router" class="%templating.helper.router.class%">

View File

@ -12,15 +12,15 @@
</parameters>
<services>
<service id="test.client" class="%test.client.class%" shared="false">
<service id="test.client" class="%test.client.class%" scope="prototype">
<argument type="service" id="kernel" />
<argument>%test.client.parameters%</argument>
<argument type="service" id="test.client.history" />
<argument type="service" id="test.client.cookiejar" />
</service>
<service id="test.client.history" class="%test.client.history.class%" shared="false" />
<service id="test.client.history" class="%test.client.history.class%" scope="prototype" />
<service id="test.client.cookiejar" class="%test.client.cookiejar.class%" shared="false" />
<service id="test.client.cookiejar" class="%test.client.cookiejar.class%" scope="prototype" />
</services>
</container>

View File

@ -0,0 +1,134 @@
<?php
namespace Symfony\Bundle\FrameworkBundle\Tests;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Bundle\FrameworkBundle\HttpKernel;
class HttpKernelTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider getProviderTypes
*/
public function testHandle($type)
{
$request = new Request();
$expected = new Response();
$container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
$container
->expects($this->once())
->method('enterScope')
->with($this->equalTo('request'))
;
if ($type !== HttpKernelInterface::MASTER_REQUEST) {
$container
->expects($this->once())
->method('leaveScope')
->with($this->equalTo('request'))
;
} else {
$container
->expects($this->never())
->method('leaveScope')
;
}
$container
->expects($this->once())
->method('set')
->with($this->equalTo('request'), $this->equalTo($request), $this->equalTo('request'))
;
$dispatcher = new EventDispatcher();
$resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface');
$kernel = new HttpKernel($container, $dispatcher, $resolver);
$controller = function() use($expected)
{
return $expected;
};
$resolver->expects($this->once())
->method('getController')
->with($request)
->will($this->returnValue($controller));
$resolver->expects($this->once())
->method('getArguments')
->with($request, $controller)
->will($this->returnValue(array()));
$actual = $kernel->handle($request, $type);
$this->assertSame($expected, $actual, '->handle() returns the response');
}
/**
* @dataProvider getProviderTypes
*/
public function testHandleRestoresThePreviousRequestOnException($type)
{
$request = new Request();
$expected = new \Exception();
$container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
$container
->expects($this->once())
->method('enterScope')
->with($this->equalTo('request'))
;
if ($type !== HttpKernelInterface::MASTER_REQUEST) {
$container
->expects($this->once())
->method('leaveScope')
->with($this->equalTo('request'))
;
} else {
$container
->expects($this->never())
->method('leaveScope')
;
}
$container
->expects($this->once())
->method('set')
->with($this->equalTo('request'), $this->equalTo($request), $this->equalTo('request'))
;
$dispatcher = new EventDispatcher();
$resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface');
$kernel = new HttpKernel($container, $dispatcher, $resolver);
$controller = function() use ($expected)
{
throw $expected;
};
$resolver->expects($this->once())
->method('getController')
->with($request)
->will($this->returnValue($controller));
$resolver->expects($this->once())
->method('getArguments')
->with($request, $controller)
->will($this->returnValue(array()));
try {
$kernel->handle($request, $type);
$this->fail('->handle() suppresses the controller exception');
} catch (\Exception $actual) {
$this->assertSame($expected, $actual, '->handle() throws the controller exception');
}
}
public function getProviderTypes()
{
return array(
array(HttpKernelInterface::MASTER_REQUEST),
array(HttpKernelInterface::SUB_REQUEST),
);
}
}

View File

@ -25,37 +25,38 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class AnalyzeServiceReferencesPass implements RepeatablePassInterface, CompilerAwareInterface
class AnalyzeServiceReferencesPass implements RepeatablePassInterface
{
protected $graph;
protected $container;
protected $currentId;
protected $currentDefinition;
protected $repeatedPass;
protected $ignoreMethodCalls;
public function __construct($ignoreMethodCalls = false)
{
$this->ignoreMethodCalls = (Boolean) $ignoreMethodCalls;
}
public function setRepeatedPass(RepeatedPass $repeatedPass) {
$this->repeatedPass = $repeatedPass;
}
public function setCompiler(Compiler $compiler)
{
$this->graph = $compiler->getServiceReferenceGraph();
}
public function process(ContainerBuilder $container)
{
$this->container = $container;
if (null === $this->graph) {
$this->graph = $this->repeatedPass->getCompiler()->getServiceReferenceGraph();
}
$this->graph = $container->getCompiler()->getServiceReferenceGraph();
$this->graph->clear();
foreach ($container->getDefinitions() as $id => $definition) {
$this->currentId = $id;
$this->currentDefinition = $definition;
$this->processArguments($definition->getArguments());
$this->processArguments($definition->getMethodCalls());
if (!$this->ignoreMethodCalls) {
$this->processArguments($definition->getMethodCalls());
}
}
foreach ($container->getAliases() as $id => $alias) {

View File

@ -0,0 +1,49 @@
<?php
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Checks your services for circular references
*
* References from method calls are ignored since we might be able to resolve
* these references depending on the order in which services are called.
*
* Circular reference from method calls will only be detected at run-time.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class CheckCircularReferencesPass implements CompilerPassInterface
{
protected $currentId;
protected $currentNode;
protected $currentPath;
public function process(ContainerBuilder $container)
{
$graph = $container->getCompiler()->getServiceReferenceGraph();
foreach ($graph->getNodes() as $id => $node) {
$this->currentId = $id;
$this->currentPath = array($id);
$this->checkOutEdges($node->getOutEdges());
}
}
protected function checkOutEdges(array $edges)
{
foreach ($edges as $edge) {
$node = $edge->getDestNode();
$this->currentPath[] = $id = $node->getId();
if ($this->currentId === $id) {
throw new \RuntimeException(sprintf('Circular reference detected for "%s", path: "%s".', $this->currentId, implode(' -> ', $this->currentPath)));
}
$this->checkOutEdges($node->getOutEdges());
array_pop($this->currentPath);
}
}
}

View File

@ -0,0 +1,123 @@
<?php
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Checks the scope of references
*
* Especially, we disallow services of wider scope to have references to
* services of a narrower scope by default since it is generally a sign for a
* wrong implementation.
*
* If someone specifically wants to allow this, then he can set the reference
* to strict=false.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class CheckReferenceScopePass implements CompilerPassInterface
{
protected $container;
protected $currentId;
protected $currentScope;
protected $currentScopeAncestors;
protected $currentScopeChildren;
public function process(ContainerBuilder $container)
{
$this->container = $container;
$children = $this->container->getScopeChildren();
$ancestors = array();
$scopes = $this->container->getScopes();
foreach ($scopes as $name => $parent) {
$ancestors[$name] = array($parent);
while (isset($scopes[$parent])) {
$ancestors[$name][] = $parent = $scopes[$parent];
}
}
foreach ($container->getDefinitions() as $id => $definition) {
$this->currentId = $id;
$this->currentScope = $scope = $definition->getScope();
if (ContainerInterface::SCOPE_PROTOTYPE === $scope) {
continue;
}
if (ContainerInterface::SCOPE_CONTAINER === $scope) {
$this->currentScopeChildren = array_keys($scopes);
$this->currentScopeAncestors = array();
} else {
$this->currentScopeChildren = $children[$scope];
$this->currentScopeAncestors = $ancestors[$scope];
}
$this->validateReferences($definition->getArguments());
$this->validateReferences($definition->getMethodCalls());
}
}
protected function validateReferences(array $arguments)
{
foreach ($arguments as $argument) {
if (is_array($argument)) {
$this->validateReferences($argument);
} else if ($argument instanceof Reference) {
if (!$argument->isStrict()) {
continue;
}
if (null === $definition = $this->getDefinition($id = (string) $argument)) {
continue;
}
if ($this->currentScope === $scope = $definition->getScope()) {
continue;
}
if (in_array($scope, $this->currentScopeChildren, true)) {
throw new \RuntimeException(sprintf(
'Scope Widening Injection detected: The definition "%s" references the service "%s" which belongs to a narrower scope. '
.'Generally, it is safer to either move "%s" to scope "%s" or alternatively rely on the provider pattern by injecting the container itself, and requesting the service "%s" each time it is needed. '
.'In rare, special cases however that might not be necessary, then you can set the reference to strict=false to get rid of this warning.',
$this->currentId,
$id,
$this->currentId,
$scope,
$id
));
}
if (!in_array($scope, $this->currentScopeAncestors, true)) {
throw new \RuntimeException(sprintf(
'Cross-Scope Injection detected: The definition "%s" references the service "%s" which belongs to another scope hierarchy. '
.'This service might not be available consistently. Generally, it is safer to either move the definition "%s" to scope "%s", or '
.'declare "%s" as a child scope of "%s". If you can be sure that the other scope is always active, you can set the reference to strict=false to get rid of this warning.',
$this->currentId,
$id,
$this->currentId,
$scope,
$this->currentScope,
$scope
));
}
}
}
}
protected function getDefinition($id)
{
if (!$this->container->hasDefinition($id)) {
return null;
}
return $this->container->getDefinition($id);
}
}

View File

@ -70,10 +70,6 @@ class Compiler
protected function startPass(CompilerPassInterface $pass)
{
if ($pass instanceof CompilerAwareInterface) {
$pass->setCompiler($this);
}
$this->currentPass = $pass;
$this->currentStartTime = microtime(true);
}

View File

@ -1,23 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Compiler;
/**
* This interface can be implemented by passes that need to access the
* compiler.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
interface CompilerAwareInterface
{
function setCompiler(Compiler $compiler);
}

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@ -32,7 +33,7 @@ class InlineServiceDefinitionsPass implements RepeatablePassInterface
public function process(ContainerBuilder $container)
{
$this->graph = $this->repeatedPass->getCompiler()->getServiceReferenceGraph();
$this->graph = $container->getCompiler()->getServiceReferenceGraph();
foreach ($container->getDefinitions() as $id => $definition) {
$definition->setArguments(
@ -56,7 +57,7 @@ class InlineServiceDefinitionsPass implements RepeatablePassInterface
}
if ($this->isInlinableDefinition($container, $id, $definition = $container->getDefinition($id))) {
if ($definition->isShared()) {
if (ContainerInterface::SCOPE_PROTOTYPE !== $definition->getScope()) {
$arguments[$k] = $definition;
} else {
$arguments[$k] = clone $definition;
@ -73,7 +74,7 @@ class InlineServiceDefinitionsPass implements RepeatablePassInterface
protected function isInlinableDefinition(ContainerBuilder $container, $id, Definition $definition)
{
if (!$definition->isShared()) {
if (ContainerInterface::SCOPE_PROTOTYPE === $definition->getScope()) {
return true;
}

View File

@ -46,6 +46,9 @@ class PassConfig
new ResolveReferencesToAliasesPass(),
new ResolveInterfaceInjectorsPass(),
new ResolveInvalidReferencesPass(),
new AnalyzeServiceReferencesPass(true),
new CheckCircularReferencesPass(),
new CheckReferenceScopePass(),
);
$this->removingPasses = array(

View File

@ -24,7 +24,6 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
class RemoveUnusedDefinitionsPass implements RepeatablePassInterface
{
protected $repeatedPass;
protected $graph;
public function setRepeatedPass(RepeatedPass $repeatedPass)
{
@ -33,7 +32,7 @@ class RemoveUnusedDefinitionsPass implements RepeatablePassInterface
public function process(ContainerBuilder $container)
{
$this->graph = $this->repeatedPass->getCompiler()->getServiceReferenceGraph();
$graph = $container->getCompiler()->getServiceReferenceGraph();
$hasChanged = false;
foreach ($container->getDefinitions() as $id => $definition) {
@ -41,8 +40,8 @@ class RemoveUnusedDefinitionsPass implements RepeatablePassInterface
continue;
}
if ($this->graph->hasNode($id)) {
$edges = $this->graph->getNode($id)->getInEdges();
if ($graph->hasNode($id)) {
$edges = $graph->getNode($id)->getInEdges();
$referencingAliases = array();
$sourceIds = array();
foreach ($edges as $edge) {

View File

@ -18,10 +18,9 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class RepeatedPass implements CompilerPassInterface, CompilerAwareInterface
class RepeatedPass implements CompilerPassInterface
{
protected $repeat;
protected $compiler;
protected $passes;
public function __construct(array $passes)
@ -37,23 +36,14 @@ class RepeatedPass implements CompilerPassInterface, CompilerAwareInterface
$this->passes = $passes;
}
public function setCompiler(Compiler $compiler)
{
$this->compiler = $compiler;
}
public function getCompiler()
{
return $this->compiler;
}
public function process(ContainerBuilder $container)
{
$compiler = $container->getCompiler();
$this->repeat = false;
foreach ($this->passes as $pass) {
$time = microtime(true);
$pass->process($container);
$this->compiler->addLogMessage(sprintf(
$compiler->addLogMessage(sprintf(
'%s finished in %.3fs', get_class($pass), microtime(true) - $time
));
}

View File

@ -51,6 +51,7 @@ use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
* (for instance, ignore a setter if the service does not exist)
*
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class Container implements ContainerInterface
{
@ -66,7 +67,13 @@ class Container implements ContainerInterface
public function __construct(ParameterBagInterface $parameterBag = null)
{
$this->parameterBag = null === $parameterBag ? new ParameterBag() : $parameterBag;
$this->services = array();
$this->services =
$this->scopes =
$this->scopeChildren =
$this->scopedServices =
$this->scopeStacks = array();
$this->set('service_container', $this);
}
@ -147,10 +154,25 @@ class Container implements ContainerInterface
*
* @param string $id The service identifier
* @param object $service The service instance
* @param string $scope The scope of the service
*/
public function set($id, $service)
public function set($id, $service, $scope = self::SCOPE_CONTAINER)
{
$this->services[strtolower($id)] = $service;
if (self::SCOPE_PROTOTYPE === $scope) {
throw new \InvalidArgumentException('You cannot set services of scope "prototype".');
}
$id = strtolower($id);
if (self::SCOPE_CONTAINER !== $scope) {
if (!isset($this->scopedServices[$scope])) {
throw new \RuntimeException('You cannot set services of inactive scopes.');
}
$this->scopedServices[$scope][$id] = $service;
}
$this->services[$id] = $service;
}
/**
@ -227,6 +249,141 @@ class Container implements ContainerInterface
return array_merge($ids, array_keys($this->services));
}
/**
* This is called when you enter a scope
*
* @param string $name
* @return void
*/
public function enterScope($name)
{
if (!isset($this->scopes[$name])) {
throw new \InvalidArgumentException(sprintf('The scope "%s" does not exist.', $name));
}
if (self::SCOPE_CONTAINER !== $this->scopes[$name] && !isset($this->scopedServices[$this->scopes[$name]])) {
throw new \RuntimeException(sprintf('The parent scope "%s" must be active when entering this scope.', $this->scopes[$name]));
}
// check if a scope of this name is already active, if so we need to
// remove all services of this scope, and those of any of its child
// scopes from the global services map
if (isset($this->scopedServices[$name])) {
$services = array($this->services, $name => $this->scopedServices[$name]);
unset($this->scopedServices[$name]);
foreach ($this->scopeChildren[$name] as $child) {
$services[$child] = $this->scopedServices[$child];
unset($this->scopedServices[$child]);
}
// update global map
$this->services = call_user_func_array('array_diff_key', $services);
array_shift($services);
// add stack entry for this scope so we can restore the removed services later
if (!isset($this->scopeStacks[$name])) {
$this->scopeStacks[$name] = new \SplStack();
}
$this->scopeStacks[$name]->push($services);
}
$this->scopedServices[$name] = array();
}
/**
* This is called to leave the current scope, and move back to the parent
* scope.
*
* @return void
*/
public function leaveScope($name)
{
if (!isset($this->scopedServices[$name])) {
throw new \InvalidArgumentException(sprintf('The scope "%s" is not active.', $name));
}
// remove all services of this scope, or any of its child scopes from
// the global service map
$services = array($this->services, $this->scopedServices[$name]);
unset($this->scopedServices[$name]);
foreach ($this->scopeChildren[$name] as $child) {
if (!isset($this->scopedServices[$child])) {
continue;
}
$services[] = $this->scopedServices[$child];
unset($this->scopedServices[$child]);
}
$this->services = call_user_func_array('array_diff_key', $services);
// check if we need to restore services of a previous scope of this type
if (isset($this->scopeStacks[$name]) && count($this->scopeStacks[$name]) > 0) {
$services = $this->scopeStacks[$name]->pop();
$this->scopedServices += $services;
array_unshift($services, $this->services);
$this->services = call_user_func_array('array_merge', $services);
}
}
/**
* Adds a scope to the container
*
* @param string $name
* @param string $parentScope
* @return void
*/
public function addScope($name, $parentScope = self::SCOPE_CONTAINER)
{
if (self::SCOPE_CONTAINER === $name || self::SCOPE_PROTOTYPE === $name) {
throw new \InvalidArgumentException(sprintf('The scope "%s" is reserved.', $name));
}
if (isset($this->scopes[$name])) {
throw new \InvalidArgumentException(sprintf('A scope with name "%s" already exists.', $name));
}
if (self::SCOPE_CONTAINER !== $parentScope && !isset($this->scopes[$parentScope])) {
throw new \InvalidArgumentException(sprintf('The parent scope "%s" does not exist, or is invalid.', $parentScope));
}
$this->scopes[$name] = $parentScope;
$this->scopeChildren[$name] = array();
// normalize the child relations
if ($parentScope !== self::SCOPE_CONTAINER) {
$this->scopeChildren[$parentScope][] = $name;
foreach ($this->scopeChildren as $pName => $childScopes) {
if (in_array($parentScope, $childScopes, true)) {
$this->scopeChildren[$pName][] = $name;
}
}
}
}
/**
* Returns whether this container has a certain scope
*
* @return Boolean
*/
public function hasScope($name)
{
return isset($this->scopes[$name]);
}
/**
* Returns whether this scope is currently active
*
* This does not actually check if the passed scope actually exists.
*
* @param string $name
* @return Boolean
*/
public function isScopeActive($name)
{
return isset($this->scopedServices[$name]);
}
static public function camelize($id)
{
return preg_replace(array('/(?:^|_)+(.)/e', '/\.(.)/e'), array("strtoupper('\\1')", "'_'.strtoupper('\\1')"), $id);

View File

@ -178,6 +178,16 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
return $this->compiler;
}
public function getScopes()
{
return $this->scopes;
}
public function getScopeChildren()
{
return $this->scopeChildren;
}
/**
* Sets a service.
*
@ -186,7 +196,7 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
*
* @throws BadMethodCallException
*/
public function set($id, $service)
public function set($id, $service, $scope = self::SCOPE_CONTAINER)
{
if ($this->isFrozen()) {
throw new \BadMethodCallException('Setting service on a frozen container is not allowed');
@ -196,7 +206,7 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
unset($this->definitions[$id], $this->aliases[$id]);
parent::set($id, $service);
parent::set($id, $service, $scope);
}
/**
@ -691,8 +701,16 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
$injector->processDefinition($definition, $service);
}
if ($definition->isShared()) {
$this->services[strtolower($id)] = $service;
if (self::SCOPE_PROTOTYPE !== $scope = $definition->getScope()) {
if (self::SCOPE_CONTAINER !== $scope && !isset($this->scopedServices[$scope])) {
throw new \RuntimeException('You tried to create a service of an inactive scope.');
}
$this->services[$lowerId = strtolower($id)] = $service;
if (self::SCOPE_CONTAINER !== $scope) {
$this->scopedServices[$scope][$lowerId] = $service;
}
}
foreach ($definition->getMethodCalls() as $call) {

View File

@ -15,20 +15,24 @@ namespace Symfony\Component\DependencyInjection;
* ContainerInterface is the interface implemented by service container classes.
*
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
interface ContainerInterface
{
const EXCEPTION_ON_INVALID_REFERENCE = 1;
const NULL_ON_INVALID_REFERENCE = 2;
const IGNORE_ON_INVALID_REFERENCE = 3;
const SCOPE_CONTAINER = 'container';
const SCOPE_PROTOTYPE = 'prototype';
/**
* Sets a service.
*
* @param string $id The service identifier
* @param object $service The service instance
* @param string $scope The scope of the service
*/
function set($id, $service);
function set($id, $service, $scope = self::SCOPE_CONTAINER);
/**
* Gets a service.
@ -52,4 +56,46 @@ interface ContainerInterface
* @return Boolean true if the service is defined, false otherwise
*/
function has($id);
/**
* Enters the given scope
*
* @param string $name
* @return void
*/
function enterScope($name);
/**
* Leaves the current scope, and re-enters the parent scope
*
* @param string $name
* @return void
*/
function leaveScope($name);
/**
* Adds a scope to the container
*
* @param string $name
* @param string $parentScope
* @return void
*/
function addScope($name, $parentScope = self::SCOPE_CONTAINER);
/**
* Whether this container has the given scope
*
* @param string $name
* @return Boolean
*/
function hasScope($name);
/**
* Determines whether the given scope is currently active.
*
* It does however not check if the scope actually exists.
*
* @return Boolean
*/
function isScopeActive($name);
}

View File

@ -22,7 +22,7 @@ class Definition
protected $file;
protected $factoryMethod;
protected $factoryService;
protected $shared;
protected $scope;
protected $arguments;
protected $calls;
protected $configurator;
@ -40,7 +40,7 @@ class Definition
$this->class = $class;
$this->arguments = $arguments;
$this->calls = array();
$this->shared = true;
$this->scope = ContainerInterface::SCOPE_CONTAINER;
$this->tags = array();
$this->public = true;
}
@ -331,27 +331,27 @@ class Definition
}
/**
* Sets if the service must be shared or not.
* Sets the scope of the service
*
* @param Boolean $shared Whether the service must be shared or not
* @param string $string Whether the service must be shared or not
*
* @return Definition The current instance
*/
public function setShared($shared)
public function setScope($scope)
{
$this->shared = (Boolean) $shared;
$this->scope = $scope;
return $this;
}
/**
* Returns true if the service must be shared.
* Returns the scope of the service
*
* @return Boolean true if the service is shared, false otherwise
* @return string
*/
public function isShared()
public function getScope()
{
return $this->shared;
return $this->scope;
}
/**

View File

@ -15,6 +15,7 @@ use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* GraphvizDumper dumps a service container as a graphviz file.
@ -135,7 +136,7 @@ class GraphvizDumper extends Dumper
$container = clone $this->container;
foreach ($container->getDefinitions() as $id => $definition) {
$nodes[$id] = array('class' => str_replace('\\', '\\\\', $this->container->getParameterBag()->resolveValue($definition->getClass())), 'attributes' => array_merge($this->options['node.definition'], array('style' => $definition->isShared() ? 'filled' : 'dotted')));
$nodes[$id] = array('class' => str_replace('\\', '\\\\', $this->container->getParameterBag()->resolveValue($definition->getClass())), 'attributes' => array_merge($this->options['node.definition'], array('style' => ContainerInterface::SCOPE_PROTOTYPE !== $definition->getScope() ? 'filled' : 'dotted')));
$container->setDefinition($id, new Definition('stdClass'));
}

View File

@ -43,7 +43,7 @@ class PhpDumper extends Dumper
protected $definitionVariables;
protected $referenceVariables;
protected $variableCount;
protected $reservedVariables = array('instance');
protected $reservedVariables = array('instance', 'class');
public function __construct(ContainerBuilder $container)
{
@ -270,8 +270,10 @@ EOF;
$simple = $this->isSimpleInstance($id, $definition);
$instantiation = '';
if ($definition->isShared()) {
if (ContainerInterface::SCOPE_CONTAINER === $definition->getScope()) {
$instantiation = "\$this->services['$id'] = ".($simple ? '' : '$instance');
} else if (ContainerInterface::SCOPE_PROTOTYPE !== $scope = $definition->getScope()) {
$instantiation = "\$this->services['$id'] = \$this->scopedServices['$scope']['$id'] = ".($simple ? '' : '$instance');
} elseif (!$simple) {
$instantiation = '$instance';
}
@ -399,7 +401,7 @@ EOF;
}
$doc = '';
if ($definition->isShared()) {
if (ContainerInterface::SCOPE_PROTOTYPE !== $definition->getScope()) {
$doc .= <<<EOF
*
@ -430,6 +432,17 @@ EOF;
EOF;
$scope = $definition->getScope();
if (ContainerInterface::SCOPE_CONTAINER !== $scope && ContainerInterface::SCOPE_PROTOTYPE !== $scope) {
$code .= <<<EOF
if (!isset(\$this->scopedServices['$scope'])) {
throw new \RuntimeException('You cannot create a service ("$id") of an inactive scope ("$scope").');
}
EOF;
}
$code .=
$this->addServiceInclude($id, $definition).
$this->addServiceLocalTempVariables($id, $definition).
@ -518,7 +531,7 @@ EOF;
{
$bagClass = $this->container->isFrozen() ? 'FrozenParameterBag' : 'ParameterBag';
return <<<EOF
$code = <<<EOF
/**
* Constructor.
@ -526,9 +539,21 @@ EOF;
public function __construct()
{
parent::__construct(new $bagClass(\$this->getDefaultParameters()));
EOF;
if (count($scopes = $this->container->getScopes()) > 0) {
$code .= "\n";
$code .= " \$this->scopes = ".$this->dumpValue($scopes).";\n";
$code .= " \$this->scopeChildren = ".$this->dumpValue($this->container->getScopeChildren()).";\n";
}
$code .= <<<EOF
}
EOF;
return $code;
}
protected function addDefaultParametersMethod()

View File

@ -121,8 +121,8 @@ class XmlDumper extends Dumper
if ($definition->getFactoryService()) {
$service->setAttribute ('factory-service', $definition->getFactoryService());
}
if (!$definition->isShared()) {
$service->setAttribute ('shared', 'false');
if (ContainerInterface::SCOPE_CONTAINER !== $scope = $definition->getScope()) {
$service->setAttribute ('scope', $scope);
}
foreach ($definition->getTags() as $name => $tags) {

View File

@ -95,8 +95,8 @@ class YamlDumper extends Dumper
$code .= sprintf(" calls:\n %s\n", str_replace("\n", "\n ", Yaml::dump($this->dumpValue($definition->getMethodCalls()), 1)));
}
if (!$definition->isShared()) {
$code .= " shared: false\n";
if (ContainerInterface::SCOPE_CONTAINER !== $scope = $definition->getScope()) {
$code .= sprintf(" scope: %s\n", $scope);
}
if ($callable = $definition->getConfigurator()) {

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\InterfaceInjector;
@ -137,7 +139,7 @@ class XmlFileLoader extends FileLoader
$definition = new Definition();
foreach (array('class', 'shared', 'public', 'factory-method', 'factory-service') as $key) {
foreach (array('class', 'scope', 'public', 'factory-method', 'factory-service') as $key) {
if (isset($service[$key])) {
$method = 'set'.str_replace('-', '', $key);
$definition->$method((string) $service->getAttributeAsPhp($key));
@ -155,7 +157,7 @@ class XmlFileLoader extends FileLoader
$definition->setConfigurator((string) $service->configurator['function']);
} else {
if (isset($service->configurator['service'])) {
$class = new Reference((string) $service->configurator['service']);
$class = new Reference((string) $service->configurator['service'], ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false);
} else {
$class = (string) $service->configurator['class'];
}

View File

@ -144,8 +144,8 @@ class YamlFileLoader extends FileLoader
$definition->setClass($service['class']);
}
if (isset($service['shared'])) {
$definition->setShared($service['shared']);
if (isset($service['scope'])) {
$definition->setScope($service['scope']);
}
if (isset($service['public'])) {
@ -237,10 +237,23 @@ class YamlFileLoader extends FileLoader
{
if (is_array($value)) {
$value = array_map(array($this, 'resolveServices'), $value);
} else if (is_string($value) && 0 === strpos($value, '@?')) {
$value = new Reference(substr($value, 2), ContainerInterface::IGNORE_ON_INVALID_REFERENCE);
} else if (is_string($value) && 0 === strpos($value, '@')) {
$value = new Reference(substr($value, 1));
} else if (is_string($value) && 0 === strpos($value, '@')) {
if (0 === strpos($value, '@?')) {
$value = substr($value, 2);
$invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
} else {
$value = substr($value, 1);
$invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
}
if ('=' === substr($value, -1)) {
$value = substr($value, 0, -1);
$strict = false;
} else {
$strict = true;
}
$value = new Reference($value, $invalidBehavior, $strict);
}
return $value;

View File

@ -101,7 +101,7 @@
</xsd:choice>
<xsd:attribute name="id" type="xsd:string" />
<xsd:attribute name="class" type="xsd:string" />
<xsd:attribute name="shared" type="boolean" />
<xsd:attribute name="scope" type="xsd:string" />
<xsd:attribute name="public" type="boolean" />
<xsd:attribute name="factory-method" type="xsd:string" />
<xsd:attribute name="factory-service" type="xsd:string" />
@ -140,6 +140,7 @@
<xsd:attribute name="id" type="xsd:string" />
<xsd:attribute name="key" type="xsd:string" />
<xsd:attribute name="on-invalid" type="xsd:string" />
<xsd:attribute name="strict" type="boolean" />
</xsd:complexType>
<xsd:complexType name="call" mixed="true">

View File

@ -20,19 +20,22 @@ class Reference
{
protected $id;
protected $invalidBehavior;
protected $strict;
/**
* Constructor.
*
* @param string $id The service identifier
* @param int $invalidBehavior The behavior when the service does not exist
* @param string $id The service identifier
* @param int $invalidBehavior The behavior when the service does not exist
* @param Boolean $strict Sets how this reference is validated
*
* @see Container
*/
public function __construct($id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE)
public function __construct($id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, $strict = true)
{
$this->id = $id;
$this->invalidBehavior = $invalidBehavior;
$this->strict = $strict;
}
/**
@ -49,4 +52,9 @@ class Reference
{
return $this->invalidBehavior;
}
public function isStrict()
{
return $this->strict;
}
}

View File

@ -42,7 +42,14 @@ class SimpleXMLElement extends \SimpleXMLElement
} elseif (isset($arg['on-invalid']) && 'null' == $arg['on-invalid']) {
$invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
}
$arguments[$key] = new Reference((string) $arg['id'], $invalidBehavior);
if (isset($arg['strict'])) {
$strict = self::phpize($arg['strict']);
} else {
$strict = true;
}
$arguments[$key] = new Reference((string) $arg['id'], $invalidBehavior, $strict);
break;
case 'collection':
$arguments[$key] = $arg->getArgumentsAsPhp($name);

View File

@ -11,6 +11,19 @@
namespace Symfony\Component\DependencyInjection;
/**
* Represents a variable.
*
* $var = new Variable('a');
*
* will be dumped as
*
* $a
*
* by the PHP dumper.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class Variable
{
protected $name;

View File

@ -27,7 +27,6 @@ class HttpKernel implements HttpKernelInterface
{
protected $dispatcher;
protected $resolver;
protected $request;
/**
* Constructor
@ -46,16 +45,10 @@ class HttpKernel implements HttpKernelInterface
*/
public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
{
// set the current request, stash the previous one
$previousRequest = $this->request;
$this->request = $request;
try {
$response = $this->handleRaw($request, $type);
} catch (\Exception $e) {
if (false === $catch) {
$this->request = $previousRequest;
throw $e;
}
@ -63,28 +56,15 @@ class HttpKernel implements HttpKernelInterface
$event = new Event($this, 'core.exception', array('request_type' => $type, 'request' => $request, 'exception' => $e));
$this->dispatcher->notifyUntil($event);
if (!$event->isProcessed()) {
$this->request = $previousRequest;
throw $e;
}
$response = $this->filterResponse($event->getReturnValue(), $request, 'A "core.exception" listener returned a non response object.', $type);
}
// restore the previous request
$this->request = $previousRequest;
return $response;
}
/**
* {@inheritdoc}
*/
public function getRequest()
{
return $this->request;
}
/**
* Handles a request to convert it to a response.
*

View File

@ -39,11 +39,4 @@ interface HttpKernelInterface
* @throws \Exception When an Exception occurs during processing
*/
function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true);
/**
* Returns the current request.
*
* @return Request|null The request currently being handled
*/
function getRequest();
}

View File

@ -288,7 +288,11 @@ class Container implements ContainerInterface
public function __construct(ParameterBagInterface $parameterBag = null)
{
$this->parameterBag = null === $parameterBag ? new ParameterBag() : $parameterBag;
$this->services = array();
$this->services =
$this->scopes =
$this->scopeChildren =
$this->scopedServices =
$this->scopeStacks = array();
$this->set('service_container', $this);
}
public function compile()
@ -316,9 +320,19 @@ class Container implements ContainerInterface
{
$this->parameterBag->set($name, $value);
}
public function set($id, $service)
public function set($id, $service, $scope = self::SCOPE_CONTAINER)
{
$this->services[strtolower($id)] = $service;
if (self::SCOPE_PROTOTYPE === $scope) {
throw new \InvalidArgumentException('You cannot set services of scope "prototype".');
}
$id = strtolower($id);
if (self::SCOPE_CONTAINER !== $scope) {
if (!isset($this->scopedServices[$scope])) {
throw new \RuntimeException('You cannot set services of inactive scopes.');
}
$this->scopedServices[$scope][$id] = $service;
}
$this->services[$id] = $service;
}
public function has($id)
{
@ -355,6 +369,82 @@ class Container implements ContainerInterface
}
return array_merge($ids, array_keys($this->services));
}
public function enterScope($name)
{
if (!isset($this->scopes[$name])) {
throw new \InvalidArgumentException(sprintf('The scope "%s" does not exist.', $name));
}
if (self::SCOPE_CONTAINER !== $this->scopes[$name] && !isset($this->scopedServices[$this->scopes[$name]])) {
throw new \RuntimeException(sprintf('The parent scope "%s" must be active when entering this scope.', $this->scopes[$name]));
}
if (isset($this->scopedServices[$name])) {
$services = array($this->services, $name => $this->scopedServices[$name]);
unset($this->scopedServices[$name]);
foreach ($this->scopeChildren[$name] as $child) {
$services[$child] = $this->scopedServices[$child];
unset($this->scopedServices[$child]);
}
$this->services = call_user_func_array('array_diff_key', $services);
array_shift($services);
if (!isset($this->scopeStacks[$name])) {
$this->scopeStacks[$name] = new \SplStack();
}
$this->scopeStacks[$name]->push($services);
}
$this->scopedServices[$name] = array();
}
public function leaveScope($name)
{
if (!isset($this->scopedServices[$name])) {
throw new \InvalidArgumentException(sprintf('The scope "%s" is not active.', $name));
}
$services = array($this->services, $this->scopedServices[$name]);
unset($this->scopedServices[$name]);
foreach ($this->scopeChildren[$name] as $child) {
if (!isset($this->scopedServices[$child])) {
continue;
}
$services[] = $this->scopedServices[$child];
unset($this->scopedServices[$child]);
}
$this->services = call_user_func_array('array_diff_key', $services);
if (isset($this->scopeStacks[$name]) && count($this->scopeStacks[$name]) > 0) {
$services = $this->scopeStacks[$name]->pop();
$this->scopedServices += $services;
array_unshift($services, $this->services);
$this->services = call_user_func_array('array_merge', $services);
}
}
public function addScope($name, $parentScope = self::SCOPE_CONTAINER)
{
if (self::SCOPE_CONTAINER === $name || self::SCOPE_PROTOTYPE === $name) {
throw new \InvalidArgumentException(sprintf('The scope "%s" is reserved.', $name));
}
if (isset($this->scopes[$name])) {
throw new \InvalidArgumentException(sprintf('A scope with name "%s" already exists.', $name));
}
if (self::SCOPE_CONTAINER !== $parentScope && !isset($this->scopes[$parentScope])) {
throw new \InvalidArgumentException(sprintf('The parent scope "%s" does not exist, or is invalid.', $parentScope));
}
$this->scopes[$name] = $parentScope;
$this->scopeChildren[$name] = array();
if ($parentScope !== self::SCOPE_CONTAINER) {
$this->scopeChildren[$parentScope][] = $name;
foreach ($this->scopeChildren as $pName => $childScopes) {
if (in_array($parentScope, $childScopes, true)) {
$this->scopeChildren[$pName][] = $name;
}
}
}
}
public function hasScope($name)
{
return isset($this->scopes[$name]);
}
public function isScopeActive($name)
{
return isset($this->scopedServices[$name]);
}
static public function camelize($id)
{
return preg_replace(array('/(?:^|_)+(.)/e', '/\.(.)/e'), array("strtoupper('\\1')", "'_'.strtoupper('\\1')"), $id);
@ -379,9 +469,16 @@ interface ContainerInterface
const EXCEPTION_ON_INVALID_REFERENCE = 1;
const NULL_ON_INVALID_REFERENCE = 2;
const IGNORE_ON_INVALID_REFERENCE = 3;
function set($id, $service);
const SCOPE_CONTAINER = 'container';
const SCOPE_PROTOTYPE = 'prototype';
function set($id, $service, $scope = self::SCOPE_CONTAINER);
function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE);
function has($id);
function enterScope($name);
function leaveScope($name);
function addScope($name, $parentScope = self::SCOPE_CONTAINER);
function hasScope($name);
function isScopeActive($name);
}
}
namespace Symfony\Component\DependencyInjection\ParameterBag

View File

@ -87,9 +87,8 @@ class AnalyzeServiceReferencesPassTest extends \PHPUnit_Framework_TestCase
protected function process(ContainerBuilder $container)
{
$pass = new RepeatedPass(array(new AnalyzeServiceReferencesPass()));
$pass->setCompiler($compiler = new Compiler());
$pass->process($container);
return $compiler->getServiceReferenceGraph();
return $container->getCompiler()->getServiceReferenceGraph();
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace Symfony\Tests\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Compiler\CheckCircularReferencesPass;
use Symfony\Component\DependencyInjection\Compiler\AnalyzeServiceReferencesPass;
use Symfony\Component\DependencyInjection\Compiler\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class CheckCircularReferencesPassTest extends \PHPUnit_Framework_TestCase
{
/**
* @expectedException \RuntimeException
*/
public function testProcess()
{
$container = new ContainerBuilder();
$container->register('a')->addArgument(new Reference('b'));
$container->register('b')->addArgument(new Reference('a'));
$this->process($container);
}
/**
* @expectedException \RuntimeException
*/
public function testProcessDetectsIndirectCircularReference()
{
$container = new ContainerBuilder();
$container->register('a')->addArgument(new Reference('b'));
$container->register('b')->addArgument(new Reference('c'));
$container->register('c')->addArgument(new Reference('a'));
$this->process($container);
}
public function testProcessIgnoresMethodCalls()
{
$container = new ContainerBuilder();
$container->register('a')->addArgument(new Reference('b'));
$container->register('b')->addMethodCall('setA', array(new Reference('a')));
$this->process($container);
}
protected function process(ContainerBuilder $container)
{
$compiler = new Compiler();
$passConfig = $compiler->getPassConfig();
$passConfig->setOptimizationPasses(array(
new AnalyzeServiceReferencesPass(true),
new CheckCircularReferencesPass(),
));
$passConfig->setRemovingPasses(array());
$compiler->compile($container);
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace Symfony\Tests\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CheckReferenceScopePass;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class CheckReferenceScopePassTest extends \PHPUnit_Framework_TestCase
{
public function testProcessIgnoresScopeWideningIfNonStrictReference()
{
$container = new ContainerBuilder();
$container->register('a')->addArgument(new Reference('b', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false));
$container->register('b')->setScope('prototype');
$this->process($container);
}
/**
* @expectedException \RuntimeException
*/
public function testProcessDetectsScopeWidening()
{
$container = new ContainerBuilder();
$container->register('a')->addArgument(new Reference('b'));
$container->register('b')->setScope('prototype');
$this->process($container);
}
public function testProcessIgnoresCrossScopeHierarchyReferenceIfNotStrict()
{
$container = new ContainerBuilder();
$container->addScope('a');
$container->addScope('b');
$container->register('a')->setScope('a')->addArgument(new Reference('b', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false));
$container->register('b')->setScope('b');
$this->process($container);
}
/**
* @expectedException \RuntimeException
*/
public function testProcessDetectsCrossScopeHierarchyReference()
{
$container = new ContainerBuilder();
$container->addScope('a');
$container->addScope('b');
$container->register('a')->setScope('a')->addArgument(new Reference('b'));
$container->register('b')->setScope('b');
$this->process($container);
}
public function testProcess()
{
$container = new ContainerBuilder();
$container->register('a')->addArgument(new Reference('b'));
$container->register('b');
$this->process($container);
}
protected function process(ContainerBuilder $container)
{
$pass = new CheckReferenceScopePass();
$pass->process($container);
}
}

View File

@ -41,7 +41,7 @@ class InlineServiceDefinitionsPassTest extends \PHPUnit_Framework_TestCase
$this->assertSame($container->getDefinition('inlinable.service'), $arguments[0]);
}
public function testProcessDoesNotInlinesWhenAliasedServiceIsNotShared()
public function testProcessDoesNotInlineWhenAliasedServiceIsNotOfPrototypeScope()
{
$container = new ContainerBuilder();
$container
@ -61,17 +61,17 @@ class InlineServiceDefinitionsPassTest extends \PHPUnit_Framework_TestCase
$this->assertSame($ref, $arguments[0]);
}
public function testProcessDoesInlineNonSharedService()
public function testProcessDoesInlineServiceOfPrototypeScope()
{
$container = new ContainerBuilder();
$container
->register('foo')
->setShared(false)
->setScope('prototype')
;
$container
->register('bar')
->setPublic(false)
->setShared(false)
->setScope('prototype')
;
$container->setAlias('moo', 'bar');
@ -113,7 +113,6 @@ class InlineServiceDefinitionsPassTest extends \PHPUnit_Framework_TestCase
protected function process(ContainerBuilder $container)
{
$repeatedPass = new RepeatedPass(array(new AnalyzeServiceReferencesPass(), new InlineServiceDefinitionsPass()));
$repeatedPass->setCompiler(new Compiler());
$repeatedPass->process($container);
}
}

View File

@ -84,7 +84,6 @@ class RemoveUnusedDefinitionsPassTest extends \PHPUnit_Framework_TestCase
protected function process(ContainerBuilder $container)
{
$repeatedPass = new RepeatedPass(array(new AnalyzeServiceReferencesPass(), new RemoveUnusedDefinitionsPass()));
$repeatedPass->setCompiler(new Compiler());
$repeatedPass->process($container);
}
}

View File

@ -112,7 +112,7 @@ class ContainerBuilderTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('Circular reference detected for service "baz" (services currently loading: baz).', $e->getMessage(), '->get() throws a LogicException if the service has a circular reference to itself');
}
$builder->register('foobar', 'stdClass')->setShared(true);
$builder->register('foobar', 'stdClass')->setScope('container');
$this->assertTrue($builder->get('bar') === $builder->get('bar'), '->get() always returns the same instance if the service is shared');
}

View File

@ -97,7 +97,7 @@ class ContainerTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(array('service_container', 'foo', 'bar'), $sc->getServiceIds(), '->getServiceIds() returns all defined service ids');
$sc = new ProjectServiceContainer();
$this->assertEquals(array('bar', 'foo_bar', 'foo.baz', 'service_container'), $sc->getServiceIds(), '->getServiceIds() returns defined service ids by getXXXService() methods');
$this->assertEquals(array('scoped', 'scoped_foo', 'bar', 'foo_bar', 'foo.baz', 'service_container'), $sc->getServiceIds(), '->getServiceIds() returns defined service ids by getXXXService() methods');
}
/**
@ -110,6 +110,37 @@ class ContainerTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($foo, $sc->get('foo'), '->set() sets a service');
}
/**
* @expectedException \InvalidArgumentException
*/
public function testSetDoesNotAllowPrototypeScope()
{
$c = new Container();
$c->set('foo', new \stdClass(), 'prototype');
}
/**
* @expectedException \RuntimeException
*/
public function testSetDoesNotAllowInactiveScope()
{
$c = new Container();
$c->addScope('foo');
$c->set('foo', new \stdClass(), 'foo');
}
public function testSetAlsoSetsScopedService()
{
$c = new Container();
$c->addScope('foo');
$c->enterScope('foo');
$c->set('foo', $foo = new \stdClass(), 'foo');
$services = $this->getField($c, 'scopedServices');
$this->assertTrue(isset($services['foo']['foo']));
$this->assertSame($foo, $services['foo']['foo']);
}
/**
* @covers Symfony\Component\DependencyInjection\Container::get
*/
@ -148,6 +179,151 @@ class ContainerTest extends \PHPUnit_Framework_TestCase
$this->assertTrue($sc->has('foo_bar'), '->has() returns true if a get*Method() is defined');
$this->assertTrue($sc->has('foo.baz'), '->has() returns true if a get*Method() is defined');
}
public function testEnterLeaveCurrentScope()
{
$container = new ProjectServiceContainer();
$container->addScope('foo');
$container->enterScope('foo');
$scoped1 = $container->get('scoped');
$scopedFoo1 = $container->get('scoped_foo');
$container->enterScope('foo');
$scoped2 = $container->get('scoped');
$scoped3 = $container->get('scoped');
$scopedFoo2 = $container->get('scoped_foo');
$container->leaveScope('foo');
$scoped4 = $container->get('scoped');
$scopedFoo3 = $container->get('scoped_foo');
$this->assertNotSame($scoped1, $scoped2);
$this->assertSame($scoped2, $scoped3);
$this->assertSame($scoped1, $scoped4);
$this->assertNotSame($scopedFoo1, $scopedFoo2);
$this->assertSame($scopedFoo1, $scopedFoo3);
}
public function testEnterLeaveScopeWithChildScopes()
{
$container = new Container();
$container->addScope('foo');
$container->addScope('bar', 'foo');
$this->assertFalse($container->isScopeActive('foo'));
$container->enterScope('foo');
$container->enterScope('bar');
$this->assertTrue($container->isScopeActive('foo'));
$this->assertFalse($container->has('a'));
$a = new \stdClass();
$container->set('a', $a, 'bar');
$services = $this->getField($container, 'scopedServices');
$this->assertTrue(isset($services['bar']['a']));
$this->assertSame($a, $services['bar']['a']);
$this->assertTrue($container->has('a'));
$container->leaveScope('foo');
$services = $this->getField($container, 'scopedServices');
$this->assertFalse(isset($services['bar']));
$this->assertFalse($container->isScopeActive('foo'));
$this->assertFalse($container->has('a'));
}
/**
* @expectedException \InvalidArgumentException
* @dataProvider getBuiltInScopes
*/
public function testAddScopeDoesNotAllowBuiltInScopes($scope)
{
$container = new Container();
$container->addScope($scope);
}
/**
* @expectedException \InvalidArgumentException
*/
public function testAddScopeDoesNotAllowExistingScope()
{
$container = new Container();
$container->addScope('foo');
$container->addScope('foo');
}
/**
* @expectedException \InvalidArgumentException
* @dataProvider getInvalidParentScopes
*/
public function testAddScopeDoesNotAllowInvalidParentScope($scope)
{
$c = new Container();
$c->addScope('foo', $scope);
}
public function testAddScope()
{
$c = new Container();
$c->addScope('foo');
$c->addScope('bar', 'foo');
$this->assertSame(array('foo' => 'container', 'bar' => 'foo'), $this->getField($c, 'scopes'));
$this->assertSame(array('foo' => array('bar'), 'bar' => array()), $this->getField($c, 'scopeChildren'));
}
public function testHasScope()
{
$c = new Container();
$this->assertFalse($c->hasScope('foo'));
$c->addScope('foo');
$this->assertTrue($c->hasScope('foo'));
}
public function testIsScopeActive()
{
$c = new Container();
$this->assertFalse($c->isScopeActive('foo'));
$c->addScope('foo');
$this->assertFalse($c->isScopeActive('foo'));
$c->enterScope('foo');
$this->assertTrue($c->isScopeActive('foo'));
$c->leaveScope('foo');
$this->assertFalse($c->isScopeActive('foo'));
}
public function getInvalidParentScopes()
{
return array(
array(ContainerInterface::SCOPE_PROTOTYPE),
array('bar'),
);
}
public function getBuiltInScopes()
{
return array(
array(ContainerInterface::SCOPE_CONTAINER),
array(ContainerInterface::SCOPE_PROTOTYPE),
);
}
protected function getField($obj, $field)
{
$reflection = new \ReflectionProperty($obj, $field);
$reflection->setAccessible(true);
return $reflection->getValue($obj);
}
}
class ProjectServiceContainer extends Container
@ -163,6 +339,24 @@ class ProjectServiceContainer extends Container
$this->__foo_baz = new \stdClass();
}
protected function getScopedService()
{
if (!isset($this->scopedServices['foo'])) {
throw new \RuntimeException('Invalid call');
}
return $this->services['scoped'] = $this->scopedServices['foo']['scoped'] = new \stdClass();
}
protected function getScopedFooService()
{
if (!isset($this->scopedServices['foo'])) {
throw new \RuntimeException('invalid call');
}
return $this->services['scoped_foo'] = $this->scopedServices['foo']['scoped_foo'] = new \stdClass();
}
protected function getBarService()
{
return $this->__bar;

View File

@ -102,15 +102,15 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase
}
/**
* @covers Symfony\Component\DependencyInjection\Definition::setShared
* @covers Symfony\Component\DependencyInjection\Definition::isShared
* @covers Symfony\Component\DependencyInjection\Definition::setScope
* @covers Symfony\Component\DependencyInjection\Definition::getScope
*/
public function testSetIsShared()
public function testSetGetScope()
{
$def = new Definition('stdClass');
$this->assertTrue($def->isShared(), '->isShared() returns true by default');
$this->assertSame($def, $def->setShared(false), '->setShared() implements a fluent interface');
$this->assertFalse($def->isShared(), '->isShared() returns false if the instance must not be shared');
$this->assertEquals('container', $def->getScope());
$this->assertSame($def, $def->setScope('foo'));
$this->assertEquals('foo', $def->getScope());
}
/**

View File

@ -14,7 +14,7 @@ $container->
addTag('foo', array('bar' => 'bar'))->
setFactoryMethod('getInstance')->
setArguments(array('foo', new Reference('foo.baz'), array('%foo%' => 'foo is %foo%', 'bar' => '%foo%'), true, new Reference('service_container')))->
setShared(false)->
setScope('prototype')->
addMethodCall('setBar', array(new Reference('bar')))->
addMethodCall('initialize')->
setConfigurator('sc_configure')
@ -22,7 +22,7 @@ $container->
$container->
register('bar', 'FooClass')->
setArguments(array('foo', new Reference('foo.baz'), new Parameter('foo_bar')))->
setShared(true)->
setScope('container')->
setConfigurator(array(new Reference('foo.baz'), 'configure'))
;
$container->

View File

@ -6,8 +6,9 @@
<services>
<service id="foo" class="FooClass" />
<service id="baz" class="BazClass" />
<service id="shared" class="FooClass" shared="true" />
<service id="non_shared" class="FooClass" shared="false" />
<service id="scope.container" class="FooClass" scope="container" />
<service id="scope.custom" class="FooClass" scope="custom" />
<service id="scope.prototype" class="FooClass" scope="prototype" />
<service id="constructor" class="FooClass" factory-method="getInstance" />
<service id="file" class="FooClass">
<file>%path%/foo.php</file>

View File

@ -6,7 +6,7 @@
<parameter key="foo">bar</parameter>
</parameters>
<services>
<service id="foo" class="FooClass" factory-method="getInstance" shared="false">
<service id="foo" class="FooClass" factory-method="getInstance" scope="prototype">
<tag name="foo" foo="foo"/>
<tag name="foo" bar="bar"/>
<argument>foo</argument>

View File

@ -1,8 +1,9 @@
services:
foo: { class: FooClass }
baz: { class: BazClass }
shared: { class: FooClass, shared: true }
non_shared: { class: FooClass, shared: false }
scope.container: { class: FooClass, scope: container }
scope.custom: { class: FooClass, scope: custom }
scope.prototype: { class: FooClass, scope: prototype }
constructor: { class: FooClass, factory_method: getInstance }
file: { class: FooClass, file: %path%/foo.php }
arguments: { class: FooClass, arguments: [foo, @foo, [true, false]] }

View File

@ -15,7 +15,7 @@ services:
- [setBar, ['@bar']]
- [initialize, { }]
shared: false
scope: prototype
configurator: sc_configure
bar:
class: FooClass

View File

@ -11,6 +11,8 @@
namespace Symfony\Tests\Component\DependencyInjection\Loader;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Definition;
@ -132,13 +134,14 @@ class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase
$this->assertTrue(isset($services['foo']), '->load() parses <service> elements');
$this->assertEquals('Symfony\\Component\\DependencyInjection\\Definition', get_class($services['foo']), '->load() converts <service> element to Definition instances');
$this->assertEquals('FooClass', $services['foo']->getClass(), '->load() parses the class attribute');
$this->assertTrue($services['shared']->isShared(), '->load() parses the shared attribute');
$this->assertFalse($services['non_shared']->isShared(), '->load() parses the shared attribute');
$this->assertEquals('container', $services['scope.container']->getScope());
$this->assertEquals('custom', $services['scope.custom']->getScope());
$this->assertEquals('prototype', $services['scope.prototype']->getScope());
$this->assertEquals('getInstance', $services['constructor']->getFactoryMethod(), '->load() parses the factory-method attribute');
$this->assertEquals('%path%/foo.php', $services['file']->getFile(), '->load() parses the file tag');
$this->assertEquals(array('foo', new Reference('foo'), array(true, false)), $services['arguments']->getArguments(), '->load() parses the argument tags');
$this->assertEquals('sc_configure', $services['configurator1']->getConfigurator(), '->load() parses the configurator tag');
$this->assertEquals(array(new Reference('baz'), 'configure'), $services['configurator2']->getConfigurator(), '->load() parses the configurator tag');
$this->assertEquals(array(new Reference('baz', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false), 'configure'), $services['configurator2']->getConfigurator(), '->load() parses the configurator tag');
$this->assertEquals(array('BazClass', 'configureStatic'), $services['configurator3']->getConfigurator(), '->load() parses the configurator tag');
$this->assertEquals(array(array('setBar', array())), $services['method_call1']->getMethodCalls(), '->load() parses the method_call tag');
$this->assertEquals(array(array('setBar', array('foo', new Reference('foo'), array(true, false)))), $services['method_call2']->getMethodCalls(), '->load() parses the method_call tag');

View File

@ -97,8 +97,9 @@ class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase
$this->assertTrue(isset($services['foo']), '->load() parses service elements');
$this->assertEquals('Symfony\\Component\\DependencyInjection\\Definition', get_class($services['foo']), '->load() converts service element to Definition instances');
$this->assertEquals('FooClass', $services['foo']->getClass(), '->load() parses the class attribute');
$this->assertTrue($services['shared']->isShared(), '->load() parses the shared attribute');
$this->assertFalse($services['non_shared']->isShared(), '->load() parses the shared attribute');
$this->assertEquals('container', $services['scope.container']->getScope());
$this->assertEquals('custom', $services['scope.custom']->getScope());
$this->assertEquals('prototype', $services['scope.prototype']->getScope());
$this->assertEquals('getInstance', $services['constructor']->getFactoryMethod(), '->load() parses the factory_method attribute');
$this->assertEquals('%path%/foo.php', $services['file']->getFile(), '->load() parses the file tag');
$this->assertEquals(array('foo', new Reference('foo'), array(true, false)), $services['arguments']->getArguments(), '->load() parses the argument tags');

View File

@ -156,122 +156,6 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('foo', $kernel->handle(new Request())->getContent());
}
/**
* @testdox A master request should be set on the kernel for the duration of handle(), then unset
*/
public function testHandleSetsTheCurrentRequest()
{
$dispatcher = new EventDispatcher();
$resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface');
$kernel = new HttpKernel($dispatcher, $resolver);
$request = new Request();
$expected = new Response();
$testCase = $this;
$controller = function() use($expected, $kernel, $testCase, $request)
{
$testCase->assertSame($request, $kernel->getRequest(), '->handle() sets the current request when there is no parent request');
return $expected;
};
$resolver->expects($this->once())
->method('getController')
->with($request)
->will($this->returnValue($controller));
$resolver->expects($this->once())
->method('getArguments')
->with($request, $controller)
->will($this->returnValue(array()));
$actual = $kernel->handle($request);
$this->assertSame($expected, $actual, '->handle() returns the response');
$this->assertNull($kernel->getRequest(), '->handle() restores the parent (null) request');
}
/**
* @testdox The parent request is restored following a sub request
* @dataProvider provideRequestTypes
*/
public function testHandleRestoresThePreviousRequest($requestType)
{
$dispatcher = new EventDispatcher();
$resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface');
$kernel = new HttpKernel($dispatcher, $resolver);
$parentRequest = new Request(array('name' => 'parent_request'));
$request = new Request(array('name' => 'current_request'));
$expected = new Response();
// sets a parent request to emulate a subrequest
$reflProp = new \ReflectionProperty($kernel, 'request');
$reflProp->setAccessible(true);
$reflProp->setValue($kernel, $parentRequest);
$testCase = $this;
$controller = function() use($expected, $kernel, $testCase, $request)
{
$testCase->assertSame($request, $kernel->getRequest(), '->handle() sets the current request when there is a parent request');
return $expected;
};
$resolver->expects($this->once())
->method('getController')
->with($request)
->will($this->returnValue($controller));
$resolver->expects($this->once())
->method('getArguments')
->with($request, $controller)
->will($this->returnValue(array()));
// the behavior should be the same, regardless of request type
$actual = $kernel->handle($request, $requestType);
$this->assertSame($expected, $actual, '->handle() returns the response');
$this->assertSame($parentRequest, $kernel->getRequest(), '->handle() restores the parent request');
}
public function provideRequestTypes()
{
return array(
array(HttpKernelInterface::MASTER_REQUEST),
array(HttpKernelInterface::SUB_REQUEST),
);
}
public function testHandleRestoresThePreviousRequestOnException()
{
$dispatcher = new EventDispatcher();
$resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface');
$kernel = new HttpKernel($dispatcher, $resolver);
$request = new Request();
$expected = new \Exception();
$controller = function() use ($expected)
{
throw $expected;
};
$resolver->expects($this->once())
->method('getController')
->with($request)
->will($this->returnValue($controller));
$resolver->expects($this->once())
->method('getArguments')
->with($request, $controller)
->will($this->returnValue(array()));
try {
$kernel->handle($request);
$this->fail('->handle() suppresses the controller exception');
} catch (\Exception $actual) {
$this->assertSame($expected, $actual, '->handle() throws the controller exception');
}
$this->assertNull($kernel->getRequest(), '->handle() restores the parent (null) request when the controller throws an exception');
}
protected function getResolver($controller = null)
{
if (null === $controller) {