ChangeLogger.php

Namespace

Drupal\package_manager\EventSubscriber

File

core/modules/package_manager/src/EventSubscriber/ChangeLogger.php

View source
<?php

declare (strict_types=1);
namespace Drupal\package_manager\EventSubscriber;

use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\package_manager\Event\PostApplyEvent;
use Drupal\package_manager\Event\PostRequireEvent;
use Drupal\package_manager\ComposerInspector;
use Drupal\package_manager\Event\PreCreateEvent;
use Drupal\package_manager\PathLocator;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Event subscriber to log changes that happen during the stage life cycle.
 *
 * @internal
 *   This is an internal part of Package Manager and may be changed or removed
 *   at any time without warning. External code should not interact with this
 *   class.
 */
final class ChangeLogger implements EventSubscriberInterface, LoggerAwareInterface {
    use LoggerAwareTrait;
    use StringTranslationTrait;
    
    /**
     * The key to store the list of packages installed when the stage is created.
     *
     * @var string
     *
     * @see ::recordInstalledPackages()
     */
    private const INSTALLED_PACKAGES_KEY = 'package_manager_installed_packages';
    
    /**
     * The metadata key under which to store the requested package versions.
     *
     * @var string
     *
     * @see ::recordRequestedPackageVersions()
     */
    private const REQUESTED_PACKAGES_KEY = 'package_manager_requested_packages';
    public function __construct(ComposerInspector $composerInspector, PathLocator $pathLocator) {
    }
    
    /**
     * Records packages installed in the project root.
     *
     * We need to do this before the staging environment has been created, so that
     * we have a complete picture of which requested packages are merely being
     * updated, and which are being newly added. Once the staging environment has
     * been created, the installed packages won't change -- if they do, a
     * validation error will be raised.
     *
     * @param \Drupal\package_manager\Event\PreCreateEvent $event
     *   The event being handled.
     *
     * @see \Drupal\package_manager\Validator\LockFileValidator
     */
    public function recordInstalledPackages(PreCreateEvent $event) : void {
        $packages = $this->composerInspector
            ->getInstalledPackagesList($this->pathLocator
            ->getProjectRoot());
        $event->stage
            ->setMetadata(static::INSTALLED_PACKAGES_KEY, $packages);
    }
    
    /**
     * Records requested packages.
     *
     * @param \Drupal\package_manager\Event\PostRequireEvent $event
     *   The event object.
     */
    public function recordRequestedPackageVersions(PostRequireEvent $event) : void {
        // There could be multiple 'require' operations, so overlay the requested
        // packages from the current operation onto the requested packages from any
        // previous 'require' operation.
        $requested_packages = array_merge($event->stage
            ->getMetadata(static::REQUESTED_PACKAGES_KEY) ?? [], $event->getRuntimePackages(), $event->getDevPackages());
        $event->stage
            ->setMetadata(static::REQUESTED_PACKAGES_KEY, $requested_packages);
    }
    
    /**
     * Logs changes made by Package Manager.
     *
     * @param \Drupal\package_manager\Event\PostApplyEvent $event
     *   The event being handled.
     */
    public function logChanges(PostApplyEvent $event) : void {
        $installed_at_start = $event->stage
            ->getMetadata(static::INSTALLED_PACKAGES_KEY);
        $installed_post_apply = $this->composerInspector
            ->getInstalledPackagesList($this->pathLocator
            ->getProjectRoot());
        // Compare the packages which were installed when the stage was created
        // against the package versions that were requested over all the stage's
        // require operations, and create a log entry listing all of it.
        $requested_log = [];
        $requested_packages = $event->stage
            ->getMetadata(static::REQUESTED_PACKAGES_KEY) ?? [];
        // Sort the requested packages by name, to make it easier to review a large
        // change list.
        ksort($requested_packages, SORT_NATURAL);
        foreach ($requested_packages as $name => $constraint) {
            $installed_version = $installed_at_start[$name]?->version;
            if ($installed_version === NULL) {
                // For clarity, make the "any version" constraint human-readable.
                if ($constraint === '*') {
                    $constraint = $this->t('* (any version)');
                }
                $requested_log[] = $this->t('- Install @name @constraint', [
                    '@name' => $name,
                    '@constraint' => $constraint,
                ]);
            }
            else {
                $requested_log[] = $this->t('- Update @name from @installed_version to @constraint', [
                    '@name' => $name,
                    '@installed_version' => $installed_version,
                    '@constraint' => $constraint,
                ]);
            }
        }
        // It's possible that $requested_log will be empty: for example, a custom
        // stage that only does removals, or some other operation, and never
        // dispatches PostRequireEvent.
        if ($requested_log) {
            $message = $this->t("Requested changes:\n@change_list", [
                '@change_list' => implode("\n", array_map('strval', $requested_log)),
            ]);
            $this->logger?->info($message);
        }
        // Create a separate log entry listing everything that actually changed.
        $applied_log = [];
        $updated_packages = $installed_post_apply->getPackagesWithDifferentVersionsIn($installed_at_start);
        // Sort the packages by name to make it easier to review large change sets.
        $updated_packages->ksort(SORT_NATURAL);
        foreach ($updated_packages as $name => $package) {
            $applied_log[] = $this->t('- Updated @name from @installed_version to @updated_version', [
                '@name' => $name,
                '@installed_version' => $installed_at_start[$name]->version,
                '@updated_version' => $package->version,
            ]);
        }
        $added_packages = $installed_post_apply->getPackagesNotIn($installed_at_start);
        $added_packages->ksort(SORT_NATURAL);
        foreach ($added_packages as $name => $package) {
            $applied_log[] = $this->t('- Installed @name @version', [
                '@name' => $name,
                '@version' => $package->version,
            ]);
        }
        $removed_packages = $installed_at_start->getPackagesNotIn($installed_post_apply);
        $removed_packages->ksort(SORT_NATURAL);
        foreach ($installed_at_start->getPackagesNotIn($installed_post_apply) as $name => $package) {
            $applied_log[] = $this->t('- Uninstalled @name', [
                '@name' => $name,
            ]);
        }
        $message = $this->t("Applied changes:\n@change_list", [
            '@change_list' => implode("\n", array_map('strval', $applied_log)),
        ]);
        $this->logger?->info($message);
    }
    
    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents() : array {
        return [
            PreCreateEvent::class => [
                'recordInstalledPackages',
            ],
            PostRequireEvent::class => [
                'recordRequestedPackageVersions',
            ],
            PostApplyEvent::class => [
                'logChanges',
            ],
        ];
    }

}

Classes

Title Deprecated Summary
ChangeLogger Event subscriber to log changes that happen during the stage life cycle.

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