├── src ├── Provider │ ├── ProviderInterface.php │ ├── AutoloadDependentProviderInterface.php │ ├── TolerantProviderInterface.php │ ├── ProviderFactoryInterface.php │ ├── LiteralProvider.php │ ├── EnvProvider.php │ ├── ProviderType.php │ ├── CacheDecoratorProvider.php │ ├── ConstantProvider.php │ ├── CallbackProvider.php │ ├── ProcessProvider.php │ ├── IncludeProvider.php │ ├── EscapeDecoratorProvider.php │ ├── LoggerDecoratorProvider.php │ ├── AutoloaderDecoratorProvider.php │ └── ProviderFactory.php ├── EventHandler │ ├── EventHandlerFactoryInterface.php │ ├── NullEventHandler.php │ ├── EventHandlerInterface.php │ ├── EventHandlerFactory.php │ ├── PreCommandRunHandler.php │ └── LegacyEventHandler.php ├── Transformer │ ├── TransformerInterface.php │ ├── NullTransformer.php │ ├── TransformerFactoryInterface.php │ ├── Transformer.php │ ├── TransformerCollection.php │ ├── TransformerFactory.php │ └── TransformerManager.php ├── Config │ ├── PluginConfigurationInterface.php │ ├── SubstitutionConfigurationInterface.php │ ├── PluginConfiguration.php │ ├── SubstitutionConfiguration.php │ └── AbstractConfiguration.php ├── Logger │ ├── VerboseLogger.php │ ├── DefaultLogger.php │ ├── LoggerFactory.php │ └── VeryVerboseLogger.php ├── utils-functions.php ├── Utils │ ├── NonRewindableIterator.php │ └── CommandHelper.php └── SubstitutionPlugin.php ├── LICENSE └── composer.json /src/Provider/ProviderInterface.php: -------------------------------------------------------------------------------- 1 | value = $value; 16 | } 17 | 18 | /** 19 | * @inheritDoc 20 | */ 21 | public function getValue() 22 | { 23 | return $this->value; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Provider/EnvProvider.php: -------------------------------------------------------------------------------- 1 | envVarName = $envVarName; 16 | } 17 | 18 | /** 19 | * @inheritDoc 20 | */ 21 | public function getValue() 22 | { 23 | return getenv($this->envVarName); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/EventHandler/EventHandlerInterface.php: -------------------------------------------------------------------------------- 1 | isDebug(): 18 | case $io->isVeryVerbose(): 19 | return new VeryVerboseLogger($io); 20 | case $io->isVerbose(): 21 | return new VerboseLogger($io); 22 | default: 23 | return new DefaultLogger($io); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Provider/ProviderType.php: -------------------------------------------------------------------------------- 1 | isInternal(); 24 | } catch (\ReflectionException $e) { 25 | return false; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Provider/CacheDecoratorProvider.php: -------------------------------------------------------------------------------- 1 | provider = $provider; 19 | } 20 | 21 | /** 22 | * @inheritDoc 23 | */ 24 | public function getValue() 25 | { 26 | if (!$this->hasValue) { 27 | $this->value = $this->provider->getValue(); 28 | $this->hasValue = true; 29 | } 30 | 31 | return $this->value; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Provider/ConstantProvider.php: -------------------------------------------------------------------------------- 1 | constantName = $constantName; 16 | } 17 | 18 | /** 19 | * @inheritDoc 20 | */ 21 | public function mustAutoload() 22 | { 23 | return !defined($this->constantName); 24 | } 25 | 26 | /** 27 | * @inheritDoc 28 | */ 29 | public function getValue() 30 | { 31 | if (!defined($this->constantName)) { 32 | throw new \InvalidArgumentException('Value is not a constant: ' . $this->constantName); 33 | } 34 | 35 | return constant($this->constantName); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Provider/CallbackProvider.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 16 | } 17 | 18 | /** 19 | * @inheritDoc 20 | */ 21 | public function getValue() 22 | { 23 | if (!is_callable($this->callback)) { 24 | throw new \InvalidArgumentException('Value is not callable: ' . $this->callback); 25 | } 26 | 27 | return call_user_func($this->callback); 28 | } 29 | 30 | /** 31 | * @inheritDoc 32 | */ 33 | public function mustAutoload() 34 | { 35 | return !\SubstitutionPlugin\isInternalCallback($this->callback); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Transformer/Transformer.php: -------------------------------------------------------------------------------- 1 | placeholder = (string) $placeholder; 22 | $this->provider = $provider; 23 | } 24 | 25 | /** 26 | * @inheritDoc 27 | */ 28 | public function transform($value) 29 | { 30 | if ($this->placeholder === '' || strpos($value, $this->placeholder) === false) { 31 | return $value; 32 | } 33 | 34 | return str_replace($this->placeholder, $this->provider->getValue(), $value); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Transformer/TransformerCollection.php: -------------------------------------------------------------------------------- 1 | transformers = $transformers; 16 | } 17 | 18 | /** 19 | * @param TransformerInterface $transformer 20 | * @return void 21 | */ 22 | public function addTransformer(TransformerInterface $transformer) 23 | { 24 | $this->transformers[] = $transformer; 25 | } 26 | 27 | /** 28 | * @inheritDoc 29 | */ 30 | public function transform($value) 31 | { 32 | foreach ($this->transformers as $transformer) { 33 | $value = $transformer->transform($value); 34 | } 35 | 36 | return $value; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Provider/ProcessProvider.php: -------------------------------------------------------------------------------- 1 | command = $command; 18 | } 19 | 20 | /** 21 | * @inheritDoc 22 | */ 23 | public function getValue() 24 | { 25 | $processExecutor = new ProcessExecutor(); 26 | $output = ''; 27 | $exitCode = $processExecutor->execute($this->command, $output); 28 | $output = is_array($output) ? implode(PHP_EOL, $output) : (string) $output; 29 | if ($exitCode > 0) { 30 | $message = sprintf('Error executing command "%s"', $this->command); 31 | if (!empty($output)) { 32 | $message .= ': ' . $output; 33 | } 34 | throw new \RuntimeException($message, $exitCode); 35 | } 36 | 37 | return $output; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Provider/IncludeProvider.php: -------------------------------------------------------------------------------- 1 | path = $path; 16 | } 17 | 18 | /** 19 | * @inheritDoc 20 | */ 21 | public function getValue() 22 | { 23 | if (stream_resolve_include_path($this->path) === false) { 24 | throw new \InvalidArgumentException('Cannot include file ' . $this->path); 25 | } 26 | 27 | return returnInclude($this->path); 28 | } 29 | 30 | /** 31 | * @inheritDoc 32 | */ 33 | public function mustAutoload() 34 | { 35 | return true; 36 | } 37 | } 38 | 39 | /** 40 | * Scope isolated include. 41 | * 42 | * Prevents access to $this/self from included files. 43 | * 44 | * @param string $file 45 | * @return mixed 46 | */ 47 | function returnInclude($file) 48 | { 49 | return include $file; 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2024 Fabien Villepinte 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Provider/EscapeDecoratorProvider.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 20 | $this->provider = $provider; 21 | } 22 | 23 | /** 24 | * @inheritDoc 25 | */ 26 | public function getValue() 27 | { 28 | if (!is_callable($this->callback)) { 29 | throw new \InvalidArgumentException('The escape callback is not callable: ' . $this->callback); 30 | } 31 | 32 | return call_user_func($this->callback, $this->provider->getValue()); 33 | } 34 | 35 | /** 36 | * @inheritDoc 37 | */ 38 | public function mustAutoload() 39 | { 40 | return !\SubstitutionPlugin\isInternalCallback($this->callback); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/EventHandler/EventHandlerFactory.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 22 | $this->configuration = $configuration; 23 | } 24 | 25 | /** 26 | * @inheritDoc 27 | */ 28 | public function getEventHandler() 29 | { 30 | switch (true) { 31 | case !$this->configuration->isEnabled(): 32 | return new NullEventHandler(); 33 | case defined('Composer\\Plugin\\PluginEvents::PRE_COMMAND_RUN'): 34 | return new PreCommandRunHandler($this->callback, $this->configuration); 35 | default: 36 | return new LegacyEventHandler($this->callback); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Provider/LoggerDecoratorProvider.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 25 | $this->configuration = $configuration; 26 | $this->provider = $provider; 27 | } 28 | 29 | /** 30 | * @inheritDoc 31 | */ 32 | public function getValue() 33 | { 34 | $value = $this->provider->getValue(); 35 | 36 | if ($value === null) { 37 | $this->logger->debug(sprintf( 38 | 'The value replacing "%s" is null.', 39 | $this->configuration->getPlaceholder() 40 | )); 41 | return ''; 42 | } 43 | 44 | if (!is_scalar($value)) { 45 | $this->logger->error(sprintf( 46 | 'The value replacing "%s" must be a string. "%s" received.', 47 | $this->configuration->getPlaceholder(), 48 | gettype($value) 49 | )); 50 | return ''; 51 | } 52 | 53 | $this->logger->debug(sprintf( 54 | 'The value replacing "%s" is: %s', 55 | $this->configuration->getPlaceholder(), 56 | $value 57 | )); 58 | return (string) $value; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Provider/AutoloaderDecoratorProvider.php: -------------------------------------------------------------------------------- 1 | composer = $composer; 28 | $this->logger = $logger; 29 | $this->provider = $provider; 30 | } 31 | 32 | /** 33 | * @inheritDoc 34 | */ 35 | public function getValue() 36 | { 37 | if (!self::$autoload) { 38 | $this->autoload(); 39 | self::$autoload = true; 40 | } 41 | 42 | return $this->provider->getValue(); 43 | } 44 | 45 | /** 46 | * @return void 47 | */ 48 | private function autoload() 49 | { 50 | $files = array(); 51 | if ($this->composer->getConfig()->has('vendor-dir')) { 52 | $files[] = $this->composer->getConfig()->get('vendor-dir') . '/autoload.php'; 53 | } 54 | 55 | foreach ($files as $file) { 56 | $this->logger->debug('Try including autoloader at: ' . $file); 57 | if (stream_resolve_include_path($file) !== false) { 58 | includeFile($file); 59 | return; 60 | } 61 | } 62 | 63 | $this->logger->warning('Cannot include autoloader'); 64 | } 65 | } 66 | 67 | /** 68 | * Scope isolated include. 69 | * 70 | * Prevents access to $this/self from included files. 71 | * 72 | * @param string $file 73 | * @return void 74 | */ 75 | function includeFile($file) 76 | { 77 | include $file; 78 | } 79 | -------------------------------------------------------------------------------- /src/Utils/NonRewindableIterator.php: -------------------------------------------------------------------------------- 1 | */ 8 | private $values = array(); 9 | 10 | /** @var string|null */ 11 | private $value = null; 12 | 13 | /** @var int */ 14 | private $total = 0; 15 | 16 | /** 17 | * @param string[] $values 18 | */ 19 | public function __construct(array $values = array()) 20 | { 21 | $this->addAll($values); 22 | } 23 | 24 | /** 25 | * @param string $value 26 | * @return void 27 | */ 28 | public function add($value) 29 | { 30 | if (!isset($this->values[$value])) { 31 | if ($this->value === null) { 32 | $this->value = $value; 33 | } 34 | $this->values[$value] = $this->total; 35 | $this->total++; 36 | } 37 | } 38 | 39 | /** 40 | * @param string[] $values 41 | * @return void 42 | */ 43 | public function addAll(array $values) 44 | { 45 | foreach ($values as $value) { 46 | $this->add($value); 47 | } 48 | } 49 | 50 | /** 51 | * @return string|null 52 | */ 53 | #[\ReturnTypeWillChange] 54 | public function current() 55 | { 56 | return $this->value; 57 | } 58 | 59 | /** 60 | * @return void 61 | */ 62 | #[\ReturnTypeWillChange] 63 | public function next() 64 | { 65 | next($this->values); 66 | $this->value = key($this->values); 67 | } 68 | 69 | /** 70 | * @return int 71 | */ 72 | #[\ReturnTypeWillChange] 73 | public function key() 74 | { 75 | return $this->values[$this->value]; 76 | } 77 | 78 | /** 79 | * @return bool 80 | */ 81 | #[\ReturnTypeWillChange] 82 | public function valid() 83 | { 84 | return $this->value !== null; 85 | } 86 | 87 | /** 88 | * @return void 89 | */ 90 | #[\ReturnTypeWillChange] 91 | public function rewind() 92 | { 93 | // no rewind 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/EventHandler/PreCommandRunHandler.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 28 | $this->configuration = $configuration; 29 | $this->cmdHelper = new CommandHelper(); 30 | } 31 | 32 | /** 33 | * @inheritDoc 34 | */ 35 | public function getSubscribedEvents() 36 | { 37 | return array( 38 | PluginEvents::PRE_COMMAND_RUN => array( 39 | array('onPreCommandRun', $this->configuration->getPriority()), 40 | ), 41 | ); 42 | } 43 | 44 | /** 45 | * @param PreCommandRunEvent $event 46 | * @return void 47 | */ 48 | public function onPreCommandRun(PreCommandRunEvent $event) 49 | { 50 | call_user_func($this->callback, $this->getScripts($event)); 51 | } 52 | 53 | /** 54 | * @param PreCommandRunEvent $event 55 | * @return array 56 | */ 57 | private function getScripts(PreCommandRunEvent $event) 58 | { 59 | return $this->cmdHelper->getScripts($event->getCommand(), $event->getInput()); 60 | } 61 | 62 | /** 63 | * @inheritDoc 64 | */ 65 | public function activate() 66 | { 67 | // Nothing to do here 68 | } 69 | 70 | /** 71 | * @inheritDoc 72 | */ 73 | public function deactivate() 74 | { 75 | // Nothing to do here 76 | } 77 | 78 | /** 79 | * @inheritDoc 80 | */ 81 | public function uninstall() 82 | { 83 | // Nothing to do here 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Transformer/TransformerFactory.php: -------------------------------------------------------------------------------- 1 | providerFactory = $providerFactory; 23 | $this->logger = $logger; 24 | } 25 | 26 | /** 27 | * @inheritDoc 28 | */ 29 | public function getTransformer(PluginConfigurationInterface $configuration) 30 | { 31 | $nbSubstitutions = count($configuration->getMapping()); 32 | if ($nbSubstitutions > 1) { 33 | $transformer = new TransformerCollection(); 34 | foreach ($configuration->getMapping() as $conf) { 35 | $transformer->addTransformer($this->buildTransformer($conf)); 36 | } 37 | } elseif ($nbSubstitutions === 1) { 38 | /** @var SubstitutionConfigurationInterface $conf */ 39 | $conf = current($configuration->getMapping()); 40 | $transformer = $this->buildTransformer($conf); 41 | } else { 42 | // not supposed to happen 43 | $this->logger->error('At least one substitution expected'); 44 | $transformer = new NullTransformer(); 45 | } 46 | 47 | return $transformer; 48 | } 49 | 50 | /** 51 | * @param SubstitutionConfigurationInterface $configuration 52 | * @return TransformerInterface 53 | */ 54 | private function buildTransformer(SubstitutionConfigurationInterface $configuration) 55 | { 56 | $provider = $this->providerFactory->getProvider($configuration); 57 | 58 | if ($provider === null) { 59 | return new NullTransformer(); 60 | } 61 | 62 | return new Transformer($configuration->getPlaceholder(), $provider); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Transformer/TransformerManager.php: -------------------------------------------------------------------------------- 1 | transformer = $transformerFactory->getTransformer($config); 23 | $this->logger = $logger; 24 | } 25 | 26 | /** 27 | * @param array $scripts 28 | * @param string[] $scriptNames 29 | * @return array 30 | */ 31 | public function applySubstitutions(array $scripts, array $scriptNames) 32 | { 33 | $scriptsToTransform = new NonRewindableIterator($scriptNames); 34 | 35 | foreach ($scriptsToTransform as $scriptName) { 36 | if (!isset($scripts[$scriptName])) { 37 | continue; 38 | } 39 | 40 | $this->logger->debug('Apply substitution on script ' . $scriptName); 41 | $listeners = &$scripts[$scriptName]; 42 | foreach ($listeners as &$listener) { 43 | $listener = $this->transformer->transform($listener); 44 | 45 | if (self::tryExtractScript($listener, $script)) { 46 | $scriptsToTransform->add($script); 47 | } 48 | } 49 | } 50 | 51 | return $scripts; 52 | } 53 | 54 | /** 55 | * @param string $listener 56 | * @param string $script 57 | * @return bool 58 | */ 59 | private static function tryExtractScript($listener, &$script = '') 60 | { 61 | if (!isset($listener[0]) || $listener[0] !== '@') { 62 | return false; 63 | } 64 | 65 | // split on white-spaces 66 | $parts = preg_split('/\s+/', substr($listener, 1), -1, PREG_SPLIT_NO_EMPTY); 67 | 68 | if (empty($parts)) { 69 | return false; 70 | } 71 | 72 | $script = current($parts); 73 | 74 | return true; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "villfa/composer-substitution-plugin", 3 | "description": "Composer plugin replacing placeholders in the scripts section by dynamic values", 4 | "license": [ 5 | "MIT" 6 | ], 7 | "type": "composer-plugin", 8 | "keywords": [ 9 | "composer", 10 | "plugin", 11 | "substitution", 12 | "replacement", 13 | "scripts" 14 | ], 15 | "authors": [ 16 | { 17 | "name": "Fabien VILLEPINTE", 18 | "email": "fabien.villepinte@gmail.com" 19 | } 20 | ], 21 | "homepage": "https://github.com/villfa/composer-substitution-plugin", 22 | "require": { 23 | "php": ">=5.3.2", 24 | "ext-ctype": "*", 25 | "ext-json": "*", 26 | "composer-plugin-api": "^1.0 || ^2.0", 27 | "psr/log": "^1.1" 28 | }, 29 | "require-dev": { 30 | "composer/composer": ">=1.1", 31 | "phpunit/phpunit": "4.8.36 || 5.7.27 || 6.5.14 || ^8.5.21 || ^9.5.10" 32 | }, 33 | "minimum-stability": "stable", 34 | "prefer-stable": true, 35 | "autoload": { 36 | "psr-4": { 37 | "SubstitutionPlugin\\": "src/" 38 | }, 39 | "files": [ 40 | "src/utils-functions.php" 41 | ] 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "SubstitutionPlugin\\": [ 46 | "tests/e2e/", 47 | "tests/unit/" 48 | ] 49 | }, 50 | "files": [ 51 | "tests/BaseTestCase.php" 52 | ], 53 | "exclude-from-classmap": [ 54 | "**/vendor/" 55 | ] 56 | }, 57 | "config": { 58 | "optimize-autoloader": true, 59 | "sort-packages": true 60 | }, 61 | "extra": { 62 | "class": "SubstitutionPlugin\\SubstitutionPlugin" 63 | }, 64 | "scripts": { 65 | "test": [ 66 | "@composer validate --no-interaction --strict", 67 | "@test:unit", 68 | "@test:e2e" 69 | ], 70 | "test:bc": "phpunit --testsuite bc_tests", 71 | "test:e2e": "phpunit --testsuite e2e_tests --stop-on-failure --debug", 72 | "test:unit": "phpunit --testsuite unit_tests" 73 | }, 74 | "scripts-descriptions": { 75 | "test": "Validates and tests the plugin.", 76 | "test:bc": "Runs backward compatibility tests", 77 | "test:e2e": "Runs end to end tests", 78 | "test:unit": "Runs unit tests" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Provider/ProviderFactory.php: -------------------------------------------------------------------------------- 1 | composer = $composer; 20 | $this->logger = $logger; 21 | } 22 | 23 | /** 24 | * @inheritDoc 25 | */ 26 | public function getProvider(SubstitutionConfigurationInterface $configuration) 27 | { 28 | try { 29 | $this->logger->debug( 30 | 'Build provider for "{placeholder}" with type {type}', 31 | array( 32 | 'placeholder' => $configuration->getPlaceholder(), 33 | 'type' => $configuration->getType(), 34 | ) 35 | ); 36 | return $this->buildProvider($configuration); 37 | } catch (\Exception $e) { 38 | $this->logger->error(sprintf( 39 | 'Error with configuration extra.substitution.mapping.%s: %s', 40 | $configuration->getPlaceholder(), 41 | $e->getMessage() 42 | )); 43 | return null; 44 | } 45 | } 46 | 47 | /** 48 | * @param SubstitutionConfigurationInterface $configuration 49 | * @return ProviderInterface|null 50 | */ 51 | private function buildProvider(SubstitutionConfigurationInterface $configuration) 52 | { 53 | switch ($configuration->getType()) { 54 | case ProviderType::LITERAL: 55 | $provider = new LiteralProvider($configuration->getValue()); 56 | break; 57 | case ProviderType::CALLBACK: 58 | $provider = new CallbackProvider($configuration->getValue()); 59 | break; 60 | case ProviderType::ENV: 61 | $provider = new EnvProvider($configuration->getValue()); 62 | break; 63 | case ProviderType::INCLUDE_PHP: 64 | $provider = new IncludeProvider($configuration->getValue()); 65 | break; 66 | case ProviderType::CONSTANT: 67 | $provider = new ConstantProvider($configuration->getValue()); 68 | break; 69 | case ProviderType::PROCESS: 70 | $provider = new ProcessProvider($configuration->getValue()); 71 | break; 72 | default: 73 | // not supposed to happen 74 | $this->logger->critical('Invalid type: ' . $configuration->getType()); 75 | return null; 76 | } 77 | 78 | if ($configuration->getEscapeCallback() !== null) { 79 | $provider = new EscapeDecoratorProvider($configuration->getEscapeCallback(), $provider); 80 | } 81 | 82 | if ($provider instanceof AutoloadDependentProviderInterface && $provider->mustAutoload()) { 83 | $provider = new AutoloaderDecoratorProvider($this->composer, $this->logger, $provider); 84 | } 85 | 86 | $provider = new LoggerDecoratorProvider($this->logger, $configuration, $provider); 87 | 88 | if ($configuration->isCached()) { 89 | $provider = new CacheDecoratorProvider($provider); 90 | } 91 | 92 | return $provider; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/EventHandler/LegacyEventHandler.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 25 | $this->cmdHelper = new CommandHelper(); 26 | } 27 | 28 | /** 29 | * @inheritDoc 30 | */ 31 | public function getSubscribedEvents() 32 | { 33 | return array(); 34 | } 35 | 36 | public function activate() 37 | { 38 | try { 39 | $scriptNames = $this->getScripts(); 40 | } catch (\Exception $e) { 41 | $scriptNames = array(); 42 | } 43 | 44 | call_user_func($this->callback, $scriptNames); 45 | } 46 | 47 | /** 48 | * @return array 49 | */ 50 | private function getScripts() 51 | { 52 | $definition = new InputDefinition(array( 53 | // default definition 54 | new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'), 55 | 56 | new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'), 57 | new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'), 58 | new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'), 59 | new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version'), 60 | new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'), 61 | new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'), 62 | new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'), 63 | 64 | // custom Composer definition 65 | new InputOption('--profile', null, InputOption::VALUE_NONE), 66 | new InputOption('--no-plugins', null, InputOption::VALUE_NONE), 67 | new InputOption('--working-dir', '-d', InputOption::VALUE_REQUIRED), 68 | new InputOption('--no-cache', null, InputOption::VALUE_NONE), 69 | 70 | // run-script 71 | new InputArgument('script', InputArgument::OPTIONAL), 72 | new InputOption('list', 'l', InputOption::VALUE_NONE), 73 | )); 74 | 75 | $input = new ArgvInput(null, $definition); 76 | $cmd = $input->getArgument('command'); 77 | 78 | if ( 79 | $input->getOption('version') 80 | || $input->getOption('help') 81 | || empty($cmd) 82 | || !is_string($cmd) 83 | ) { 84 | return array(); 85 | } 86 | 87 | return $this->cmdHelper->getScripts($cmd, $input); 88 | } 89 | 90 | public function deactivate() 91 | { 92 | // Nothing to do here 93 | } 94 | 95 | public function uninstall() 96 | { 97 | // Nothing to do here 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Config/PluginConfiguration.php: -------------------------------------------------------------------------------- 1 | = 2.2 22 | $this->enabled = version_compare(Composer::VERSION, '2.2', '>='); 23 | self::setLogger($logger); 24 | 25 | if (!isset($extra['substitution'])) { 26 | self::$logger->debug('Configuration extra.substitution is missing.'); 27 | $this->enabled = false; 28 | } elseif (!is_array($extra['substitution'])) { 29 | self::$logger->warning('Configuration extra.substitution must be an object.'); 30 | $this->enabled = false; 31 | } else { 32 | $this->parseConfiguration($extra['substitution']); 33 | } 34 | } 35 | 36 | /** 37 | * @param array $conf 38 | * @return void 39 | */ 40 | private function parseConfiguration(array $conf) 41 | { 42 | if (isset($conf['enable'])) { 43 | $this->enabled = self::parseBool('enable', $conf['enable'], $this->enabled); 44 | } 45 | if (!$this->enabled) { 46 | // no need to go further 47 | return; 48 | } 49 | if (!isset($conf['mapping'])) { 50 | $this->enabled = false; 51 | self::$logger->notice('Configuration extra.substitution.mapping missing. Plugin disabled.'); 52 | return; 53 | } elseif (!is_array($conf['mapping'])) { 54 | $this->enabled = false; 55 | self::$logger->warning('Configuration extra.substitution.mapping must be an object. Plugin disabled.'); 56 | return; 57 | } else { 58 | $this->mapping = $this->parseMapping($conf['mapping']); 59 | } 60 | 61 | if (count($this->mapping) === 0) { 62 | $this->enabled = false; 63 | self::$logger->notice('Configuration extra.substitution.mapping empty. Plugin disabled.'); 64 | return; 65 | } 66 | 67 | if (isset($conf['priority'])) { 68 | $this->priority = (int) self::parseInt('priority', $conf['priority'], $this->priority); 69 | } 70 | } 71 | 72 | /** 73 | * @param array $conf 74 | * @return SubstitutionConfiguration[] 75 | */ 76 | private function parseMapping(array $conf) 77 | { 78 | $mapping = array(); 79 | 80 | foreach ($conf as $placeholder => $value) { 81 | $substitution = SubstitutionConfiguration::parseConfiguration((string)$placeholder, $value, self::$logger); 82 | if ($substitution !== null) { 83 | $mapping[] = $substitution; 84 | } 85 | } 86 | 87 | return $mapping; 88 | } 89 | 90 | /** 91 | * @inheritDoc 92 | */ 93 | public function isEnabled() 94 | { 95 | return $this->enabled; 96 | } 97 | 98 | /** 99 | * @inheritDoc 100 | */ 101 | public function getPriority() 102 | { 103 | return $this->priority; 104 | } 105 | 106 | /** 107 | * @inheritDoc 108 | */ 109 | public function getMapping() 110 | { 111 | return $this->mapping; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Config/SubstitutionConfiguration.php: -------------------------------------------------------------------------------- 1 | placeholder = $placeholder; 39 | $this->type = $type; 40 | $this->value = $value; 41 | $this->cached = $cached; 42 | $this->escapeCallback = $escapeCallback; 43 | } 44 | 45 | /** 46 | * @param string $placeholder 47 | * @param mixed $conf 48 | * @param LoggerInterface $logger 49 | * @return SubstitutionConfiguration|null 50 | */ 51 | public static function parseConfiguration($placeholder, $conf, LoggerInterface $logger) 52 | { 53 | self::setLogger($logger); 54 | 55 | if ($placeholder === '') { 56 | self::$logger->warning("Configuration extra.substitution.mapping doesn't accept empty keys."); 57 | return null; 58 | } 59 | 60 | if (!is_array($conf)) { 61 | self::$logger->warning("Configuration extra.substitution.mapping.$placeholder must be an object."); 62 | return null; 63 | } 64 | 65 | if (!isset($conf['value'])) { 66 | self::$logger->warning("Configuration extra.substitution.mapping.$placeholder.value is missing."); 67 | return null; 68 | } 69 | 70 | if (!isset($conf['type'])) { 71 | self::$logger->warning("Configuration extra.substitution.mapping.$placeholder.type is missing."); 72 | return null; 73 | } 74 | 75 | if ( 76 | null === ($value = self::parseString("mapping.$placeholder.value", $conf['value'])) 77 | || null === ($type = self::parseEnum("mapping.$placeholder.type", $conf['type'], ProviderType::all())) 78 | ) { 79 | return null; 80 | } 81 | 82 | $cached = isset($conf['cached']) && self::parseBool("mapping.$placeholder.cached", $conf['cached']); 83 | $escape = isset($conf['escape']) ? self::parseString("mapping.$placeholder.escape", $conf['escape']) : null; 84 | 85 | return new SubstitutionConfiguration($placeholder, $type, $value, $cached, $escape); 86 | } 87 | 88 | /** 89 | * @inheritDoc 90 | */ 91 | public function getPlaceholder() 92 | { 93 | return $this->placeholder; 94 | } 95 | 96 | /** 97 | * @inheritDoc 98 | */ 99 | public function getType() 100 | { 101 | return $this->type; 102 | } 103 | 104 | /** 105 | * @inheritDoc 106 | */ 107 | public function getValue() 108 | { 109 | return $this->value; 110 | } 111 | 112 | /** 113 | * @inheritDoc 114 | */ 115 | public function isCached() 116 | { 117 | return $this->cached; 118 | } 119 | 120 | /** 121 | * @inheritDoc 122 | */ 123 | public function getEscapeCallback() 124 | { 125 | return $this->escapeCallback; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Utils/CommandHelper.php: -------------------------------------------------------------------------------- 1 | normalizeName($command); 19 | 20 | if ($command === 'run-script') { 21 | if ($input->getOption('list')) { 22 | return array(); 23 | } 24 | 25 | $scriptNames = array($input->getArgument('script')); 26 | } elseif (!$this->tryGetScriptsFromCommand($command, $scriptNames)) { 27 | $scriptNames = array($command); 28 | } 29 | 30 | $scriptNames = array_filter($scriptNames); 31 | $scriptNames[] = 'command'; 32 | 33 | return $scriptNames; 34 | } 35 | 36 | /** 37 | * @param string|null $commandName 38 | * @return string 39 | */ 40 | private function normalizeName($commandName) 41 | { 42 | global $application; 43 | $app = $application === null ? new Application() : $application; 44 | 45 | try { 46 | $cmd = $app->find($commandName); 47 | $name = $cmd->getName(); 48 | $getDefaultNameFunc = array($cmd, 'getDefaultName'); 49 | if ($name === null && is_callable($getDefaultNameFunc)) { 50 | $name = call_user_func($getDefaultNameFunc); 51 | } 52 | if ($name !== null) { 53 | return $name; 54 | } 55 | } catch (CommandNotFoundException $e) { 56 | } 57 | 58 | return (string) $commandName; 59 | } 60 | 61 | /** 62 | * @param string $command 63 | * @param array $scriptNames 64 | * @return bool 65 | */ 66 | private function tryGetScriptsFromCommand($command, &$scriptNames = array()) 67 | { 68 | switch ($command) { 69 | case 'install': 70 | $scriptNames = array( 71 | 'pre-install-cmd', 72 | 'post-install-cmd', 73 | 'pre-autoload-dump', 74 | 'post-autoload-dump', 75 | 'pre-dependencies-solving', 76 | 'post-dependencies-solving', 77 | 'pre-package-install', 78 | 'post-package-install', 79 | 'pre-operations-exec', 80 | 'pre-pool-create', 81 | 'post-file-download', 82 | ); 83 | break; 84 | case 'update': 85 | $scriptNames = array( 86 | 'pre-update-cmd', 87 | 'post-update-cmd', 88 | 'pre-autoload-dump', 89 | 'post-autoload-dump', 90 | 'pre-package-update', 91 | 'post-package-update', 92 | 'pre-package-uninstall', 93 | 'post-package-uninstall', 94 | 'pre-operations-exec', 95 | 'pre-pool-create', 96 | 'post-file-download', 97 | ); 98 | break; 99 | case 'remove': 100 | $scriptNames = array( 101 | 'pre-package-uninstall', 102 | 'post-package-uninstall', 103 | 'pre-operations-exec', 104 | ); 105 | break; 106 | case 'dump-autoload': 107 | $scriptNames = array( 108 | 'pre-autoload-dump', 109 | 'post-autoload-dump' 110 | ); 111 | break; 112 | case 'status': 113 | $scriptNames = array( 114 | 'pre-status-cmd', 115 | 'post-status-cmd', 116 | ); 117 | break; 118 | case 'archive': 119 | $scriptNames = array( 120 | 'pre-archive-cmd', 121 | 'post-archive-cmd', 122 | ); 123 | break; 124 | default: 125 | return false; 126 | } 127 | 128 | return true; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Config/AbstractConfiguration.php: -------------------------------------------------------------------------------- 1 | notice("Configuration extra.substitution.$key should be a boolean."); 35 | return true; 36 | } 37 | if ($value === 0 || (is_string($value) && in_array(strtolower($value), array('false', 'off', '0')))) { 38 | self::$logger->notice("Configuration extra.substitution.$key should be a boolean."); 39 | return false; 40 | } 41 | 42 | self::$logger->warning("Invalid value for configuration extra.substitution.$key."); 43 | return $defaultValue; 44 | } 45 | 46 | /** 47 | * @param string $key 48 | * @param mixed $value 49 | * @param string|null $defaultValue 50 | * @return string|null 51 | */ 52 | protected static function parseString($key, $value, $defaultValue = null) 53 | { 54 | if (!is_scalar($value)) { 55 | self::$logger->warning("Configuration extra.substitution.$key must be a string."); 56 | return $defaultValue; 57 | } 58 | if (!is_string($value)) { 59 | self::$logger->notice("Configuration extra.substitution.$key should be a string."); 60 | } 61 | 62 | return (string) $value; 63 | } 64 | 65 | /** 66 | * @param string $key 67 | * @param mixed $value 68 | * @param string[] $acceptedValues 69 | * @param string|null $defaultValue 70 | * @return string|null 71 | */ 72 | protected static function parseEnum($key, $value, array $acceptedValues, $defaultValue = null) 73 | { 74 | if (is_string($value)) { 75 | if (in_array($value, $acceptedValues)) { 76 | return $value; 77 | } 78 | if (in_array(strtolower($value), $acceptedValues)) { 79 | self::$logger->notice("Configuration extra.substitution.$key ($value) should be in lowercase."); 80 | return strtolower($value); 81 | } 82 | } 83 | 84 | self::$logger->warning( 85 | "Invalid value for configuration extra.substitution.$key. Accepted values: {values}. {default}", 86 | array( 87 | 'values' => function () use ($acceptedValues) { 88 | return implode(', ', $acceptedValues); 89 | }, 90 | 'default' => function () use ($defaultValue) { 91 | return $defaultValue === null ? '' : "Default to '$defaultValue'."; 92 | }, 93 | ) 94 | ); 95 | 96 | return $defaultValue; 97 | } 98 | 99 | /** 100 | * @param string $key 101 | * @param mixed $value 102 | * @param int|null $defaultValue 103 | * @return int|null 104 | */ 105 | protected static function parseInt($key, $value, $defaultValue = null) 106 | { 107 | if (is_int($value)) { 108 | return $value; 109 | } 110 | if (is_float($value)) { 111 | if ($value != (int) $value) { 112 | self::$logger->notice("Configuration extra.substitution.$key must be an integer."); 113 | } 114 | return (int) $value; 115 | } 116 | if (is_string($value) && is_numeric($value)) { 117 | if (!ctype_digit($value)) { 118 | self::$logger->notice("Configuration extra.substitution.$key must be an integer."); 119 | } 120 | return (int) $value; 121 | } 122 | 123 | self::$logger->warning( 124 | "Invalid value for configuration extra.substitution.$key. It must be an integer. {default}", 125 | array( 126 | 'default' => function () use ($defaultValue) { 127 | return $defaultValue === null ? '' : "Default to $defaultValue."; 128 | }, 129 | ) 130 | ); 131 | 132 | return $defaultValue; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/SubstitutionPlugin.php: -------------------------------------------------------------------------------- 1 | includeRequiredFiles(); 42 | $this->composer = $composer; 43 | $this->logger = $logger = LoggerFactory::getLogger($io); 44 | $config = new PluginConfiguration($composer->getPackage()->getExtra(), $logger); 45 | 46 | $this->logger->info( 47 | 'Plugin ' . ($config->isEnabled() ? 'enabled. {priority}' : 'disabled.'), 48 | array( 49 | 'priority' => function () use ($config) { 50 | return 'Priority set to ' . strval($config->getPriority()) . '.'; 51 | }, 52 | ) 53 | ); 54 | $providerFactory = new ProviderFactory($composer, $logger); 55 | $transformerFactory = new TransformerFactory($providerFactory, $logger); 56 | $this->transformerManager = new TransformerManager($transformerFactory, $config, $logger); 57 | $eventHandlerFactory = new EventHandlerFactory(array($this, 'execute'), $config); 58 | self::$eventHandler = $eventHandlerFactory->getEventHandler(); 59 | self::$eventHandler->activate(); 60 | } 61 | 62 | /** 63 | * @param Composer $composer 64 | * @param IOInterface $io 65 | * @return void 66 | */ 67 | public function deactivate(Composer $composer, IOInterface $io) 68 | { 69 | self::$eventHandler->deactivate(); 70 | } 71 | 72 | /** 73 | * @param Composer $composer 74 | * @param IOInterface $io 75 | * @return void 76 | */ 77 | public function uninstall(Composer $composer, IOInterface $io) 78 | { 79 | self::$eventHandler->uninstall(); 80 | } 81 | 82 | /** 83 | * @inheritDoc 84 | */ 85 | public static function getSubscribedEvents() 86 | { 87 | return self::$eventHandler->getSubscribedEvents(); 88 | } 89 | 90 | /** 91 | * This is needed because getSubscribedEvents() can not return callbacks pointing to other classes. 92 | * 93 | * @param string $name 94 | * @param array $args 95 | * @return mixed 96 | */ 97 | public function __call($name, array $args) 98 | { 99 | /** @var callable $callback */ 100 | $callback = array(self::$eventHandler, $name); 101 | 102 | return call_user_func_array($callback, $args); 103 | } 104 | 105 | /** 106 | * @param string[] $scriptNames 107 | * @return void 108 | */ 109 | public function execute(array $scriptNames) { 110 | if (empty($scriptNames)) { 111 | return; 112 | } 113 | 114 | $package = $this->composer->getPackage(); 115 | if ($package instanceof AliasPackage) { 116 | $package = $package->getAliasOf(); 117 | } 118 | 119 | if ($package instanceof CompletePackage) { 120 | $package->setScripts($this->applySubstitutions($package->getScripts(), $scriptNames)); 121 | } 122 | } 123 | 124 | /** 125 | * @param array $scripts 126 | * @param string[] $scriptNames 127 | * @return array 128 | */ 129 | private function applySubstitutions(array $scripts, array $scriptNames) 130 | { 131 | $this->logger->info('Start applying substitutions on scripts: ' . implode(', ', $scriptNames)); 132 | 133 | return $this->transformerManager->applySubstitutions($scripts, $scriptNames); 134 | } 135 | 136 | /** 137 | * @return void 138 | */ 139 | private function includeRequiredFiles() 140 | { 141 | require_once __DIR__ . '/utils-functions.php'; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Logger/VeryVerboseLogger.php: -------------------------------------------------------------------------------- 1 | io = $io; 19 | } 20 | 21 | /** 22 | * @inheritDoc 23 | */ 24 | public function log($level, $message, array $context = array()) 25 | { 26 | $log = self::LOG_PREFIX . $this->interpolate($message, $context); 27 | 28 | switch ($level) { 29 | case LogLevel::DEBUG: 30 | $this->io->write($log); 31 | break; 32 | case LogLevel::INFO: 33 | $this->io->write('' . $log . ''); 34 | break; 35 | case LogLevel::NOTICE: 36 | case LogLevel::WARNING: 37 | $this->io->write('' . $log . ''); 38 | break; 39 | default: 40 | $this->io->writeError('' . $log . ''); 41 | break; 42 | } 43 | } 44 | 45 | /** 46 | * Interpolates context values into the message placeholders. 47 | * 48 | * @author PHP Framework Interoperability Group 49 | * 50 | * @param string $message 51 | * @param array $context 52 | * @return string 53 | */ 54 | private function interpolate($message, array $context) 55 | { 56 | if (false === strpos($message, '{')) { 57 | return $message; 58 | } 59 | 60 | $replacements = array(); 61 | foreach ($context as $key => $val) { 62 | if (is_callable($val)) { 63 | $val = call_user_func($val); 64 | } 65 | if (null === $val || is_scalar($val) || (\is_object($val) && method_exists($val, '__toString'))) { 66 | $replacements["{{$key}}"] = $val; 67 | } elseif ($val instanceof \DateTime || $val instanceof \DateTimeInterface) { 68 | $replacements["{{$key}}"] = $val->format(\DateTime::RFC3339); 69 | } elseif (\is_object($val)) { 70 | $replacements["{{$key}}"] = '[object '.\get_class($val).']'; 71 | } else { 72 | $replacements["{{$key}}"] = '['.\gettype($val).']'; 73 | } 74 | } 75 | 76 | return strtr($message, $replacements); 77 | } 78 | /** 79 | * System is unusable. 80 | * 81 | * @param string $message 82 | * @param array $context 83 | * 84 | * @return void 85 | */ 86 | public function emergency($message, array $context = array()) 87 | { 88 | $this->log(LogLevel::EMERGENCY, $message, $context); 89 | } 90 | 91 | /** 92 | * Action must be taken immediately. 93 | * 94 | * Example: Entire website down, database unavailable, etc. This should 95 | * trigger the SMS alerts and wake you up. 96 | * 97 | * @param string $message 98 | * @param array $context 99 | * 100 | * @return void 101 | */ 102 | public function alert($message, array $context = array()) 103 | { 104 | $this->log(LogLevel::ALERT, $message, $context); 105 | } 106 | 107 | /** 108 | * Critical conditions. 109 | * 110 | * Example: Application component unavailable, unexpected exception. 111 | * 112 | * @param string $message 113 | * @param array $context 114 | * 115 | * @return void 116 | */ 117 | public function critical($message, array $context = array()) 118 | { 119 | $this->log(LogLevel::CRITICAL, $message, $context); 120 | } 121 | 122 | /** 123 | * Runtime errors that do not require immediate action but should typically 124 | * be logged and monitored. 125 | * 126 | * @param string $message 127 | * @param array $context 128 | * 129 | * @return void 130 | */ 131 | public function error($message, array $context = array()) 132 | { 133 | $this->log(LogLevel::ERROR, $message, $context); 134 | } 135 | 136 | /** 137 | * Exceptional occurrences that are not errors. 138 | * 139 | * Example: Use of deprecated APIs, poor use of an API, undesirable things 140 | * that are not necessarily wrong. 141 | * 142 | * @param string $message 143 | * @param array $context 144 | * 145 | * @return void 146 | */ 147 | public function warning($message, array $context = array()) 148 | { 149 | $this->log(LogLevel::WARNING, $message, $context); 150 | } 151 | 152 | /** 153 | * Normal but significant events. 154 | * 155 | * @param string $message 156 | * @param array $context 157 | * 158 | * @return void 159 | */ 160 | public function notice($message, array $context = array()) 161 | { 162 | $this->log(LogLevel::NOTICE, $message, $context); 163 | } 164 | 165 | /** 166 | * Interesting events. 167 | * 168 | * Example: User logs in, SQL logs. 169 | * 170 | * @param string $message 171 | * @param array $context 172 | * 173 | * @return void 174 | */ 175 | public function info($message, array $context = array()) 176 | { 177 | $this->log(LogLevel::INFO, $message, $context); 178 | } 179 | 180 | /** 181 | * Detailed debug information. 182 | * 183 | * @param string $message 184 | * @param array $context 185 | * 186 | * @return void 187 | */ 188 | public function debug($message, array $context = array()) 189 | { 190 | $this->log(LogLevel::DEBUG, $message, $context); 191 | } 192 | } 193 | --------------------------------------------------------------------------------