Issue #2531564 by neclimdul, andypost, chx, cburschka, Spokje, pdenooijer, Charlie ChX Negyesi, znerol, dawehner, longwave, Fabianx, geek-merlin, catch, alexpott: Fix leaky and brittle container serialization solution

merge-requests/2760/head
catch 2022-09-13 09:52:09 +01:00
parent 35ba1d5ce7
commit 667d8b008f
28 changed files with 359 additions and 205 deletions

View File

@ -136,7 +136,7 @@ class Drupal {
/** /**
* The currently active container object, or NULL if not initialized yet. * The currently active container object, or NULL if not initialized yet.
* *
* @var \Symfony\Component\DependencyInjection\ContainerInterface|null * @var \Drupal\Component\DependencyInjection\ContainerInterface|null
*/ */
protected static $container; protected static $container;
@ -160,7 +160,7 @@ class Drupal {
/** /**
* Returns the currently active global container. * Returns the currently active global container.
* *
* @return \Symfony\Component\DependencyInjection\ContainerInterface * @return \Drupal\Component\DependencyInjection\ContainerInterface
* *
* @throws \Drupal\Core\DependencyInjection\ContainerNotInitializedException * @throws \Drupal\Core\DependencyInjection\ContainerNotInitializedException
*/ */

View File

@ -2,7 +2,6 @@
namespace Drupal\Component\DependencyInjection; namespace Drupal\Component\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Exception\RuntimeException;
@ -46,6 +45,8 @@ use Symfony\Contracts\Service\ResetInterface;
*/ */
class Container implements ContainerInterface, ResetInterface { class Container implements ContainerInterface, ResetInterface {
use ServiceIdHashTrait;
/** /**
* The parameters of the container. * The parameters of the container.
* *
@ -536,10 +537,7 @@ class Container implements ContainerInterface, ResetInterface {
} }
/** /**
* Gets all defined service IDs. * {@inheritdoc}
*
* @return array
* An array of all defined service IDs.
*/ */
public function getServiceIds() { public function getServiceIds() {
return array_keys($this->serviceDefinitions + $this->services); return array_keys($this->serviceDefinitions + $this->services);

View File

@ -0,0 +1,42 @@
<?php
namespace Drupal\Component\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerInterface as BaseContainerInterface;
/**
* The interface for Drupal service container classes.
*
* This interface extends Symfony's ContainerInterface and adds methods for
* managing mappings of instantiated services to its IDs.
*/
interface ContainerInterface extends BaseContainerInterface {
/**
* Gets all defined service IDs.
*
* @return array
* An array of all defined service IDs.
*/
public function getServiceIds();
/**
* Collect a mapping between service to ids.
*
* @return array
* Service ids keyed by a unique hash.
*/
public function getServiceIdMappings(): array;
/**
* Generate a unique hash for a service object.
*
* @param object $object
* Object needing a unique hash.
*
* @return string
* A unique hash identifying the object.
*/
public function generateServiceIdHash(object $object): string;
}

View File

@ -433,6 +433,7 @@ class OptimizedPhpArrayDumper extends Dumper {
elseif (is_object($value)) { elseif (is_object($value)) {
// Drupal specific: Instantiated objects have a _serviceId parameter. // Drupal specific: Instantiated objects have a _serviceId parameter.
if (isset($value->_serviceId)) { if (isset($value->_serviceId)) {
@trigger_error('_serviceId is deprecated in drupal:9.5.0 and is removed from drupal:11.0.0. Use \Drupal\Core\DrupalKernelInterface::getServiceIdMapping() instead. See https://www.drupal.org/node/3292540', E_USER_DEPRECATED);
return $this->getReferenceCall($value->_serviceId); return $this->getReferenceCall($value->_serviceId);
} }
throw new RuntimeException('Unable to dump a service container if a parameter is an object without _serviceId.'); throw new RuntimeException('Unable to dump a service container if a parameter is an object without _serviceId.');

View File

@ -0,0 +1,37 @@
<?php
namespace Drupal\Component\DependencyInjection;
/**
* A trait for service id hashing implementations.
*
* Handles delayed cache tag invalidations.
*/
trait ServiceIdHashTrait {
/**
* Implements \Drupal\Component\DependencyInjection\ContainerInterface::getServiceIdMappings()
*/
public function getServiceIdMappings(): array {
$mapping = [];
foreach ($this->getServiceIds() as $service_id) {
if ($this->initialized($service_id) && $service_id !== 'service_container') {
$mapping[$this->generateServiceIdHash($this->get($service_id))] = $service_id;
}
}
return $mapping;
}
/**
* Implements \Drupal\Component\DependencyInjection\ContainerInterface::generateServiceIdHash()
*/
public function generateServiceIdHash(object $object): string {
// Include class name as an additional namespace for the hash since
// spl_object_hash's return can be recycled. This still is not a 100%
// guarantee to be unique but makes collisions incredibly difficult and even
// then the interface would be preserved.
// @see https://php.net/spl_object_hash#refsect1-function.spl-object-hash-notes
return hash('sha256', get_class($object) . spl_object_hash($object));
}
}

View File

@ -1064,13 +1064,13 @@ class ConfigImporter {
* keep the services used by the importer in sync. * keep the services used by the importer in sync.
*/ */
protected function reInjectMe() { protected function reInjectMe() {
$this->_serviceIds = []; // When rebuilding the container,
$vars = get_object_vars($this); // \Drupal\Core\DrupalKernel::initializeContainer() saves the hashes of the
foreach ($vars as $key => $value) { // old container and passes them to the new one. So __sleep() will
if (is_object($value) && isset($value->_serviceId)) { // recognize the old services and then __wakeup() will restore them from
$this->$key = \Drupal::service($value->_serviceId); // the new container.
} $this->__sleep();
} $this->__wakeup();
} }
} }

View File

@ -10,7 +10,6 @@ use Drupal\Core\DependencyInjection\Compiler\CorsCompilerPass;
use Drupal\Core\DependencyInjection\Compiler\DeprecatedServicePass; use Drupal\Core\DependencyInjection\Compiler\DeprecatedServicePass;
use Drupal\Core\DependencyInjection\Compiler\ContextProvidersPass; use Drupal\Core\DependencyInjection\Compiler\ContextProvidersPass;
use Drupal\Core\DependencyInjection\Compiler\ProxyServicesPass; use Drupal\Core\DependencyInjection\Compiler\ProxyServicesPass;
use Drupal\Core\DependencyInjection\Compiler\DependencySerializationTraitPass;
use Drupal\Core\DependencyInjection\Compiler\StackedKernelPass; use Drupal\Core\DependencyInjection\Compiler\StackedKernelPass;
use Drupal\Core\DependencyInjection\Compiler\StackedSessionHandlerPass; use Drupal\Core\DependencyInjection\Compiler\StackedSessionHandlerPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterStreamWrappersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterStreamWrappersPass;
@ -95,7 +94,6 @@ class CoreServiceProvider implements ServiceProviderInterface, ServiceModifierIn
// Register plugin managers. // Register plugin managers.
$container->addCompilerPass(new PluginManagerPass()); $container->addCompilerPass(new PluginManagerPass());
$container->addCompilerPass(new DependencySerializationTraitPass());
$container->addCompilerPass(new DeprecatedServicePass()); $container->addCompilerPass(new DeprecatedServicePass());
} }

View File

@ -8,6 +8,11 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
/** /**
* Sets the _serviceId property on all services. * Sets the _serviceId property on all services.
* *
* @deprecated in drupal:9.5.0 and is removed from drupal:11.0.0. The _serviceId
* property is no longer part of the container. Use
* \Drupal\Core\DrupalKernelInterface::getServiceIdMapping() instead.
*
* @see https://www.drupal.org/node/3292540
* @see \Drupal\Core\DependencyInjection\DependencySerializationTrait * @see \Drupal\Core\DependencyInjection\DependencySerializationTrait
*/ */
class DependencySerializationTraitPass implements CompilerPassInterface { class DependencySerializationTraitPass implements CompilerPassInterface {
@ -16,35 +21,6 @@ class DependencySerializationTraitPass implements CompilerPassInterface {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function process(ContainerBuilder $container) { public function process(ContainerBuilder $container) {
$decorations = new \SplPriorityQueue();
$order = PHP_INT_MAX;
foreach ($container->getDefinitions() as $service_id => $definition) {
// Only add the property to services that are public (as private services
// can not be reloaded through Container::get()) and are objects.
if (!$definition->hasTag('parameter_service') && $definition->isPublic()) {
$definition->setProperty('_serviceId', $service_id);
}
if ($decorated = $definition->getDecoratedService()) {
$decorations->insert([$service_id, $definition], [$decorated[2], --$order]);
}
}
foreach ($decorations as list($service_id, $definition)) {
list($inner, $renamedId) = $definition->getDecoratedService();
if (!$renamedId) {
$renamedId = $service_id . '.inner';
}
$original = $container->getDefinition($inner);
if ($original->isPublic()) {
// The old service is renamed.
$original->setProperty('_serviceId', $renamedId);
// The decorating service takes over the old ID.
$definition->setProperty('_serviceId', $inner);
}
}
} }
} }

View File

@ -9,18 +9,6 @@ use Drupal\Component\DependencyInjection\Container as DrupalContainer;
*/ */
class Container extends DrupalContainer { class Container extends DrupalContainer {
/**
* {@inheritdoc}
*/
public function set($id, $service) {
parent::set($id, $service);
// Ensure that the _serviceId property is set on synthetic services as well.
if (isset($this->services[$id]) && is_object($this->services[$id]) && !isset($this->services[$id]->_serviceId)) {
$this->services[$id]->_serviceId = $id;
}
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */

View File

@ -2,6 +2,8 @@
namespace Drupal\Core\DependencyInjection; namespace Drupal\Core\DependencyInjection;
use Drupal\Component\DependencyInjection\ContainerInterface;
use Drupal\Component\DependencyInjection\ServiceIdHashTrait;
use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ContainerBuilder as SymfonyContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder as SymfonyContainerBuilder;
use Symfony\Component\DependencyInjection\Container as SymfonyContainer; use Symfony\Component\DependencyInjection\Container as SymfonyContainer;
@ -15,7 +17,9 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
* *
* @ingroup container * @ingroup container
*/ */
class ContainerBuilder extends SymfonyContainerBuilder { class ContainerBuilder extends SymfonyContainerBuilder implements ContainerInterface {
use ServiceIdHashTrait;
/** /**
* {@inheritdoc} * {@inheritdoc}
@ -41,11 +45,6 @@ class ContainerBuilder extends SymfonyContainerBuilder {
throw new \InvalidArgumentException("Service ID names must be lowercase: $id"); throw new \InvalidArgumentException("Service ID names must be lowercase: $id");
} }
SymfonyContainer::set($id, $service); SymfonyContainer::set($id, $service);
// Ensure that the _serviceId property is set on synthetic services as well.
if (isset($this->services[$id]) && is_object($this->services[$id]) && !isset($this->services[$id]->_serviceId)) {
$this->services[$id]->_serviceId = $id;
}
} }
/** /**

View File

@ -3,7 +3,8 @@
namespace Drupal\Core\DependencyInjection; namespace Drupal\Core\DependencyInjection;
use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
/** /**
* Provides dependency injection friendly methods for serialization. * Provides dependency injection friendly methods for serialization.
@ -30,33 +31,50 @@ trait DependencySerializationTrait {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function __sleep() { public function __sleep() {
$this->_serviceIds = [];
$vars = get_object_vars($this); $vars = get_object_vars($this);
foreach ($vars as $key => $value) { try {
if (is_object($value) && isset($value->_serviceId)) { $container = \Drupal::getContainer();
// If a class member was instantiated by the dependency injection $mapping = \Drupal::service('kernel')->getServiceIdMapping();
// container, only store its ID so it can be used to get a fresh object foreach ($vars as $key => $value) {
// on unserialization. if ($value instanceof EntityStorageInterface) {
$this->_serviceIds[$key] = $value->_serviceId; // If a class member is an entity storage, only store the entity type
unset($vars[$key]); // ID the storage is for, so it can be used to get a fresh object on
} // unserialization. By doing this we prevent possible memory leaks
// Special case the container, which might not have a service ID. // when the storage is serialized and it contains a static cache of
elseif ($value instanceof ContainerInterface) { // entity objects. Additionally we ensure that we'll not have multiple
$this->_serviceIds[$key] = 'service_container'; // storage objects for the same entity type and therefore prevent
unset($vars[$key]); // returning different references for the same entity.
} $this->_entityStorages[$key] = $value->getEntityTypeId();
elseif ($value instanceof EntityStorageInterface) { unset($vars[$key]);
// If a class member is an entity storage, only store the entity type ID }
// the storage is for so it can be used to get a fresh object on elseif (is_object($value)) {
// unserialization. By doing this we prevent possible memory leaks when $service_id = FALSE;
// the storage is serialized when it contains a static cache of entity // Special case the container.
// objects and additionally we ensure that we'll not have multiple if ($value instanceof SymfonyContainerInterface) {
// storage objects for the same entity type and therefore prevent $service_id = 'service_container';
// returning different references for the same entity. }
$this->_entityStorages[$key] = $value->getEntityTypeId(); else {
unset($vars[$key]); $id = $container->generateServiceIdHash($value);
if (isset($mapping[$id])) {
$service_id = $mapping[$id];
}
}
if ($service_id) {
// If a class member was instantiated by the dependency injection
// container, only store its ID so it can be used to get a fresh object
// on unserialization.
$this->_serviceIds[$key] = $service_id;
unset($vars[$key]);
}
}
} }
} }
catch (ContainerNotInitializedException $e) {
// No container, no problem.
}
catch (ServiceNotFoundException $e) {
// No kernel, very strange, but still no problem.
}
return array_keys($vars); return array_keys($vars);
} }

View File

@ -105,7 +105,7 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
/** /**
* Holds the container instance. * Holds the container instance.
* *
* @var \Symfony\Component\DependencyInjection\ContainerInterface * @var \Drupal\Component\DependencyInjection\ContainerInterface
*/ */
protected $container; protected $container;
@ -242,6 +242,11 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
*/ */
protected $root; protected $root;
/**
* A mapping from service classes to service IDs.
*/
protected $serviceIdMapping = [];
/** /**
* Create a DrupalKernel object from a request. * Create a DrupalKernel object from a request.
* *
@ -815,6 +820,32 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
} }
} }
/**
* Generate a unique hash for a service object.
*
* @param object $object
* A service object.
*
* @return string
* A unique hash value.
*/
public static function generateServiceIdHash($object) {
// Include class name as an additional namespace for the hash since
// spl_object_hash's return can be recycled. This still is not a 100%
// guarantee to be unique but makes collisions incredibly difficult and even
// then the interface would be preserved.
// @see https://php.net/spl_object_hash#refsect1-function.spl-object-hash-notes
return hash('sha256', get_class($object) . spl_object_hash($object));
}
/**
* {@inheritdoc}
*/
public function getServiceIdMapping() {
$this->collectServiceIdMapping();
return $this->serviceIdMapping;
}
/** /**
* Returns the container cache key based on the environment. * Returns the container cache key based on the environment.
* *
@ -860,6 +891,8 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
if ($this->container->initialized('current_user')) { if ($this->container->initialized('current_user')) {
$current_user_id = $this->container->get('current_user')->id(); $current_user_id = $this->container->get('current_user')->id();
} }
// Save the current services.
$this->collectServiceIdMapping();
// If there is a session, close and save it. // If there is a session, close and save it.
if ($this->container->initialized('session')) { if ($this->container->initialized('session')) {
@ -1567,6 +1600,17 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
$this->serviceYamls['site'] = array_filter($service_yamls, 'file_exists'); $this->serviceYamls['site'] = array_filter($service_yamls, 'file_exists');
} }
/**
* Collect a mapping between service to ids.
*/
protected function collectServiceIdMapping() {
if (isset($this->container)) {
foreach ($this->container->getServiceIdMappings() as $hash => $service_id) {
$this->serviceIdMapping[$hash] = $service_id;
}
}
}
/** /**
* Gets the active install profile. * Gets the active install profile.
* *

View File

@ -137,4 +137,9 @@ interface DrupalKernelInterface extends HttpKernelInterface, ContainerAwareInter
*/ */
public function loadLegacyIncludes(); public function loadLegacyIncludes();
/**
* Get a mapping from service hashes to service IDs.
*/
public function getServiceIdMapping();
} }

View File

@ -2,6 +2,7 @@
namespace Drupal\Core\Test; namespace Drupal\Core\Test;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DrupalKernel; use Drupal\Core\DrupalKernel;
/** /**
@ -22,4 +23,23 @@ class TestKernel extends DrupalKernel {
parent::__construct($environment, $class_loader, $allow_dumping); parent::__construct($environment, $class_loader, $allow_dumping);
} }
/**
* Sets a container with a kernel service on the Drupal class.
*
* @return \Drupal\Component\DependencyInjection\ContainerInterface
* A container with the kernel service set.
*/
public static function setContainerWithKernel() {
$container = new ContainerBuilder();
$kernel = new DrupalKernel('test', NULL);
// Objects of the same type will have access to each others private and
// protected members even though they are not the same instances. This is
// because the implementation specific details are already known when
// inside those objects.
$kernel->container = $container;
$container->set('kernel', $kernel);
\Drupal::setContainer($container);
return $container;
}
} }

View File

@ -22,12 +22,16 @@ class DecoratedServiceTest extends KernelTestBase {
public function testDecoratedServiceId() { public function testDecoratedServiceId() {
// Service decorated once. // Service decorated once.
$test_service = $this->container->get('test_service'); $test_service = $this->container->get('test_service');
$this->assertEquals('test_service', $test_service->_serviceId); $hash = $this->container->generateServiceIdHash($test_service);
$mappings = $this->container->getServiceIdMappings();
$this->assertEquals('test_service', $mappings[$hash]);
$this->assertInstanceOf(TestServiceDecorator::class, $test_service); $this->assertInstanceOf(TestServiceDecorator::class, $test_service);
// Service decorated twice. // Service decorated twice.
$test_service2 = $this->container->get('test_service2'); $test_service2 = $this->container->get('test_service2');
$this->assertEquals('test_service2', $test_service2->_serviceId); $hash = $this->container->generateServiceIdHash($test_service2);
$mappings = $this->container->getServiceIdMappings();
$this->assertEquals('test_service2', $mappings[$hash]);
$this->assertInstanceOf(TestServiceDecorator::class, $test_service2); $this->assertInstanceOf(TestServiceDecorator::class, $test_service2);
} }

View File

@ -114,10 +114,6 @@ class ViewUIObjectTest extends UnitTestCase {
* Tests serialization of the ViewUI object. * Tests serialization of the ViewUI object.
*/ */
public function testSerialization() { public function testSerialization() {
// Set a container so the DependencySerializationTrait has it.
$container = new ContainerBuilder();
\Drupal::setContainer($container);
$storage = new View([], 'view'); $storage = new View([], 'view');
$executable = $this->getMockBuilder('Drupal\views\ViewExecutable') $executable = $this->getMockBuilder('Drupal\views\ViewExecutable')
->disableOriginalConstructor() ->disableOriginalConstructor()

View File

@ -0,0 +1,64 @@
<?php
namespace Drupal\KernelTests\Core\Entity;
use Drupal\Core\Entity\EntityType;
use Drupal\Core\StringTranslation\TranslationManager;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests general features of entity types.
*
* @group Entity
*/
class EntityTypeTest extends KernelTestBase {
/**
* Sets up an EntityType object for a given set of values.
*
* @param array $definition
* An array of values to use for the EntityType.
*
* @return \Drupal\Core\Entity\EntityTypeInterface
*/
protected function setUpEntityType($definition) {
$definition += [
'id' => 'example_entity_type',
];
return new EntityType($definition);
}
/**
* Tests that the EntityType object can be serialized.
*/
public function testIsSerializable() {
$entity_type = $this->setUpEntityType([]);
$translation_service = new class () extends TranslationManager {
/**
* Constructs a UnserializableTranslationManager object.
*/
public function __construct() {
}
/**
* @return array
*/
public function __serialize(): array {
throw new \Exception();
}
};
$this->container->set('bar', $translation_service);
$entity_type->setStringTranslation($this->container->get('string_translation'));
// This should not throw an exception.
$tmp = serialize($entity_type);
$entity_type = unserialize($tmp);
// And this should have the correct id.
$this->assertEquals('example_entity_type', $entity_type->id());
}
}

View File

@ -30,7 +30,7 @@ class ContainerTest extends TestCase {
/** /**
* The tested container. * The tested container.
* *
* @var \Symfony\Component\DependencyInjection\ContainerInterface * @var \Drupal\Component\DependencyInjection\ContainerInterface
*/ */
protected $container; protected $container;
@ -685,6 +685,20 @@ class ContainerTest extends TestCase {
$this->assertEquals(['ccc'], $this->container->get('service_with_raw_argument')->getArguments()); $this->assertEquals(['ccc'], $this->container->get('service_with_raw_argument')->getArguments());
} }
/**
* @covers \Drupal\Component\DependencyInjection\ServiceIdHashTrait::getServiceIdMappings
* @covers \Drupal\Component\DependencyInjection\ServiceIdHashTrait::generateServiceIdHash
*/
public function testGetServiceIdMappings() {
$this->assertEquals([], $this->container->getServiceIdMappings());
$s1 = $this->container->get('other.service');
$s2 = $this->container->get('late.service');
$this->assertEquals([
$this->container->generateServiceIdHash($s1) => 'other.service',
$this->container->generateServiceIdHash($s2) => 'late.service',
], $this->container->getServiceIdMappings());
}
/** /**
* Gets a mock container definition. * Gets a mock container definition.
* *

View File

@ -10,6 +10,7 @@ namespace Drupal\Tests\Component\DependencyInjection\Dumper {
use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\Crypt;
use Drupal\Tests\PhpUnitCompatibilityTrait; use Drupal\Tests\PhpUnitCompatibilityTrait;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Parameter; use Symfony\Component\DependencyInjection\Parameter;
@ -26,6 +27,7 @@ namespace Drupal\Tests\Component\DependencyInjection\Dumper {
class OptimizedPhpArrayDumperTest extends TestCase { class OptimizedPhpArrayDumperTest extends TestCase {
use PhpUnitCompatibilityTrait; use PhpUnitCompatibilityTrait;
use ExpectDeprecationTrait;
/** /**
* The container builder instance. * The container builder instance.
@ -346,16 +348,6 @@ namespace Drupal\Tests\Component\DependencyInjection\Dumper {
'arguments_expected' => $this->getCollection([$this->getParameterCall('llama_parameter')]), 'arguments_expected' => $this->getCollection([$this->getParameterCall('llama_parameter')]),
] + $base_service_definition; ] + $base_service_definition;
// Test objects that have _serviceId property.
$drupal_service = new \stdClass();
$drupal_service->_serviceId = 'bar';
$service_definitions[] = [
'arguments' => [$drupal_service],
'arguments_count' => 1,
'arguments_expected' => $this->getCollection([$this->getServiceCall('bar')]),
] + $base_service_definition;
// Test getMethodCalls. // Test getMethodCalls.
$calls = [ $calls = [
['method', $this->getCollection([])], ['method', $this->getCollection([])],
@ -589,6 +581,37 @@ namespace Drupal\Tests\Component\DependencyInjection\Dumper {
$this->dumper->getArray(); $this->dumper->getArray();
} }
/**
* Tests that the correct RuntimeException is thrown for dumping an object.
*
* @covers ::dumpValue
* @group legacy
*/
public function testGetServiceDefinitionForObjectServiceId() {
$service = new \stdClass();
$service->_serviceId = 'foo';
$services['foo'] = new Definition('\stdClass');
$services['bar'] = new Definition('\stdClass');
$services['bar']->addArgument($service);
foreach ($services as $s) {
$s->setPublic(TRUE);
}
$this->containerBuilder->getDefinitions()->willReturn($services);
$this->containerBuilder->getDefinition('foo')->willReturn($services['foo']);
$this->containerBuilder->getDefinition('bar')->willReturn($services['bar']);
$this->expectDeprecation('_serviceId is deprecated in drupal:9.5.0 and is removed from drupal:11.0.0. Use \Drupal\Core\DrupalKernelInterface::getServiceIdMapping() instead. See https://www.drupal.org/node/3292540');
$a = $this->dumper->getArray();
$this->assertEquals(
$this->serializeDefinition([
'class' => '\stdClass',
// Legacy code takes care of converting _serviceId into this.
'arguments' => $this->getCollection([$this->getServiceCall('foo')]),
'arguments_count' => 1,
]), $a['services']['bar']);
}
/** /**
* Tests that the correct RuntimeException is thrown for dumping a resource. * Tests that the correct RuntimeException is thrown for dumping a resource.
* *

View File

@ -97,8 +97,8 @@ class DrupalComponentTest extends TestCase {
$contents = file_get_contents($class_path); $contents = file_get_contents($class_path);
preg_match_all('/^.*Drupal\\\Core.*$/m', $contents, $matches); preg_match_all('/^.*Drupal\\\Core.*$/m', $contents, $matches);
$matches = array_filter($matches[0], function ($line) { $matches = array_filter($matches[0], function ($line) {
// Filter references to @see as they don't really matter. // Filter references that don't really matter.
return strpos($line, '@see') === FALSE; return preg_match('/@see|E_USER_DEPRECATED|expectDeprecation/', $line) === 0;
}); });
$this->assertEmpty($matches, "Checking for illegal reference to 'Drupal\\Core' namespace in $class_path"); $this->assertEmpty($matches, "Checking for illegal reference to 'Drupal\\Core' namespace in $class_path");
} }

View File

@ -15,6 +15,7 @@ use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Language\Language; use Drupal\Core\Language\Language;
use Drupal\Core\Plugin\DefaultLazyPluginCollection; use Drupal\Core\Plugin\DefaultLazyPluginCollection;
use Drupal\Core\Test\TestKernel;
use Drupal\Tests\Core\Config\Entity\Fixtures\ConfigEntityBaseWithPluginCollections; use Drupal\Tests\Core\Config\Entity\Fixtures\ConfigEntityBaseWithPluginCollections;
use Drupal\Tests\Core\Plugin\Fixtures\TestConfigurablePlugin; use Drupal\Tests\Core\Plugin\Fixtures\TestConfigurablePlugin;
use Drupal\Tests\UnitTestCase; use Drupal\Tests\UnitTestCase;
@ -357,9 +358,8 @@ class ConfigEntityBaseUnitTest extends UnitTestCase {
// Also set up a container with the plugin manager so that we can assert // Also set up a container with the plugin manager so that we can assert
// that the plugin manager itself is also not serialized. // that the plugin manager itself is also not serialized.
$container = new ContainerBuilder(); $container = TestKernel::setContainerWithKernel();
$container->set('plugin.manager.foo', $plugin_manager); $container->set('plugin.manager.foo', $plugin_manager->reveal());
\Drupal::setContainer($container);
$entity_values = ['the_plugin_collection_config' => [$instance_id => ['foo' => 'original_value']]]; $entity_values = ['the_plugin_collection_config' => [$instance_id => ['foo' => 'original_value']]];
$entity = new TestConfigEntityWithPluginCollections($entity_values, $this->entityTypeId); $entity = new TestConfigEntityWithPluginCollections($entity_values, $this->entityTypeId);

View File

@ -24,16 +24,6 @@ class ContainerBuilderTest extends UnitTestCase {
$this->assertInstanceOf(BarClass::class, $result); $this->assertInstanceOf(BarClass::class, $result);
} }
/**
* @covers ::set
*/
public function testSet() {
$container = new ContainerBuilder();
$class = new BarClass();
$container->set('bar', $class);
$this->assertEquals('bar', $class->_serviceId);
}
/** /**
* @covers ::set * @covers ::set
*/ */

View File

@ -4,7 +4,6 @@ namespace Drupal\Tests\Core\DependencyInjection;
use Drupal\Core\DependencyInjection\Container; use Drupal\Core\DependencyInjection\Container;
use Drupal\Tests\UnitTestCase; use Drupal\Tests\UnitTestCase;
use Drupal\Tests\Core\DependencyInjection\Fixture\BarClass;
/** /**
* @coversDefaultClass \Drupal\Core\DependencyInjection\Container * @coversDefaultClass \Drupal\Core\DependencyInjection\Container
@ -21,15 +20,4 @@ class ContainerTest extends UnitTestCase {
serialize($container); serialize($container);
} }
/**
* @covers ::set
*/
public function testSet() {
$container = new Container();
$class = new BarClass();
$container->set('bar', $class);
// Ensure that _serviceId is set on the object.
$this->assertEquals('bar', $class->_serviceId);
}
} }

View File

@ -7,8 +7,8 @@
namespace Drupal\Tests\Core\DependencyInjection; namespace Drupal\Tests\Core\DependencyInjection;
use Drupal\Core\DependencyInjection\Container;
use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Test\TestKernel;
use Drupal\Tests\UnitTestCase; use Drupal\Tests\UnitTestCase;
use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
@ -26,11 +26,9 @@ class DependencySerializationTest extends UnitTestCase {
public function testSerialization() { public function testSerialization() {
// Create a pseudo service and dependency injected object. // Create a pseudo service and dependency injected object.
$service = new \stdClass(); $service = new \stdClass();
$service->_serviceId = 'test_service'; $container = TestKernel::setContainerWithKernel();
$container = new Container();
$container->set('test_service', $service); $container->set('test_service', $service);
$container->set('service_container', $container); $this->assertSame($container, $container->get('service_container'));
\Drupal::setContainer($container);
$dependencySerialization = new DependencySerializationTestDummy($service); $dependencySerialization = new DependencySerializationTestDummy($service);
$dependencySerialization->setContainer($container); $dependencySerialization->setContainer($container);
@ -44,29 +42,6 @@ class DependencySerializationTest extends UnitTestCase {
$this->assertEmpty($dependencySerialization->getServiceIds()); $this->assertEmpty($dependencySerialization->getServiceIds());
} }
/**
* @covers ::__sleep
* @covers ::__wakeup
*/
public function testSerializationWithMissingService() {
// Create a pseudo service and dependency injected object.
$service = new \stdClass();
$service->_serviceId = 'test_service_not_existing';
$container = new Container();
$container->set('test_service', $service);
$container->set('service_container', $container);
\Drupal::setContainer($container);
$dependencySerialization = new DependencySerializationTestDummy($service);
$dependencySerialization->setContainer($container);
$string = serialize($dependencySerialization);
/** @var \Drupal\Tests\Core\DependencyInjection\DependencySerializationTestDummy $dependencySerialization */
$dependencySerialization = unserialize($string);
$this->assertSame($container, $dependencySerialization->container);
}
} }
/** /**

View File

@ -3,6 +3,8 @@
namespace Drupal\Tests\Core\DrupalKernel; namespace Drupal\Tests\Core\DrupalKernel;
use Drupal\Core\DrupalKernel; use Drupal\Core\DrupalKernel;
use Drupal\Core\Test\TestKernel;
use Drupal\Tests\Core\DependencyInjection\Fixture\BarClass;
use Drupal\Tests\UnitTestCase; use Drupal\Tests\UnitTestCase;
use org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStream;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@ -136,6 +138,16 @@ EOD;
$this->assertEquals('sites/example', DrupalKernel::findSitePath($request, FALSE, $vfs_root->url('drupal_root'))); $this->assertEquals('sites/example', DrupalKernel::findSitePath($request, FALSE, $vfs_root->url('drupal_root')));
} }
/**
* @covers ::getServiceIdMapping
*/
public function testGetServiceIdMapping() {
$service = new BarClass();
$container = TestKernel::setContainerWithKernel();
$container->set('bar', $service);
$this->assertEquals($container->get('kernel')->getServiceIdMapping()[$container->generateServiceIdHash($service)], 'bar');
}
} }
/** /**

View File

@ -7,7 +7,6 @@ use Drupal\Core\Entity\Entity\EntityFormMode;
use Drupal\Core\Entity\EntityType; use Drupal\Core\Entity\EntityType;
use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\StringTranslation\TranslationManager;
use Drupal\Tests\UnitTestCase; use Drupal\Tests\UnitTestCase;
/** /**
@ -512,39 +511,4 @@ class EntityTypeTest extends UnitTestCase {
$this->assertFalse($entity_type->isSubclassOf(\DateTimeInterface::class)); $this->assertFalse($entity_type->isSubclassOf(\DateTimeInterface::class));
} }
/**
* Tests that the EntityType object can be serialized.
*/
public function testIsSerializable() {
$entity_type = $this->setUpEntityType([]);
$translation_service = new UnserializableTranslationManager();
$translation_service->_serviceId = 'string_translation';
$entity_type->setStringTranslation($translation_service);
$entity_type = unserialize(serialize($entity_type));
$this->assertEquals('example_entity_type', $entity_type->id());
}
}
/**
* Test class.
*/
class UnserializableTranslationManager extends TranslationManager {
/**
* Constructs a UnserializableTranslationManager object.
*/
public function __construct() {
}
/**
* @return array
*/
public function __serialize(): array {
throw new \Exception();
}
} }

View File

@ -16,11 +16,11 @@ use Drupal\Component\Plugin\Exception\ContextException;
use Drupal\Component\Plugin\Exception\MissingValueContextException; use Drupal\Component\Plugin\Exception\MissingValueContextException;
use Drupal\Core\Cache\NullBackend; use Drupal\Core\Cache\NullBackend;
use Drupal\Core\DependencyInjection\ClassResolverInterface; use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\Context\ContextDefinition; use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\ContextHandler; use Drupal\Core\Plugin\Context\ContextHandler;
use Drupal\Core\Plugin\ContextAwarePluginInterface; use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Test\TestKernel;
use Drupal\Core\TypedData\TypedDataManager; use Drupal\Core\TypedData\TypedDataManager;
use Drupal\Core\Validation\ConstraintManager; use Drupal\Core\Validation\ConstraintManager;
use Drupal\Tests\UnitTestCase; use Drupal\Tests\UnitTestCase;
@ -63,7 +63,7 @@ class ContextHandlerTest extends UnitTestCase {
new ConstraintManager($namespaces, $cache_backend, $module_handler->reveal()) new ConstraintManager($namespaces, $cache_backend, $module_handler->reveal())
); );
$container = new ContainerBuilder(); $container = TestKernel::setContainerWithKernel();
$container->set('typed_data_manager', $type_data_manager); $container->set('typed_data_manager', $type_data_manager);
\Drupal::setContainer($container); \Drupal::setContainer($container);
} }

View File

@ -7,11 +7,11 @@ use Drupal\Core\Http\RequestStack;
use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface; use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
use Drupal\Core\Session\AccountProxyInterface; use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\TempStore\Lock; use Drupal\Core\TempStore\Lock;
use Drupal\Core\Test\TestKernel;
use Drupal\Core\TempStore\SharedTempStoreFactory; use Drupal\Core\TempStore\SharedTempStoreFactory;
use Drupal\Tests\UnitTestCase; use Drupal\Tests\UnitTestCase;
use Drupal\Core\TempStore\SharedTempStore; use Drupal\Core\TempStore\SharedTempStore;
use Drupal\Core\TempStore\TempStoreException; use Drupal\Core\TempStore\TempStoreException;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface;
@ -360,12 +360,10 @@ class SharedTempStoreTest extends UnitTestCase {
$unserializable_request = new UnserializableRequest(); $unserializable_request = new UnserializableRequest();
$this->requestStack->push($unserializable_request); $this->requestStack->push($unserializable_request);
$this->requestStack->_serviceId = 'request_stack';
$container = $this->prophesize(ContainerInterface::class); $container = TestKernel::setContainerWithKernel();
$container->get('request_stack')->willReturn($this->requestStack); $container->set('request_stack', $this->requestStack);
$container->has('request_stack')->willReturn(TRUE); \Drupal::setContainer($container);
\Drupal::setContainer($container->reveal());
$store = unserialize(serialize($this->tempStore)); $store = unserialize(serialize($this->tempStore));
$this->assertInstanceOf(SharedTempStore::class, $store); $this->assertInstanceOf(SharedTempStore::class, $store);