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(private PackageInterface $package, private Composer $composer, private RootComposer $rootComposer, private UnpackCollection $unpackCollection, private UnpackOptions $unpackOptions, private 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.