Commit 36faf4e8 authored by Alex Pott's avatar Alex Pott
Browse files

Issue #2302617 by dawehner, chx | Crell: Define a standard mechaism for...

Issue #2302617 by dawehner, chx | Crell: Define a standard mechaism for backend-aware service overrides.
parent 64387e5c
Loading
Loading
Loading
Loading
+3 −0
Changes for core/lib/Drupal/Core/CoreServiceProvider.php: 3 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -9,6 +9,7 @@

use Drupal\Core\Cache\CacheContextsPass;
use Drupal\Core\Cache\ListCacheBinsPass;
use Drupal\Core\DependencyInjection\Compiler\BackendCompilerPass;
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\Compiler\ModifyServiceDefinitionsPass;
@@ -49,6 +50,8 @@ public function register(ContainerBuilder $container) {
    // list-building passes are operating on the post-alter services list.
    $container->addCompilerPass(new ModifyServiceDefinitionsPass());

    $container->addCompilerPass(new BackendCompilerPass());

    // Collect tagged handler services as method calls on consumer services.
    $container->addCompilerPass(new TaggedHandlersPass());

+63 −0
Changes for core/lib/Drupal/Core/DependencyInjection/Compiler/BackendCompilerPass.php: 63 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

/**
 * @file
 * Contains \Drupal\Core\DependencyInjection\Compiler\BackendCompilerPass.
 */

namespace Drupal\Core\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
 * Defines a compiler pass to allow automatic override per backend.
 *
 * A module developer has to tag his backend service with "backend_overridable":
 * @code
 * custom_service:
 *   class: ...
 *   tags:
 *     - { name: backend_overridable }
 * @endcode
 *
 * As a site admin you set the 'default_backend' in your services.yml file:
 * @code
 * parameters:
 *   default_backend: sqlite
 * @endcode
 *
 * As a developer for alternative storage engines you register a service with
 * $yourbackend.$original_service:
 *
 * @code
 * sqlite.custom_service:
 *   class: ...
 * @endcode
 */
class BackendCompilerPass implements CompilerPassInterface {

  /**
   * {@inheritdoc}
   */
  public function process(ContainerBuilder $container) {
    $default_backend = $container->hasParameter('default_backend') ? $container->getParameter('default_backend') : NULL;
    // No default backend was configured, so continue as normal.
    if (!isset($default_backend)) {
      return;
    }

    foreach ($container->findTaggedServiceIds('backend_overridable') as $id => $attributes) {
      // If the service is already an alias it is not the original backend, so
      // we don't want to fallback to other storages any longer.
      if ($container->hasAlias($id)) {
        continue;
      }
      if ($container->hasDefinition("$default_backend.$id") || $container->hasAlias("$default_backend.$id")) {
        $container->setAlias($id, new Alias("$default_backend.$id"));
      }
    }
  }

}
+93 −0
Changes for core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/BackendCompilerPassTest.php: 93 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

/**
 * @file
 * Contains \Drupal\Tests\Core\DependencyInjection\Compiler\BackendCompilerPassTest.
 */

namespace Drupal\Tests\Core\DependencyInjection\Compiler;

use Drupal\Core\DependencyInjection\Compiler\BackendCompilerPass;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;

/**
 * @coversDefaultClass \Drupal\Core\DependencyInjection\Compiler\BackendCompilerPass
 * @group DependencyInjection
 */
class BackendCompilerPassTest extends UnitTestCase {

  /**
   * The tested backend compiler pass.
   *
   * @var \Drupal\Core\DependencyInjection\Compiler\BackendCompilerPass
   */
  protected $backendPass;

  /**
   * {@inheritdoc}
   */
  protected function setUp() {
    $this->backendPass = new BackendCompilerPass();
  }

  /**
   * Tests the process method.
   *
   * @param string $expected_class
   *   The expected used class.
   * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
   *   The container.
   *
   * @dataProvider providerTestProcess
   *
   * @covers ::process
   */
  public function testProcess($expected_class, ContainerBuilder $container) {
    $this->backendPass->process($container);

    $this->assertInstanceOf($expected_class, $container->get('service'));
  }

  /**
   * Provides test data for testProcess().
   *
   * @return array
   */
  public function providerTestProcess() {
    $data = array();
    // Add a container with no set default_backend.
    $container = new ContainerBuilder();
    $prefix = '\\' . __NAMESPACE__ . '\\';
    $container->setDefinition('service', (new Definition($prefix . 'ServiceClassDefault'))->addTag('backend_overridable'));
    $container->setDefinition('mysql.service', new Definition($prefix . 'ServiceClassMysql'));

    $data[] = array($prefix . 'ServiceClassDefault', $container);

    // Set the default_backend so the mysql service should be used.
    $container = clone $container;
    $container->setParameter('default_backend', 'mysql');
    $data[] = array($prefix . 'ServiceClassMysql', $container);

    // Configure a manual alias for the service, so ensure that it is not
    // overridden by the default backend.
    $container = clone $container;
    $container->setDefinition('mariadb.service', new Definition($prefix . 'ServiceClassMariaDb'));
    $container->setAlias('service', new Alias('mariadb.service'));
    $data[] = array($prefix . 'ServiceClassMariaDb', $container);

    return $data;
  }

}

class ServiceClassDefault {
}

class ServiceClassMysql extends ServiceClassDefault {
}

class ServiceClassMariaDb extends ServiceClassMysql {
}