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

Breadcrumb

  1. Drupal Core 11.1.x

Plugin.php

Same filename in this branch
  1. 11.1.x vendor/dealerdirect/phpcodesniffer-composer-installer/src/Plugin.php
  2. 11.1.x vendor/phpstan/extension-installer/src/Plugin.php
  3. 11.1.x vendor/php-http/discovery/src/Composer/Plugin.php
  4. 11.1.x vendor/composer/installers/src/Composer/Installers/Plugin.php
  5. 11.1.x composer/Plugin/Scaffold/Plugin.php
  6. 11.1.x core/lib/Drupal/Component/Annotation/Plugin.php
  7. 11.1.x core/lib/Drupal/Component/Plugin/Attribute/Plugin.php

Namespace

Nevay\SPI\Composer

File

vendor/tbachert/spi/src/Composer/Plugin.php

View source
<?php

declare (strict_types=1);
namespace Nevay\SPI\Composer;

use Composer\Composer;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\IO\IOInterface;
use Composer\Package\Package;
use Composer\Package\PackageInterface;
use Composer\Plugin\PluginInterface;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
use Composer\Util\Filesystem;
use Composer\Util\Platform;
use Nevay\SPI\ServiceLoader;
use Nevay\SPI\ServiceProviderRequirementRuntimeValidated;
use ReflectionAttribute;
use ReflectionClass;
use RuntimeException;
use function array_diff;
use function array_fill_keys;
use function array_unique;
use function class_exists;
use function getcwd;
use function implode;
use function is_string;
use function json_encode;
use function preg_match;
use function realpath;
use function sprintf;
use function var_export;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;
final class Plugin implements PluginInterface, EventSubscriberInterface {
    private const FQCN_REGEX = '/^\\\\?[a-zA-Z_\\x80-\\xff][a-zA-Z0-9_\\x80-\\xff]*(?:\\\\[a-zA-Z_\\x80-\\xff][a-zA-Z0-9_\\x80-\\xff]*)*$/';
    public static function getSubscribedEvents() : array {
        return [
            ScriptEvents::PRE_AUTOLOAD_DUMP => 'preAutoloadDump',
        ];
    }
    public function activate(Composer $composer, IOInterface $io) : void {
        // no-op
    }
    public function deactivate(Composer $composer, IOInterface $io) : void {
        // no-op
    }
    public function uninstall(Composer $composer, IOInterface $io) : void {
        $filesystem = new Filesystem();
        $vendorDir = $this->vendorDir($composer, $filesystem);
        $filesystem->remove($vendorDir . '/composer/GeneratedServiceProviderData.php');
    }
    public function preAutoloadDump(Event $event) : void {
        $package = $event->getComposer()
            ->getPackage();
        $packages = $event->getComposer()
            ->getRepositoryManager()
            ->getLocalRepository()
            ->getCanonicalPackages();
        $packageMap = $event->getComposer()
            ->getAutoloadGenerator()
            ->buildPackageMap($event->getComposer()
            ->getInstallationManager(), $package, $packages);
        $map = $event->getComposer()
            ->getAutoloadGenerator()
            ->parseAutoloads($packageMap, $package);
        $loader = $event->getComposer()
            ->getAutoloadGenerator()
            ->createLoader($map, $event->getComposer()
            ->getConfig()
            ->get('vendor-dir'));
        $loader->register();
        try {
            $this->dumpGeneratedServiceProviderData($event);
        } finally {
            $loader->unregister();
        }
    }
    private function dumpGeneratedServiceProviderData(Event $event) : void {
        $match = '';
        foreach ($this->serviceProviders($event->getComposer(), $event->getIO()) as $service => $providers) {
            if (!preg_match(self::FQCN_REGEX, $service)) {
                $event->getIO()
                    ->warning(sprintf('Invalid extra.spi configuration, expected class name, got "%s" (%s)', $service, implode(', ', array_unique($providers))));
                continue;
            }
            if (!ServiceLoader::serviceAvailable($service)) {
                $event->getIO()
                    ->info(sprintf('Skipping extra.spi service "%s", service not available (%s)', $service, implode(', ', array_unique($providers))));
                continue;
            }
            if ($service[0] !== '\\') {
                $service = '\\' . $service;
            }
            $match .= "\n            {$service}::class => [";
            foreach ($providers as $provider => $package) {
                if (!preg_match(self::FQCN_REGEX, $provider)) {
                    $event->getIO()
                        ->warning(sprintf('Invalid extra.spi configuration, expected class name, got "%s" for "%s" (%s)', $provider, $service, $package));
                    continue;
                }
                if (!class_exists($provider)) {
                    $event->getIO()
                        ->info(sprintf('Skipping extra.spi configuration, provider class "%s" for "%s" does not exist (%s)', $provider, $service, $package));
                    continue;
                }
                if (!ServiceLoader::providerAvailable($provider, skipRuntimeValidatedRequirements: true)) {
                    $event->getIO()
                        ->info(sprintf('Skipping extra.spi provider "%s" for "%s", provider not available (%s)', $provider, $service, $package));
                    continue;
                }
                if ($provider[0] !== '\\') {
                    $provider = '\\' . $provider;
                }
                if ($condition = self::providerRuntimeValidatedRequirements($provider)) {
                    $match .= "\n                ...(({$condition}) ? [";
                    $match .= "\n                {$provider}::class, // {$package}";
                    $match .= "\n                ] : []),";
                }
                else {
                    $match .= "\n                {$provider}::class, // {$package}";
                }
            }
            $match .= "\n            ],";
        }
        $code = <<<PHP
<?php declare(strict_types=1);
namespace Nevay\\SPI;

/**
 * @internal 
 */
final class GeneratedServiceProviderData {

    public const VERSION = 1;

    /**
     * @param class-string \$service
     * @return list<class-string>
     */
    public static function providers(string \$service): array {
        return match (\$service) {
            default => [],{<span class="php-variable">$match</span>}
        };
    }
}
PHP;
        $filesystem = new Filesystem();
        $vendorDir = $this->vendorDir($event->getComposer(), $filesystem);
        $filesystem->ensureDirectoryExists($vendorDir . '/composer');
        $filesystem->filePutContentsIfModified($vendorDir . '/composer/GeneratedServiceProviderData.php', $code);
        $package = $event->getComposer()
            ->getPackage();
        $autoload = $package->getAutoload();
        $autoload['classmap'][] = $vendorDir . '/composer/GeneratedServiceProviderData.php';
        $package->setAutoload($autoload);
    }
    private static function providerRuntimeValidatedRequirements(string $provider) : ?string {
        // The current implementation is suboptimal if multiple runtime validated requirements are specified
        // - should check all hashes of not satisfied requirements before calling ::isSatisfied()
        // - should deduplicate requirements
        $condition = var_export(true, true);
        
        /** @var ReflectionAttribute<ServiceProviderRequirementRuntimeValidated> $attribute */
        foreach ((new ReflectionClass($provider))->getAttributes(ServiceProviderRequirementRuntimeValidated::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
            $requirement = $attribute->newInstance();
            $class = '\\' . $requirement::class;
            $args = '';
            foreach ($attribute->getArguments() as $key => $value) {
                $args and $args .= ', ';
                if (is_string($key)) {
                    $args .= $key;
                    $args .= ': ';
                }
                $args .= var_export($value, true);
            }
            $hash = var_export($requirement->hash(), true);
            $condition .= $requirement->isSatisfied() ? " && ((\$r = new {$class}({$args}))->hash() === {$hash} || \$r->isSatisfied())" : " && ((\$r = new {$class}({$args}))->hash() !== {$hash} && \$r->isSatisfied())";
        }
        if (!isset($requirement)) {
            return null;
        }
        return $condition;
    }
    private function vendorDir(Composer $composer, Filesystem $filesystem) : string {
        return $filesystem->normalizePath($composer->getConfig()
            ->get('vendor-dir'));
    }
    
    /**
     * @return array<class-string, array<class-string, string>>
     */
    private function serviceProviders(Composer $composer, IOInterface $io) : array {
        $mappings = [];
        $this->serviceProvidersFromExtraSpi($composer->getPackage(), $mappings);
        $this->serviceProvidersFromAutoloadFiles($composer->getPackage(), $mappings, self::getCwd(), $io);
        foreach ($composer->getRepositoryManager()
            ->getLocalRepository()
            ->getPackages() as $package) {
            $this->serviceProvidersFromExtraSpi($package, $mappings);
            if (($installPath = $composer->getInstallationManager()
                ->getInstallPath($package)) !== null) {
                $this->serviceProvidersFromAutoloadFiles($package, $mappings, $installPath, $io);
            }
        }
        return $mappings;
    }
    private function serviceProvidersFromExtraSpi(PackageInterface $package, array &$mappings) : void {
        foreach ($package->getExtra()['spi'] ?? [] as $service => $providers) {
            $providers = (array) $providers;
            $mappings[$service] ??= [];
            $mappings[$service] += array_fill_keys($providers, $package->getPrettyString() . ' (extra.spi)');
        }
    }
    private function serviceProvidersFromAutoloadFiles(PackageInterface $package, array &$mappings, string $installPath, IOInterface $io) : void {
        $autoloadFiles = $package->getAutoload()['files'] ?? [];
        $spiAutoloadFiles = $package->getExtra()['spi-config']['autoload-files'] ?? false ?: [];
        $spiPruneAutoloadFiles = $package->getExtra()['spi-config']['prune-autoload-files'] ?? null;
        if ($spiAutoloadFiles === true) {
            $spiAutoloadFiles = $autoloadFiles;
        }
        if ($spiPruneAutoloadFiles === true) {
            $spiPruneAutoloadFiles = $spiAutoloadFiles;
        }
        $includeFile = (static fn(string $file) => require $file)->bindTo(null, null);
        foreach ($spiAutoloadFiles as $index => $file) {
            $io->debug(sprintf('Loading service providers from "%s" (%s)', $file, $package->getPrettyString()));
            if (!($includedProviders = ServiceLoader::collectProviders($includeFile, $installPath . '/' . $file))) {
                unset($spiAutoloadFiles[$index]);
                continue;
            }
            foreach ($includedProviders as $service => $providers) {
                $mappings[$service] ??= [];
                $mappings[$service] += array_fill_keys($providers, $package->getPrettyString() . ' (' . json_encode($file, flags: JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . ')');
            }
        }
        $spiPruneAutoloadFiles ??= $spiAutoloadFiles;
        if ($spiPruneAutoloadFiles && $autoloadFiles && $package instanceof Package) {
            $io->debug(sprintf('Pruning autoload.files (%s): %s', $package->getPrettyString(), implode(', ', $spiPruneAutoloadFiles)));
            $autoload = $package->getAutoload();
            $autoload['files'] = array_diff($autoloadFiles, $spiPruneAutoloadFiles);
            $package->setAutoload($autoload);
        }
    }
    
    /**
     * @see Platform::getCwd()
     */
    private static function getCwd() : string {
        $cwd = getcwd();
        if ($cwd === false) {
            $cwd = realpath('');
        }
        if ($cwd === false) {
            throw new RuntimeException('Could not determine the current working directory');
        }
        return $cwd;
    }

}

Classes

Title Deprecated Summary
Plugin

API Navigation

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