Skip to content

Commit 43af613

Browse files
authored
Cache outdated composer.json by *.lock or *.json project file hash (#2)
* [cli] give clean output * make use of 1 week project cache * add OutdatedComposerFactoryTest
1 parent 3219f6d commit 43af613

File tree

10 files changed

+210
-16
lines changed

10 files changed

+210
-16
lines changed

ecs.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
declare(strict_types=1);
44

5+
use Symplify\CodingStandard\Fixer\LineLength\LineLengthFixer;
56
use Symplify\EasyCodingStandard\Config\ECSConfig;
67

78
return ECSConfig::configure()
89
->withPreparedSets(psr12: true, common: true, symplify: true)
10+
->withRules([LineLengthFixer::class])
911
->withPaths([__DIR__ . '/src', __DIR__ . '/tests'])
1012
->withRootFiles();

src/Composer/ComposerOutdatedResponseProvider.php

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@
44

55
namespace Rector\Jack\Composer;
66

7+
use Nette\Utils\DateTime;
78
use Nette\Utils\FileSystem;
89
use Symfony\Component\Process\Process;
910

1011
final class ComposerOutdatedResponseProvider
1112
{
1213
public function provide(): string
1314
{
14-
// load from cache, temporarily - @todo cache on json hash + week timeout
15-
$outdatedFilename = __DIR__ . '/../../dumped-outdated.json';
16-
if (is_file($outdatedFilename)) {
17-
return FileSystem::read($outdatedFilename);
15+
$composerOutdatedFilePath = $this->resolveComposerOutdatedFilePath();
16+
17+
// let's use cache
18+
if ($this->shouldLoadCacheFile($composerOutdatedFilePath)) {
19+
/** @var string $composerOutdatedFilePath */
20+
return FileSystem::read($composerOutdatedFilePath);
1821
}
1922

2023
$composerOutdatedProcess = Process::fromShellCommandline(
@@ -23,9 +26,60 @@ public function provide(): string
2326
);
2427

2528
$composerOutdatedProcess->mustRun();
29+
2630
$processResult = $composerOutdatedProcess->getOutput();
2731

28-
FileSystem::write($outdatedFilename, $processResult);
32+
if (is_string($composerOutdatedFilePath)) {
33+
FileSystem::write($composerOutdatedFilePath, $processResult);
34+
}
35+
2936
return $processResult;
3037
}
38+
39+
private function resolveProjectComposerHash(): ?string
40+
{
41+
if (file_exists(getcwd() . '/composer.lock')) {
42+
return sha1(getcwd() . '/composer.lock');
43+
}
44+
45+
if (file_exists(getcwd() . '/composer.json')) {
46+
return getcwd() . '/composer.json';
47+
}
48+
49+
return null;
50+
}
51+
52+
private function resolveComposerOutdatedFilePath(): ?string
53+
{
54+
$projectComposerHash = $this->resolveProjectComposerHash();
55+
if ($projectComposerHash) {
56+
// load from cache, temporarily - @todo cache on json hash + week timeout
57+
return sys_get_temp_dir() . '/jack/composer-outdated-' . $projectComposerHash . '.json';
58+
}
59+
60+
return null;
61+
}
62+
63+
private function isFileYoungerThanWeek(string $filePath): bool
64+
{
65+
$fileTime = filemtime($filePath);
66+
if ($fileTime === false) {
67+
return false;
68+
}
69+
70+
return (time() - $fileTime) < DateTime::WEEK;
71+
}
72+
73+
private function shouldLoadCacheFile(?string $cacheFilePath): bool
74+
{
75+
if (! is_string($cacheFilePath)) {
76+
return false;
77+
}
78+
79+
if (! file_exists($cacheFilePath)) {
80+
return false;
81+
}
82+
83+
return $this->isFileYoungerThanWeek($cacheFilePath);
84+
}
3185
}

src/Composer/NextVersionResolver.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@
1111
/**
1212
* @see \Rector\Jack\Tests\Composer\NextVersionResolver\NextVersionResolverTest
1313
*/
14-
final class NextVersionResolver
14+
final readonly class NextVersionResolver
1515
{
1616
private const MAJOR = 'major';
1717

1818
private const MINOR = 'minor';
1919

2020
public function __construct(
21-
private readonly VersionParser $versionParser
21+
private VersionParser $versionParser
2222
) {
2323
}
2424

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Jack\Console\Command;
6+
7+
use Symfony\Component\Console\Application;
8+
use Symfony\Component\Console\Command\Command;
9+
use Symfony\Component\Console\Command\ListCommand;
10+
use Symfony\Component\Console\Descriptor\ApplicationDescription;
11+
use Symfony\Component\Console\Input\InputInterface;
12+
use Symfony\Component\Console\Output\OutputInterface;
13+
use Webmozart\Assert\Assert;
14+
15+
/**
16+
* Simple command list, without bloated options
17+
*/
18+
final class CleanListCommand extends ListCommand
19+
{
20+
protected function execute(InputInterface $input, OutputInterface $output): int
21+
{
22+
Assert::isInstanceOf($this->getApplication(), Application::class);
23+
24+
$output->writeln($this->getApplication()->getName());
25+
$output->writeln('');
26+
$output->writeln('<comment>Available commands:</>');
27+
28+
$applicationDescription = new ApplicationDescription($this->getApplication());
29+
$this->describeCommands($applicationDescription, $output);
30+
31+
return self::SUCCESS;
32+
}
33+
34+
/**
35+
* @param non-empty-array<Command> $commands
36+
*/
37+
private function resolveCommandNameColumnWidth(array $commands): int
38+
{
39+
$commandNameLengths = [];
40+
foreach ($commands as $command) {
41+
$commandNameLengths[] = strlen((string) $command->getName());
42+
}
43+
44+
return max($commandNameLengths) + 4;
45+
}
46+
47+
private function describeCommands(ApplicationDescription $applicationDescription, OutputInterface $output): void
48+
{
49+
if ($applicationDescription->getCommands() === []) {
50+
return;
51+
}
52+
53+
$commands = $applicationDescription->getCommands();
54+
$commandNameColumnWidth = $this->resolveCommandNameColumnWidth($commands);
55+
56+
foreach ($commands as $command) {
57+
$spacingWidth = $commandNameColumnWidth - strlen((string) $command->getName());
58+
59+
$output->writeln(sprintf(
60+
' <info>%s</>%s%s',
61+
$command->getName(),
62+
str_repeat(' ', $spacingWidth),
63+
$command->getDescription()
64+
));
65+
}
66+
}
67+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Jack\Console;
6+
7+
use Rector\Jack\Console\Command\CleanListCommand;
8+
use Symfony\Component\Console\Application;
9+
use Symfony\Component\Console\Command\CompleteCommand;
10+
use Symfony\Component\Console\Command\DumpCompletionCommand;
11+
use Symfony\Component\Console\Command\HelpCommand;
12+
13+
final class JackConsoleApplication extends Application
14+
{
15+
protected function getDefaultCommands(): array
16+
{
17+
return [
18+
new HelpCommand(),
19+
new CompleteCommand(),
20+
new DumpCompletionCommand(),
21+
22+
// clean list, without bloated options
23+
new CleanListCommand(),
24+
];
25+
}
26+
}

src/DependencyInjection/ContainerFactory.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Rector\Jack\DependencyInjection;
66

77
use Illuminate\Container\Container;
8+
use Rector\Jack\Console\JackConsoleApplication;
89
use Symfony\Component\Console\Application;
910
use Symfony\Component\Console\Input\ArrayInput;
1011
use Symfony\Component\Console\Output\ConsoleOutput;
@@ -23,20 +24,20 @@ public function create(): Container
2324

2425
// console
2526
$container->singleton(Application::class, function (Container $container): Application {
26-
$application = new Application('Rector Jack');
27+
$jackConsoleApplication = new JackConsoleApplication('Rector Jack');
2728

2829
$commandClasses = $this->findCommandClasses();
2930

3031
// register commands
3132
foreach ($commandClasses as $commandClass) {
3233
$command = $container->make($commandClass);
33-
$application->add($command);
34+
$jackConsoleApplication->add($command);
3435
}
3536

3637
// remove basic command to make output clear
37-
$this->hideDefaultCommands($application);
38+
$this->hideDefaultCommands($jackConsoleApplication);
3839

39-
return $application;
40+
return $jackConsoleApplication;
4041
});
4142

4243
$container->singleton(

src/OutdatedComposerFactory.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
use Rector\Jack\Mapper\OutdatedPackageMapper;
88
use Rector\Jack\ValueObject\OutdatedComposer;
99

10+
/**
11+
* @see \Rector\Jack\Tests\OutdatedComposerFactory\OutdatedComposerFactoryTest
12+
*/
1013
final readonly class OutdatedComposerFactory
1114
{
1215
public function __construct(

src/ValueObject/OutdatedComposer.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,7 @@ public function getPackagesShuffled(bool $onlyDev = false): array
6666
{
6767
// adds random effect, not to always update by A-Z, as would force too narrow pattern
6868
// this is also more fun :)
69-
if ($onlyDev) {
70-
$outdatedPackages = $this->getDevPackages();
71-
} else {
72-
$outdatedPackages = $this->outdatedPackages;
73-
}
69+
$outdatedPackages = $onlyDev ? $this->getDevPackages() : $this->outdatedPackages;
7470

7571
shuffle($outdatedPackages);
7672

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "some/project",
3+
"require": {
4+
"symfony/console": "^3.5"
5+
}
6+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Jack\Tests\OutdatedComposerFactory;
6+
7+
use Rector\Jack\OutdatedComposerFactory;
8+
use Rector\Jack\Tests\AbstractTestCase;
9+
use Rector\Jack\ValueObject\OutdatedPackage;
10+
11+
final class OutdatedComposerFactoryTest extends AbstractTestCase
12+
{
13+
public function test(): void
14+
{
15+
$outdatedComposerFactory = $this->make(OutdatedComposerFactory::class);
16+
17+
$outdatedComposer = $outdatedComposerFactory->createOutdatedComposer([
18+
[
19+
'name' => 'symfony/console',
20+
'direct-dependency' => true,
21+
'homepage' => 'https://symfony.com',
22+
'source' => 'https://github.com/symfony/console/tree/v6.4.20',
23+
'version' => 'v6.4.20',
24+
'release-age' => '1 month old',
25+
'release-date' => '2025-03-03T17:16:38+00:00',
26+
'latest' => 'v7.2.6',
27+
'latest-status' => 'update-possible',
28+
'latest-release-date' => '2025-04-07T19:09:28+00:00',
29+
'description' => 'Eases the creation of beautiful and testable command line interfaces',
30+
'abandoned' => false,
31+
],
32+
], __DIR__ . '/Fixture/some-composer.json');
33+
34+
$this->assertCount(1, $outdatedComposer->getProdPackages());
35+
$this->assertContainsOnlyInstancesOf(OutdatedPackage::class, $outdatedComposer->getProdPackages());
36+
37+
$this->assertCount(0, $outdatedComposer->getDevPackages());
38+
}
39+
}

0 commit comments

Comments
 (0)