diff -u b/core/lib/Drupal/Component/Scaffold/Handler.php b/core/lib/Drupal/Component/Scaffold/Handler.php --- b/core/lib/Drupal/Component/Scaffold/Handler.php +++ b/core/lib/Drupal/Component/Scaffold/Handler.php @@ -9,9 +9,9 @@ use Composer\Package\PackageInterface; use Composer\Plugin\CommandEvent; use Composer\Util\Filesystem; -use Drupal\Component\Scaffold\Operations\OperationCollection; use Drupal\Component\Scaffold\Operations\OperationData; use Drupal\Component\Scaffold\Operations\OperationFactory; +use Drupal\Component\Scaffold\Operations\ScaffoldFileCollection; /** * Core class of the plugin. @@ -155,11 +155,15 @@ $file_mappings = $this->getFileMappingsFromPackages($allowed_packages); // Analyze the list of file mappings, and determine which take priority. - $scaffold_collection = new OperationCollection($this->io); $location_replacements = $this->manageOptions->getLocationReplacements(); - - // Write the collected scaffold files to the designated location on disk. - $scaffold_results = $scaffold_collection->process($file_mappings, $location_replacements, $this->manageOptions->getOptions()); + $scaffold_files = ScaffoldFileCollection::create($file_mappings, $location_replacements); + $scaffold_results = []; + // Process all of the files that were collated one package at a time. + iterator_apply( + $scaffold_files, + [ScaffoldFileCollection::class, 'process'], + [$scaffold_files, $this->io, $this->manageOptions->getOptions(), &$scaffold_results] + ); // Generate an autoload file in the document root that includes the // autoload.php file in the vendor directory, wherever that is. Drupal reverted: --- b/core/lib/Drupal/Component/Scaffold/Operations/OperationCollection.php +++ /dev/null @@ -1,79 +0,0 @@ -io = $io; - } - - /** - * Process all of the scaffold files listed in the provided file mappings. - * - * @param array $file_mappings - * An multidimensional array of file mappings, as returned by - * self::getFileMappingsFromPackages(). - * @param \Drupal\Component\Scaffold\Interpolator $location_replacements - * An object with the location mappings (e.g. [web-root]). - * @param \Drupal\Component\Scaffold\ScaffoldOptions $options - * Configuration options from the top-level composer.json file. - * - * @return \Drupal\Component\Scaffold\Operations\ScaffoldResult[] - * Associative array keyed by destination path and values as the scaffold - * result for each scaffolded file. - */ - public function process(array $file_mappings, Interpolator $location_replacements, ScaffoldOptions $options) { - $collatedScaffoldFiles = new ScaffoldFileCollator($this->io); - $collatedScaffoldFiles->collateScaffoldFiles($file_mappings, $location_replacements); - return $this->processScaffoldFiles($collatedScaffoldFiles, $options); - } - - /** - * Scaffolds the files in our scaffold collection, package-by-package. - * - * @param ScaffoldFileCollator $collatedScaffoldFiles - * Collection of collated scaffold files. - * @param \Drupal\Component\Scaffold\ScaffoldOptions $options - * Configuration options from the top-level composer.json file. - * - * @return \Drupal\Component\Scaffold\Operations\ScaffoldResult[] - * Associative array keyed by destination path and values as the scaffold - * result for each scaffolded file. - */ - protected function processScaffoldFiles(ScaffoldFileCollator $collatedScaffoldFiles, ScaffoldOptions $options) { - $result = []; - // Process all of the files that were collated one package at a time. - foreach ($collatedScaffoldFiles->resolvedScaffoldFiles() as $package_name => $package_scaffold_files) { - $this->io->write("Scaffolding files for {$package_name}:"); - foreach ($package_scaffold_files as $dest_rel_path => $scaffold_file) { - if (!$collatedScaffoldFiles->overridden($scaffold_file)) { - $result[$dest_rel_path] = $scaffold_file->process($this->io, $options); - } - } - } - return $result; - } - -} reverted: --- b/core/lib/Drupal/Component/Scaffold/Operations/ScaffoldFileCollator.php +++ /dev/null @@ -1,139 +0,0 @@ -io = $io; - } - - /** - * Organizes provided file mappings by destination and package. - * - * @param array $file_mappings - * An multidimensional array of file mappings, as returned by - * self::getFileMappingsFromPackages(). - * @param \Drupal\Component\Scaffold\Interpolator $location_replacements - * An object with the location mappings (e.g. [web-root]). - */ - public function collateScaffoldFiles(array $file_mappings, Interpolator $location_replacements) { - foreach ($file_mappings as $package_name => $package_file_mappings) { - foreach ($package_file_mappings as $destination_rel_path => $op) { - $destination = ScaffoldFilePath::destinationPath($package_name, $destination_rel_path, $location_replacements); - // If there was already a scaffolding operation happening at this path, - // and the new operation is Conjoinable, then use a ConjunctionOp to - // join together both operations. This will cause both operations to - // run, one after the other. At the moment, only AppendOp is - // conjoinable; all other operations simply replace anything at the same - // path. - if (isset($this->listOfScaffoldFiles[$destination_rel_path]) && $op instanceof ConjoinableInterface) { - $op = new ConjunctionOp($this->listOfScaffoldFiles[$destination_rel_path]->op(), $op); - } - - $scaffold_file = new ScaffoldFileInfo($destination, $op); - $this->listOfScaffoldFiles[$destination_rel_path] = $scaffold_file; - $this->resolvedFileMappings[$package_name][$destination_rel_path] = $scaffold_file; - } - } - } - - /** - * Provides access to the resolved scaffold files. - * - * @return \Drupal\Component\Scaffold\ScaffoldFileInfo[][] - * Collation of all scaffold files. - */ - public function resolvedScaffoldFiles() { - return $this->resolvedFileMappings; - } - - /** - * Determines whether a given location has been overridden. - * - * As a side effect, reports a status message noting that the file was - * skipped. - * - * @param \Drupal\Component\Scaffold\ScaffoldFileInfo $scaffold_file - * Scaffold file location to test overridden status. - * - * @return bool - * TRUE if specified location is overridden by another package. - */ - public function overridden(ScaffoldFileInfo $scaffold_file) { - $overriding_package = $this->findProvidingPackage($scaffold_file); - $overridden = $scaffold_file->overridden($overriding_package); - if ($overridden) { - $this->io->write($scaffold_file->interpolate(" - Skip [dest-rel-path]: overridden in {$overriding_package}")); - } - return $overridden; - } - - /** - * Finds the package name that provides the scaffold file. - * - * This will usually be $scaffold_file->packageName(), unless there are - * multiple scaffold files with the same destination path. In the case - * that there are multiple scaffold files, the name of the last package - * that provided a scaffold file at that path will be returned. - * - * @param \Drupal\Component\Scaffold\ScaffoldFileInfo $scaffold_file - * The scaffold file to use to find a providing package name. - * - * @return string - * The name of the package that provided the scaffold file information. - */ - protected function findProvidingPackage(ScaffoldFileInfo $scaffold_file) { - // The scaffold file should always be in our list, but we will check - // just to be sure that it really is. - $dest_rel_path = $scaffold_file->destination()->relativePath(); - if (!array_key_exists($dest_rel_path, $this->listOfScaffoldFiles)) { - $msg = $scaffold_file->interpolate(" - Scaffold file [dest-rel-path] not found in list of all scaffold files."); - throw new \RuntimeException($msg); - } - return $this->listOfScaffoldFiles[$dest_rel_path]->packageName(); - } - -} diff -u b/core/lib/Drupal/Component/Scaffold/Operations/SkipOp.php b/core/lib/Drupal/Component/Scaffold/Operations/SkipOp.php --- b/core/lib/Drupal/Component/Scaffold/Operations/SkipOp.php +++ b/core/lib/Drupal/Component/Scaffold/Operations/SkipOp.php @@ -17,11 +17,28 @@ const ID = 'skip'; /** + * The message to output while processing. + * + * @var string + */ + protected $message; + + /** + * SkipOp constructor. + * + * @param string $message + * (optional) A custom message to output while skipping. + */ + public function __construct($message = " - Skip [dest-rel-path]: disabled") { + $this->message = $message; + } + + /** * {@inheritdoc} */ public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) { $interpolator = $destination->getInterpolator(); - $io->write($interpolator->interpolate(" - Skip [dest-rel-path]: disabled")); + $io->write($interpolator->interpolate($this->message)); return new ScaffoldResult($destination, FALSE); } reverted: --- b/core/tests/Drupal/Tests/Component/Scaffold/Integration/ScaffoldFileCollatorTest.php +++ /dev/null @@ -1,131 +0,0 @@ -getLocationReplacements(); - $scaffold_file_fixtures = [ - 'fixtures/drupal-assets-fixture' => [ - '[web-root]/index.php' => $fixtures->replaceOp('drupal-assets-fixture', 'index.php'), - '[web-root]/.htaccess' => $fixtures->replaceOp('drupal-assets-fixture', '.htaccess'), - '[web-root]/robots.txt' => $fixtures->replaceOp('drupal-assets-fixture', 'robots.txt'), - '[web-root]/sites/default/default.services.yml' => $fixtures->replaceOp('drupal-assets-fixture', 'default.services.yml'), - ], - 'fixtures/drupal-profile' => [ - '[web-root]/sites/default/default.services.yml' => $fixtures->replaceOp('drupal-profile', 'profile.default.services.yml'), - ], - 'fixtures/drupal-drupal' => [ - '[web-root]/.htaccess' => new SkipOp(), - '[web-root]/robots.txt' => $fixtures->appendOp('drupal-drupal-test-append', 'append-to-robots.txt'), - ], - ]; - $sut = new ScaffoldFileCollator($fixtures->io()); - // Test the system under test. - $sut->collateScaffoldFiles($scaffold_file_fixtures, $locationReplacements); - $scaffold_list = $this->accessProtected($sut, 'listOfScaffoldFiles'); - $resolved_file_mappings = $sut->resolvedScaffoldFiles(); - // Confirm that the keys of the output are the same as the keys of the - // input. - $this->assertEquals(array_keys($scaffold_file_fixtures), array_keys($resolved_file_mappings)); - // Also assert that we have the right ScaffoldFileInfo objects in the - // destination. - $this->assertResolvedToSameOp('fixtures/drupal-assets-fixture', '[web-root]/index.php', $scaffold_file_fixtures, $scaffold_list, $resolved_file_mappings); - $this->assertResolvedToSameOp('fixtures/drupal-profile', '[web-root]/sites/default/default.services.yml', $scaffold_file_fixtures, $scaffold_list, $resolved_file_mappings); - $this->assertResolvedToSameOp('fixtures/drupal-drupal', '[web-root]/robots.txt', $scaffold_file_fixtures, $scaffold_list, $resolved_file_mappings); - // Assert that the files below have been overridden. - $this->assertOverridden('fixtures/drupal-assets-fixture', '[web-root]/.htaccess', $scaffold_list, $resolved_file_mappings); - $this->assertOverridden('fixtures/drupal-assets-fixture', '[web-root]/robots.txt', $scaffold_list, $resolved_file_mappings); - } - - /** - * Checks to see if a given file was not overridden. - * - * The package name in the scaffold list for the provided destination should - * match the package name from the specified project. - * - * @param string $project - * The project to check. - * @param string $dest - * The destination to check. - * @param array $scaffold_file_fixtures - * The test file mappings keyed by project and destination. - * @param \Drupal\Component\Scaffold\ScaffoldFileInfo[] $scaffold_list - * The list of scaffolded files keyed by destination. - * @param array $resolved_file_mappings - * The list of resolved file mappings keyed by project and destination. - */ - protected function assertResolvedToSameOp($project, $dest, array $scaffold_file_fixtures, array $scaffold_list, array $resolved_file_mappings) { - $resolved_file_info = $resolved_file_mappings[$project][$dest]; - $this->assertEquals(get_class($resolved_file_info), ScaffoldFileInfo::class); - $resolved_scaffold_op = $resolved_file_info->op(); - // If this is an append op then it will be part of a conjunction op. - $expected = get_class($scaffold_file_fixtures[$project][$dest]); - if ($expected == AppendOp::class) { - $this->assertEquals(ConjunctionOp::class, get_class($resolved_scaffold_op)); - } - else { - $this->assertEquals($expected, get_class($resolved_scaffold_op)); - $this->assertEquals($scaffold_file_fixtures[$project][$dest], $resolved_scaffold_op); - } - $this->assertEquals($project, $scaffold_list[$dest]->packageName()); - } - - /** - * Checks if a given file was overridden. - * - * Assert that the file in the scaffold list at the specified destination - * comes from a different package than the one in the file info. - * - * @param string $project - * The project to check. - * @param string $dest - * The destination to check. - * @param \Drupal\Component\Scaffold\ScaffoldFileInfo[] $scaffold_list - * The list of scaffolded files keyed by destination. - * @param array $resolved_file_mappings - * The list of resolved file mappings keyed by project and destination. - */ - protected function assertOverridden($project, $dest, array $scaffold_list, array $resolved_file_mappings) { - $resolved_file_info = $resolved_file_mappings[$project][$dest]; - $this->assertEquals(get_class($resolved_file_info), ScaffoldFileInfo::class); - $this->assertNotEquals($project, $scaffold_list[$dest]->packageName()); - } - - /** - * Uses reflection to access a protected field of an object. - * - * @param mixed $obj - * The object to inspect. - * @param string $prop - * The name of the property to access. - * - * @return mixed - * The value of the requested property. - */ - protected function accessProtected($obj, $prop) { - $reflection = new \ReflectionClass($obj); - $property = $reflection->getProperty($prop); - $property->setAccessible(TRUE); - return $property->getValue($obj); - } - -} only in patch2: unchanged: --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/Operations/ScaffoldFileCollection.php @@ -0,0 +1,103 @@ + $package_file_mappings) { + foreach ($package_file_mappings as $destination_rel_path => $op) { + $destination = ScaffoldFilePath::destinationPath($package_name, $destination_rel_path, $location_replacements); + // If there was already a scaffolding operation happening at this path, + // and the new operation is Conjoinable, then use a ConjunctionOp to + // join together both operations. This will cause both operations to + // run, one after the other. At the moment, only AppendOp is + // conjoinable; all other operations simply replace anything at the same + // path. + if (isset($scaffoldFiles[$destination_rel_path])) { + $previous_scaffold_file = $scaffoldFiles[$destination_rel_path]; + if ($op instanceof ConjoinableInterface) { + $op = new ConjunctionOp($previous_scaffold_file->op(), $op); + // Remove the previous file so we only do the operation once. + unset($scaffoldFilesByProject[$previous_scaffold_file->packageName()][$destination_rel_path]); + } + else { + $message = " - Skip [dest-rel-path]: overridden in {$package_name}"; + $scaffoldFilesByProject[$previous_scaffold_file->packageName()][$destination_rel_path] = new ScaffoldFileInfo($destination, new SkipOp($message)); + } + } + $scaffold_file = new ScaffoldFileInfo($destination, $op); + $scaffoldFiles[$destination_rel_path] = $scaffold_file; + $scaffoldFilesByProject[$package_name][$destination_rel_path] = $scaffold_file; + } + } + return new \RecursiveArrayIterator($scaffoldFilesByProject, \RecursiveArrayIterator::CHILD_ARRAYS_ONLY); + } + + /** + * Processes the iterator created by ScaffoldFileCollection::create(). + * + * @param \RecursiveIterator $iterator + * The iterator to process. + * @param \Composer\IO\IOInterface $io + * The Composer IO object. + * @param \Drupal\Component\Scaffold\ScaffoldOptions $scaffold_options + * The scaffold options. + * @param \Drupal\Component\Scaffold\Operations\ScaffoldResult[] $results + * The results array, passed by-reference and is populated by this method. + */ + public static function process(RecursiveIterator $iterator, IOInterface $io, ScaffoldOptions $scaffold_options, array &$results) { + while ($iterator->valid()) { + if ($iterator->hasChildren()) { + $io->write("Scaffolding files for {$iterator->key()}:"); + static::process($iterator->getChildren(), $io, $scaffold_options, $results); + } + else { + $scaffold_file = $iterator->current(); + $results[$scaffold_file->destination()->relativePath()] = $scaffold_file->process($io, $scaffold_options); + } + $iterator->next(); + } + } + +} only in patch2: unchanged: --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/Integration/ScaffoldFileCollectionTest.php @@ -0,0 +1,67 @@ +getLocationReplacements(); + $scaffold_file_fixtures = [ + 'fixtures/drupal-assets-fixture' => [ + '[web-root]/index.php' => $fixtures->replaceOp('drupal-assets-fixture', 'index.php'), + '[web-root]/.htaccess' => $fixtures->replaceOp('drupal-assets-fixture', '.htaccess'), + '[web-root]/robots.txt' => $fixtures->replaceOp('drupal-assets-fixture', 'robots.txt'), + '[web-root]/sites/default/default.services.yml' => $fixtures->replaceOp('drupal-assets-fixture', 'default.services.yml'), + ], + 'fixtures/drupal-profile' => [ + '[web-root]/sites/default/default.services.yml' => $fixtures->replaceOp('drupal-profile', 'profile.default.services.yml'), + ], + 'fixtures/drupal-drupal' => [ + '[web-root]/.htaccess' => new SkipOp(), + '[web-root]/robots.txt' => $fixtures->appendOp('drupal-drupal-test-append', 'append-to-robots.txt'), + ], + ]; + $sut = ScaffoldFileCollection::create($scaffold_file_fixtures, $locationReplacements); + $resolved_file_mappings = iterator_to_array($sut); + // Confirm that the keys of the output are the same as the keys of the + // input. + $this->assertEquals(array_keys($scaffold_file_fixtures), array_keys($resolved_file_mappings)); + // Ensure that '[web-root]/robots.txt' has been removed as it is now part of + // a conjunction operation. + $this->assertEquals([ + '[web-root]/index.php', + '[web-root]/.htaccess', + '[web-root]/sites/default/default.services.yml', + ], array_keys($resolved_file_mappings['fixtures/drupal-assets-fixture'])); + + $this->assertEquals([ + '[web-root]/sites/default/default.services.yml', + ], array_keys($resolved_file_mappings['fixtures/drupal-profile'])); + + $this->assertEquals([ + '[web-root]/.htaccess', + '[web-root]/robots.txt', + ], array_keys($resolved_file_mappings['fixtures/drupal-drupal'])); + + // Test that .htaccess is skipped. + $this->assertInstanceOf(SkipOp::class, $resolved_file_mappings['fixtures/drupal-assets-fixture']['[web-root]/.htaccess']->op()); + // Test that the expected conjunction operation exists. + $this->assertInstanceOf(ConjunctionOp::class, $resolved_file_mappings['fixtures/drupal-drupal']['[web-root]/robots.txt']->op()); + } + +}