diff core/modules/simpletest/src/KernelTestBase.php @ 0:4c8ae668cc8c

Initial import (non-working)
author Chris Cannam
date Wed, 29 Nov 2017 16:09:58 +0000
parents
children 7a779792577d
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/modules/simpletest/src/KernelTestBase.php	Wed Nov 29 16:09:58 2017 +0000
@@ -0,0 +1,642 @@
+<?php
+
+namespace Drupal\simpletest;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Component\Utility\Variable;
+use Drupal\Core\Config\Development\ConfigSchemaChecker;
+use Drupal\Core\Database\Database;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\DrupalKernel;
+use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
+use Drupal\Core\Extension\ExtensionDiscovery;
+use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
+use Drupal\Core\Language\Language;
+use Drupal\Core\Site\Settings;
+use Symfony\Component\DependencyInjection\Parameter;
+use Drupal\Core\StreamWrapper\StreamWrapperInterface;
+use Symfony\Component\DependencyInjection\Reference;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Base class for functional integration tests.
+ *
+ * This base class should be useful for testing some types of integrations which
+ * don't require the overhead of a fully-installed Drupal instance, but which
+ * have many dependencies on parts of Drupal which can't or shouldn't be mocked.
+ *
+ * This base class partially boots a fixture Drupal. The state of the fixture
+ * Drupal is comparable to the state of a system during the early part of the
+ * installation process.
+ *
+ * Tests extending this base class can access services and the database, but the
+ * system is initially empty. This Drupal runs in a minimal mocked filesystem
+ * which operates within vfsStream.
+ *
+ * Modules specified in the $modules property are added to the service container
+ * for each test. The module/hook system is functional. Additional modules
+ * needed in a test should override $modules. Modules specified in this way will
+ * be added to those specified in superclasses.
+ *
+ * Unlike \Drupal\Tests\BrowserTestBase, the modules are not installed. They are
+ * loaded such that their services and hooks are available, but the install
+ * process has not been performed.
+ *
+ * Other modules can be made available in this way using
+ * KernelTestBase::enableModules().
+ *
+ * Some modules can be brought into a fully-installed state using
+ * KernelTestBase::installConfig(), KernelTestBase::installSchema(), and
+ * KernelTestBase::installEntitySchema(). Alternately, tests which need modules
+ * to be fully installed could inherit from \Drupal\Tests\BrowserTestBase.
+ *
+ * @see \Drupal\Tests\KernelTestBase::$modules
+ * @see \Drupal\Tests\KernelTestBase::enableModules()
+ * @see \Drupal\Tests\KernelTestBase::installConfig()
+ * @see \Drupal\Tests\KernelTestBase::installEntitySchema()
+ * @see \Drupal\Tests\KernelTestBase::installSchema()
+ * @see \Drupal\Tests\BrowserTestBase
+ *
+ * @deprecated in Drupal 8.0.x, will be removed before Drupal 9.0.0. Use
+ *   \Drupal\KernelTests\KernelTestBase instead.
+ *
+ * @ingroup testing
+ */
+abstract class KernelTestBase extends TestBase {
+
+  use AssertContentTrait;
+
+  /**
+   * Modules to enable.
+   *
+   * Test classes extending this class, and any classes in the hierarchy up to
+   * this class, may specify individual lists of modules to enable by setting
+   * this property. The values of all properties in all classes in the hierarchy
+   * are merged.
+   *
+   * Any modules specified in the $modules property are automatically loaded and
+   * set as the fixed module list.
+   *
+   * Unlike WebTestBase::setUp(), the specified modules are loaded only, but not
+   * automatically installed. Modules need to be installed manually, if needed.
+   *
+   * @see \Drupal\simpletest\KernelTestBase::enableModules()
+   * @see \Drupal\simpletest\KernelTestBase::setUp()
+   *
+   * @var array
+   */
+  public static $modules = [];
+
+  private $moduleFiles;
+  private $themeFiles;
+
+  /**
+   * The configuration directories for this test run.
+   *
+   * @var array
+   */
+  protected $configDirectories = [];
+
+  /**
+   * A KeyValueMemoryFactory instance to use when building the container.
+   *
+   * @var \Drupal\Core\KeyValueStore\KeyValueMemoryFactory.
+   */
+  protected $keyValueFactory;
+
+  /**
+   * Array of registered stream wrappers.
+   *
+   * @var array
+   */
+  protected $streamWrappers = [];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct($test_id = NULL) {
+    parent::__construct($test_id);
+    $this->skipClasses[__CLASS__] = TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function beforePrepareEnvironment() {
+    // Copy/prime extension file lists once to avoid filesystem scans.
+    if (!isset($this->moduleFiles)) {
+      $this->moduleFiles = \Drupal::state()->get('system.module.files') ?: [];
+      $this->themeFiles = \Drupal::state()->get('system.theme.files') ?: [];
+    }
+  }
+
+  /**
+   * Create and set new configuration directories.
+   *
+   * @see config_get_config_directory()
+   *
+   * @throws \RuntimeException
+   *   Thrown when CONFIG_SYNC_DIRECTORY cannot be created or made writable.
+   */
+  protected function prepareConfigDirectories() {
+    $this->configDirectories = [];
+    include_once DRUPAL_ROOT . '/core/includes/install.inc';
+    // Assign the relative path to the global variable.
+    $path = $this->siteDirectory . '/config_' . CONFIG_SYNC_DIRECTORY;
+    $GLOBALS['config_directories'][CONFIG_SYNC_DIRECTORY] = $path;
+    // Ensure the directory can be created and is writeable.
+    if (!file_prepare_directory($path, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
+      throw new \RuntimeException("Failed to create '" . CONFIG_SYNC_DIRECTORY . "' config directory $path");
+    }
+    // Provide the already resolved path for tests.
+    $this->configDirectories[CONFIG_SYNC_DIRECTORY] = $path;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    $this->keyValueFactory = new KeyValueMemoryFactory();
+
+    // Back up settings from TestBase::prepareEnvironment().
+    $settings = Settings::getAll();
+
+    // Allow for test-specific overrides.
+    $directory = DRUPAL_ROOT . '/' . $this->siteDirectory;
+    $settings_services_file = DRUPAL_ROOT . '/' . $this->originalSite . '/testing.services.yml';
+    $container_yamls = [];
+    if (file_exists($settings_services_file)) {
+      // Copy the testing-specific service overrides in place.
+      $testing_services_file = $directory . '/services.yml';
+      copy($settings_services_file, $testing_services_file);
+      $container_yamls[] = $testing_services_file;
+    }
+    $settings_testing_file = DRUPAL_ROOT . '/' . $this->originalSite . '/settings.testing.php';
+    if (file_exists($settings_testing_file)) {
+      // Copy the testing-specific settings.php overrides in place.
+      copy($settings_testing_file, $directory . '/settings.testing.php');
+    }
+
+    if (file_exists($directory . '/settings.testing.php')) {
+      // Add the name of the testing class to settings.php and include the
+      // testing specific overrides
+      $hash_salt = Settings::getHashSalt();
+      $test_class = get_class($this);
+      $container_yamls_export = Variable::export($container_yamls);
+      $php = <<<EOD
+<?php
+
+\$settings['hash_salt'] = '$hash_salt';
+\$settings['container_yamls'] = $container_yamls_export;
+
+\$test_class = '$test_class';
+include DRUPAL_ROOT . '/' . \$site_path . '/settings.testing.php';
+EOD;
+      file_put_contents($directory . '/settings.php', $php);
+    }
+
+    // Add this test class as a service provider.
+    // @todo Remove the indirection; implement ServiceProviderInterface instead.
+    $GLOBALS['conf']['container_service_providers']['TestServiceProvider'] = 'Drupal\simpletest\TestServiceProvider';
+
+    // Bootstrap a new kernel.
+    $class_loader = require DRUPAL_ROOT . '/autoload.php';
+    $this->kernel = new DrupalKernel('testing', $class_loader, FALSE);
+    $request = Request::create('/');
+    $site_path = DrupalKernel::findSitePath($request);
+    $this->kernel->setSitePath($site_path);
+    if (file_exists($directory . '/settings.testing.php')) {
+      Settings::initialize(DRUPAL_ROOT, $site_path, $class_loader);
+    }
+    $this->kernel->boot();
+
+    // Ensure database install tasks have been run.
+    require_once __DIR__ . '/../../../includes/install.inc';
+    $connection = Database::getConnection();
+    $errors = db_installer_object($connection->driver())->runTasks();
+    if (!empty($errors)) {
+      $this->fail('Failed to run installer database tasks: ' . implode(', ', $errors));
+    }
+
+    // Reboot the kernel because the container might contain a connection to the
+    // database that has been closed during the database install tasks. This
+    // prevents any services created during the first boot from having stale
+    // database connections, for example, \Drupal\Core\Config\DatabaseStorage.
+    $this->kernel->shutdown();
+    $this->kernel->boot();
+
+
+    // Save the original site directory path, so that extensions in the
+    // site-specific directory can still be discovered in the test site
+    // environment.
+    // @see \Drupal\Core\Extension\ExtensionDiscovery::scan()
+    $settings['test_parent_site'] = $this->originalSite;
+
+    // Restore and merge settings.
+    // DrupalKernel::boot() initializes new Settings, and the containerBuild()
+    // method sets additional settings.
+    new Settings($settings + Settings::getAll());
+
+    // Create and set new configuration directories.
+    $this->prepareConfigDirectories();
+
+    // Set the request scope.
+    $this->container = $this->kernel->getContainer();
+    $this->container->get('request_stack')->push($request);
+
+    // Re-inject extension file listings into state, unless the key/value
+    // service was overridden (in which case its storage does not exist yet).
+    if ($this->container->get('keyvalue') instanceof KeyValueMemoryFactory) {
+      $this->container->get('state')->set('system.module.files', $this->moduleFiles);
+      $this->container->get('state')->set('system.theme.files', $this->themeFiles);
+    }
+
+    // Create a minimal core.extension configuration object so that the list of
+    // enabled modules can be maintained allowing
+    // \Drupal\Core\Config\ConfigInstaller::installDefaultConfig() to work.
+    // Write directly to active storage to avoid early instantiation of
+    // the event dispatcher which can prevent modules from registering events.
+    \Drupal::service('config.storage')->write('core.extension', ['module' => [], 'theme' => [], 'profile' => '']);
+
+    // Collect and set a fixed module list.
+    $class = get_class($this);
+    $modules = [];
+    while ($class) {
+      if (property_exists($class, 'modules')) {
+        // Only add the modules, if the $modules property was not inherited.
+        $rp = new \ReflectionProperty($class, 'modules');
+        if ($rp->class == $class) {
+          $modules[$class] = $class::$modules;
+        }
+      }
+      $class = get_parent_class($class);
+    }
+    // Modules have been collected in reverse class hierarchy order; modules
+    // defined by base classes should be sorted first. Then, merge the results
+    // together.
+    $modules = array_reverse($modules);
+    $modules = call_user_func_array('array_merge_recursive', $modules);
+    if ($modules) {
+      $this->enableModules($modules);
+    }
+
+    // Tests based on this class are entitled to use Drupal's File and
+    // StreamWrapper APIs.
+    // @todo Move StreamWrapper management into DrupalKernel.
+    // @see https://www.drupal.org/node/2028109
+    file_prepare_directory($this->publicFilesDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+    $this->settingsSet('file_public_path', $this->publicFilesDirectory);
+    $this->streamWrappers = [];
+    $this->registerStreamWrapper('public', 'Drupal\Core\StreamWrapper\PublicStream');
+    // The temporary stream wrapper is able to operate both with and without
+    // configuration.
+    $this->registerStreamWrapper('temporary', 'Drupal\Core\StreamWrapper\TemporaryStream');
+
+    // Manually configure the test mail collector implementation to prevent
+    // tests from sending out emails and collect them in state instead.
+    // While this should be enforced via settings.php prior to installation,
+    // some tests expect to be able to test mail system implementations.
+    $GLOBALS['config']['system.mail']['interface']['default'] = 'test_mail_collector';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function tearDown() {
+    if ($this->kernel instanceof DrupalKernel) {
+      $this->kernel->shutdown();
+    }
+    // Before tearing down the test environment, ensure that no stream wrapper
+    // of this test leaks into the parent environment. Unlike all other global
+    // state variables in Drupal, stream wrappers are a global state construct
+    // of PHP core, which has to be maintained manually.
+    // @todo Move StreamWrapper management into DrupalKernel.
+    // @see https://www.drupal.org/node/2028109
+    foreach ($this->streamWrappers as $scheme => $type) {
+      $this->unregisterStreamWrapper($scheme, $type);
+    }
+    parent::tearDown();
+  }
+
+  /**
+   * Sets up the base service container for this test.
+   *
+   * Extend this method in your test to register additional service overrides
+   * that need to persist a DrupalKernel reboot. This method is called whenever
+   * the kernel is rebuilt.
+   *
+   * @see \Drupal\simpletest\KernelTestBase::setUp()
+   * @see \Drupal\simpletest\KernelTestBase::enableModules()
+   * @see \Drupal\simpletest\KernelTestBase::disableModules()
+   */
+  public function containerBuild(ContainerBuilder $container) {
+    // Keep the container object around for tests.
+    $this->container = $container;
+
+    // Set the default language on the minimal container.
+    $this->container->setParameter('language.default_values', $this->defaultLanguageData());
+
+    $container->register('lock', 'Drupal\Core\Lock\NullLockBackend');
+    $container->register('cache_factory', 'Drupal\Core\Cache\MemoryBackendFactory');
+
+    $container
+      ->register('config.storage', 'Drupal\Core\Config\DatabaseStorage')
+      ->addArgument(Database::getConnection())
+      ->addArgument('config');
+
+    if ($this->strictConfigSchema) {
+      $container
+        ->register('simpletest.config_schema_checker', ConfigSchemaChecker::class)
+        ->addArgument(new Reference('config.typed'))
+        ->addArgument($this->getConfigSchemaExclusions())
+        ->addTag('event_subscriber');
+    }
+
+    $keyvalue_options = $container->getParameter('factory.keyvalue') ?: [];
+    $keyvalue_options['default'] = 'keyvalue.memory';
+    $container->setParameter('factory.keyvalue', $keyvalue_options);
+    $container->set('keyvalue.memory', $this->keyValueFactory);
+    if (!$container->has('keyvalue')) {
+      // TestBase::setUp puts a completely empty container in
+      // $this->container which is somewhat the mirror of the empty
+      // environment being set up. Unit tests need not to waste time with
+      // getting a container set up for them. Drupal Unit Tests might just get
+      // away with a simple container holding the absolute bare minimum. When
+      // a kernel is overridden then there's no need to re-register the keyvalue
+      // service but when a test is happy with the superminimal container put
+      // together here, it still might a keyvalue storage for anything using
+      // \Drupal::state() -- that's why a memory service was added in the first
+      // place.
+      $container->register('settings', 'Drupal\Core\Site\Settings')
+        ->setFactoryClass('Drupal\Core\Site\Settings')
+        ->setFactoryMethod('getInstance');
+
+      $container
+        ->register('keyvalue', 'Drupal\Core\KeyValueStore\KeyValueFactory')
+        ->addArgument(new Reference('service_container'))
+        ->addArgument(new Parameter('factory.keyvalue'));
+
+      $container->register('state', 'Drupal\Core\State\State')
+        ->addArgument(new Reference('keyvalue'));
+    }
+
+    if ($container->hasDefinition('path_processor_alias')) {
+      // Prevent the alias-based path processor, which requires a url_alias db
+      // table, from being registered to the path processor manager. We do this
+      // by removing the tags that the compiler pass looks for. This means the
+      // url generator can safely be used within tests.
+      $definition = $container->getDefinition('path_processor_alias');
+      $definition->clearTag('path_processor_inbound')->clearTag('path_processor_outbound');
+    }
+
+    if ($container->hasDefinition('password')) {
+      $container->getDefinition('password')->setArguments([1]);
+    }
+
+    // Register the stream wrapper manager.
+    $container
+      ->register('stream_wrapper_manager', 'Drupal\Core\StreamWrapper\StreamWrapperManager')
+      ->addArgument(new Reference('module_handler'))
+      ->addMethodCall('setContainer', [new Reference('service_container')]);
+
+    $request = Request::create('/');
+    $container->get('request_stack')->push($request);
+  }
+
+  /**
+   * Provides the data for setting the default language on the container.
+   *
+   * @return array
+   *   The data array for the default language.
+   */
+  protected function defaultLanguageData() {
+    return Language::$defaultValues;
+  }
+
+  /**
+   * Installs default configuration for a given list of modules.
+   *
+   * @param array $modules
+   *   A list of modules for which to install default configuration.
+   *
+   * @throws \RuntimeException
+   *   Thrown when any module listed in $modules is not enabled.
+   */
+  protected function installConfig(array $modules) {
+    foreach ($modules as $module) {
+      if (!$this->container->get('module_handler')->moduleExists($module)) {
+        throw new \RuntimeException("'$module' module is not enabled");
+      }
+      \Drupal::service('config.installer')->installDefaultConfig('module', $module);
+    }
+    $this->pass(format_string('Installed default config: %modules.', [
+      '%modules' => implode(', ', $modules),
+    ]));
+  }
+
+  /**
+   * Installs a specific table from a module schema definition.
+   *
+   * @param string $module
+   *   The name of the module that defines the table's schema.
+   * @param string|array $tables
+   *   The name or an array of the names of the tables to install.
+   *
+   * @throws \RuntimeException
+   *   Thrown when $module is not enabled or when the table schema cannot be
+   *   found in the module specified.
+   */
+  protected function installSchema($module, $tables) {
+    // drupal_get_module_schema() is technically able to install a schema
+    // of a non-enabled module, but its ability to load the module's .install
+    // file depends on many other factors. To prevent differences in test
+    // behavior and non-reproducible test failures, we only allow the schema of
+    // explicitly loaded/enabled modules to be installed.
+    if (!$this->container->get('module_handler')->moduleExists($module)) {
+      throw new \RuntimeException("'$module' module is not enabled");
+    }
+
+    $tables = (array) $tables;
+    foreach ($tables as $table) {
+      $schema = drupal_get_module_schema($module, $table);
+      if (empty($schema)) {
+        // BC layer to avoid some contrib tests to fail.
+        // @todo Remove the BC layer before 8.1.x release.
+        // @see https://www.drupal.org/node/2670360
+        // @see https://www.drupal.org/node/2670454
+        if ($module == 'system') {
+          continue;
+        }
+        throw new \RuntimeException("Unknown '$table' table schema in '$module' module.");
+      }
+      $this->container->get('database')->schema()->createTable($table, $schema);
+    }
+    $this->pass(format_string('Installed %module tables: %tables.', [
+      '%tables' => '{' . implode('}, {', $tables) . '}',
+      '%module' => $module,
+    ]));
+  }
+
+
+  /**
+   * Installs the storage schema for a specific entity type.
+   *
+   * @param string $entity_type_id
+   *   The ID of the entity type.
+   */
+  protected function installEntitySchema($entity_type_id) {
+    /** @var \Drupal\Core\Entity\EntityManagerInterface $entity_manager */
+    $entity_manager = $this->container->get('entity.manager');
+    $entity_type = $entity_manager->getDefinition($entity_type_id);
+    $entity_manager->onEntityTypeCreate($entity_type);
+
+    // For test runs, the most common storage backend is a SQL database. For
+    // this case, ensure the tables got created.
+    $storage = $entity_manager->getStorage($entity_type_id);
+    if ($storage instanceof SqlEntityStorageInterface) {
+      $tables = $storage->getTableMapping()->getTableNames();
+      $db_schema = $this->container->get('database')->schema();
+      $all_tables_exist = TRUE;
+      foreach ($tables as $table) {
+        if (!$db_schema->tableExists($table)) {
+          $this->fail(SafeMarkup::format('Installed entity type table for the %entity_type entity type: %table', [
+            '%entity_type' => $entity_type_id,
+            '%table' => $table,
+          ]));
+          $all_tables_exist = FALSE;
+        }
+      }
+      if ($all_tables_exist) {
+        $this->pass(SafeMarkup::format('Installed entity type tables for the %entity_type entity type: %tables', [
+          '%entity_type' => $entity_type_id,
+          '%tables' => '{' . implode('}, {', $tables) . '}',
+        ]));
+      }
+    }
+  }
+
+  /**
+   * Enables modules for this test.
+   *
+   * To install test modules outside of the testing environment, add
+   * @code
+   * $settings['extension_discovery_scan_tests'] = TRUE;
+   * @endcode
+   * to your settings.php.
+   *
+   * @param array $modules
+   *   A list of modules to enable. Dependencies are not resolved; i.e.,
+   *   multiple modules have to be specified with dependent modules first.
+   *   The new modules are only added to the active module list and loaded.
+   */
+  protected function enableModules(array $modules) {
+    // Perform an ExtensionDiscovery scan as this function may receive a
+    // profile that is not the current profile, and we don't yet have a cached
+    // way to receive inactive profile information.
+    // @todo Remove as part of https://www.drupal.org/node/2186491
+    $listing = new ExtensionDiscovery(\Drupal::root());
+    $module_list = $listing->scan('module');
+    // In ModuleHandlerTest we pass in a profile as if it were a module.
+    $module_list += $listing->scan('profile');
+    // Set the list of modules in the extension handler.
+    $module_handler = $this->container->get('module_handler');
+
+    // Write directly to active storage to avoid early instantiation of
+    // the event dispatcher which can prevent modules from registering events.
+    $active_storage = \Drupal::service('config.storage');
+    $extensions = $active_storage->read('core.extension');
+
+    foreach ($modules as $module) {
+      $module_handler->addModule($module, $module_list[$module]->getPath());
+      // Maintain the list of enabled modules in configuration.
+      $extensions['module'][$module] = 0;
+    }
+    $active_storage->write('core.extension', $extensions);
+
+    // Update the kernel to make their services available.
+    $module_filenames = $module_handler->getModuleList();
+    $this->kernel->updateModules($module_filenames, $module_filenames);
+
+    // Ensure isLoaded() is TRUE in order to make
+    // \Drupal\Core\Theme\ThemeManagerInterface::render() work.
+    // Note that the kernel has rebuilt the container; this $module_handler is
+    // no longer the $module_handler instance from above.
+    $this->container->get('module_handler')->reload();
+    $this->pass(format_string('Enabled modules: %modules.', [
+      '%modules' => implode(', ', $modules),
+    ]));
+  }
+
+  /**
+   * Disables modules for this test.
+   *
+   * @param array $modules
+   *   A list of modules to disable. Dependencies are not resolved; i.e.,
+   *   multiple modules have to be specified with dependent modules first.
+   *   Code of previously active modules is still loaded. The modules are only
+   *   removed from the active module list.
+   */
+  protected function disableModules(array $modules) {
+    // Unset the list of modules in the extension handler.
+    $module_handler = $this->container->get('module_handler');
+    $module_filenames = $module_handler->getModuleList();
+    $extension_config = $this->config('core.extension');
+    foreach ($modules as $module) {
+      unset($module_filenames[$module]);
+      $extension_config->clear('module.' . $module);
+    }
+    $extension_config->save();
+    $module_handler->setModuleList($module_filenames);
+    $module_handler->resetImplementations();
+    // Update the kernel to remove their services.
+    $this->kernel->updateModules($module_filenames, $module_filenames);
+
+    // Ensure isLoaded() is TRUE in order to make
+    // \Drupal\Core\Theme\ThemeManagerInterface::render() work.
+    // Note that the kernel has rebuilt the container; this $module_handler is
+    // no longer the $module_handler instance from above.
+    $module_handler = $this->container->get('module_handler');
+    $module_handler->reload();
+    $this->pass(format_string('Disabled modules: %modules.', [
+      '%modules' => implode(', ', $modules),
+    ]));
+  }
+
+  /**
+   * Registers a stream wrapper for this test.
+   *
+   * @param string $scheme
+   *   The scheme to register.
+   * @param string $class
+   *   The fully qualified class name to register.
+   * @param int $type
+   *   The Drupal Stream Wrapper API type. Defaults to
+   *   StreamWrapperInterface::NORMAL.
+   */
+  protected function registerStreamWrapper($scheme, $class, $type = StreamWrapperInterface::NORMAL) {
+    $this->container->get('stream_wrapper_manager')->registerWrapper($scheme, $class, $type);
+  }
+
+  /**
+   * Renders a render array.
+   *
+   * @param array $elements
+   *   The elements to render.
+   *
+   * @return string
+   *   The rendered string output (typically HTML).
+   */
+  protected function render(array &$elements) {
+    // Use the bare HTML page renderer to render our links.
+    $renderer = $this->container->get('bare_html_page_renderer');
+    $response = $renderer->renderBarePage($elements, '', 'maintenance_page');
+
+    // Glean the content from the response object.
+    $content = $response->getContent();
+    $this->setRawContent($content);
+    $this->verbose('<pre style="white-space: pre-wrap">' . Html::escape($content));
+    return $content;
+  }
+
+}