Skip to content

Commit

Permalink
Merge pull request #5 from zaporylie/issue/3
Browse files Browse the repository at this point in the history
Allow setting custom lowest-tags in root composer.json

Fixes #6, #3, #7
  • Loading branch information
zaporylie authored Feb 20, 2019
2 parents 174c30d + 004c111 commit 173c198
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 21 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/vendor/
composer.lock
.phpunit.result.cache
/test
36 changes: 30 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Optimize Composer for Drupal 8.5+ projects
Optimize Composer for Drupal projects
====
[![Build Status](https://travis-ci.org/zaporylie/composer-drupal-optimizations.svg?branch=master)](https://travis-ci.org/zaporylie/composer-drupal-optimizations)
![Packagist](https://img.shields.io/packagist/v/zaporylie/composer-drupal-optimizations.svg)
Expand All @@ -18,9 +18,7 @@ No configuration required 🎊

# Optimizations

- Reduce memory usage and CPU usage by removing legacy symfony tags (see also https://github.com/symfony/flex/pull/378)

(only one at the moment)
- Reduce memory usage and CPU usage by removing legacy symfony tags

# Benchmark

Expand All @@ -29,15 +27,41 @@ Following numbers are for clean https://github.com/drupal-composer/drupal-projec
Before:

```
Memory usage: 304.16MB (peak: 876.79MB), time: 17.13s
Memory usage: 323.19MB (peak: 1121.09MB), time: 13.68s
```

After:

```
Memory usage: 218.72MB (peak: 250.44MB), time: 4.83s
Memory usage: 238.66MB (peak: 297.17MB), time: 4.84s
```

> php 7.2, macOS High Sierra, i7, 16GB RAM
# Configuration

If no configuration is provided this package will provide sensible defaults based on the content of project's composer.json
file. Default configuration should cover 99% of the cases. However, in case you want to manually specify the tags
that should be filtered out you are welcome to use the `extra` section:

```json
{
"extra": {
"composer-drupal-optimizations": {
"require": {
"symfony/symfony": ">3.4"
}
}
}
}
```

***Recommendation note:***
Use defaults (skip config above) if possible - this package will be maintained throughout the Drupal's lifecycle in order
to optimize legacy constraints in parallel with Drupal's requirements.

All you have to do is to make sure your drupal core constraint is set to `drupal/core: ^8.5` or above.

# Credits

- Symfony community - idea and development; Special thanks to @nicolas-grekas
Expand Down
96 changes: 84 additions & 12 deletions src/Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,36 @@
namespace zaporylie\ComposerDrupalOptimizations;

use Composer\Cache as BaseCache;
use Composer\Semver\Constraint\Constraint;
use Composer\Semver\VersionParser;

/**
* Class Cache
* @package zaporylie\ComposerDrupalOptimizations
*/
class Cache extends BaseCache
{
protected static $lowestTags = [
'symfony/symfony' => 'v3.4.0',
];

/**
* @var array
*/
protected $packages = [];

/**
* @var \Composer\Semver\VersionParser
*/
protected $versionParser;

/**
* {@inheritdoc}
*/
public function read($file)
{
$content = $this->readFile($file);
if (!\is_array($data = json_decode($content, true))) {
return $content;
}
foreach (array_keys(static::$lowestTags) as $key) {
foreach (array_keys($this->packages) as $key) {
list($provider, ) = explode('/', $key, 2);
if (0 === strpos($file, "provider-$provider\$")) {
$data = $this->removeLegacyTags($data);
Expand All @@ -35,21 +47,81 @@ protected function readFile($file)
return parent::read($file);
}

/**
* Removes legacy tags from $data.
*
* @param array $data
* @return array
*/
public function removeLegacyTags(array $data)
{
foreach (self::$lowestTags as $package => $lowestVersion) {
if (!isset($data['packages'][$package][$lowestVersion])) {
// Skip if the list of packages is empty.
if (!$this->packages || empty($data['packages'])) {
return $data;
}

// Skip if none of the packages was found.
if (!array_diff_key($data['packages'], $this->packages)) {
return $data;
}

foreach ($this->packages as $packageName => $packageVersionConstraint) {
if (!isset($data['packages'][$packageName])) {
continue;
}
foreach ($data['packages'] as $package => $versions) {
foreach ($versions as $version => $composerJson) {
if (version_compare($version, $lowestVersion, '<')) {
unset($data['packages'][$package][$version]);
}
$packages = [];
$specificPackage = $data['packages'][$packageName];
foreach ($specificPackage as $version => $composerJson) {
if ('dev-master' === $version) {
$normalizedVersion = $this->versionParser->normalize($composerJson['extra']['branch-alias']['dev-master']);
} else {
$normalizedVersion = $composerJson['version_normalized'];
}
$packageConstraint = $this->versionParser->parseConstraints($packageVersionConstraint);
$versionConstraint = new Constraint('==', $normalizedVersion);
if ($packageConstraint->matches($versionConstraint)) {
$packages += isset($composerJson['replace']) ? $composerJson['replace'] : [];
} else {
unset($specificPackage[$version]);
}
}

// Ignore requirements: their intersection with versions of the package gives empty result.
if (!$specificPackage) {
continue;
}
$data['packages'][$packageName] = $specificPackage;

unset($specificPackage['dev-master']);
foreach ($data['packages'] as $name => $versions) {
if (!isset($packages[$name]) || null === $devMasterAlias = (isset($versions['dev-master']['extra']['branch-alias']['dev-master']) ? $versions['dev-master']['extra']['branch-alias']['dev-master'] : null)) {
continue;
}
$devMaster = $versions['dev-master'];
$versions = array_intersect_key($versions, $specificPackage);
$packageConstraint = $this->versionParser->parseConstraints($packageVersionConstraint);
$versionConstraint = new Constraint('==', $this->versionParser->normalize($devMasterAlias));
if ($packageConstraint->matches($versionConstraint)) {
$versions['dev-master'] = $devMaster;
}
if ($versions) {
$data['packages'][$name] = $versions;
}
}
break;
}
return $data;

}

/**
* @param array $packages
*
* @return $this
*/
public function setRequiredVersionConstraints(array $packages) {
$this->versionParser = new VersionParser();
$this->packages = $packages;
return $this;
}

}
42 changes: 41 additions & 1 deletion src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,64 @@
use Composer\Plugin\PluginInterface;
use Composer\Repository\RepositoryFactory;
use Composer\Repository\RepositoryManager;
use Composer\Semver\Constraint\Constraint;
use Composer\Semver\Constraint\ConstraintInterface;

class Plugin implements PluginInterface
{

public function activate(Composer $composer, IOInterface $io)
{
// Set default version constraints based on the composer requirements.
$extra = $composer->getPackage()->getExtra();
$packages = $composer->getPackage()->getRequires();
if (!isset($extra['composer-drupal-optimizations']['require']) && isset($packages['drupal/core'])) {
$coreConstraint = $packages['drupal/core']->getConstraint();
$extra['composer-drupal-optimizations']['require'] = static::getDefaultRequire($coreConstraint);
if (!empty($extra['composer-drupal-optimizations']['require']) && $io->isVerbose()) {
$io->write('Required tags were not explicitly set so the zaporylie/composer-drupal-optimizations set default based on project\'s composer.json content.');
}
}
if (!empty($extra['composer-drupal-optimizations']['require']) && $io->isVerbose()) {
foreach ($extra['composer-drupal-optimizations']['require'] as $package => $version) {
$io->write(sprintf('extra.commerce-drupal-optimizations.require.%s: \'%s\'', $package, $version));
}
}

$rfs = Factory::createRemoteFilesystem($io, $composer->getConfig());
$manager = RepositoryFactory::manager($io, $composer->getConfig(), $composer->getEventDispatcher(), $rfs);
$setRepositories = \Closure::bind(function (RepositoryManager $manager) {
$setRepositories = \Closure::bind(function (RepositoryManager $manager) use ($extra) {
$manager->repositoryClasses = $this->repositoryClasses;
$manager->setRepositoryClass('composer', TruncatedComposerRepository::class);
$manager->repositories = $this->repositories;
$i = 0;
foreach (RepositoryFactory::defaultRepos(null, $this->config, $manager) as $repo) {
$manager->repositories[$i++] = $repo;
if ($repo instanceof TruncatedComposerRepository && !empty($extra['composer-drupal-optimizations']['require'])) {
$repo->setRequiredVersionConstraints($extra['composer-drupal-optimizations']['require']);
}
}
$manager->setLocalRepository($this->getLocalRepository());
}, $composer->getRepositoryManager(), RepositoryManager::class);
$setRepositories($manager);
$composer->setRepositoryManager($manager);
}

/**
* Negotiates default require constraint and package for given drupal/core.
*
* @param \Composer\Semver\Constraint\ConstraintInterface
*
* @return array
*/
static public function getDefaultRequire(ConstraintInterface $coreConstraint)
{
if ((new Constraint('>=', '8.5.0'))->matches($coreConstraint)
&& !(new Constraint('<', '8.5.0'))->matches($coreConstraint)) {
return [
'symfony/symfony' => '>3.4',
];
}
return [];
}
}
5 changes: 4 additions & 1 deletion src/TruncatedComposerRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public function __construct(array $repoConfig, IOInterface $io, Config $config,
protected function fetchFile($filename, $cacheKey = null, $sha256 = null, $storeLastModifiedTime = false)
{
$data = parent::fetchFile($filename, $cacheKey, $sha256, $storeLastModifiedTime);
return $this->cache->removeLegacyTags($data);
return \is_array($data) ? $this->cache->removeLegacyTags($data) : $data;
}
public function setRequiredVersionConstraints(array $packages) {
$this->cache->setRequiredVersionConstraints($packages);
}
}
108 changes: 107 additions & 1 deletion tests/CacheTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ class CacheTest extends TestCase
* @param $expected
*
* @dataProvider provideReadTest
* @covers \zaporylie\ComposerDrupalOptimizations\Cache::read
*/
public function testRead($provided, $expected)
{
$cache = new class(new NullIO(), 'test') extends Cache {
protected static $lowestTags = [
protected $packages = [
'vendor/package' => 'version',
];
protected function readFile($file)
Expand Down Expand Up @@ -52,4 +53,109 @@ function provideReadTest()
yield 'matching-incorrect-provider' => ['{"provider":"vendor"}', '{"provider":"vendor"}'];
yield 'matching' => ['provider-vendor${"provider":"vendor"}', '{"provider":"vendor","status":"ok"}'];
}

/**
* @dataProvider provideRemoveLegacyTags
* @covers \zaporylie\ComposerDrupalOptimizations\Cache::removeLegacyTags
*/
public function testRemoveLegacyTags(array $expected, array $packages, array $versionConstraints)
{
/** @var Cache $cache */
$cache = (new \ReflectionClass(Cache::class))->newInstanceWithoutConstructor();
$cache->setRequiredVersionConstraints($versionConstraints);
$this->assertSame(['packages' => $expected], $cache->removeLegacyTags(['packages' => $packages]));
}

/**
* Test data.
*/
public function provideRemoveLegacyTags()
{
yield 'no-symfony/symfony' => [[123], [123], ['symfony/symfony' => '~1']];
$branchAlias = function ($versionAlias) {
return [
'extra' => [
'branch-alias' => [
'dev-master' => $versionAlias.'-dev',
],
],
];
};
$packages = [
'foo/unrelated' => [
'1.0.0' => [],
],
'symfony/symfony' => [
'3.3.0' => [
'version_normalized' => '3.3.0.0',
'replace' => ['symfony/foo' => 'self.version'],
],
'3.4.0' => [
'version_normalized' => '3.4.0.0',
'replace' => ['symfony/foo' => 'self.version'],
],
'dev-master' => $branchAlias('3.5') + [
'replace' => ['symfony/foo' => 'self.version'],
],
],
'symfony/foo' => [
'3.3.0' => ['version_normalized' => '3.3.0.0'],
'3.4.0' => ['version_normalized' => '3.4.0.0'],
'dev-master' => $branchAlias('3.5'),
],
];
yield 'empty-intersection-ignores' => [$packages, $packages, ['symfony/symfony' => '~2.0']];
yield 'empty-intersection-ignores' => [$packages, $packages, ['symfony/symfony' => '~4.0']];
$expected = $packages;
unset($expected['symfony/symfony']['3.3.0']);
unset($expected['symfony/foo']['3.3.0']);
yield 'non-empty-intersection-filters' => [$expected, $packages, ['symfony/symfony' => '~3.4']];
unset($expected['symfony/symfony']['3.4.0']);
unset($expected['symfony/foo']['3.4.0']);
yield 'master-only' => [$expected, $packages, ['symfony/symfony' => '~3.5']];
$packages = [
'symfony/symfony' => [
'2.8.0' => [
'version_normalized' => '2.8.0.0',
'replace' => [
'symfony/legacy' => 'self.version',
'symfony/foo' => 'self.version',
],
],
],
'symfony/legacy' => [
'2.8.0' => ['version_normalized' => '2.8.0.0'],
'dev-master' => $branchAlias('2.8'),
],
];
yield 'legacy-are-not-filtered' => [$packages, $packages, ['symfony/symfony' => '~3.0']];
$packages = [
'symfony/symfony' => [
'2.8.0' => [
'version_normalized' => '2.8.0.0',
'replace' => [
'symfony/foo' => 'self.version',
'symfony/new' => 'self.version',
],
],
'dev-master' => $branchAlias('3.0') + [
'replace' => [
'symfony/foo' => 'self.version',
'symfony/new' => 'self.version',
],
],
],
'symfony/foo' => [
'2.8.0' => ['version_normalized' => '2.8.0.0'],
'dev-master' => $branchAlias('3.0'),
],
'symfony/new' => [
'dev-master' => $branchAlias('3.0'),
],
];
$expected = $packages;
unset($expected['symfony/symfony']['dev-master']);
unset($expected['symfony/foo']['dev-master']);
yield 'master-is-filtered-only-when-in-range' => [$expected, $packages, ['symfony/symfony' => '~2.8']];
}
}
Loading

0 comments on commit 173c198

Please sign in to comment.