Skip to main content
Drupal API
User account menu
  • Log in

Breadcrumb

  1. Drupal Core 11.1.x
  2. Plugin.php

class Plugin

Same name in this branch
  1. 11.1.x vendor/dealerdirect/phpcodesniffer-composer-installer/src/Plugin.php \PHPCSStandards\Composer\Plugin\Installers\PHPCodeSniffer\Plugin
  2. 11.1.x vendor/phpstan/extension-installer/src/Plugin.php \PHPStan\ExtensionInstaller\Plugin
  3. 11.1.x vendor/composer/installers/src/Composer/Installers/Plugin.php \Composer\Installers\Plugin
  4. 11.1.x vendor/tbachert/spi/src/Composer/Plugin.php \Nevay\SPI\Composer\Plugin
  5. 11.1.x composer/Plugin/Scaffold/Plugin.php \Drupal\Composer\Plugin\Scaffold\Plugin
  6. 11.1.x core/lib/Drupal/Component/Annotation/Plugin.php \Drupal\Component\Annotation\Plugin
  7. 11.1.x core/lib/Drupal/Component/Plugin/Attribute/Plugin.php \Drupal\Component\Plugin\Attribute\Plugin

Auto-installs missing implementations.

When a dependency requires both this package and one of the supported `*-implementation` virtual packages, this plugin will auto-install a well-known implementation if none is found. The plugin will first look at already installed packages and figure out the preferred implementation to install based on the below stickyness rules (or on the first listed implementation if no rules match.)

Don't miss updating src/Strategy/Common*Strategy.php when adding a new supported package.

@author Nicolas Grekas <p@tchwork.com>

@internal

Hierarchy

  • class \Http\Discovery\Composer\Plugin implements \Composer\Plugin\PluginInterface, \Composer\EventDispatcher\EventSubscriberInterface

Expanded class hierarchy of Plugin

14 string references to 'Plugin'
Action::create in core/modules/system/src/Entity/Action.php
Constructs a new entity object, without permanently saving it.
block.schema.yml in core/modules/block/config/schema/block.schema.yml
core/modules/block/config/schema/block.schema.yml
DisplayPluginBase::calculateCacheMetadata in core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
Calculates the display's cache metadata by inspecting each handler/plugin.
DisplayPluginBase::getAllPlugins in core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
Gets all the plugins used by the display.
EntityBlock::getEntityId in core/modules/block/src/Plugin/migrate/destination/EntityBlock.php
Gets the entity ID of the row.

... See full list

File

vendor/php-http/discovery/src/Composer/Plugin.php, line 39

Namespace

Http\Discovery\Composer
View source
class Plugin implements PluginInterface, EventSubscriberInterface {
    
    /**
     * Describes, for every supported virtual implementation, which packages
     * provide said implementation and which extra dependencies each package
     * requires to provide the implementation.
     */
    private const PROVIDE_RULES = [
        'php-http/async-client-implementation' => [
            'symfony/http-client:>=6.3' => [
                'guzzlehttp/promises',
                'psr/http-factory-implementation',
                'php-http/httplug',
            ],
            'symfony/http-client' => [
                'guzzlehttp/promises',
                'php-http/message-factory',
                'psr/http-factory-implementation',
                'php-http/httplug',
            ],
            'php-http/guzzle7-adapter' => [],
            'php-http/guzzle6-adapter' => [],
            'php-http/curl-client' => [],
            'php-http/react-adapter' => [],
        ],
        'php-http/client-implementation' => [
            'symfony/http-client:>=6.3' => [
                'psr/http-factory-implementation',
                'php-http/httplug',
            ],
            'symfony/http-client' => [
                'php-http/message-factory',
                'psr/http-factory-implementation',
                'php-http/httplug',
            ],
            'php-http/guzzle7-adapter' => [],
            'php-http/guzzle6-adapter' => [],
            'php-http/cakephp-adapter' => [],
            'php-http/curl-client' => [],
            'php-http/react-adapter' => [],
            'php-http/buzz-adapter' => [],
            'php-http/artax-adapter' => [],
            'kriswallsmith/buzz:^1' => [],
        ],
        'psr/http-client-implementation' => [
            'symfony/http-client' => [
                'psr/http-factory-implementation',
                'psr/http-client',
            ],
            'guzzlehttp/guzzle' => [],
            'kriswallsmith/buzz:^1' => [],
        ],
        'psr/http-message-implementation' => [
            'php-http/discovery' => [
                'psr/http-factory-implementation',
            ],
        ],
        'psr/http-factory-implementation' => [
            'nyholm/psr7' => [],
            'guzzlehttp/psr7:>=2' => [],
            'slim/psr7' => [],
            'laminas/laminas-diactoros' => [],
            'phalcon/cphalcon:^4' => [],
            'http-interop/http-factory-guzzle' => [],
            'http-interop/http-factory-diactoros' => [],
            'http-interop/http-factory-slim' => [],
            'httpsoft/http-message' => [],
        ],
    ];
    
    /**
     * Describes which package should be preferred on the left side
     * depending on which one is already installed on the right side.
     */
    private const STICKYNESS_RULES = [
        'symfony/http-client' => 'symfony/framework-bundle',
        'php-http/guzzle7-adapter' => 'guzzlehttp/guzzle:^7',
        'php-http/guzzle6-adapter' => 'guzzlehttp/guzzle:^6',
        'php-http/guzzle5-adapter' => 'guzzlehttp/guzzle:^5',
        'php-http/cakephp-adapter' => 'cakephp/cakephp',
        'php-http/react-adapter' => 'react/event-loop',
        'php-http/buzz-adapter' => 'kriswallsmith/buzz:^0.15.1',
        'php-http/artax-adapter' => 'amphp/artax:^3',
        'http-interop/http-factory-guzzle' => 'guzzlehttp/psr7:^1',
        'http-interop/http-factory-slim' => 'slim/slim:^3',
    ];
    private const INTERFACE_MAP = [
        'php-http/async-client-implementation' => [
            'Http\\Client\\HttpAsyncClient',
        ],
        'php-http/client-implementation' => [
            'Http\\Client\\HttpClient',
        ],
        'psr/http-client-implementation' => [
            'Psr\\Http\\Client\\ClientInterface',
        ],
        'psr/http-factory-implementation' => [
            'Psr\\Http\\Message\\RequestFactoryInterface',
            'Psr\\Http\\Message\\ResponseFactoryInterface',
            'Psr\\Http\\Message\\ServerRequestFactoryInterface',
            'Psr\\Http\\Message\\StreamFactoryInterface',
            'Psr\\Http\\Message\\UploadedFileFactoryInterface',
            'Psr\\Http\\Message\\UriFactoryInterface',
        ],
    ];
    public static function getSubscribedEvents() : array {
        return [
            ScriptEvents::PRE_AUTOLOAD_DUMP => 'preAutoloadDump',
            ScriptEvents::POST_UPDATE_CMD => 'postUpdate',
        ];
    }
    public function activate(Composer $composer, IOInterface $io) : void {
    }
    public function deactivate(Composer $composer, IOInterface $io) {
    }
    public function uninstall(Composer $composer, IOInterface $io) {
    }
    public function postUpdate(Event $event) {
        $composer = $event->getComposer();
        $repo = $composer->getRepositoryManager()
            ->getLocalRepository();
        $requires = [
            $composer->getPackage()
                ->getRequires(),
            $composer->getPackage()
                ->getDevRequires(),
        ];
        $pinnedAbstractions = [];
        $pinned = $composer->getPackage()
            ->getExtra()['discovery'] ?? [];
        foreach (self::INTERFACE_MAP as $abstraction => $interfaces) {
            foreach (isset($pinned[$abstraction]) ? [] : $interfaces as $interface) {
                if (!isset($pinned[$interface])) {
                    continue 2;
                }
            }
            $pinnedAbstractions[$abstraction] = true;
        }
        $missingRequires = $this->getMissingRequires($repo, $requires, 'project' === $composer->getPackage()
            ->getType(), $pinnedAbstractions);
        $missingRequires = [
            'require' => array_fill_keys(array_merge([], ...array_values($missingRequires[0])), '*'),
            'require-dev' => array_fill_keys(array_merge([], ...array_values($missingRequires[1])), '*'),
            'remove' => array_fill_keys(array_merge([], ...array_values($missingRequires[2])), '*'),
        ];
        if (!($missingRequires = array_filter($missingRequires))) {
            return;
        }
        $composerJsonContents = file_get_contents(Factory::getComposerFile());
        $this->updateComposerJson($missingRequires, $composer->getConfig()
            ->get('sort-packages'));
        $installer = null;
        // Find the composer installer, hack borrowed from symfony/flex
        foreach (debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT) as $trace) {
            if (isset($trace['object']) && $trace['object'] instanceof Installer) {
                $installer = $trace['object'];
                break;
            }
        }
        if (!$installer) {
            return;
        }
        $event->stopPropagation();
        $dispatcher = $composer->getEventDispatcher();
        $disableScripts = !method_exists($dispatcher, 'setRunScripts') || !((array) $dispatcher)["\x00*\x00runScripts"];
        $composer = Factory::create($event->getIO(), null, false, $disableScripts);
        
        /** @var Installer $installer */
        $installer = clone $installer;
        if (method_exists($installer, 'setAudit')) {
            $trace['object']->setAudit(false);
        }
        // we need a clone of the installer to preserve its configuration state but with our own service objects
        $installer->__construct($event->getIO(), $composer->getConfig(), $composer->getPackage(), $composer->getDownloadManager(), $composer->getRepositoryManager(), $composer->getLocker(), $composer->getInstallationManager(), $composer->getEventDispatcher(), $composer->getAutoloadGenerator());
        if (method_exists($installer, 'setPlatformRequirementFilter')) {
            $installer->setPlatformRequirementFilter(((array) $trace['object'])["\x00*\x00platformRequirementFilter"]);
        }
        if (0 !== $installer->run()) {
            file_put_contents(Factory::getComposerFile(), $composerJsonContents);
            return;
        }
        $versionSelector = new VersionSelector(ClassDiscovery::safeClassExists(RepositorySet::class) ? new RepositorySet() : new Pool());
        $updateComposerJson = false;
        foreach ($composer->getRepositoryManager()
            ->getLocalRepository()
            ->getPackages() as $package) {
            foreach ([
                'require',
                'require-dev',
            ] as $key) {
                if (!isset($missingRequires[$key][$package->getName()])) {
                    continue;
                }
                $updateComposerJson = true;
                $missingRequires[$key][$package->getName()] = $versionSelector->findRecommendedRequireVersion($package);
            }
        }
        if ($updateComposerJson) {
            $this->updateComposerJson($missingRequires, $composer->getConfig()
                ->get('sort-packages'));
            $this->updateComposerLock($composer, $event->getIO());
        }
    }
    public function getMissingRequires(InstalledRepositoryInterface $repo, array $requires, bool $isProject, array $pinnedAbstractions) : array {
        $allPackages = [];
        $devPackages = method_exists($repo, 'getDevPackageNames') ? array_fill_keys($repo->getDevPackageNames(), true) : [];
        // One must require "php-http/discovery"
        // to opt-in for auto-installation of virtual package implementations
        if (!isset($requires[0]['php-http/discovery'])) {
            $requires = [
                [],
                [],
            ];
        }
        foreach ($repo->getPackages() as $package) {
            $allPackages[$package->getName()] = true;
            if (1 < \count($names = $package->getNames(false))) {
                $allPackages += array_fill_keys($names, false);
                if (isset($devPackages[$package->getName()])) {
                    $devPackages += $names;
                }
            }
            if (isset($package->getRequires()['php-http/discovery'])) {
                $requires[(int) isset($devPackages[$package->getName()])] += $package->getRequires();
            }
        }
        $missingRequires = [
            [],
            [],
            [],
        ];
        $versionParser = new VersionParser();
        if (ClassDiscovery::safeClassExists(\Phalcon\Http\Message\RequestFactory::class, false)) {
            $missingRequires[0]['psr/http-factory-implementation'] = [];
            $missingRequires[1]['psr/http-factory-implementation'] = [];
        }
        foreach ($requires as $dev => $rules) {
            $abstractions = [];
            $rules = array_intersect_key(self::PROVIDE_RULES, $rules);
            while ($rules) {
                $abstraction = key($rules);
                if (isset($pinnedAbstractions[$abstraction])) {
                    unset($rules[$abstraction]);
                    continue;
                }
                $abstractions[] = $abstraction;
                foreach (array_shift($rules) as $candidate => $deps) {
                    [
                        $candidate,
                        $version,
                    ] = explode(':', $candidate, 2) + [
                        1 => null,
                    ];
                    if (!isset($allPackages[$candidate])) {
                        continue;
                    }
                    if (null !== $version && !$repo->findPackage($candidate, $versionParser->parseConstraints($version))) {
                        continue;
                    }
                    if ($isProject && !$dev && isset($devPackages[$candidate])) {
                        $missingRequires[0][$abstraction] = [
                            $candidate,
                        ];
                        $missingRequires[2][$abstraction] = [
                            $candidate,
                        ];
                    }
                    else {
                        $missingRequires[$dev][$abstraction] = [];
                    }
                    foreach ($deps as $dep) {
                        if (isset(self::PROVIDE_RULES[$dep])) {
                            $rules[$dep] = self::PROVIDE_RULES[$dep];
                        }
                        elseif (!isset($allPackages[$dep])) {
                            $missingRequires[$dev][$abstraction][] = $dep;
                        }
                        elseif ($isProject && !$dev && isset($devPackages[$dep])) {
                            $missingRequires[0][$abstraction][] = $dep;
                            $missingRequires[2][$abstraction][] = $dep;
                        }
                    }
                    break;
                }
            }
            while ($abstractions) {
                $abstraction = array_shift($abstractions);
                if (isset($missingRequires[$dev][$abstraction])) {
                    continue;
                }
                $candidates = self::PROVIDE_RULES[$abstraction];
                foreach ($candidates as $candidate => $deps) {
                    [
                        $candidate,
                        $version,
                    ] = explode(':', $candidate, 2) + [
                        1 => null,
                    ];
                    if (null !== $version && !$repo->findPackage($candidate, $versionParser->parseConstraints($version))) {
                        continue;
                    }
                    if (isset($allPackages[$candidate]) && (!$isProject || $dev || !isset($devPackages[$candidate]))) {
                        continue 2;
                    }
                }
                foreach (array_intersect_key(self::STICKYNESS_RULES, $candidates) as $candidate => $stickyRule) {
                    [
                        $stickyName,
                        $stickyVersion,
                    ] = explode(':', $stickyRule, 2) + [
                        1 => null,
                    ];
                    if (!isset($allPackages[$stickyName]) || $isProject && !$dev && isset($devPackages[$stickyName])) {
                        continue;
                    }
                    if (null !== $stickyVersion && !$repo->findPackage($stickyName, $versionParser->parseConstraints($stickyVersion))) {
                        continue;
                    }
                    $candidates = [
                        $candidate => $candidates[$candidate],
                    ];
                    break;
                }
                $dep = key($candidates);
                [
                    $dep,
                ] = explode(':', $dep, 2);
                $missingRequires[$dev][$abstraction] = [
                    $dep,
                ];
                if ($isProject && !$dev && isset($devPackages[$dep])) {
                    $missingRequires[2][$abstraction][] = $dep;
                }
            }
        }
        $missingRequires[1] = array_diff_key($missingRequires[1], $missingRequires[0]);
        return $missingRequires;
    }
    public function preAutoloadDump(Event $event) {
        $filesystem = new Filesystem();
        // Double realpath() on purpose, see https://bugs.php.net/72738
        $vendorDir = $filesystem->normalizePath(realpath(realpath($event->getComposer()
            ->getConfig()
            ->get('vendor-dir'))));
        $filesystem->ensureDirectoryExists($vendorDir . '/composer');
        $pinned = $event->getComposer()
            ->getPackage()
            ->getExtra()['discovery'] ?? [];
        $candidates = [];
        $allInterfaces = array_merge(...array_values(self::INTERFACE_MAP));
        foreach ($pinned as $abstraction => $class) {
            if (isset(self::INTERFACE_MAP[$abstraction])) {
                $interfaces = self::INTERFACE_MAP[$abstraction];
            }
            elseif (false !== ($k = array_search($abstraction, $allInterfaces, true))) {
                $interfaces = [
                    $allInterfaces[$k],
                ];
            }
            else {
                throw new \UnexpectedValueException(sprintf('Invalid "extra.discovery" pinned in composer.json: "%s" is not one of ["%s"].', $abstraction, implode('", "', array_keys(self::INTERFACE_MAP))));
            }
            foreach ($interfaces as $interface) {
                $candidates[] = sprintf("case %s: return [['class' => %s]];\n", var_export($interface, true), var_export($class, true));
            }
        }
        $file = $vendorDir . '/composer/GeneratedDiscoveryStrategy.php';
        if (!$candidates) {
            if (file_exists($file)) {
                unlink($file);
            }
            return;
        }
        $candidates = implode('            ', $candidates);
        $code = <<<EOPHP
<?php

namespace Http\\Discovery\\Strategy;

class GeneratedDiscoveryStrategy implements DiscoveryStrategy
{
    public static function getCandidates(\$type)
    {
        switch (\$type) {
            {<span class="php-variable">$candidates</span>}
            default: return [];
        }
    }
}

EOPHP;
        if (!file_exists($file) || $code !== file_get_contents($file)) {
            file_put_contents($file, $code);
        }
        $rootPackage = $event->getComposer()
            ->getPackage();
        $autoload = $rootPackage->getAutoload();
        $autoload['classmap'][] = $vendorDir . '/composer/GeneratedDiscoveryStrategy.php';
        $rootPackage->setAutoload($autoload);
    }
    private function updateComposerJson(array $missingRequires, bool $sortPackages) {
        $file = Factory::getComposerFile();
        $contents = file_get_contents($file);
        $manipulator = new JsonManipulator($contents);
        foreach ($missingRequires as $key => $packages) {
            foreach ($packages as $package => $constraint) {
                if ('remove' === $key) {
                    $manipulator->removeSubNode('require-dev', $package);
                }
                else {
                    $manipulator->addLink($key, $package, $constraint, $sortPackages);
                }
            }
        }
        file_put_contents($file, $manipulator->getContents());
    }
    private function updateComposerLock(Composer $composer, IOInterface $io) {
        if (false === $composer->getConfig()
            ->get('lock')) {
            return;
        }
        $lock = substr(Factory::getComposerFile(), 0, -4) . 'lock';
        $composerJson = file_get_contents(Factory::getComposerFile());
        $lockFile = new JsonFile($lock, null, $io);
        $locker = ClassDiscovery::safeClassExists(RepositorySet::class) ? new Locker($io, $lockFile, $composer->getInstallationManager(), $composerJson) : new Locker($io, $lockFile, $composer->getRepositoryManager(), $composer->getInstallationManager(), $composerJson);
        if (!$locker->isLocked()) {
            return;
        }
        $lockData = $locker->getLockData();
        $lockData['content-hash'] = Locker::getContentHash($composerJson);
        $lockFile->write($lockData);
    }

}

Members

Title Sort descending Modifiers Object type Summary Overriden Title
Plugin::activate public function Apply plugin modifications to Composer Overrides PluginInterface::activate
Plugin::deactivate public function Remove any hooks from Composer Overrides PluginInterface::deactivate
Plugin::getMissingRequires public function
Plugin::getSubscribedEvents public static function Returns an array of event names this subscriber wants to listen to. Overrides EventSubscriberInterface::getSubscribedEvents
Plugin::INTERFACE_MAP private constant
Plugin::postUpdate public function
Plugin::preAutoloadDump public function
Plugin::PROVIDE_RULES private constant Describes, for every supported virtual implementation, which packages
provide said implementation and which extra dependencies each package
requires to provide the implementation.
Plugin::STICKYNESS_RULES private constant Describes which package should be preferred on the left side
depending on which one is already installed on the right side.
Plugin::uninstall public function Prepare the plugin to be uninstalled Overrides PluginInterface::uninstall
Plugin::updateComposerJson private function
Plugin::updateComposerLock private function
PluginInterface::PLUGIN_API_VERSION public constant Version number of the internal composer-plugin-api package

API Navigation

  • Drupal Core 11.1.x
  • Topics
  • Classes
  • Functions
  • Constants
  • Globals
  • Files
  • Namespaces
  • Deprecated
  • Services
RSS feed
Powered by Drupal