acf6b3e899884c6bccbf7282828b2ae9be92a491
[yaffs-website] / KernelTestBase.php
1 <?php
2
3 namespace Drupal\simpletest;
4
5 use Drupal\Component\Utility\Html;
6 use Drupal\Component\Render\FormattableMarkup;
7 use Drupal\Component\Utility\Variable;
8 use Drupal\Core\Config\Development\ConfigSchemaChecker;
9 use Drupal\Core\Database\Database;
10 use Drupal\Core\DependencyInjection\ContainerBuilder;
11 use Drupal\Core\DrupalKernel;
12 use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
13 use Drupal\Core\Extension\ExtensionDiscovery;
14 use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
15 use Drupal\Core\Language\Language;
16 use Drupal\Core\Site\Settings;
17 use Drupal\KernelTests\TestServiceProvider;
18 use Symfony\Component\DependencyInjection\Parameter;
19 use Drupal\Core\StreamWrapper\StreamWrapperInterface;
20 use Symfony\Component\DependencyInjection\Reference;
21 use Symfony\Component\HttpFoundation\Request;
22
23 /**
24  * Base class for functional integration tests.
25  *
26  * This base class should be useful for testing some types of integrations which
27  * don't require the overhead of a fully-installed Drupal instance, but which
28  * have many dependencies on parts of Drupal which can't or shouldn't be mocked.
29  *
30  * This base class partially boots a fixture Drupal. The state of the fixture
31  * Drupal is comparable to the state of a system during the early part of the
32  * installation process.
33  *
34  * Tests extending this base class can access services and the database, but the
35  * system is initially empty. This Drupal runs in a minimal mocked filesystem
36  * which operates within vfsStream.
37  *
38  * Modules specified in the $modules property are added to the service container
39  * for each test. The module/hook system is functional. Additional modules
40  * needed in a test should override $modules. Modules specified in this way will
41  * be added to those specified in superclasses.
42  *
43  * Unlike \Drupal\Tests\BrowserTestBase, the modules are not installed. They are
44  * loaded such that their services and hooks are available, but the install
45  * process has not been performed.
46  *
47  * Other modules can be made available in this way using
48  * KernelTestBase::enableModules().
49  *
50  * Some modules can be brought into a fully-installed state using
51  * KernelTestBase::installConfig(), KernelTestBase::installSchema(), and
52  * KernelTestBase::installEntitySchema(). Alternately, tests which need modules
53  * to be fully installed could inherit from \Drupal\Tests\BrowserTestBase.
54  *
55  * @see \Drupal\Tests\KernelTestBase::$modules
56  * @see \Drupal\Tests\KernelTestBase::enableModules()
57  * @see \Drupal\Tests\KernelTestBase::installConfig()
58  * @see \Drupal\Tests\KernelTestBase::installEntitySchema()
59  * @see \Drupal\Tests\KernelTestBase::installSchema()
60  * @see \Drupal\Tests\BrowserTestBase
61  *
62  * @deprecated in Drupal 8.0.x, will be removed before Drupal 9.0.0. Use
63  *   \Drupal\KernelTests\KernelTestBase instead.
64  *
65  * @ingroup testing
66  */
67 abstract class KernelTestBase extends TestBase {
68
69   use AssertContentTrait;
70
71   /**
72    * Modules to enable.
73    *
74    * Test classes extending this class, and any classes in the hierarchy up to
75    * this class, may specify individual lists of modules to enable by setting
76    * this property. The values of all properties in all classes in the hierarchy
77    * are merged.
78    *
79    * Any modules specified in the $modules property are automatically loaded and
80    * set as the fixed module list.
81    *
82    * Unlike WebTestBase::setUp(), the specified modules are loaded only, but not
83    * automatically installed. Modules need to be installed manually, if needed.
84    *
85    * @see \Drupal\simpletest\KernelTestBase::enableModules()
86    * @see \Drupal\simpletest\KernelTestBase::setUp()
87    *
88    * @var array
89    */
90   public static $modules = [];
91
92   private $moduleFiles;
93   private $themeFiles;
94
95   /**
96    * The configuration directories for this test run.
97    *
98    * @var array
99    */
100   protected $configDirectories = [];
101
102   /**
103    * A KeyValueMemoryFactory instance to use when building the container.
104    *
105    * @var \Drupal\Core\KeyValueStore\KeyValueMemoryFactory
106    */
107   protected $keyValueFactory;
108
109   /**
110    * Array of registered stream wrappers.
111    *
112    * @var array
113    */
114   protected $streamWrappers = [];
115
116   /**
117    * {@inheritdoc}
118    */
119   public function __construct($test_id = NULL) {
120     parent::__construct($test_id);
121     $this->skipClasses[__CLASS__] = TRUE;
122   }
123
124   /**
125    * {@inheritdoc}
126    */
127   protected function beforePrepareEnvironment() {
128     // Copy/prime extension file lists once to avoid filesystem scans.
129     if (!isset($this->moduleFiles)) {
130       $this->moduleFiles = \Drupal::state()->get('system.module.files') ?: [];
131       $this->themeFiles = \Drupal::state()->get('system.theme.files') ?: [];
132     }
133   }
134
135   /**
136    * Create and set new configuration directories.
137    *
138    * @see config_get_config_directory()
139    *
140    * @throws \RuntimeException
141    *   Thrown when CONFIG_SYNC_DIRECTORY cannot be created or made writable.
142    */
143   protected function prepareConfigDirectories() {
144     $this->configDirectories = [];
145     include_once DRUPAL_ROOT . '/core/includes/install.inc';
146     // Assign the relative path to the global variable.
147     $path = $this->siteDirectory . '/config_' . CONFIG_SYNC_DIRECTORY;
148     $GLOBALS['config_directories'][CONFIG_SYNC_DIRECTORY] = $path;
149     // Ensure the directory can be created and is writeable.
150     if (!file_prepare_directory($path, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
151       throw new \RuntimeException("Failed to create '" . CONFIG_SYNC_DIRECTORY . "' config directory $path");
152     }
153     // Provide the already resolved path for tests.
154     $this->configDirectories[CONFIG_SYNC_DIRECTORY] = $path;
155   }
156
157   /**
158    * {@inheritdoc}
159    */
160   protected function setUp() {
161     $this->keyValueFactory = new KeyValueMemoryFactory();
162
163     // Back up settings from TestBase::prepareEnvironment().
164     $settings = Settings::getAll();
165
166     // Allow for test-specific overrides.
167     $directory = DRUPAL_ROOT . '/' . $this->siteDirectory;
168     $settings_services_file = DRUPAL_ROOT . '/' . $this->originalSite . '/testing.services.yml';
169     $container_yamls = [];
170     if (file_exists($settings_services_file)) {
171       // Copy the testing-specific service overrides in place.
172       $testing_services_file = $directory . '/services.yml';
173       copy($settings_services_file, $testing_services_file);
174       $container_yamls[] = $testing_services_file;
175     }
176     $settings_testing_file = DRUPAL_ROOT . '/' . $this->originalSite . '/settings.testing.php';
177     if (file_exists($settings_testing_file)) {
178       // Copy the testing-specific settings.php overrides in place.
179       copy($settings_testing_file, $directory . '/settings.testing.php');
180     }
181
182     if (file_exists($directory . '/settings.testing.php')) {
183       // Add the name of the testing class to settings.php and include the
184       // testing specific overrides
185       $hash_salt = Settings::getHashSalt();
186       $test_class = get_class($this);
187       $container_yamls_export = Variable::export($container_yamls);
188       $php = <<<EOD
189 <?php
190
191 \$settings['hash_salt'] = '$hash_salt';
192 \$settings['container_yamls'] = $container_yamls_export;
193
194 \$test_class = '$test_class';
195 include DRUPAL_ROOT . '/' . \$site_path . '/settings.testing.php';
196 EOD;
197       file_put_contents($directory . '/settings.php', $php);
198     }
199
200     // Add this test class as a service provider.
201     // @todo Remove the indirection; implement ServiceProviderInterface instead.
202     $GLOBALS['conf']['container_service_providers']['TestServiceProvider'] = TestServiceProvider::class;
203
204     // Bootstrap a new kernel.
205     $class_loader = require DRUPAL_ROOT . '/autoload.php';
206     $this->kernel = new DrupalKernel('testing', $class_loader, FALSE);
207     $request = Request::create('/');
208     $site_path = DrupalKernel::findSitePath($request);
209     $this->kernel->setSitePath($site_path);
210     if (file_exists($directory . '/settings.testing.php')) {
211       Settings::initialize(DRUPAL_ROOT, $site_path, $class_loader);
212     }
213     $this->kernel->boot();
214
215     // Ensure database install tasks have been run.
216     require_once __DIR__ . '/../../../includes/install.inc';
217     $connection = Database::getConnection();
218     $errors = db_installer_object($connection->driver())->runTasks();
219     if (!empty($errors)) {
220       $this->fail('Failed to run installer database tasks: ' . implode(', ', $errors));
221     }
222
223     // Reboot the kernel because the container might contain a connection to the
224     // database that has been closed during the database install tasks. This
225     // prevents any services created during the first boot from having stale
226     // database connections, for example, \Drupal\Core\Config\DatabaseStorage.
227     $this->kernel->shutdown();
228     $this->kernel->boot();
229
230     // Save the original site directory path, so that extensions in the
231     // site-specific directory can still be discovered in the test site
232     // environment.
233     // @see \Drupal\Core\Extension\ExtensionDiscovery::scan()
234     $settings['test_parent_site'] = $this->originalSite;
235
236     // Restore and merge settings.
237     // DrupalKernel::boot() initializes new Settings, and the containerBuild()
238     // method sets additional settings.
239     new Settings($settings + Settings::getAll());
240
241     // Create and set new configuration directories.
242     $this->prepareConfigDirectories();
243
244     // Set the request scope.
245     $this->container = $this->kernel->getContainer();
246     $this->container->get('request_stack')->push($request);
247
248     // Re-inject extension file listings into state, unless the key/value
249     // service was overridden (in which case its storage does not exist yet).
250     if ($this->container->get('keyvalue') instanceof KeyValueMemoryFactory) {
251       $this->container->get('state')->set('system.module.files', $this->moduleFiles);
252       $this->container->get('state')->set('system.theme.files', $this->themeFiles);
253     }
254
255     // Create a minimal core.extension configuration object so that the list of
256     // enabled modules can be maintained allowing
257     // \Drupal\Core\Config\ConfigInstaller::installDefaultConfig() to work.
258     // Write directly to active storage to avoid early instantiation of
259     // the event dispatcher which can prevent modules from registering events.
260     \Drupal::service('config.storage')->write('core.extension', ['module' => [], 'theme' => [], 'profile' => '']);
261
262     // Collect and set a fixed module list.
263     $class = get_class($this);
264     $modules = [];
265     while ($class) {
266       if (property_exists($class, 'modules')) {
267         // Only add the modules, if the $modules property was not inherited.
268         $rp = new \ReflectionProperty($class, 'modules');
269         if ($rp->class == $class) {
270           $modules[$class] = $class::$modules;
271         }
272       }
273       $class = get_parent_class($class);
274     }
275     // Modules have been collected in reverse class hierarchy order; modules
276     // defined by base classes should be sorted first. Then, merge the results
277     // together.
278     $modules = array_reverse($modules);
279     $modules = call_user_func_array('array_merge_recursive', $modules);
280     if ($modules) {
281       $this->enableModules($modules);
282     }
283
284     // Tests based on this class are entitled to use Drupal's File and
285     // StreamWrapper APIs.
286     // @todo Move StreamWrapper management into DrupalKernel.
287     // @see https://www.drupal.org/node/2028109
288     file_prepare_directory($this->publicFilesDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
289     $this->settingsSet('file_public_path', $this->publicFilesDirectory);
290     $this->streamWrappers = [];
291     $this->registerStreamWrapper('public', 'Drupal\Core\StreamWrapper\PublicStream');
292     // The temporary stream wrapper is able to operate both with and without
293     // configuration.
294     $this->registerStreamWrapper('temporary', 'Drupal\Core\StreamWrapper\TemporaryStream');
295
296     // Manually configure the test mail collector implementation to prevent
297     // tests from sending out emails and collect them in state instead.
298     // While this should be enforced via settings.php prior to installation,
299     // some tests expect to be able to test mail system implementations.
300     $GLOBALS['config']['system.mail']['interface']['default'] = 'test_mail_collector';
301   }
302
303   /**
304    * {@inheritdoc}
305    */
306   protected function tearDown() {
307     if ($this->kernel instanceof DrupalKernel) {
308       $this->kernel->shutdown();
309     }
310     // Before tearing down the test environment, ensure that no stream wrapper
311     // of this test leaks into the parent environment. Unlike all other global
312     // state variables in Drupal, stream wrappers are a global state construct
313     // of PHP core, which has to be maintained manually.
314     // @todo Move StreamWrapper management into DrupalKernel.
315     // @see https://www.drupal.org/node/2028109
316     foreach ($this->streamWrappers as $scheme => $type) {
317       $this->unregisterStreamWrapper($scheme, $type);
318     }
319     parent::tearDown();
320   }
321
322   /**
323    * Sets up the base service container for this test.
324    *
325    * Extend this method in your test to register additional service overrides
326    * that need to persist a DrupalKernel reboot. This method is called whenever
327    * the kernel is rebuilt.
328    *
329    * @see \Drupal\simpletest\KernelTestBase::setUp()
330    * @see \Drupal\simpletest\KernelTestBase::enableModules()
331    * @see \Drupal\simpletest\KernelTestBase::disableModules()
332    */
333   public function containerBuild(ContainerBuilder $container) {
334     // Keep the container object around for tests.
335     $this->container = $container;
336
337     // Set the default language on the minimal container.
338     $this->container->setParameter('language.default_values', $this->defaultLanguageData());
339
340     $container->register('lock', 'Drupal\Core\Lock\NullLockBackend');
341     $container->register('cache_factory', 'Drupal\Core\Cache\MemoryBackendFactory');
342
343     $container
344       ->register('config.storage', 'Drupal\Core\Config\DatabaseStorage')
345       ->addArgument(Database::getConnection())
346       ->addArgument('config');
347
348     if ($this->strictConfigSchema) {
349       $container
350         ->register('simpletest.config_schema_checker', ConfigSchemaChecker::class)
351         ->addArgument(new Reference('config.typed'))
352         ->addArgument($this->getConfigSchemaExclusions())
353         ->addTag('event_subscriber');
354     }
355
356     $keyvalue_options = $container->getParameter('factory.keyvalue') ?: [];
357     $keyvalue_options['default'] = 'keyvalue.memory';
358     $container->setParameter('factory.keyvalue', $keyvalue_options);
359     $container->set('keyvalue.memory', $this->keyValueFactory);
360     if (!$container->has('keyvalue')) {
361       // TestBase::setUp puts a completely empty container in
362       // $this->container which is somewhat the mirror of the empty
363       // environment being set up. Unit tests need not to waste time with
364       // getting a container set up for them. Drupal Unit Tests might just get
365       // away with a simple container holding the absolute bare minimum. When
366       // a kernel is overridden then there's no need to re-register the keyvalue
367       // service but when a test is happy with the superminimal container put
368       // together here, it still might a keyvalue storage for anything using
369       // \Drupal::state() -- that's why a memory service was added in the first
370       // place.
371       $container->register('settings', 'Drupal\Core\Site\Settings')
372         ->setFactoryClass('Drupal\Core\Site\Settings')
373         ->setFactoryMethod('getInstance');
374
375       $container
376         ->register('keyvalue', 'Drupal\Core\KeyValueStore\KeyValueFactory')
377         ->addArgument(new Reference('service_container'))
378         ->addArgument(new Parameter('factory.keyvalue'));
379
380       $container->register('state', 'Drupal\Core\State\State')
381         ->addArgument(new Reference('keyvalue'));
382     }
383
384     if ($container->hasDefinition('path_processor_alias')) {
385       // Prevent the alias-based path processor, which requires a url_alias db
386       // table, from being registered to the path processor manager. We do this
387       // by removing the tags that the compiler pass looks for. This means the
388       // url generator can safely be used within tests.
389       $definition = $container->getDefinition('path_processor_alias');
390       $definition->clearTag('path_processor_inbound')->clearTag('path_processor_outbound');
391     }
392
393     if ($container->hasDefinition('password')) {
394       $container->getDefinition('password')->setArguments([1]);
395     }
396
397     // Register the stream wrapper manager.
398     $container
399       ->register('stream_wrapper_manager', 'Drupal\Core\StreamWrapper\StreamWrapperManager')
400       ->addArgument(new Reference('module_handler'))
401       ->addMethodCall('setContainer', [new Reference('service_container')]);
402
403     $request = Request::create('/');
404     $container->get('request_stack')->push($request);
405   }
406
407   /**
408    * Provides the data for setting the default language on the container.
409    *
410    * @return array
411    *   The data array for the default language.
412    */
413   protected function defaultLanguageData() {
414     return Language::$defaultValues;
415   }
416
417   /**
418    * Installs default configuration for a given list of modules.
419    *
420    * @param array $modules
421    *   A list of modules for which to install default configuration.
422    *
423    * @throws \RuntimeException
424    *   Thrown when any module listed in $modules is not enabled.
425    */
426   protected function installConfig(array $modules) {
427     foreach ($modules as $module) {
428       if (!$this->container->get('module_handler')->moduleExists($module)) {
429         throw new \RuntimeException("'$module' module is not enabled");
430       }
431       \Drupal::service('config.installer')->installDefaultConfig('module', $module);
432     }
433     $this->pass(format_string('Installed default config: %modules.', [
434       '%modules' => implode(', ', $modules),
435     ]));
436   }
437
438   /**
439    * Installs a specific table from a module schema definition.
440    *
441    * @param string $module
442    *   The name of the module that defines the table's schema.
443    * @param string|array $tables
444    *   The name or an array of the names of the tables to install.
445    *
446    * @throws \RuntimeException
447    *   Thrown when $module is not enabled or when the table schema cannot be
448    *   found in the module specified.
449    */
450   protected function installSchema($module, $tables) {
451     // drupal_get_module_schema() is technically able to install a schema
452     // of a non-enabled module, but its ability to load the module's .install
453     // file depends on many other factors. To prevent differences in test
454     // behavior and non-reproducible test failures, we only allow the schema of
455     // explicitly loaded/enabled modules to be installed.
456     if (!$this->container->get('module_handler')->moduleExists($module)) {
457       throw new \RuntimeException("'$module' module is not enabled");
458     }
459
460     $tables = (array) $tables;
461     foreach ($tables as $table) {
462       $schema = drupal_get_module_schema($module, $table);
463       if (empty($schema)) {
464         // BC layer to avoid some contrib tests to fail.
465         // @todo Remove the BC layer before 8.1.x release.
466         // @see https://www.drupal.org/node/2670360
467         // @see https://www.drupal.org/node/2670454
468         if ($module == 'system') {
469           continue;
470         }
471         throw new \RuntimeException("Unknown '$table' table schema in '$module' module.");
472       }
473       $this->container->get('database')->schema()->createTable($table, $schema);
474     }
475     $this->pass(format_string('Installed %module tables: %tables.', [
476       '%tables' => '{' . implode('}, {', $tables) . '}',
477       '%module' => $module,
478     ]));
479   }
480
481   /**
482    * Installs the storage schema for a specific entity type.
483    *
484    * @param string $entity_type_id
485    *   The ID of the entity type.
486    */
487   protected function installEntitySchema($entity_type_id) {
488     /** @var \Drupal\Core\Entity\EntityManagerInterface $entity_manager */
489     $entity_manager = $this->container->get('entity.manager');
490     $entity_type = $entity_manager->getDefinition($entity_type_id);
491     $entity_manager->onEntityTypeCreate($entity_type);
492
493     // For test runs, the most common storage backend is a SQL database. For
494     // this case, ensure the tables got created.
495     $storage = $entity_manager->getStorage($entity_type_id);
496     if ($storage instanceof SqlEntityStorageInterface) {
497       $tables = $storage->getTableMapping()->getTableNames();
498       $db_schema = $this->container->get('database')->schema();
499       $all_tables_exist = TRUE;
500       foreach ($tables as $table) {
501         if (!$db_schema->tableExists($table)) {
502           $this->fail(new FormattableMarkup('Installed entity type table for the %entity_type entity type: %table', [
503             '%entity_type' => $entity_type_id,
504             '%table' => $table,
505           ]));
506           $all_tables_exist = FALSE;
507         }
508       }
509       if ($all_tables_exist) {
510         $this->pass(new FormattableMarkup('Installed entity type tables for the %entity_type entity type: %tables', [
511           '%entity_type' => $entity_type_id,
512           '%tables' => '{' . implode('}, {', $tables) . '}',
513         ]));
514       }
515     }
516   }
517
518   /**
519    * Enables modules for this test.
520    *
521    * To install test modules outside of the testing environment, add
522    * @code
523    * $settings['extension_discovery_scan_tests'] = TRUE;
524    * @endcode
525    * to your settings.php.
526    *
527    * @param array $modules
528    *   A list of modules to enable. Dependencies are not resolved; i.e.,
529    *   multiple modules have to be specified with dependent modules first.
530    *   The new modules are only added to the active module list and loaded.
531    */
532   protected function enableModules(array $modules) {
533     // Perform an ExtensionDiscovery scan as this function may receive a
534     // profile that is not the current profile, and we don't yet have a cached
535     // way to receive inactive profile information.
536     // @todo Remove as part of https://www.drupal.org/node/2186491
537     $listing = new ExtensionDiscovery(\Drupal::root());
538     $module_list = $listing->scan('module');
539     // In ModuleHandlerTest we pass in a profile as if it were a module.
540     $module_list += $listing->scan('profile');
541     // Set the list of modules in the extension handler.
542     $module_handler = $this->container->get('module_handler');
543
544     // Write directly to active storage to avoid early instantiation of
545     // the event dispatcher which can prevent modules from registering events.
546     $active_storage = \Drupal::service('config.storage');
547     $extensions = $active_storage->read('core.extension');
548
549     foreach ($modules as $module) {
550       $module_handler->addModule($module, $module_list[$module]->getPath());
551       // Maintain the list of enabled modules in configuration.
552       $extensions['module'][$module] = 0;
553     }
554     $active_storage->write('core.extension', $extensions);
555
556     // Update the kernel to make their services available.
557     $module_filenames = $module_handler->getModuleList();
558     $this->kernel->updateModules($module_filenames, $module_filenames);
559
560     // Ensure isLoaded() is TRUE in order to make
561     // \Drupal\Core\Theme\ThemeManagerInterface::render() work.
562     // Note that the kernel has rebuilt the container; this $module_handler is
563     // no longer the $module_handler instance from above.
564     $this->container->get('module_handler')->reload();
565     $this->pass(format_string('Enabled modules: %modules.', [
566       '%modules' => implode(', ', $modules),
567     ]));
568   }
569
570   /**
571    * Disables modules for this test.
572    *
573    * @param array $modules
574    *   A list of modules to disable. Dependencies are not resolved; i.e.,
575    *   multiple modules have to be specified with dependent modules first.
576    *   Code of previously active modules is still loaded. The modules are only
577    *   removed from the active module list.
578    */
579   protected function disableModules(array $modules) {
580     // Unset the list of modules in the extension handler.
581     $module_handler = $this->container->get('module_handler');
582     $module_filenames = $module_handler->getModuleList();
583     $extension_config = $this->config('core.extension');
584     foreach ($modules as $module) {
585       unset($module_filenames[$module]);
586       $extension_config->clear('module.' . $module);
587     }
588     $extension_config->save();
589     $module_handler->setModuleList($module_filenames);
590     $module_handler->resetImplementations();
591     // Update the kernel to remove their services.
592     $this->kernel->updateModules($module_filenames, $module_filenames);
593
594     // Ensure isLoaded() is TRUE in order to make
595     // \Drupal\Core\Theme\ThemeManagerInterface::render() work.
596     // Note that the kernel has rebuilt the container; this $module_handler is
597     // no longer the $module_handler instance from above.
598     $module_handler = $this->container->get('module_handler');
599     $module_handler->reload();
600     $this->pass(format_string('Disabled modules: %modules.', [
601       '%modules' => implode(', ', $modules),
602     ]));
603   }
604
605   /**
606    * Registers a stream wrapper for this test.
607    *
608    * @param string $scheme
609    *   The scheme to register.
610    * @param string $class
611    *   The fully qualified class name to register.
612    * @param int $type
613    *   The Drupal Stream Wrapper API type. Defaults to
614    *   StreamWrapperInterface::NORMAL.
615    */
616   protected function registerStreamWrapper($scheme, $class, $type = StreamWrapperInterface::NORMAL) {
617     $this->container->get('stream_wrapper_manager')->registerWrapper($scheme, $class, $type);
618   }
619
620   /**
621    * Renders a render array.
622    *
623    * @param array $elements
624    *   The elements to render.
625    *
626    * @return string
627    *   The rendered string output (typically HTML).
628    */
629   protected function render(array &$elements) {
630     // Use the bare HTML page renderer to render our links.
631     $renderer = $this->container->get('bare_html_page_renderer');
632     $response = $renderer->renderBarePage($elements, '', 'maintenance_page');
633
634     // Glean the content from the response object.
635     $content = $response->getContent();
636     $this->setRawContent($content);
637     $this->verbose('<pre style="white-space: pre-wrap">' . Html::escape($content));
638     return $content;
639   }
640
641 }