[TASK] Add additional configuration for functional tests
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Tests / FunctionalTestCaseBootstrapUtility.php
1 <?php
2 namespace TYPO3\CMS\Core\Tests;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2013 Christian Kuhn <lolli@schwarzbu.ch>
8 * All rights reserved
9 *
10 * This script is part of the TYPO3 project. The TYPO3 project is
11 * free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * The GNU General Public License can be found at
17 * http://www.gnu.org/copyleft/gpl.html.
18 *
19 * This script is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * This copyright notice MUST APPEAR in all copies of the script!
25 ***************************************************************/
26
27 /**
28 * Utility class to set up and bootstrap TYPO3 CMS for functional tests
29 */
30 class FunctionalTestCaseBootstrapUtility {
31
32 /**
33 * @var string Identifier calculated from test case class
34 */
35 protected $identifier;
36
37 /**
38 * @var string Absolute path to test instance document root
39 */
40 protected $instancePath;
41
42 /**
43 * @var string Name of test database
44 */
45 protected $databaseName;
46
47 /**
48 * @var string Name of original database
49 */
50 protected $originalDatabaseName;
51
52 /**
53 * @var array These extensions are always loaded
54 */
55 protected $defaultActivatedCoreExtensions = array(
56 'core',
57 'backend',
58 'frontend',
59 'cms',
60 'lang',
61 'sv',
62 'extensionmanager',
63 'recordlist',
64 'extbase',
65 'fluid',
66 'cshmanual',
67 'install',
68 'saltedpasswords'
69 );
70
71 /**
72 * Set up creates a test instance and database.
73 *
74 * @param string $testCaseClassName Name of test case class
75 * @param array $coreExtensionsToLoad Array of core extensions to load
76 * @param array $testExtensionsToLoad Array of test extensions to load
77 * @param array $pathsToLinkInTestInstance Array of source => destination path pairs to be linked
78 * @param array $configurationToUse Array of TYPO3_CONF_VARS that need to be overridden
79 * @return string Path to TYPO3 CMS test installation for this test case
80 */
81 public function setUp(
82 $testCaseClassName,
83 array $coreExtensionsToLoad,
84 array $testExtensionsToLoad,
85 array $pathsToLinkInTestInstance,
86 array $configurationToUse
87 ) {
88 $this->setUpIdentifier($testCaseClassName);
89 $this->setUpInstancePath();
90 $this->removeOldInstanceIfExists();
91 $this->setUpInstanceDirectories();
92 $this->setUpInstanceCoreLinks();
93 $this->linkTestExtensionsToInstance($testExtensionsToLoad);
94 $this->linkPathsInTestInstance($pathsToLinkInTestInstance);
95 $this->setUpLocalConfiguration($configurationToUse);
96 $this->setUpPackageStates($coreExtensionsToLoad, $testExtensionsToLoad);
97 $this->setUpBasicTypo3Bootstrap();
98 $this->setUpTestDatabase();
99 \TYPO3\CMS\Core\Core\Bootstrap::getInstance()->loadExtensionTables(TRUE);
100 $this->createDatabaseStructure();
101
102 return $this->instancePath;
103 }
104
105 /**
106 * Tear down destroys the instance and database.
107 *
108 * @throws Exception
109 * @return void
110 */
111 public function tearDown() {
112 $classLoader = \TYPO3\CMS\Core\Core\Bootstrap::getInstance()->getEarlyInstance('TYPO3\\CMS\\Core\\Core\\ClassLoader');
113 spl_autoload_unregister(array($classLoader, 'loadClass'));
114 if (empty($this->identifier)) {
115 throw new Exception(
116 'Test identifier not set. Is parent::setUp() called in setUp()?',
117 1376739702
118 );
119 }
120 $this->tearDownTestDatabase();
121 $this->removeInstance();
122 }
123
124 /**
125 * Calculate a "unique" identifier for the test database and the
126 * instance patch based on the given test case class name.
127 *
128 * As a result, the database name will be identical between different
129 * test runs, but different between each test case.
130 */
131 protected function setUpIdentifier($testCaseClassName) {
132 // 7 characters of sha1 should be enough for a unique identification
133 $this->identifier = substr(sha1($testCaseClassName), 0, 7);
134 }
135
136 /**
137 * Calculates path to TYPO3 CMS test installation for this test case.
138 *
139 * @return void
140 */
141 protected function setUpInstancePath() {
142 $this->instancePath = ORIGINAL_ROOT . 'typo3temp/functional-' . $this->identifier;
143 }
144
145 /**
146 * Remove test instance folder structure in setUp() if it exists.
147 * This may happen if a functional test before threw a fatal.
148 *
149 * @return void
150 */
151 protected function removeOldInstanceIfExists() {
152 if (is_dir($this->instancePath)) {
153 $this->removeInstance();
154 }
155 }
156
157 /**
158 * Create folder structure of test instance.
159 *
160 * @throws Exception
161 * @return void
162 */
163 protected function setUpInstanceDirectories() {
164 $foldersToCreate = array(
165 '',
166 '/fileadmin',
167 '/typo3temp',
168 '/typo3conf',
169 '/typo3conf/ext',
170 '/uploads'
171 );
172 foreach ($foldersToCreate as $folder) {
173 $success = mkdir($this->instancePath . $folder);
174 if (!$success) {
175 throw new Exception(
176 'Creating directory failed: ' . $this->instancePath . $folder,
177 1376657189
178 );
179 }
180 }
181 }
182
183 /**
184 * Link TYPO3 CMS core from "parent" instance.
185 *
186 * @throws Exception
187 * @return void
188 */
189 protected function setUpInstanceCoreLinks() {
190 $linksToSet = array(
191 ORIGINAL_ROOT . 'typo3' => $this->instancePath . '/typo3',
192 ORIGINAL_ROOT . 'index.php' => $this->instancePath . '/index.php'
193 );
194 foreach ($linksToSet as $from => $to) {
195 $success = symlink($from, $to);
196 if (!$success) {
197 throw new Exception(
198 'Creating link failed: from ' . $from . ' to: ' . $to,
199 1376657199
200 );
201 }
202 }
203 }
204
205 /**
206 * Link test extensions to the typo3conf/ext folder of the instance.
207 *
208 * @param array $extensionPaths Contains paths to extensions relative to document root
209 * @throws Exception
210 * @return void
211 */
212 protected function linkTestExtensionsToInstance(array $extensionPaths) {
213 foreach ($extensionPaths as $extensionPath) {
214 $absoluteExtensionPath = ORIGINAL_ROOT . $extensionPath;
215 if (!is_dir($absoluteExtensionPath)) {
216 throw new Exception(
217 'Test extension path ' . $absoluteExtensionPath . ' not found',
218 1376745645
219 );
220 }
221 $destinationPath = $this->instancePath . '/typo3conf/ext/'. basename($absoluteExtensionPath);
222 $success = symlink($absoluteExtensionPath, $destinationPath);
223 if (!$success) {
224 throw new Exception(
225 'Can not link extension folder: ' . $absoluteExtensionPath . ' to ' . $destinationPath,
226 1376657142
227 );
228 }
229 }
230 }
231
232 /**
233 * Link paths inside the test instance, e.g. from a fixture fileadmin subfolder to the
234 * test instance fileadmin folder
235 *
236 * @param array $pathsToLinkInTestInstance Contains paths as array of source => destination in key => value pairs of folders relative to test instance root
237 * @throws \TYPO3\CMS\Core\Tests\Exception if a source path could not be found
238 * @throws \TYPO3\CMS\Core\Tests\Exception on failing creating the symlink
239 * @return void
240 * @see \TYPO3\CMS\Core\Tests\FunctionalTestCase::$pathsToLinkInTestInstance
241 */
242 protected function linkPathsInTestInstance(array $pathsToLinkInTestInstance) {
243 foreach ($pathsToLinkInTestInstance as $sourcePathToLinkInTestInstance => $destinationPathToLinkInTestInstance) {
244 $sourcePath = $this->instancePath . '/' . ltrim($sourcePathToLinkInTestInstance, '/');
245 if (!file_exists($sourcePath)) {
246 throw new Exception(
247 'Path ' . $sourcePath . ' not found',
248 1376745645
249 );
250 }
251 $destinationPath = $this->instancePath . '/' . ltrim($destinationPathToLinkInTestInstance, '/');
252 $success = symlink($sourcePath, $destinationPath);
253 if (!$success) {
254 throw new Exception(
255 'Can not link the path ' . $sourcePath . ' to ' . $destinationPath,
256 1389969623
257 );
258 }
259 }
260 }
261
262 /**
263 * Create LocalConfiguration.php file in the test instance
264 *
265 * @param array $configurationToMerge
266 * @throws Exception
267 * @return void
268 */
269 protected function setUpLocalConfiguration(array $configurationToMerge) {
270 $originalConfigurationArray = require ORIGINAL_ROOT . 'typo3conf/LocalConfiguration.php';
271 // Base of final LocalConfiguration is core factory configuration
272 $finalConfigurationArray = require ORIGINAL_ROOT .'typo3/sysext/core/Configuration/FactoryConfiguration.php';
273
274 $this->mergeRecursiveWithOverrule($finalConfigurationArray, require ORIGINAL_ROOT .'typo3/sysext/core/Build/Configuration/FunctionalTestsConfiguration.php');
275 $this->mergeRecursiveWithOverrule($finalConfigurationArray, $configurationToMerge);
276 $finalConfigurationArray['DB'] = $originalConfigurationArray['DB'];
277 // Calculate and set new database name
278 $this->originalDatabaseName = $originalConfigurationArray['DB']['database'];
279 $this->databaseName = $this->originalDatabaseName . '_ft' . $this->identifier;
280
281 // Maximum database name length for mysql is 64 characters
282 if (strlen($this->databaseName) > 64) {
283 $maximumOriginalDatabaseName = 64 - strlen('_ft' . $this->identifier);
284 throw new Exception(
285 'The name of the database that is used for the functional test (' . $this->databaseName . ')' .
286 ' exceeds the maximum length of 64 character allowed by MySQL. You have to shorten your' .
287 ' original database name to ' . $maximumOriginalDatabaseName . ' characters',
288 1377600104
289 );
290 }
291
292 $finalConfigurationArray['DB']['database'] = $this->databaseName;
293
294 $result = $this->writeFile(
295 $this->instancePath . '/typo3conf/LocalConfiguration.php',
296 '<?php' . chr(10) .
297 'return ' .
298 $this->arrayExport(
299 $finalConfigurationArray
300 ) .
301 ';' . chr(10) .
302 '?>'
303 );
304 if (!$result) {
305 throw new Exception('Can not write local configuration', 1376657277);
306 }
307 }
308
309 /**
310 * Compile typo3conf/PackageStates.php containing default packages like core,
311 * a functional test specific list of additional core extensions, and a list of
312 * test extensions.
313 *
314 * @param array $coreExtensionsToLoad Additional core extensions to load
315 * @param array $testExtensionPaths Paths to extensions relative to document root
316 * @throws Exception
317 * @TODO Figure out what the intention of the upper arguments is
318 */
319 protected function setUpPackageStates(array $coreExtensionsToLoad, array $testExtensionPaths) {
320 $packageStates = array(
321 'packages' => array(),
322 'version' => 4,
323 );
324
325 // Register default list of extensions and set active
326 foreach ($this->defaultActivatedCoreExtensions as $extensionName) {
327 $packageStates['packages'][$extensionName] = array(
328 'state' => 'active',
329 'packagePath' => 'typo3/sysext/' . $extensionName . '/',
330 'classesPath' => 'Classes/',
331 );
332 }
333
334 // Register additional core extensions and set active
335 foreach ($coreExtensionsToLoad as $extensionName) {
336 if (isset($packageSates['packages'][$extensionName])) {
337 throw new Exception(
338 $extensionName . ' is already registered as default core extension to load, no need to load it explicitly',
339 1390913893
340 );
341 }
342 $packageStates['packages'][$extensionName] = array(
343 'state' => 'active',
344 'packagePath' => 'typo3/sysext/' . $extensionName . '/',
345 'classesPath' => 'Classes/',
346 );
347 }
348
349 // Activate test extensions that have been symlinked before
350 foreach ($testExtensionPaths as $extensionPath) {
351 $extensionName = basename($extensionPath);
352 if (isset($packageSates['packages'][$extensionName])) {
353 throw new Exception(
354 $extensionName . ' is already registered as extension to load, no need to load it explicitly',
355 1390913894
356 );
357 }
358 $packageStates['packages'][$extensionName] = array(
359 'state' => 'active',
360 'packagePath' => 'typo3conf/ext/' . $extensionName . '/',
361 'classesPath' => 'Classes/',
362 );
363 }
364
365 $result = $this->writeFile(
366 $this->instancePath . '/typo3conf/PackageStates.php',
367 '<?php' . chr(10) .
368 'return ' .
369 $this->arrayExport(
370 $packageStates
371 ) .
372 ';' . chr(10) .
373 '?>'
374 );
375 if (!$result) {
376 throw new Exception('Can not write PackageStates', 1381612729);
377 }
378 }
379
380 /**
381 * Bootstrap basic TYPO3
382 *
383 * @return void
384 */
385 protected function setUpBasicTypo3Bootstrap() {
386 $_SERVER['PWD'] = $this->instancePath;
387 $_SERVER['argv'][0] = 'index.php';
388
389 define('TYPO3_MODE', 'BE');
390 define('TYPO3_cliMode', TRUE);
391
392 require_once $this->instancePath . '/typo3/sysext/core/Classes/Core/CliBootstrap.php';
393 \TYPO3\CMS\Core\Core\CliBootstrap::checkEnvironmentOrDie();
394
395 require_once $this->instancePath . '/typo3/sysext/core/Classes/Core/Bootstrap.php';
396 \TYPO3\CMS\Core\Core\Bootstrap::getInstance()
397 ->baseSetup('')
398 ->loadConfigurationAndInitialize(FALSE)
399 ->loadTypo3LoadedExtAndExtLocalconf(FALSE)
400 ->applyAdditionalConfigurationSettings();
401 }
402
403 /**
404 * Populate $GLOBALS['TYPO3_DB'] and create test database
405 *
406 * @throws \TYPO3\CMS\Core\Tests\Exception
407 * @return void
408 */
409 protected function setUpTestDatabase() {
410 \TYPO3\CMS\Core\Core\Bootstrap::getInstance()->initializeTypo3DbGlobal();
411 /** @var \TYPO3\CMS\Core\Database\DatabaseConnection $database */
412 $database = $GLOBALS['TYPO3_DB'];
413 if(!$database->sql_pconnect()) {
414 throw new Exception(
415 'TYPO3 Fatal Error: The current username, password or host was not accepted when the'
416 . ' connection to the database was attempted to be established!',
417 1377620117
418 );
419 }
420
421 // Drop database in case a previous test had a fatal and did not clean up properly
422 $database->admin_query('DROP DATABASE IF EXISTS `' . $this->databaseName . '`');
423 $createDatabaseResult = $database->admin_query('CREATE DATABASE `' . $this->databaseName . '`');
424 if (!$createDatabaseResult) {
425 $user = $GLOBALS['TYPO3_CONF_VARS']['DB']['username'];
426 $host = $GLOBALS['TYPO3_CONF_VARS']['DB']['host'];
427 throw new Exception(
428 'Unable to create database with name ' . $this->databaseName . '. This is probably a permission problem.'
429 . ' For this instance this could be fixed executing'
430 . ' "GRANT ALL ON `' . $this->originalDatabaseName . '_ft%`.* TO `' . $user . '`@`' . $host . '`;"',
431 1376579070
432 );
433 }
434 $database->setDatabaseName($this->databaseName);
435 $database->sql_select_db($this->databaseName);
436 }
437
438 /**
439 * Create tables and import static rows
440 *
441 * @return void
442 */
443 protected function createDatabaseStructure() {
444 /** @var \TYPO3\CMS\Install\Service\SqlSchemaMigrationService $schemaMigrationService */
445 $schemaMigrationService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Install\\Service\\SqlSchemaMigrationService');
446 /** @var \TYPO3\CMS\Extbase\Object\ObjectManager $objectManager */
447 $objectManager = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Extbase\\Object\\ObjectManager');
448 /** @var \TYPO3\CMS\Install\Service\SqlExpectedSchemaService $expectedSchemaService */
449 $expectedSchemaService = $objectManager->get('TYPO3\\CMS\\Install\\Service\\SqlExpectedSchemaService');
450
451 // Raw concatenated ext_tables.sql and friends string
452 $expectedSchemaString = $expectedSchemaService->getTablesDefinitionString(TRUE);
453 $statements = $schemaMigrationService->getStatementArray($expectedSchemaString, TRUE);
454 list($_, $insertCount) = $schemaMigrationService->getCreateTables($statements, TRUE);
455
456 $fieldDefinitionsFile = $schemaMigrationService->getFieldDefinitions_fileContent($expectedSchemaString);
457 $fieldDefinitionsDatabase = $schemaMigrationService->getFieldDefinitions_database();
458 $difference = $schemaMigrationService->getDatabaseExtra($fieldDefinitionsFile, $fieldDefinitionsDatabase);
459 $updateStatements = $schemaMigrationService->getUpdateSuggestions($difference);
460
461 $schemaMigrationService->performUpdateQueries($updateStatements['add'], $updateStatements['add']);
462 $schemaMigrationService->performUpdateQueries($updateStatements['change'], $updateStatements['change']);
463 $schemaMigrationService->performUpdateQueries($updateStatements['create_table'], $updateStatements['create_table']);
464
465 foreach ($insertCount as $table => $count) {
466 $insertStatements = $schemaMigrationService->getTableInsertStatements($statements, $table);
467 foreach ($insertStatements as $insertQuery) {
468 $insertQuery = rtrim($insertQuery, ';');
469 /** @var \TYPO3\CMS\Core\Database\DatabaseConnection $database */
470 $database = $GLOBALS['TYPO3_DB'];
471 $database->admin_query($insertQuery);
472 }
473 }
474 }
475
476 /**
477 * Drop test database.
478 *
479 * @throws \TYPO3\CMS\Core\Tests\Exception
480 * @return void
481 */
482 protected function tearDownTestDatabase() {
483 /** @var \TYPO3\CMS\Core\Database\DatabaseConnection $database */
484 $database = $GLOBALS['TYPO3_DB'];
485 $result = $database->admin_query('DROP DATABASE `' . $this->databaseName . '`');
486 if (!$result) {
487 throw new Exception(
488 'Dropping test database ' . $this->databaseName . ' failed',
489 1376583188
490 );
491 }
492 }
493
494 /**
495 * Removes instance directories and files
496 *
497 * @throws \TYPO3\CMS\Core\Tests\Exception
498 * @return void
499 */
500 protected function removeInstance() {
501 $success = $this->rmdir($this->instancePath, TRUE);
502 if (!$success) {
503 throw new Exception(
504 'Can not remove folder: ' . $this->instancePath,
505 1376657210
506 );
507 }
508 }
509
510 /**
511 * COPIED FROM GeneralUtility
512 *
513 * Wrapper function for rmdir, allowing recursive deletion of folders and files
514 *
515 * @param string $path Absolute path to folder, see PHP rmdir() function. Removes trailing slash internally.
516 * @param boolean $removeNonEmpty Allow deletion of non-empty directories
517 * @return boolean TRUE if @rmdir went well!
518 */
519 protected function rmdir($path, $removeNonEmpty = FALSE) {
520 $OK = FALSE;
521 // Remove trailing slash
522 $path = preg_replace('|/$|', '', $path);
523 if (file_exists($path)) {
524 $OK = TRUE;
525 if (!is_link($path) && is_dir($path)) {
526 if ($removeNonEmpty == TRUE && ($handle = opendir($path))) {
527 while ($OK && FALSE !== ($file = readdir($handle))) {
528 if ($file == '.' || $file == '..') {
529 continue;
530 }
531 $OK = $this->rmdir($path . '/' . $file, $removeNonEmpty);
532 }
533 closedir($handle);
534 }
535 if ($OK) {
536 $OK = @rmdir($path);
537 }
538 } else {
539 // If $path is a file, simply remove it
540 $OK = unlink($path);
541 }
542 clearstatcache();
543 } elseif (is_link($path)) {
544 $OK = unlink($path);
545 clearstatcache();
546 }
547 return $OK;
548 }
549
550 /**
551 * COPIED FROM GeneralUtility
552 *
553 * Writes $content to the file $file
554 *
555 * @param string $file Filepath to write to
556 * @param string $content Content to write
557 * @return boolean TRUE if the file was successfully opened and written to.
558 */
559 protected function writeFile($file, $content) {
560 if ($fd = fopen($file, 'wb')) {
561 $res = fwrite($fd, $content);
562 fclose($fd);
563 if ($res === FALSE) {
564 return FALSE;
565 }
566 return TRUE;
567 }
568 return FALSE;
569 }
570
571 /**
572 * COPIED FROM ArrayUtility
573 *
574 * Exports an array as string.
575 * Similar to var_export(), but representation follows the TYPO3 core CGL.
576 *
577 * See unit tests for detailed examples
578 *
579 * @param array $array Array to export
580 * @param integer $level Internal level used for recursion, do *not* set from outside!
581 * @return string String representation of array
582 * @throws \RuntimeException
583 */
584 protected function arrayExport(array $array = array(), $level = 0) {
585 $lines = 'array(' . chr(10);
586 $level++;
587 $writeKeyIndex = FALSE;
588 $expectedKeyIndex = 0;
589 foreach ($array as $key => $value) {
590 if ($key === $expectedKeyIndex) {
591 $expectedKeyIndex++;
592 } else {
593 // Found a non integer or non consecutive key, so we can break here
594 $writeKeyIndex = TRUE;
595 break;
596 }
597 }
598 foreach ($array as $key => $value) {
599 // Indention
600 $lines .= str_repeat(chr(9), $level);
601 if ($writeKeyIndex) {
602 // Numeric / string keys
603 $lines .= is_int($key) ? $key . ' => ' : '\'' . $key . '\' => ';
604 }
605 if (is_array($value)) {
606 if (count($value) > 0) {
607 $lines .= $this->arrayExport($value, $level);
608 } else {
609 $lines .= 'array(),' . chr(10);
610 }
611 } elseif (is_int($value) || is_float($value)) {
612 $lines .= $value . ',' . chr(10);
613 } elseif (is_null($value)) {
614 $lines .= 'NULL' . ',' . chr(10);
615 } elseif (is_bool($value)) {
616 $lines .= $value ? 'TRUE' : 'FALSE';
617 $lines .= ',' . chr(10);
618 } elseif (is_string($value)) {
619 // Quote \ to \\
620 $stringContent = str_replace('\\', '\\\\', $value);
621 // Quote ' to \'
622 $stringContent = str_replace('\'', '\\\'', $stringContent);
623 $lines .= '\'' . $stringContent . '\'' . ',' . chr(10);
624 } else {
625 throw new \RuntimeException('Objects are not supported', 1342294986);
626 }
627 }
628 $lines .= str_repeat(chr(9), ($level - 1)) . ')' . ($level - 1 == 0 ? '' : ',' . chr(10));
629 return $lines;
630 }
631
632 /**
633 * COPIED FROM ArrayUtility
634 *
635 * Merges two arrays recursively and "binary safe" (integer keys are
636 * overridden as well), overruling similar values in the original array
637 * with the values of the overrule array.
638 * In case of identical keys, ie. keeping the values of the overrule array.
639 *
640 * This method takes the original array by reference for speed optimization with large arrays
641 *
642 * The differences to the existing PHP function array_merge_recursive() are:
643 * * Keys of the original array can be unset via the overrule array. ($enableUnsetFeature)
644 * * Much more control over what is actually merged. ($addKeys, $includeEmptyValues)
645 * * Elements or the original array get overwritten if the same key is present in the overrule array.
646 *
647 * @param array $original Original array. It will be *modified* by this method and contains the result afterwards!
648 * @param array $overrule Overrule array, overruling the original array
649 * @param boolean $addKeys If set to FALSE, keys that are NOT found in $original will not be set. Thus only existing value can/will be overruled from overrule array.
650 * @param boolean $includeEmptyValues If set, values from $overrule will overrule if they are empty or zero.
651 * @param boolean $enableUnsetFeature If set, special values "__UNSET" can be used in the overrule array in order to unset array keys in the original array.
652 * @return void
653 */
654 protected function mergeRecursiveWithOverrule(array &$original, array $overrule, $addKeys = TRUE, $includeEmptyValues = TRUE, $enableUnsetFeature = TRUE) {
655 foreach (array_keys($overrule) as $key) {
656 if ($enableUnsetFeature && $overrule[$key] === '__UNSET') {
657 unset($original[$key]);
658 continue;
659 }
660 if (isset($original[$key]) && is_array($original[$key])) {
661 if (is_array($overrule[$key])) {
662 self::mergeRecursiveWithOverrule($original[$key], $overrule[$key], $addKeys, $includeEmptyValues, $enableUnsetFeature);
663 }
664 } elseif (
665 ($addKeys || isset($original[$key])) &&
666 ($includeEmptyValues || $overrule[$key])
667 ) {
668 $original[$key] = $overrule[$key];
669 }
670 }
671 // This line is kept for backward compatibility reasons.
672 reset($original);
673 }
674 }