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