vaimo/composer-patches

View on GitHub
src/Repository/PatchesApplier.php

Summary

Maintainability
B
6 hrs
Test Coverage
<?php
/**
 * Copyright © Vaimo Group. All rights reserved.
 * See LICENSE_VAIMO.txt for license details.
 */
namespace Vaimo\ComposerPatches\Repository;

use Composer\Repository\WritableRepositoryInterface as Repository;
use Composer\Package\PackageInterface as Package;

use Vaimo\ComposerPatches\Patch\Definition as Patch;

/**
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 * @SuppressWarnings(PHPMD.TooManyFields)
 */
class PatchesApplier
{
    /**
     * @var \Composer\Composer
     */
    private $composer;

    /**
     * @var \Vaimo\ComposerPatches\Package\Collector
     */
    private $packageCollector;

    /**
     * @var \Vaimo\ComposerPatches\Managers\RepositoryManager
     */
    private $repositoryManager;

    /**
     * @var \Vaimo\ComposerPatches\Package\PatchApplier
     */
    private $packagePatchApplier;

    /**
     * @var \Vaimo\ComposerPatches\Repository\PatchesApplier\QueueGenerator
     */
    private $queueGenerator;

    /**
     * @var \Vaimo\ComposerPatches\Managers\PatcherStateManager
     */
    private $patcherStateManager;

    /**
     * @var \Vaimo\ComposerPatches\Repository\StateGenerator
     */
    private $repoStateGenerator;

    /**
     * @var \Vaimo\ComposerPatches\Package\PatchApplier\InfoLogger
     */
    private $patchInfoLogger;

    /**
     * @var \Vaimo\ComposerPatches\Strategies\OutputStrategy
     */
    private $outputStrategy;

    /**
     * @var \Vaimo\ComposerPatches\Logger
     */
    private $logger;

    /**
     * @var \Vaimo\ComposerPatches\Patch\DefinitionList\Analyser
     */
    private $patchListAnalyser;

    /**
     * @var \Vaimo\ComposerPatches\Patch\DefinitionList\Transformer
     */
    private $patchListTransformer;

    /**
     * @var \Vaimo\ComposerPatches\Package\PatchApplier\StatusConfig
     */
    private $statusConfig;

    /**
     * @var \Vaimo\ComposerPatches\Utils\PackageUtils
     */
    private $packageUtils;

    /**
     * @var \Vaimo\ComposerPatches\Utils\DataUtils
     */
    private $dataUtils;

    /**
     * @var \Vaimo\ComposerPatches\Console\OutputGenerator
     */
    private $outputGenerator;

    /**
     * @var \Vaimo\ComposerPatches\Compatibility\Executor
     */
    private $compExecutor;

    public function __construct(
        \Composer\Composer $composer,
        \Vaimo\ComposerPatches\Package\Collector $packageCollector,
        \Vaimo\ComposerPatches\Managers\RepositoryManager $repositoryManager,
        \Vaimo\ComposerPatches\Package\PatchApplier $patchApplier,
        \Vaimo\ComposerPatches\Repository\PatchesApplier\QueueGenerator $queueGenerator,
        \Vaimo\ComposerPatches\Managers\PatcherStateManager $patcherStateManager,
        \Vaimo\ComposerPatches\Package\PatchApplier\InfoLogger $patchInfoLogger,
        \Vaimo\ComposerPatches\Strategies\OutputStrategy $outputStrategy,
        \Vaimo\ComposerPatches\Logger $logger
    ) {
        $this->composer = $composer;
        $this->packageCollector = $packageCollector;
        $this->repositoryManager = $repositoryManager;
        $this->packagePatchApplier = $patchApplier;
        $this->queueGenerator = $queueGenerator;
        $this->patcherStateManager = $patcherStateManager;
        $this->patchInfoLogger = $patchInfoLogger;
        $this->outputStrategy = $outputStrategy;
        $this->logger = $logger;

        $this->repoStateGenerator = new \Vaimo\ComposerPatches\Repository\StateGenerator(
            $this->packageCollector
        );

        $this->outputGenerator = new \Vaimo\ComposerPatches\Console\OutputGenerator($logger);

        $this->patchListAnalyser = new \Vaimo\ComposerPatches\Patch\DefinitionList\Analyser();
        $this->patchListTransformer = new \Vaimo\ComposerPatches\Patch\DefinitionList\Transformer();
        $this->statusConfig = new \Vaimo\ComposerPatches\Package\PatchApplier\StatusConfig();
        $this->packageUtils = new \Vaimo\ComposerPatches\Utils\PackageUtils();
        $this->dataUtils = new \Vaimo\ComposerPatches\Utils\DataUtils();
        $this->compExecutor = new \Vaimo\ComposerPatches\Compatibility\Executor();
    }

    /**
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
     * @SuppressWarnings(PHPMD.NPathComplexity)
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
     *
     * @param Repository $repository
     * @param array $patches
     * @return bool
     * @throws \Vaimo\ComposerPatches\Exceptions\PackageNotFound
     * @throws \Vaimo\ComposerPatches\Exceptions\PackageResetException
     * @throws \Vaimo\ComposerPatches\Exceptions\PatchFailureException
     */
    public function apply(Repository $repository, array $patches)
    {
        $packages = $this->packageCollector->collect($repository);
        $packagesUpdated = false;
        $repositoryState = $this->repoStateGenerator->generate($repository);
        $applyQueue = $this->queueGenerator->generateApplyQueue($patches, $repositoryState);
        $removeQueue = $this->queueGenerator->generateRemovalQueue($applyQueue, $repositoryState);
        $resetQueue = $this->queueGenerator->generateResetQueue($applyQueue);
        $applyQueue = array_map('array_filter', $applyQueue);
        $patchQueueFootprints = $this->patchListTransformer->createSimplifiedList($applyQueue);

        $labels = array_diff_key(
            $this->statusConfig->getLabels(),
            array(Patch::STATUS_UNKNOWN => true)
        );

        $applyQueue = $this->updateStatusLabels($applyQueue, $labels);
        $removeQueue = $this->updateStatusLabels($removeQueue, $labels);

        foreach ($packages as $packageName => $package) {
            $hasPatches = !empty($applyQueue[$packageName]);

            $patchTargets = $hasPatches ?
                $this->patchListAnalyser->getAllTargets(array($applyQueue[$packageName]))
                : array($packageName);

            $itemsToReset = $this->dataUtils->extractItems($resetQueue, $patchTargets);

            $resetResult = array();
            $resets = array();

            foreach ($itemsToReset as $targetName) {
                $resetTarget = $packages[$targetName];

                $resetPatches = $this->packageUtils->resetAppliedPatches($resetTarget);

                $resetResult[$targetName] = is_array($resetPatches)
                    ? $resetPatches
                    : array();

                if (!$hasPatches && $resetPatches && !isset($patchQueueFootprints[$targetName])) {
                    $this->logger->writeRaw(
                        'Resetting patches for <info>%s</info> (%s)',
                        array($targetName, count($resetResult[$targetName]))
                    );
                }

                $resets[] = $this->repositoryManager->resetPackage($repository, $resetTarget);
            }

            $this->compExecutor->waitForCompletion($this->composer, $resets);
            $packagesUpdated = $packagesUpdated || (bool)array_filter($resetResult);

            if (!$hasPatches) {
                continue;
            }

            $changedTargets = $this->resolveChangedTargets($packages, $patchTargets, $patchQueueFootprints);

            if (empty($changedTargets)) {
                continue;
            }

            $queuedPatches = array_filter(
                $applyQueue[$packageName],
                function ($data) use ($changedTargets) {
                    return array_intersect($data[Patch::TARGETS], $changedTargets);
                }
            );

            $this->updatePackage(
                $package,
                $repository,
                $queuedPatches,
                $this->dataUtils->extractValue($removeQueue, $packageName, array())
            );

            $packagesUpdated = true;
        }

        return $packagesUpdated;
    }

    private function updatePackage(Package $package, Repository $repository, array $additions, array $removals)
    {
        $muteDepth = null;

        $packageName = $package->getName();

        if (!$this->shouldAllowOutput($additions, $removals)) {
            $muteDepth = $this->logger->mute();
        }

        $this->logger->writeRaw(
            'Applying patches for <info>%s</info> (%s)',
            array($packageName, count($additions))
        );

        $this->processQueues($package, $repository, $additions, $removals);

        $this->logger->writeNewLine();

        if ($muteDepth !== null) {
            $this->logger->unMute($muteDepth);
        }
    }

    private function updateStatusLabels(array $queue, array $labels)
    {
        foreach ($queue as $target => $group) {
            foreach ($group as $path => $item) {
                $status = isset($item[Patch::STATUS])
                    ? $item[Patch::STATUS]
                    : Patch::STATUS_UNKNOWN;

                if (!isset($labels[$status])) {
                    continue;
                }

                $queue[$target][$path][Patch::STATUS_LABEL] = $labels[$status];
            }
        }

        return $queue;
    }

    private function processQueues(Package $package, Repository $repository, $additions, $removals)
    {
        try {
            if ($removals) {
                $processIndentation = $this->logger->push('~');

                foreach ($removals as $item) {
                    $this->patchInfoLogger->outputPatchInfo($item);
                }

                $this->logger->reset($processIndentation);
            }

            $this->processPatchesForPackage($repository, $package, $additions);
        } catch (\Exception $exception) {
            $this->logger->unMute();

            throw $exception;
        }
    }

    private function resolveChangedTargets(array $packages, array $patchTargets, array $patchFootprints)
    {
        $changesMap = array();

        foreach ($patchTargets as $targetName) {
            $targetQueue = array();

            if (isset($patchFootprints[$targetName])) {
                $targetQueue = $patchFootprints[$targetName];
            }

            if (!isset($packages[$targetName])) {
                throw new \Vaimo\ComposerPatches\Exceptions\PackageNotFound(
                    sprintf(
                        'Unknown target "%s" found when checking patch changes for: %s',
                        $targetName,
                        implode(',', array_keys($targetQueue))
                    )
                );
            }

            $changesMap[$targetName] = $this->packageUtils->hasPatchChanges(
                $packages[$targetName],
                $targetQueue
            );
        }

        return array_keys(array_filter($changesMap));
    }

    private function processPatchesForPackage(Repository $repository, Package $package, array $patchesQueue)
    {
        $processIndentation = $this->logger->push('~');

        try {
            $appliedPatches = $this->packagePatchApplier->applyPatches($package, $patchesQueue);

            $this->patcherStateManager->registerAppliedPatches($repository, $appliedPatches);

            $this->logger->reset($processIndentation);
        } catch (\Vaimo\ComposerPatches\Exceptions\PatchFailureException $exception) {
            $this->logger->push();

            $this->logger->write('error', $exception->getMessage());

            $previousError = $exception->getPrevious();

            if ($previousError) {
                $this->outputGenerator->generateForException($previousError);
            }

            $failedPath = $exception->getFailedPatchPath();

            $paths = array_keys($patchesQueue);

            $failureIndex = array_search($failedPath, $paths, true);

            $appliedPatches = array();

            if (is_int($failureIndex)) {
                $appliedPaths = array_slice($paths, 0, $failureIndex);

                $appliedPatches = array_intersect_key(
                    $patchesQueue,
                    array_flip($appliedPaths)
                );
            }

            $this->patcherStateManager->registerAppliedPatches($repository, $appliedPatches);

            throw $exception;
        }
    }

    private function shouldAllowOutput(array $patches, array $removals)
    {
        return $this->outputStrategy->shouldAllowForPatches($patches)
            || $this->outputStrategy->shouldAllowForPatches($removals);
    }
}