comparison core/modules/simpletest/src/KernelTestBase.php @ 0:c75dbcec494b

Initial commit from drush-created site
author Chris Cannam
date Thu, 05 Jul 2018 14:24:15 +0000
parents
children a9cd425dd02b
comparison
equal deleted inserted replaced
-1:000000000000 0:c75dbcec494b
1 <?php
2
3 namespace Drupal\simpletest;
4
5 use Drupal\Component\Utility\Html;
6 use Drupal\Component\Utility\SafeMarkup;
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 Symfony\Component\DependencyInjection\Parameter;
18 use Drupal\Core\StreamWrapper\StreamWrapperInterface;
19 use Symfony\Component\DependencyInjection\Reference;
20 use Symfony\Component\HttpFoundation\Request;
21
22 /**
23 * Base class for functional integration tests.
24 *
25 * This base class should be useful for testing some types of integrations which
26 * don't require the overhead of a fully-installed Drupal instance, but which
27 * have many dependencies on parts of Drupal which can't or shouldn't be mocked.
28 *
29 * This base class partially boots a fixture Drupal. The state of the fixture
30 * Drupal is comparable to the state of a system during the early part of the
31 * installation process.
32 *
33 * Tests extending this base class can access services and the database, but the
34 * system is initially empty. This Drupal runs in a minimal mocked filesystem
35 * which operates within vfsStream.
36 *
37 * Modules specified in the $modules property are added to the service container
38 * for each test. The module/hook system is functional. Additional modules
39 * needed in a test should override $modules. Modules specified in this way will
40 * be added to those specified in superclasses.
41 *
42 * Unlike \Drupal\Tests\BrowserTestBase, the modules are not installed. They are
43 * loaded such that their services and hooks are available, but the install
44 * process has not been performed.
45 *
46 * Other modules can be made available in this way using
47 * KernelTestBase::enableModules().
48 *
49 * Some modules can be brought into a fully-installed state using
50 * KernelTestBase::installConfig(), KernelTestBase::installSchema(), and
51 * KernelTestBase::installEntitySchema(). Alternately, tests which need modules
52 * to be fully installed could inherit from \Drupal\Tests\BrowserTestBase.
53 *
54 * @see \Drupal\Tests\KernelTestBase::$modules
55 * @see \Drupal\Tests\KernelTestBase::enableModules()
56 * @see \Drupal\Tests\KernelTestBase::installConfig()
57 * @see \Drupal\Tests\KernelTestBase::installEntitySchema()
58 * @see \Drupal\Tests\KernelTestBase::installSchema()
59 * @see \Drupal\Tests\BrowserTestBase
60 *
61 * @deprecated in Drupal 8.0.x, will be removed before Drupal 9.0.0. Use
62 * \Drupal\KernelTests\KernelTestBase instead.
63 *
64 * @ingroup testing
65 */
66 abstract class KernelTestBase extends TestBase {
67
68 use AssertContentTrait;
69
70 /**
71 * Modules to enable.
72 *
73 * Test classes extending this class, and any classes in the hierarchy up to
74 * this class, may specify individual lists of modules to enable by setting
75 * this property. The values of all properties in all classes in the hierarchy
76 * are merged.
77 *
78 * Any modules specified in the $modules property are automatically loaded and
79 * set as the fixed module list.
80 *
81 * Unlike WebTestBase::setUp(), the specified modules are loaded only, but not
82 * automatically installed. Modules need to be installed manually, if needed.
83 *
84 * @see \Drupal\simpletest\KernelTestBase::enableModules()
85 * @see \Drupal\simpletest\KernelTestBase::setUp()
86 *
87 * @var array
88 */
89 public static $modules = [];
90
91 private $moduleFiles;
92 private $themeFiles;
93
94 /**
95 * The configuration directories for this test run.
96 *
97 * @var array
98 */
99 protected $configDirectories = [];
100
101 /**
102 * A KeyValueMemoryFactory instance to use when building the container.
103 *
104 * @var \Drupal\Core\KeyValueStore\KeyValueMemoryFactory.
105 */
106 protected $keyValueFactory;
107
108 /**
109 * Array of registered stream wrappers.
110 *
111 * @var array
112 */
113 protected $streamWrappers = [];
114
115 /**
116 * {@inheritdoc}
117 */
118 public function __construct($test_id = NULL) {
119 parent::__construct($test_id);
120 $this->skipClasses[__CLASS__] = TRUE;
121 }
122
123 /**
124 * {@inheritdoc}
125 */
126 protected function beforePrepareEnvironment() {
127 // Copy/prime extension file lists once to avoid filesystem scans.
128 if (!isset($this->moduleFiles)) {
129 $this->moduleFiles = \Drupal::state()->get('system.module.files') ?: [];
130 $this->themeFiles = \Drupal::state()->get('system.theme.files') ?: [];
131 }
132 }
133
134 /**
135 * Create and set new configuration directories.
136 *
137 * @see config_get_config_directory()
138 *
139 * @throws \RuntimeException
140 * Thrown when CONFIG_SYNC_DIRECTORY cannot be created or made writable.
141 */
142 protected function prepareConfigDirectories() {
143 $this->configDirectories = [];
144 include_once DRUPAL_ROOT . '/core/includes/install.inc';
145 // Assign the relative path to the global variable.
146 $path = $this->siteDirectory . '/config_' . CONFIG_SYNC_DIRECTORY;
147 $GLOBALS['config_directories'][CONFIG_SYNC_DIRECTORY] = $path;
148 // Ensure the directory can be created and is writeable.
149 if (!file_prepare_directory($path, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
150 throw new \RuntimeException("Failed to create '" . CONFIG_SYNC_DIRECTORY . "' config directory $path");
151 }
152 // Provide the already resolved path for tests.
153 $this->configDirectories[CONFIG_SYNC_DIRECTORY] = $path;
154 }
155
156 /**
157 * {@inheritdoc}
158 */
159 protected function setUp() {
160 $this->keyValueFactory = new KeyValueMemoryFactory();
161
162 // Back up settings from TestBase::prepareEnvironment().
163 $settings = Settings::getAll();
164
165 // Allow for test-specific overrides.
166 $directory = DRUPAL_ROOT . '/' . $this->siteDirectory;
167 $settings_services_file = DRUPAL_ROOT . '/' . $this->originalSite . '/testing.services.yml';
168 $container_yamls = [];
169 if (file_exists($settings_services_file)) {
170 // Copy the testing-specific service overrides in place.
171 $testing_services_file = $directory . '/services.yml';
172 copy($settings_services_file, $testing_services_file);
173 $container_yamls[] = $testing_services_file;
174 }
175 $settings_testing_file = DRUPAL_ROOT . '/' . $this->originalSite . '/settings.testing.php';
176 if (file_exists($settings_testing_file)) {
177 // Copy the testing-specific settings.php overrides in place.
178 copy($settings_testing_file, $directory . '/settings.testing.php');
179 }
180
181 if (file_exists($directory . '/settings.testing.php')) {
182 // Add the name of the testing class to settings.php and include the
183 // testing specific overrides
184 $hash_salt = Settings::getHashSalt();
185 $test_class = get_class($this);
186 $container_yamls_export = Variable::export($container_yamls);
187 $php = <<<EOD
188 <?php
189
190 \$settings['hash_salt'] = '$hash_salt';
191 \$settings['container_yamls'] = $container_yamls_export;
192
193 \$test_class = '$test_class';
194 include DRUPAL_ROOT . '/' . \$site_path . '/settings.testing.php';
195 EOD;
196 file_put_contents($directory . '/settings.php', $php);
197 }
198
199 // Add this test class as a service provider.
200 // @todo Remove the indirection; implement ServiceProviderInterface instead.
201 $GLOBALS['conf']['container_service_providers']['TestServiceProvider'] = 'Drupal\simpletest\TestServiceProvider';
202
203 // Bootstrap a new kernel.
204 $class_loader = require DRUPAL_ROOT . '/autoload.php';
205 $this->kernel = new DrupalKernel('testing', $class_loader, FALSE);
206 $request = Request::create('/');
207 $site_path = DrupalKernel::findSitePath($request);
208 $this->kernel->setSitePath($site_path);
209 if (file_exists($directory . '/settings.testing.php')) {
210 Settings::initialize(DRUPAL_ROOT, $site_path, $class_loader);
211 }
212 $this->kernel->boot();
213
214 // Ensure database install tasks have been run.
215 require_once __DIR__ . '/../../../includes/install.inc';
216 $connection = Database::getConnection();
217 $errors = db_installer_object($connection->driver())->runTasks();
218 if (!empty($errors)) {
219 $this->fail('Failed to run installer database tasks: ' . implode(', ', $errors));
220 }
221
222 // Reboot the kernel because the container might contain a connection to the
223 // database that has been closed during the database install tasks. This
224 // prevents any services created during the first boot from having stale
225 // database connections, for example, \Drupal\Core\Config\DatabaseStorage.
226 $this->kernel->shutdown();
227 $this->kernel->boot();
228
229 // Save the original site directory path, so that extensions in the
230 // site-specific directory can still be discovered in the test site
231 // environment.
232 // @see \Drupal\Core\Extension\ExtensionDiscovery::scan()
233 $settings['test_parent_site'] = $this->originalSite;
234
235 // Restore and merge settings.
236 // DrupalKernel::boot() initializes new Settings, and the containerBuild()
237 // method sets additional settings.
238 new Settings($settings + Settings::getAll());
239
240 // Create and set new configuration directories.
241 $this->prepareConfigDirectories();
242
243 // Set the request scope.
244 $this->container = $this->kernel->getContainer();
245 $this->container->get('request_stack')->push($request);
246
247 // Re-inject extension file listings into state, unless the key/value
248 // service was overridden (in which case its storage does not exist yet).
249 if ($this->container->get('keyvalue') instanceof KeyValueMemoryFactory) {
250 $this->container->get('state')->set('system.module.files', $this->moduleFiles);
251 $this->container->get('state')->set('system.theme.files', $this->themeFiles);
252 }
253
254 // Create a minimal core.extension configuration object so that the list of
255 // enabled modules can be maintained allowing
256 // \Drupal\Core\Config\ConfigInstaller::installDefaultConfig() to work.
257 // Write directly to active storage to avoid early instantiation of
258 // the event dispatcher which can prevent modules from registering events.
259 \Drupal::service('config.storage')->write('core.extension', ['module' => [], 'theme' => [], 'profile' => '']);
260
261 // Collect and set a fixed module list.
262 $class = get_class($this);
263 $modules = [];
264 while ($class) {
265 if (property_exists($class, 'modules')) {
266 // Only add the modules, if the $modules property was not inherited.
267 $rp = new \ReflectionProperty($class, 'modules');
268 if ($rp->class == $class) {
269 $modules[$class] = $class::$modules;
270 }
271 }
272 $class = get_parent_class($class);
273 }
274 // Modules have been collected in reverse class hierarchy order; modules
275 // defined by base classes should be sorted first. Then, merge the results
276 // together.
277 $modules = array_reverse($modules);
278 $modules = call_user_func_array('array_merge_recursive', $modules);
279 if ($modules) {
280 $this->enableModules($modules);
281 }
282
283 // Tests based on this class are entitled to use Drupal's File and
284 // StreamWrapper APIs.
285 // @todo Move StreamWrapper management into DrupalKernel.
286 // @see https://www.drupal.org/node/2028109
287 file_prepare_directory($this->publicFilesDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
288 $this->settingsSet('file_public_path', $this->publicFilesDirectory);
289 $this->streamWrappers = [];
290 $this->registerStreamWrapper('public', 'Drupal\Core\StreamWrapper\PublicStream');
291 // The temporary stream wrapper is able to operate both with and without
292 // configuration.
293 $this->registerStreamWrapper('temporary', 'Drupal\Core\StreamWrapper\TemporaryStream');
294
295 // Manually configure the test mail collector implementation to prevent
296 // tests from sending out emails and collect them in state instead.
297 // While this should be enforced via settings.php prior to installation,
298 // some tests expect to be able to test mail system implementations.
299 $GLOBALS['config']['system.mail']['interface']['default'] = 'test_mail_collector';
300 }
301
302 /**
303 * {@inheritdoc}
304 */
305 protected function tearDown() {
306 if ($this->kernel instanceof DrupalKernel) {
307 $this->kernel->shutdown();
308 }
309 // Before tearing down the test environment, ensure that no stream wrapper
310 // of this test leaks into the parent environment. Unlike all other global
311 // state variables in Drupal, stream wrappers are a global state construct
312 // of PHP core, which has to be maintained manually.
313 // @todo Move StreamWrapper management into DrupalKernel.
314 // @see https://www.drupal.org/node/2028109
315 foreach ($this->streamWrappers as $scheme => $type) {
316 $this->unregisterStreamWrapper($scheme, $type);
317 }
318 parent::tearDown();
319 }
320
321 /**
322 * Sets up the base service container for this test.
323 *
324 * Extend this method in your test to register additional service overrides
325 * that need to persist a DrupalKernel reboot. This method is called whenever
326 * the kernel is rebuilt.
327 *
328 * @see \Drupal\simpletest\KernelTestBase::setUp()
329 * @see \Drupal\simpletest\KernelTestBase::enableModules()
330 * @see \Drupal\simpletest\KernelTestBase::disableModules()
331 */
332 public function containerBuild(ContainerBuilder $container) {
333 // Keep the container object around for tests.
334 $this->container = $container;
335
336 // Set the default language on the minimal container.
337 $this->container->setParameter('language.default_values', $this->defaultLanguageData());
338
339 $container->register('lock', 'Drupal\Core\Lock\NullLockBackend');
340 $container->register('cache_factory', 'Drupal\Core\Cache\MemoryBackendFactory');
341
342 $container
343 ->register('config.storage', 'Drupal\Core\Config\DatabaseStorage')
344 ->addArgument(Database::getConnection())
345 ->addArgument('config');
346
347 if ($this->strictConfigSchema) {
348 $container
349 ->register('simpletest.config_schema_checker', ConfigSchemaChecker::class)
350 ->addArgument(new Reference('config.typed'))
351 ->addArgument($this->getConfigSchemaExclusions())
352 ->addTag('event_subscriber');
353 }
354
355 $keyvalue_options = $container->getParameter('factory.keyvalue') ?: [];
356 $keyvalue_options['default'] = 'keyvalue.memory';
357 $container->setParameter('factory.keyvalue', $keyvalue_options);
358 $container->set('keyvalue.memory', $this->keyValueFactory);
359 if (!$container->has('keyvalue')) {
360 // TestBase::setUp puts a completely empty container in
361 // $this->container which is somewhat the mirror of the empty
362 // environment being set up. Unit tests need not to waste time with
363 // getting a container set up for them. Drupal Unit Tests might just get
364 // away with a simple container holding the absolute bare minimum. When
365 // a kernel is overridden then there's no need to re-register the keyvalue
366 // service but when a test is happy with the superminimal container put
367 // together here, it still might a keyvalue storage for anything using
368 // \Drupal::state() -- that's why a memory service was added in the first
369 // place.
370 $container->register('settings', 'Drupal\Core\Site\Settings')
371 ->setFactoryClass('Drupal\Core\Site\Settings')
372 ->setFactoryMethod('getInstance');
373
374 $container
375 ->register('keyvalue', 'Drupal\Core\KeyValueStore\KeyValueFactory')
376 ->addArgument(new Reference('service_container'))
377 ->addArgument(new Parameter('factory.keyvalue'));
378
379 $container->register('state', 'Drupal\Core\State\State')
380 ->addArgument(new Reference('keyvalue'));
381 }
382
383 if ($container->hasDefinition('path_processor_alias')) {
384 // Prevent the alias-based path processor, which requires a url_alias db
385 // table, from being registered to the path processor manager. We do this
386 // by removing the tags that the compiler pass looks for. This means the
387 // url generator can safely be used within tests.
388 $definition = $container->getDefinition('path_processor_alias');
389 $definition->clearTag('path_processor_inbound')->clearTag('path_processor_outbound');
390 }
391
392 if ($container->hasDefinition('password')) {
393 $container->getDefinition('password')->setArguments([1]);
394 }
395
396 // Register the stream wrapper manager.
397 $container
398 ->register('stream_wrapper_manager', 'Drupal\Core\StreamWrapper\StreamWrapperManager')
399 ->addArgument(new Reference('module_handler'))
400 ->addMethodCall('setContainer', [new Reference('service_container')]);
401
402 $request = Request::create('/');
403 $container->get('request_stack')->push($request);
404 }
405
406 /**
407 * Provides the data for setting the default language on the container.
408 *
409 * @return array
410 * The data array for the default language.
411 */
412 protected function defaultLanguageData() {
413 return Language::$defaultValues;
414 }
415
416 /**
417 * Installs default configuration for a given list of modules.
418 *
419 * @param array $modules
420 * A list of modules for which to install default configuration.
421 *
422 * @throws \RuntimeException
423 * Thrown when any module listed in $modules is not enabled.
424 */
425 protected function installConfig(array $modules) {
426 foreach ($modules as $module) {
427 if (!$this->container->get('module_handler')->moduleExists($module)) {
428 throw new \RuntimeException("'$module' module is not enabled");
429 }
430 \Drupal::service('config.installer')->installDefaultConfig('module', $module);
431 }
432 $this->pass(format_string('Installed default config: %modules.', [
433 '%modules' => implode(', ', $modules),
434 ]));
435 }
436
437 /**
438 * Installs a specific table from a module schema definition.
439 *
440 * @param string $module
441 * The name of the module that defines the table's schema.
442 * @param string|array $tables
443 * The name or an array of the names of the tables to install.
444 *
445 * @throws \RuntimeException
446 * Thrown when $module is not enabled or when the table schema cannot be
447 * found in the module specified.
448 */
449 protected function installSchema($module, $tables) {
450 // drupal_get_module_schema() is technically able to install a schema
451 // of a non-enabled module, but its ability to load the module's .install
452 // file depends on many other factors. To prevent differences in test
453 // behavior and non-reproducible test failures, we only allow the schema of
454 // explicitly loaded/enabled modules to be installed.
455 if (!$this->container->get('module_handler')->moduleExists($module)) {
456 throw new \RuntimeException("'$module' module is not enabled");
457 }
458
459 $tables = (array) $tables;
460 foreach ($tables as $table) {
461 $schema = drupal_get_module_schema($module, $table);
462 if (empty($schema)) {
463 // BC layer to avoid some contrib tests to fail.
464 // @todo Remove the BC layer before 8.1.x release.
465 // @see https://www.drupal.org/node/2670360
466 // @see https://www.drupal.org/node/2670454
467 if ($module == 'system') {
468 continue;
469 }
470 throw new \RuntimeException("Unknown '$table' table schema in '$module' module.");
471 }
472 $this->container->get('database')->schema()->createTable($table, $schema);
473 }
474 $this->pass(format_string('Installed %module tables: %tables.', [
475 '%tables' => '{' . implode('}, {', $tables) . '}',
476 '%module' => $module,
477 ]));
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(SafeMarkup::format('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(SafeMarkup::format('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 }