From c2be70518e757b63c1abbf6051e6111378fd3032 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 31 Oct 2016 15:24:00 +0100 Subject: [PATCH 1/2] Initial commit of a version which works by changing PHP_CodeSniffer's configuration --- README.md | 40 +-------- composer.json | 3 + src/Installer.php | 137 ------------------------------ src/Plugin.php | 206 +++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 209 insertions(+), 177 deletions(-) delete mode 100644 src/Installer.php diff --git a/README.md b/README.md index b17828cb..255dd97c 100644 --- a/README.md +++ b/README.md @@ -27,38 +27,9 @@ Add the following lines to your `composer.json` file: "require-dev": { "squizlabs/php_codesniffer": "^2.0.0", "dealerdirect/phpcodesniffer-composer-installer" : "*", - "wimg/php-compatibility": "*" -}, -"extra": { - "phpcodesniffer-mapping": { - "wimg/php-compatibility": "PHPCompatibility" - } + "frenck/php-compatibility": "*" } ``` -## Mapping Coding Standards - -In case a coding standard does not provide its PHP_CodeSniffer standard name, this plugin -will create a name for it. Like in the example above, the `wimg/php-compatibility` package -provides a coding standard called 'PHPCompatibility', nevertheless, the package does not -provide this name. - -The `phpcodesniffer-mapping` parameter in the `extra` section of your `composer.json` allows -you to provide a name yourself. - -The following example will install the same repository as above, but will name the standard -`PHPCompat` instead of `PHPCompatibility` as show in the first version. - -```json -"extra": { - "phpcodesniffer-mapping": { - "wimg/php-compatibility": "PHPCompat" - } -} -``` - -The above mapping can also be used to override the coding standard name, if the package -provided one. This may be useful to avoid collisions between different coding standards -using the same name. ## Developing Coding Standards @@ -74,20 +45,15 @@ Create a composer package of your coding standard by adding a `composer.json` fi "php" : ">=5.4.0,<8.0.0-dev", "squizlabs/php_codesniffer" : "^2.0" }, - "type" : "phpcodesniffer-standard", - "extra": { - "phpcodesniffer-standard": "ACME" - } + "type" : "phpcodesniffer-standard" } ``` Requirements: -* Only one (1) standard per repository is allowed. If you'll need to bundle multiple standards, please consider separate packages bundled using a Composer [metapackage]. +* The repository may contain one or more standards. Each in their separate directory in the root of your repository. * The package `type` must be `phpcodesniffer-standard`. Without this, the plugin will not trigger. -* Please provide a standard name by specifing the `phpcodesniffer-standard` option in the `extra` section of your `composer.json` With this name, the coding standard will be visible in [PHP_CodeSniffer]. [this]: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Coding-Standard-Tutorial -[metapackage]: https://getcomposer.org/doc/04-schema.md#type ## Contributing diff --git a/composer.json b/composer.json index 7df7b5a6..9b26bb0d 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,9 @@ "composer-plugin-api": "^1.0", "squizlabs/php_codesniffer": "*" }, + "require-dev": { + "composer/composer": "*" + }, "suggest": { "dealerdirect/qa-tools": "All the PHP QA tools you'll need" }, diff --git a/src/Installer.php b/src/Installer.php deleted file mode 100644 index e3267e47..00000000 --- a/src/Installer.php +++ /dev/null @@ -1,137 +0,0 @@ - - */ -class Installer extends LibraryInstaller -{ - /** - * {@inheritDoc} - */ - public function getInstallPath(PackageInterface $package) - { - $this->initializeVendorDir(); - - $packages = $this->composer->getRepositoryManager()->getLocalRepository()->getPackages(); - $packages[] = $this->composer->getPackage(); - - $mapping = []; - foreach($packages as $localPackage) { - $extra = $localPackage->getExtra(); - if (isset($extra['phpcodesniffer-mapping']) === true) { - $mapping = array_merge($mapping, $extra['phpcodesniffer-mapping']); - } - } - - $packageExtra = $package->getExtra(); - - if (isset($mapping[$package->getPrettyName()]) === true) { - $standardDir = $mapping[$package->getPrettyName()]; - } elseif (isset($packageExtra['phpcodesniffer-standard']) === true) { - $standardDir = $packageExtra['phpcodesniffer-standard']; - } else { - $standardDir = $this->generateStandardNameFromPackage($package); - } - - return implode( - DIRECTORY_SEPARATOR, - [ - $this->vendorDir ? $this->vendorDir : '.', - 'squizlabs', - 'php_codesniffer', - $this->getCodeSnifferSourceDirectory(), - 'Standards', - $standardDir, - ] - ); - } - - /** - * {@inheritDoc} - */ - protected function getPackageBasePath(PackageInterface $package) - { - return $this->getInstallPath($package); - } - - /** - * {@inheritDoc} - */ - public function supports($packageType) - { - return ($packageType === 'phpcodesniffer-standard' || $packageType === 'phpcs-standard'); - } - - /** - * Get the source directory of the PHP_CodeSniffer version installed. - * - * For version 3.* the source folder name has changed. - * - * @return string PHP_CodeSniffer source directory - */ - protected function getCodeSnifferSourceDirectory() - { - $localRepository = $this->composer->getRepositoryManager()->getLocalRepository(); - - if ($localRepository->findPackage('squizlabs/php_codesniffer', '^3.0.0') === null) { - return 'CodeSniffer'; - } - - return 'src'; - } - - /** - * Generates a coding standard name. - * - * In case there is no coding standard name provided, this function - * will generate one based on the Composer package name. - * - * @param PackageInterface $package Composer package - * - * @return string Coding standard name - */ - protected function generateStandardNameFromPackage(PackageInterface $package) - { - list($vendorName, $packageName) = explode('/', $package->getPrettyName(), 2); - - $sanitizePackageNamePatterns = [ - '/^standards?$/i', - '/^coding-?standards?$/i', - '/^standards?-/i', - '/^coding-?standards?-/i', - '/^(php)?codesniffer-/i', - '/-coding-?standards?$/i', - '/-standards?$/i', - ]; - - $packageName = preg_replace($sanitizePackageNamePatterns, '', $packageName); - $packageName = str_replace('-', ' ', $packageName); - $vendorName = str_replace('-', ' ', $vendorName); - - if ($packageName === '') { - $standardName = $vendorName; - } else { - $standardName = sprintf('%s - %s', $vendorName, $packageName); - } - - $standardName = ucwords($standardName); - $standardDir = str_replace(' ', '', $standardName); - - return $standardDir; - } -} diff --git a/src/Plugin.php b/src/Plugin.php index 48cd18a0..b179f3de 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -11,22 +11,222 @@ namespace Dealerdirect\Composer\Plugin\Installers\PHPCodeSniffer; use Composer\Composer; +use Composer\EventDispatcher\EventSubscriberInterface; use Composer\IO\IOInterface; +use Composer\Package\AliasPackage; +use Composer\Package\PackageInterface; use Composer\Plugin\PluginInterface; +use Composer\Script\ScriptEvents; +use Symfony\Component\Process\Exception\LogicException; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Exception\RuntimeException; +use Symfony\Component\Process\Process; /** * PHP_CodeSniffer standard installation manager. * * @author Franck Nijhof */ -class Plugin implements PluginInterface +class Plugin implements PluginInterface, EventSubscriberInterface { + + const PACKAGE_TYPE = 'phpcodesniffer-standard'; + + /** + * @var Composer + */ + protected $composer; + + /** + * @var IOInterface + */ + protected $io; + + /** + * @var array + */ + private $installedPaths; + + /** + * @var string + */ + private $phpCodeSnifferBin; + /** * {@inheritDoc} + * + * @throws \RuntimeException + * @throws LogicException + * @throws RuntimeException + * @throws ProcessFailedException */ public function activate(Composer $composer, IOInterface $io) { - $installer = new Installer($io, $composer); - $composer->getInstallationManager()->addInstaller($installer); + $this->composer = $composer; + $this->io = $io; + $this->installedPaths = []; + $this->phpCodeSnifferBin = $composer->getConfig()->get('bin-dir') . DIRECTORY_SEPARATOR . 'phpcs'; + + $this->loadInstalledPaths(); + } + + /** + * {@inheritDoc} + */ + public static function getSubscribedEvents() + { + return [ + ScriptEvents::POST_INSTALL_CMD => [ + ['onScriptPost', 0], + ], + ScriptEvents::POST_UPDATE_CMD => [ + ['onScriptPost', 0], + ], + ]; + } + + /** + * Entry point for post install and post update events + * + * @throws RuntimeException + * @throws LogicException + * @throws ProcessFailedException + */ + public function onScriptPost() + { + // Ensure PHP_CodeSniffer is installed + if ($this->isPHPCodeSnifferInstalled() === false) { + return; + } + + // Clean and update. Trigger a save when something changed. + if ($this->cleanInstalledPaths() === true || $this->updateInstalledPaths() === true) { + $this->saveInstalledPaths(); + } + } + + /** + * Load all paths from PHP_CodeSniffer into an array + * + * @throws RuntimeException + * @throws LogicException + * @throws ProcessFailedException + */ + private function loadInstalledPaths() + { + $phpcs = new Process($this->phpCodeSnifferBin . ' --config-show installed_paths'); + $phpcs->mustRun(); + + $phpcsInstalledPaths = str_replace('installed_paths: ', '', $phpcs->getOutput()); + $phpcsInstalledPaths = trim($phpcsInstalledPaths); + + if ($phpcsInstalledPaths !== '') { + $this->installedPaths = explode(',', $phpcsInstalledPaths); + } + } + + /** + * Save all coding standard paths back into PHP_CodeSniffer + * + * @throws RuntimeException + * @throws LogicException + * @throws ProcessFailedException + */ + private function saveInstalledPaths() + { + // In case we have no installed paths anymore, ensure the config key is deleted, else update it. + if (count($this->installedPaths) === 0) { + $phpcs = new Process( + $this->phpCodeSnifferBin . ' --config-delete installed_paths' + ); + } else { + $phpcsInstalledPaths = implode(',', $this->installedPaths); + $phpcs = new Process( + $this->phpCodeSnifferBin . ' --config-set installed_paths "' . $phpcsInstalledPaths . '"' + ); + } + + $phpcs->run(); + } + + /** + * Iterate trough all known paths and check if they are still valid. + * + * If path does not exists, is not an directory or isn't readble, the path is removed from the list. + * + * @return bool True if changes where made, false otherwise + */ + private function cleanInstalledPaths() + { + $changes = false; + foreach ($this->installedPaths as $key => $path) { + if (file_exists($path) === false || is_dir($path) === false || is_readable($path) === false) { + unset($this->installedPaths[$key]); + $changes = true; + } + } + return $changes; + } + + /** + * Check all installed packages against the installed paths from PHP_CodeSniffer and add the missing ones. + * + * @return bool True if changes where made, false otherwise + */ + private function updateInstalledPaths() + { + $changes = false; + $codingStandardPackages = $this->getPHPCodingStandardPackages(); + + foreach ($codingStandardPackages as $package) { + $packageInstallPath = $this->composer->getInstallationManager()->getInstallPath($package); + if (in_array($packageInstallPath, $this->installedPaths, true) === false) { + $this->installedPaths[] = $packageInstallPath; + $changes = true; + } + } + + return $changes; + } + + /** + * Iterates trough Composers' local repository looking for valid Coding Standard packages + * + * @return array Composer packages containing coding standard(s) + */ + private function getPHPCodingStandardPackages() + { + $codingStandardPackages = array_filter( + $this->composer->getRepositoryManager()->getLocalRepository()->getPackages(), + function (PackageInterface $package) { + if ($package instanceof AliasPackage) { + return false; + } + return $package->getType() === Plugin::PACKAGE_TYPE; + } + ); + + if ($this->composer->getPackage()->getType() === self::PACKAGE_TYPE) { + $codingStandardPackages[] = $this->composer->getPackage(); + } + + return $codingStandardPackages; + } + + /** + * Simple check if PHP_CodeSniffer is installed. + * + * @return bool PHP_CodeSniffer is installed + */ + private function isPHPCodeSnifferInstalled() + { + // Check if PHP_CodeSniffer is actually installed + return (count( + $this + ->composer + ->getRepositoryManager() + ->getLocalRepository() + ->findPackages('squizlabs/php_codesniffer') + ) !== 0); } } From 9b40c25de44e9bd371ebcc56defba451536a250e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Nov 2016 11:06:54 +0100 Subject: [PATCH 2/2] Updated plugin based on PR comments. Added the ProcessBuilder and changed naming and visibility of some methods & variables. --- src/Plugin.php | 64 +++++++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/src/Plugin.php b/src/Plugin.php index b179f3de..55fd1974 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -20,7 +20,7 @@ use Symfony\Component\Process\Exception\LogicException; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Exception\RuntimeException; -use Symfony\Component\Process\Process; +use Symfony\Component\Process\ProcessBuilder; /** * PHP_CodeSniffer standard installation manager. @@ -35,12 +35,12 @@ class Plugin implements PluginInterface, EventSubscriberInterface /** * @var Composer */ - protected $composer; + private $composer; /** * @var IOInterface */ - protected $io; + private $io; /** * @var array @@ -48,9 +48,9 @@ class Plugin implements PluginInterface, EventSubscriberInterface private $installedPaths; /** - * @var string + * @var ProcessBuilder */ - private $phpCodeSnifferBin; + private $processBuilder; /** * {@inheritDoc} @@ -65,7 +65,9 @@ public function activate(Composer $composer, IOInterface $io) $this->composer = $composer; $this->io = $io; $this->installedPaths = []; - $this->phpCodeSnifferBin = $composer->getConfig()->get('bin-dir') . DIRECTORY_SEPARATOR . 'phpcs'; + + $this->processBuilder = new ProcessBuilder(); + $this->processBuilder->setPrefix($composer->getConfig()->get('bin-dir') . DIRECTORY_SEPARATOR . 'phpcs'); $this->loadInstalledPaths(); } @@ -77,10 +79,10 @@ public static function getSubscribedEvents() { return [ ScriptEvents::POST_INSTALL_CMD => [ - ['onScriptPost', 0], + ['onDependenciesChangedEvent', 0], ], ScriptEvents::POST_UPDATE_CMD => [ - ['onScriptPost', 0], + ['onDependenciesChangedEvent', 0], ], ]; } @@ -92,16 +94,15 @@ public static function getSubscribedEvents() * @throws LogicException * @throws ProcessFailedException */ - public function onScriptPost() + public function onDependenciesChangedEvent() { - // Ensure PHP_CodeSniffer is installed - if ($this->isPHPCodeSnifferInstalled() === false) { - return; - } + if ($this->isPHPCodeSnifferInstalled() === true ) { + $installPathCleaned = $this->cleanInstalledPaths(); + $installPathUpdated = $this->updateInstalledPaths(); - // Clean and update. Trigger a save when something changed. - if ($this->cleanInstalledPaths() === true || $this->updateInstalledPaths() === true) { - $this->saveInstalledPaths(); + if ($installPathCleaned === true || $installPathUpdated === true) { + $this->saveInstalledPaths(); + } } } @@ -114,10 +115,14 @@ public function onScriptPost() */ private function loadInstalledPaths() { - $phpcs = new Process($this->phpCodeSnifferBin . ' --config-show installed_paths'); - $phpcs->mustRun(); - $phpcsInstalledPaths = str_replace('installed_paths: ', '', $phpcs->getOutput()); + $output = $this->processBuilder + ->setArguments(['--config-show', 'installed_paths']) + ->getProcess() + ->mustRun() + ->getOutput(); + + $phpcsInstalledPaths = str_replace('installed_paths: ', '', $output); $phpcsInstalledPaths = trim($phpcsInstalledPaths); if ($phpcsInstalledPaths !== '') { @@ -134,19 +139,18 @@ private function loadInstalledPaths() */ private function saveInstalledPaths() { - // In case we have no installed paths anymore, ensure the config key is deleted, else update it. - if (count($this->installedPaths) === 0) { - $phpcs = new Process( - $this->phpCodeSnifferBin . ' --config-delete installed_paths' - ); - } else { - $phpcsInstalledPaths = implode(',', $this->installedPaths); - $phpcs = new Process( - $this->phpCodeSnifferBin . ' --config-set installed_paths "' . $phpcsInstalledPaths . '"' - ); + // By default we delete the installed paths + $arguments = ['--config-delete', 'installed_paths']; + + // This changes in case we do have installed_paths + if (count($this->installedPaths) !== 0) { + $arguments = ['--config-set', 'installed_paths', implode(',', $this->installedPaths)]; } - $phpcs->run(); + $this->processBuilder + ->setArguments($arguments) + ->getProcess() + ->mustRun(); } /**