├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json └── src ├── Cache.php ├── Plugin.php └── TruncatedComposerRepository.php /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: php 3 | dist: trusty 4 | 5 | php: 6 | - 5.5 7 | - 5.6 8 | - 7.0 9 | - 7.1 10 | - 7.2 11 | 12 | before_script: 13 | - composer self-update 14 | - php -i 15 | - composer -V 16 | - composer install 17 | 18 | script: 19 | - find src -type f -name "*.php" -print0 | xargs -0 -n1 -P8 php -l 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2018 rubenrua 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Symfony Clean Tags Composer Plugin 2 | ================================== 3 | 4 | Motivation 5 | ---------- 6 | 7 | It was recently identified that Composer consumes high CPU + memory on packages that have a lot of historical tags. See [composer/composer#7577](https://github.com/composer/composer/issues/7577) 8 | 9 | This means the composer+packagist infrastructure has a scalability issue: as time passes, the list of tags per packages grows, and the "Composer experience" degrades. This is significant for `symfony/*` today, and will become also a pain for any other packages over time. 10 | 11 | symfony/flex solves this issue with a patch from @nicolas-grekas using a new extra parameter `extra.symfony.require`: [symfony/flex#378](https://github.com/symfony/flex/pull/378) and [symfony/flex#409](https://github.com/symfony/flex/pull/409) 12 | 13 | This project extracts this patch into a separete composer plugin for legacy projects (PHP5 and Symony 2/3) 14 | 15 | 16 | | | Internal big project | [Sylius/Sylius-Standard](https://github.com/Sylius/Sylius-Standard) | [laravel/laravel](https://github.com/laravel/laravel) 17 | | ----- | ----- | ---- | --- | 18 | | `extra.symfony.require` | "2.8.*" | "^3.4\|^4.1" | "~4.0" | 19 | | Before | Memory: 337.9MB (peak: 1582.09MB), time: 31.84s| Memory: 384.84MB (peak: 1670.44MB), time: 28.11s | Memory: 265.09MB (peak: 417.44MB), time: 6.57s 20 | | After | Memory: 183.05MB (peak: 286.56MB), time: 11.04s | Memory: 218.76MB (peak: 251.73MB), time: 5.02s| Memory: 210.17MB (peak: 236.37MB), time: 4.38s 21 | 22 | Installation 23 | ------------ 24 | 25 | ### Step 1: Profile application without the plugin 26 | 27 | Open a command console, enter your project directory and execute the following command to profile the current memory and CPU time usage. 28 | 29 | ``` 30 | $ composer update --profile --ignore-platform-reqs --dry-run 31 | .... 32 | [833.9MB/199.98s] Memory usage: 833.86MB (peak: 2811.34MB), time: 199.98s 33 | ``` 34 | Write down it to compare with the final step. 35 | 36 | ### Step 2: Download the Bundle 37 | 38 | Execute the following command to installs the composer plugin: 39 | 40 | ``` 41 | $ composer require rubenrua/symfony-clean-tags-composer-plugin 42 | ``` 43 | 44 | or globally with: 45 | 46 | ``` 47 | $ composer global require rubenrua/symfony-clean-tags-composer-plugin 48 | ``` 49 | 50 | ### Step 3: Configure the new extra parameter 51 | 52 | Configure `extra.symfony.require` with the same symfony version constraints used in the application. For instance, if you are using symfony 2.8, execute the following command to modify the config composer section: 53 | 54 | ``` 55 | $ composer config extra.symfony.require 2.8.* 56 | ``` 57 | 58 | Also the `SYMFONY_REQUIRE` environment variable can be used instead of `extra.symfony.require`. See [`symfony/symfony` travis configuration for a example](https://github.com/symfony/symfony/commit/940ec8f2d5c562bc1b2424f67ab0cbd1f3c59e51#diff-354f30a63fb0907d4ad57269548329e3). 59 | 60 | ### Step 4: Profile application with the plugin 61 | 62 | Finally profile the current memory and CPU time usage. Execute again the following command: 63 | 64 | ``` 65 | $ composer update --profile --ignore-platform-reqs --dry-run 66 | .... 67 | [230.7MB/31.02s] Memory usage: 230.67MB (peak: 387.3MB), time: 31.02s 68 | ``` 69 | 70 | Please, feel free to comment the [issue #3](https://github.com/rubenrua/symfony-clean-tags-composer-plugin/issues/3) with your improvement. 71 | 72 | Notes 73 | ----- 74 | 75 | * MIT license. 76 | * Thank you @nicolasgrekas 77 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rubenrua/symfony-clean-tags-composer-plugin", 3 | "type": "composer-plugin", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Ruben Gonzalez", 8 | "email": "rubenrua@gmail.com" 9 | } 10 | ], 11 | "require": { 12 | "composer-plugin-api": "^1.0" 13 | }, 14 | "autoload": { 15 | "psr-4": { 16 | "Rubenrua\\SymfonyCleanTagsComposerPlugin\\": "src" 17 | } 18 | }, 19 | "extra": { 20 | "class": "Rubenrua\\SymfonyCleanTagsComposerPlugin\\Plugin" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Cache.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * Origin: https://github.com/symfony/flex/blob/master/src/Cache.php 12 | */ 13 | 14 | namespace Rubenrua\SymfonyCleanTagsComposerPlugin; 15 | 16 | use Composer\Cache as BaseCache; 17 | use Composer\IO\IOInterface; 18 | use Composer\Semver\Constraint\Constraint; 19 | use Composer\Semver\VersionParser; 20 | 21 | /** 22 | * @author Nicolas Grekas 23 | */ 24 | class Cache extends BaseCache 25 | { 26 | private $versionParser; 27 | private $symfonyRequire; 28 | private $symfonyConstraints; 29 | private $io; 30 | 31 | public function setSymfonyRequire($symfonyRequire, IOInterface $io = null) 32 | { 33 | $this->versionParser = new VersionParser(); 34 | $this->symfonyRequire = $symfonyRequire; 35 | $this->symfonyConstraints = $this->versionParser->parseConstraints($symfonyRequire); 36 | $this->io = $io; 37 | } 38 | 39 | public function read($file) 40 | { 41 | $content = parent::read($file); 42 | 43 | if (0 === strpos($file, 'provider-symfony$') && \is_array($data = json_decode($content, true))) { 44 | $content = json_encode($this->removeLegacyTags($data)); 45 | } 46 | 47 | return $content; 48 | } 49 | 50 | public function removeLegacyTags(array $data) 51 | { 52 | if (!$this->symfonyConstraints || !isset($data['packages']['symfony/symfony'])) { 53 | return $data; 54 | } 55 | 56 | $symfonyPackages = []; 57 | $symfonySymfony = $data['packages']['symfony/symfony']; 58 | 59 | foreach ($symfonySymfony as $version => $composerJson) { 60 | if (null !== $alias = (isset($composerJson['extra']['branch-alias'][$version]) ? $composerJson['extra']['branch-alias'][$version] : null)) { 61 | $normalizedVersion = $this->versionParser->normalize($alias); 62 | } elseif (null === $normalizedVersion = isset($composerJson['version_normalized']) ? $composerJson['version_normalized'] : null) { 63 | continue; 64 | } 65 | 66 | if ($this->symfonyConstraints->matches(new Constraint('==', $normalizedVersion))) { 67 | $symfonyPackages += $composerJson['replace']; 68 | } else { 69 | if (null !== $this->io) { 70 | $this->io->writeError(sprintf('Restricting packages listed in "symfony/symfony" to "%s"', $this->symfonyRequire)); 71 | $this->io = null; 72 | } 73 | unset($symfonySymfony[$version]); 74 | } 75 | } 76 | 77 | if (!$symfonySymfony) { 78 | // ignore requirements: their intersection with versions of symfony/symfony is empty 79 | return $data; 80 | } 81 | 82 | $data['packages']['symfony/symfony'] = $symfonySymfony; 83 | unset($symfonySymfony['dev-master']); 84 | 85 | foreach ($data['packages'] as $name => $versions) { 86 | $devMasterAlias = isset($versions['dev-master']['extra']['branch-alias']['dev-master']) ? 87 | $versions['dev-master']['extra']['branch-alias']['dev-master'] : 88 | null; 89 | if (!isset($symfonyPackages[$name]) || null === $devMasterAlias) { 90 | continue; 91 | } 92 | $devMaster = $versions['dev-master']; 93 | $versions = array_intersect_key($versions, $symfonySymfony); 94 | 95 | if ($this->symfonyConstraints->matches(new Constraint('==', $this->versionParser->normalize($devMasterAlias)))) { 96 | $versions['dev-master'] = $devMaster; 97 | } 98 | 99 | if ($versions) { 100 | $data['packages'][$name] = $versions; 101 | } 102 | } 103 | 104 | return $data; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Plugin.php: -------------------------------------------------------------------------------- 1 | isDebug()) { 18 | $io->writeError('symfony/flex is active: Skip the activation'); 19 | } 20 | 21 | return; 22 | } 23 | 24 | $symfonyRequire = null; 25 | 26 | if (getenv('SYMFONY_REQUIRE')) { 27 | $symfonyRequire = getenv('SYMFONY_REQUIRE'); 28 | } 29 | $extra = $composer->getPackage()->getExtra(); 30 | if (isset($extra['symfony']['require'])) { 31 | $symfonyRequire = $composer->getPackage()->getExtra()['symfony']['require']; 32 | } 33 | 34 | if ($symfonyRequire) { 35 | $config = $composer->getConfig(); 36 | $config->merge(array('config' => array('symfony_require' => $symfonyRequire))); 37 | 38 | $manager = $composer->getRepositoryManager(); 39 | $setRepositories = \Closure::bind(function (RepositoryManager $manager) use ($symfonyRequire) { 40 | $manager->repositoryClasses = $this->repositoryClasses; 41 | $manager->setRepositoryClass('composer', TruncatedComposerRepository::class); 42 | $manager->repositories = $this->repositories; 43 | $i = 0; 44 | foreach (RepositoryFactory::defaultRepos(null, $this->config, $manager) as $repo) { 45 | $manager->repositories[$i++] = $repo; 46 | if ($repo instanceof TruncatedComposerRepository && $symfonyRequire) { 47 | $repo->setSymfonyRequire($symfonyRequire, $this->io); 48 | } 49 | } 50 | $manager->setLocalRepository($this->getLocalRepository()); 51 | }, $composer->getRepositoryManager(), RepositoryManager::class); 52 | 53 | $setRepositories($manager); 54 | $composer->setRepositoryManager($manager); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/TruncatedComposerRepository.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * origin: https://github.com/symfony/flex/blob/master/src/TruncatedComposerRepository.php 12 | */ 13 | 14 | namespace Rubenrua\SymfonyCleanTagsComposerPlugin; 15 | 16 | use Composer\Config; 17 | use Composer\EventDispatcher\EventDispatcher; 18 | use Composer\IO\IOInterface; 19 | use Composer\Repository\ComposerRepository as BaseComposerRepository; 20 | use Composer\Util\RemoteFilesystem; 21 | 22 | /** 23 | * @author Nicolas Grekas 24 | */ 25 | class TruncatedComposerRepository extends BaseComposerRepository 26 | { 27 | public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null) 28 | { 29 | parent::__construct($repoConfig, $io, $config, $eventDispatcher, $rfs); 30 | 31 | $this->cache = new Cache($io, $config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->url), 'a-z0-9.$'); 32 | } 33 | 34 | public function setSymfonyRequire($symfonyRequire, IOInterface $io) 35 | { 36 | $this->cache->setSymfonyRequire($symfonyRequire, $io); 37 | } 38 | 39 | protected function fetchFile($filename, $cacheKey = null, $sha256 = null, $storeLastModifiedTime = false) 40 | { 41 | $data = parent::fetchFile($filename, $cacheKey, $sha256, $storeLastModifiedTime); 42 | 43 | return \is_array($data) ? $this->cache->removeLegacyTags($data) : $data; 44 | } 45 | } 46 | --------------------------------------------------------------------------------