UpdateCompilerPass.php 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. <?php
  2. namespace Drupal\Core\Update;
  3. use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
  4. use Symfony\Component\DependencyInjection\ContainerBuilder;
  5. use Symfony\Component\DependencyInjection\ContainerInterface;
  6. use Symfony\Component\DependencyInjection\Reference;
  7. /**
  8. * Removes services with unmet dependencies.
  9. *
  10. * Updates can install new modules that add services that existing services now
  11. * depend on. This compiler pass allows the update system to work in such cases.
  12. */
  13. class UpdateCompilerPass implements CompilerPassInterface {
  14. /**
  15. * {@inheritdoc}
  16. */
  17. public function process(ContainerBuilder $container) {
  18. $process_aliases = FALSE;
  19. // Loop over the defined services and remove any with unmet dependencies.
  20. // The kernel cannot be booted if the container has such services. This
  21. // allows modules to run their update hooks to enable newly added
  22. // dependencies.
  23. do {
  24. $has_changed = FALSE;
  25. foreach ($container->getDefinitions() as $key => $definition) {
  26. // Ensure all the definition's arguments are valid.
  27. foreach ($definition->getArguments() as $argument) {
  28. if ($this->isArgumentMissingService($argument, $container)) {
  29. $container->removeDefinition($key);
  30. $container->log($this, sprintf('Removed service "%s"; reason: depends on non-existent service "%s".', $key, (string) $argument));
  31. $has_changed = TRUE;
  32. $process_aliases = TRUE;
  33. // Process the next definition.
  34. continue 2;
  35. }
  36. }
  37. // Ensure all the method call arguments are valid.
  38. foreach ($definition->getMethodCalls() as $call) {
  39. foreach ($call[1] as $argument) {
  40. if ($this->isArgumentMissingService($argument, $container)) {
  41. $container->removeDefinition($key);
  42. $container->log($this, sprintf('Removed service "%s"; reason: method call "%s" depends on non-existent service "%s".', $key, $call[0], (string) $argument));
  43. $has_changed = TRUE;
  44. $process_aliases = TRUE;
  45. // Process the next definition.
  46. continue 3;
  47. }
  48. }
  49. }
  50. }
  51. // Repeat if services have been removed.
  52. } while ($has_changed);
  53. // Remove aliases to services that have been removed. This does not need to
  54. // be part of the loop above because references to aliases have already been
  55. // resolved by Symfony's ResolveReferencesToAliasesPass.
  56. if ($process_aliases) {
  57. foreach ($container->getAliases() as $key => $alias) {
  58. $id = (string) $alias;
  59. if (!$container->has($id)) {
  60. $container->removeAlias($key);
  61. $container->log($this, sprintf('Removed alias "%s"; reason: alias to non-existent service "%s".', $key, $id));
  62. }
  63. }
  64. }
  65. }
  66. /**
  67. * Checks if a reference argument is to a missing service.
  68. *
  69. * @param mixed $argument
  70. * The argument to check.
  71. * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
  72. * The container.
  73. *
  74. * @return bool
  75. * TRUE if the argument is a reference to a service that is missing from the
  76. * container and the reference is required, FALSE if not.
  77. */
  78. private function isArgumentMissingService($argument, ContainerBuilder $container) {
  79. if ($argument instanceof Reference) {
  80. $argument_id = (string) $argument;
  81. if (!$container->has($argument_id) && $argument->getInvalidBehavior() === ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
  82. return TRUE;
  83. }
  84. }
  85. return FALSE;
  86. }
  87. }