class Unpacker

Handles the details of unpacking a specific recipe.

@internal

Hierarchy

  • class \Drupal\Composer\Plugin\RecipeUnpack\Unpacker

Expanded class hierarchy of Unpacker

File

composer/Plugin/RecipeUnpack/Unpacker.php, line 16

Namespace

Drupal\Composer\Plugin\RecipeUnpack
View source
final readonly class Unpacker {
    
    /**
     * The version parser.
     */
    private VersionParser $versionParser;
    public function __construct(PackageInterface $package, Composer $composer, RootComposer $rootComposer, UnpackCollection $unpackCollection, UnpackOptions $unpackOptions, IOInterface $io) {
        $this->versionParser = new VersionParser();
    }
    
    /**
     * Unpacks the package's dependencies to the root composer.json and lock file.
     */
    public function unpackDependencies() : void {
        $this->updateComposerJsonPackages();
        $this->updateComposerLockContent();
        $this->unpackCollection
            ->markPackageUnpacked($this->package);
    }
    
    /**
     * Processes dependencies of the package that is being unpacked.
     *
     * If the dependency is a recipe and should be unpacked, we add it into the
     * package queue so that it will be unpacked as well. If the dependency is not
     * a recipe, or an ignored recipe, the package link will be yielded.
     *
     * @param array<string, \Composer\Package\Link> $package_dependency_links
     *   The package dependencies to process.
     *
     * @return iterable<\Composer\Package\Link>
     *   The package dependencies to add to composer.json.
     */
    private function processPackageDependencies(array $package_dependency_links) : iterable {
        foreach ($package_dependency_links as $link) {
            if ($link->getTarget() === $this->package
                ->getName()) {
                // This dependency is the same as the current package, so let's skip it.
                continue;
            }
            $package = $this->getPackageFromLinkTarget($link);
            // If we can't find the package in the local repository that's because it
            // has already been removed therefore skip it.
            if ($package === NULL) {
                continue;
            }
            if ($package->getType() === Plugin::RECIPE_PACKAGE_TYPE) {
                if ($this->unpackCollection
                    ->isUnpacked($package)) {
                    // This dependency is already unpacked.
                    continue;
                }
                if (!$this->unpackOptions
                    ->isIgnored($package)) {
                    // This recipe should be unpacked as well.
                    $this->unpackCollection
                        ->add($package);
                    continue;
                }
                else {
                    // This recipe should not be unpacked. But it might need to be added
                    // to the root composer.json
                    $this->io
                        ->write(sprintf('<info>%s</info> not unpacked because it is ignored.', $package->getName()), verbosity: IOInterface::VERBOSE);
                }
            }
            (yield $link);
        }
    }
    
    /**
     * Updates the composer.json content with the package being unpacked.
     *
     * This method will add all the package dependencies to the root composer.json
     * content and also remove the package itself from the root composer.json.
     *
     * @throws \RuntimeException
     *   If the composer.json could not be updated.
     */
    private function updateComposerJsonPackages() : void {
        $composer_manipulator = $this->rootComposer
            ->getComposerManipulator();
        $composer_config = $this->composer
            ->getConfig();
        $sort_packages = $composer_config->get('sort-packages');
        $root_package = $this->composer
            ->getPackage();
        $root_requires = $root_package->getRequires();
        $root_dev_requires = $root_package->getDevRequires();
        foreach ($this->processPackageDependencies($this->package
            ->getRequires()) as $package_dependency) {
            $dependency_name = $package_dependency->getTarget();
            $recipe_constraint_string = $package_dependency->getPrettyConstraint();
            if (isset($root_requires[$dependency_name])) {
                $recipe_constraint_string = SemVer::minimizeConstraints($this->versionParser, $recipe_constraint_string, $root_requires[$dependency_name]->getPrettyConstraint());
                if ($recipe_constraint_string === $root_requires[$dependency_name]) {
                    // This dependency is already in the required section with the
                    // correct constraint.
                    continue;
                }
            }
            elseif (isset($root_dev_requires[$dependency_name])) {
                $recipe_constraint_string = SemVer::minimizeConstraints($this->versionParser, $recipe_constraint_string, $root_dev_requires[$dependency_name]->getPrettyConstraint());
                // This dependency is already in the require-dev section. We will
                // move it to the require section.
                $composer_manipulator->removeSubNode('require-dev', $dependency_name);
            }
            // Add the dependency to the required section. If it cannot be added, then
            // throw an exception.
            if (!$composer_manipulator->addLink('require', $dependency_name, $recipe_constraint_string, $sort_packages)) {
                throw new \RuntimeException(sprintf('Unable to manipulate composer.json during the unpack of %s', $dependency_name));
            }
            $link = new Link($root_package->getName(), $dependency_name, $this->versionParser
                ->parseConstraints($recipe_constraint_string), Link::TYPE_REQUIRE, $recipe_constraint_string);
            $root_requires[$dependency_name] = $link;
            unset($root_dev_requires[$dependency_name]);
            $this->io
                ->write(sprintf('Adding <info>%s</info> (<comment>%s</comment>) to composer.json during the unpack of <info>%s</info>', $dependency_name, $recipe_constraint_string, $this->package
                ->getName()), verbosity: IOInterface::VERBOSE);
        }
        // Ensure the written packages are no longer in the dev package names.
        $local_repo = $this->composer
            ->getRepositoryManager()
            ->getLocalRepository();
        $local_repo->setDevPackageNames(array_diff($local_repo->getDevPackageNames(), array_keys($root_requires)));
        // Update the root package to reflect the changes.
        $root_package->setDevRequires($root_dev_requires);
        $root_package->setRequires($root_requires);
        $composer_manipulator->removeSubNode(UnpackManager::isDevRequirement($this->package) ? 'require-dev' : 'require', $this->package
            ->getName());
        $this->io
            ->write(sprintf('Removing <info>%s</info> from composer.json', $this->package
            ->getName()), verbosity: IOInterface::VERBOSE);
        $composer_manipulator->removeMainKeyIfEmpty('require-dev');
    }
    
    /**
     * Updates the composer.lock content and keeps the local repo in sync.
     *
     * This method will remove the package itself from the composer.lock content
     * in the root composer.
     */
    private function updateComposerLockContent() : void {
        $composer_locker_content = $this->rootComposer
            ->getComposerLockedContent();
        $root_package = $this->composer
            ->getPackage();
        $root_requires = $root_package->getRequires();
        $root_dev_requires = $root_package->getDevRequires();
        $local_repo = $this->composer
            ->getRepositoryManager()
            ->getLocalRepository();
        if (isset($root_requires[$this->package
            ->getName()])) {
            unset($root_requires[$this->package
                ->getName()]);
            $root_package->setRequires($root_requires);
        }
        foreach ($composer_locker_content['packages'] as $key => $lock_data) {
            // Find the package being unpacked in the composer.lock content and
            // remove it.
            if ($lock_data['name'] === $this->package
                ->getName()) {
                $this->rootComposer
                    ->removeFromComposerLock('packages', $key);
                // If the package is in require-dev we need to move the lock data.
                if (isset($root_dev_requires[$lock_data['name']])) {
                    $this->rootComposer
                        ->addToComposerLock('packages-dev', $lock_data);
                    $dev_package_names = $local_repo->getDevPackageNames();
                    $dev_package_names[] = $lock_data['name'];
                    $local_repo->setDevPackageNames($dev_package_names);
                    return;
                }
                break;
            }
        }
        $local_repo->setDevPackageNames(array_diff($local_repo->getDevPackageNames(), [
            $this->package
                ->getName(),
        ]));
        $local_repo->removePackage($this->package);
        if (isset($root_dev_requires[$this->package
            ->getName()])) {
            unset($root_dev_requires[$this->package
                ->getName()]);
            $root_package->setDevRequires($root_dev_requires);
        }
    }
    
    /**
     * Gets the package object from a link's target.
     *
     * @param \Composer\Package\Link $dependency
     *   The link dependency.
     *
     * @return \Composer\Package\PackageInterface|null
     *   The package object.
     */
    private function getPackageFromLinkTarget(Link $dependency) : ?PackageInterface {
        return $this->composer
            ->getRepositoryManager()
            ->getLocalRepository()
            ->findPackage($dependency->getTarget(), $dependency->getConstraint());
    }

}

Members

Title Sort descending Modifiers Object type Summary
Unpacker::$versionParser private property The version parser.
Unpacker::getPackageFromLinkTarget private function Gets the package object from a link&#039;s target.
Unpacker::processPackageDependencies private function Processes dependencies of the package that is being unpacked.
Unpacker::unpackDependencies public function Unpacks the package&#039;s dependencies to the root composer.json and lock file.
Unpacker::updateComposerJsonPackages private function Updates the composer.json content with the package being unpacked.
Unpacker::updateComposerLockContent private function Updates the composer.lock content and keeps the local repo in sync.
Unpacker::__construct public function

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.