Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Issue #80: Add fix-dependencies command #83

Merged
merged 1 commit into from
Jun 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"ext-json": "*",
"ext-openssl": "*",
"cpliakas/git-wrapper": "^3.0",
"nikic/php-parser": "^4.5",
"squizlabs/php_codesniffer": "^3.3",
"symfony/console": "^5.0",
"symfony/filesystem": "^5.0",
Expand Down
93 changes: 93 additions & 0 deletions src/Command/FixDependenciesCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

declare(strict_types=1);

namespace Yiisoft\YiiDevTool\Command;

use Yiisoft\YiiDevTool\Component\CodeUsage\ComposerPackageUsageAnalyzer;
use Yiisoft\YiiDevTool\Component\Composer\ComposerConfig;
use Yiisoft\YiiDevTool\Component\Composer\ComposerConfigDependenciesModifier;
use Yiisoft\YiiDevTool\Component\Composer\ComposerPackage;
use Yiisoft\YiiDevTool\Component\CodeUsage\CodeUsageEnvironment;
use Yiisoft\YiiDevTool\Component\CodeUsage\NamespaceUsageFinder;
use Yiisoft\YiiDevTool\Component\Console\PackageCommand;
use Yiisoft\YiiDevTool\Component\Package\Package;

class FixDependenciesCommand extends PackageCommand
{
protected function configure()
{
$this
->setName('fix-dependencies')
->setDescription('Fix <fg=yellow;options=bold>require</> and <fg=yellow;options=bold>require-dev</> sections in <fg=blue;options=bold>composer.json</> according to the actual use of classes');

$this->addPackageArgument();
}

protected function getMessageWhenNothingHasBeenOutput(): ?string
{
return '<success>✔ Nothing to fix</success>';
}

protected function processPackage(Package $package): void
{
$io = $this->getIO();
$io->preparePackageHeader($package, "Fixing package {package} dependencies");

$package = new ComposerPackage($package->getName(), $package->getPath());

if (!$package->installed()) {
$io->warning([
"Package <package>{$package->getName()}</package> is not installed.",
"Dependencies fixing skipped.",
]);

return;
}

$dependencyPackages = $package->getDependencyPackages('yiisoft');
foreach ($dependencyPackages as $dependencyPackage) {
if (!$dependencyPackage->installed()) {
$io->warning([
"Dependency <package>{$dependencyPackage->getName()}</package> is not installed.",
"Dependencies fixing skipped.",
]);

return;
}
}

$namespaceUsages =
(new NamespaceUsageFinder())
->addTargetPaths(CodeUsageEnvironment::PRODUCTION, [
'config/common.php',
'config/web.php',
'src',
], $package->getPath())
->addTargetPaths(CodeUsageEnvironment::DEV, [
'config/tests.php',
'tests',
], $package->getPath())
->getUsages();

$analyzer = new ComposerPackageUsageAnalyzer($dependencyPackages, $namespaceUsages);
$analyzer->analyze();

$composerConfig = $package->getComposerConfig();

(new ComposerConfigDependenciesModifier($composerConfig))
->removeDependencies($analyzer->getUnusedPackages())
->ensureDependenciesUsedOnlyInSection(
$analyzer->getPackagesUsedInSpecifiedEnvironment(CodeUsageEnvironment::PRODUCTION),
ComposerConfig::SECTION_REQUIRE,
)
->ensureDependenciesUsedOnlyInSection(
$analyzer->getPackagesUsedOnlyInSpecifiedEnvironment(CodeUsageEnvironment::DEV),
ComposerConfig::SECTION_REQUIRE_DEV,
);

$composerConfig->writeToFile($package->getComposerConfigPath());

$io->done();
}
}
86 changes: 86 additions & 0 deletions src/Component/CodeUsage/CodeUsage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

declare(strict_types=1);

namespace Yiisoft\YiiDevTool\Component\CodeUsage;

use InvalidArgumentException;

class CodeUsage
{
private string $identifier;

/**
* @var string[]
*/
private array $environments = [];

/**
* @param string $identifier Unique identifier of code usage: namespace, package name, etc.
* @param string[] $environments Environment in which the code is used.
*/
public function __construct(string $identifier, array $environments)
{
foreach ($environments as $environment) {
if (!is_string($environment)) {
throw new InvalidArgumentException('Each environment must be a string.');
}
}

$this->identifier = $identifier;
$this->environments = $environments;
}

public function getIdentifier(): string
{
return $this->identifier;
}

/**
* @return string[]
*/
public function getEnvironments(): array
{
return $this->environments;
}

public function registerUsageInEnvironment(string $environment): void
{
if (!in_array($environment, $this->environments, true)) {
$this->environments[] = $environment;
}
}

/**
* @param string[] $environments
*/
public function registerUsageInEnvironments(array $environments): void
{
foreach ($environments as $environment) {
if (!is_string($environment)) {
throw new InvalidArgumentException('Each environment must be a string.');
}

$this->registerUsageInEnvironment($environment);
}
}

public function usedInEnvironment(string $environment): bool
{
return in_array($environment, $this->environments, true);
}

public function usedOnlyInSpecifiedEnvironment(string $environment): bool
{
if (count($this->environments) !== 1) {
return false;
}

return in_array($environment, $this->environments, true);
}

public function used(): bool
{
return count($this->environments) > 0;
}
}
11 changes: 11 additions & 0 deletions src/Component/CodeUsage/CodeUsageEnvironment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Yiisoft\YiiDevTool\Component\CodeUsage;

class CodeUsageEnvironment
{
public const PRODUCTION = 'production';
public const DEV = 'dev';
}
118 changes: 118 additions & 0 deletions src/Component/CodeUsage/ComposerPackageUsageAnalyzer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

namespace Yiisoft\YiiDevTool\Component\CodeUsage;

use InvalidArgumentException;
use Yiisoft\YiiDevTool\Component\Composer\ComposerPackage;

class ComposerPackageUsageAnalyzer
{
/**
* @var ComposerPackage[]
*/
private array $packages = [];

/**
* @var CodeUsage[]
*/
private array $namespaceUsages = [];

/**
* @var CodeUsage[]
*/
private array $packageUsages = [];

/**
* @param ComposerPackage[] $packages
* @param CodeUsage[] $namespaceUsages
*/
public function __construct(array $packages, array $namespaceUsages)
{
foreach ($packages as $package) {
if (!$package instanceof ComposerPackage) {
throw new InvalidArgumentException('$packages must be an array of ComposerPackage objects.');
}
}

foreach ($namespaceUsages as $namespaceUsage) {
if (!$namespaceUsage instanceof CodeUsage) {
throw new InvalidArgumentException('$namespaceUsages must be an array of CodeUsage objects.');
}
}

foreach ($packages as $package) {
$this->packages[$package->getName()] = $package;
}

foreach ($namespaceUsages as $namespaceUsage) {
$this->namespaceUsages[$namespaceUsage->getIdentifier()] = $namespaceUsage;
}
}

public function analyze(): void
{
foreach ($this->packages as $package) {
foreach ($package->getNamespaces() as $packageNamespace) {
foreach ($this->namespaceUsages as $namespaceUsage) {
if (strpos($namespaceUsage->getIdentifier(), "\\$packageNamespace") === 0) {
$this->registerPackageUsage($package->getName(), $namespaceUsage->getEnvironments());
}
}
}
}
}

public function getPackagesUsedInSpecifiedEnvironment(string $environment): array
{
$result = [];

foreach ($this->packageUsages as $packageUsage) {
if ($packageUsage->usedInEnvironment($environment)) {
$result[] = $this->packages[$packageUsage->getIdentifier()];
}
}

return $result;
}

public function getPackagesUsedOnlyInSpecifiedEnvironment(string $environment): array
{
$result = [];

foreach ($this->packageUsages as $packageUsage) {
if ($packageUsage->usedOnlyInSpecifiedEnvironment($environment)) {
$result[] = $this->packages[$packageUsage->getIdentifier()];
}
}

return $result;
}

public function getUnusedPackages(): array
{
$result = [];

foreach ($this->packageUsages as $packageUsage) {
if (!$packageUsage->used()) {
$result[] = $this->packages[$packageUsage->getIdentifier()];
}
}

return $result;
}

/**
* @param string $packageName
* @param string[] $environments
*/
private function registerPackageUsage(string $packageName, array $environments): void
{
if (!array_key_exists($packageName, $this->packageUsages)) {
$this->packageUsages[$packageName] = new CodeUsage($packageName, $environments);
} else {
$this->packageUsages[$packageName]->registerUsageInEnvironments($environments);
}
}
}