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