Task::getRelevantUsedFiles()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 9
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
namespace AppBundle\ShowUnusedComposerPackages;
4
5
use Composer\Composer;
6
use Composer\Factory;
7
use Composer\IO\BufferIO;
8
use Composer\Package\PackageInterface;
9
use Helper\FileSystem;
10
use Helper\NullStyle;
11
use Symfony\Component\Console\Style\StyleInterface;
12
13
/**
14
 * Get unused composer packages.
15
 *
16
 * The idea is that packages are required either directly from the root package or indirectly. We call these packages
17
 * n-th degree requirements, where n is the number of links between the package in question and the root package.
18
 * E.g. a 2nd degree requirement is a package that is required by a package that in turn is directly required by the
19
 * root package.
20
 *
21
 * Deleting a requirement of 2nd or higher degree alone makes no sense, as it will still be required by a first degree
22
 * requirement and therefore be installed. Hence we concentrate on the first level requirements only.
23
 *
24
 * If the only logged use of a package is a *Bundle.php, it's probably only registered in the AppKernel and not really
25
 * used, i.e. can be deleted.
26
 */
27
final class Task
28
{
29
    /** @var string */
30
    private $pathToVendor;
31
32
    /** @var StyleInterface */
33
    private $ioStyle;
34
35
    /**
36
     * @param string $pathToComposerJson
37
     * @param string|null $pathToVendor
38
     * @param string $pathToUsedFiles
39
     * @param string|null $pathToBlacklist
40
     * @param StyleInterface|null $ioStyle
41
     */
42
    public function getUnusedPackagePaths($pathToComposerJson, $pathToVendor, $pathToUsedFiles, $pathToBlacklist = null, StyleInterface $ioStyle = null)
43
    {
44
        $this->ioStyle = $ioStyle ?: new NullStyle();
45
46
        $usedFiles = FileSystem::readFileIntoArray($pathToUsedFiles);
47
        $this->ioStyle->text('Found ' . count($usedFiles) . ' used files.');
48
49
        $usedFiles = $this->getRelevantUsedFiles($usedFiles);
50
51
        $pathToVendor = $pathToVendor ?: $this->getDefaultPathToVendor($pathToComposerJson);
52
        $this->pathToVendor = $this->assertPathToVendorIsValid($pathToVendor);
53
54
        $unusedPackagePaths = [];
55
        foreach ($this->getRelevantPackagePaths($pathToComposerJson, $pathToBlacklist) as $packagePath) {
56
            if (!$this->atLeastOneFileIsInPath($usedFiles, $packagePath)) {
57
                $unusedPackagePaths[] = $packagePath;
58
            }
59
        }
60
61
        $this->ioStyle->newLine();
62
        $this->ioStyle->text('Calculated ' . count($unusedPackagePaths) . ' potentially unused packages:');
63
        $this->ioStyle->listing($unusedPackagePaths);
64
        $this->ioStyle->success('Finished listing potentially unused packages.');
65
    }
66
67
    /**
68
     * @param string[] $usedFiles
69
     * @return string[]
70
     */
71
    private function getRelevantUsedFiles(array $usedFiles)
72
    {
73
        $filteredFiles = array_filter($usedFiles, function ($usedFile) { return strpos($usedFile, 'Bundle.php') === false; });
74
        $difference = count($usedFiles) - count($filteredFiles);
75
        if ($difference > 0) {
76
            $this->ioStyle->text('Removed ' . $difference . ' *Bundle.php files from used files as they are likely irrelevant.');
77
        }
78
79
        return $filteredFiles;
80
    }
81
82
    /**
83
     * @param string $pathToComposerJson
84
     * @return string
85
     */
86
    private function getDefaultPathToVendor($pathToComposerJson)
87
    {
88
        $projectRoot = realpath(dirname($pathToComposerJson));
89
        $vendorDir = $projectRoot . '/vendor/';
90
        $this->ioStyle->text('Assume vendor directory to be ' . $vendorDir . ' (you can set it with the --' . Command::OPTION_VENDOR_DIRECTORY . ' option).');
91
92
        return $vendorDir;
93
    }
94
95
    /**
96
     * @param string $path
97
     * @return string path to a readable directory with a trailing slash
98
     */
99
    private function assertPathToVendorIsValid($path)
100
    {
101
        if (is_dir($path) === false) {
102
            $message = 'The path "' . $path . '" is no valid directory.';
103
        } elseif (is_readable($path) === false) {
104
            $message = 'The directory "' . $path . '" is not readable.';
105
        }
106
107
        if (isset($message)) {
108
            $message .= ' Please specify a readable directory with the ' . Command::OPTION_VENDOR_DIRECTORY . ' option.';
109
            $this->ioStyle->error($message);
110
            throw new \InvalidArgumentException($message);
111
        }
112
113
        return rtrim($path, '/') . '/';
114
    }
115
116
    /**
117
     * @param string $pathToComposerJson
118
     * @param string $pathToBlacklist
119
     * @return string[]
120
     */
121
    private function getRelevantPackagePaths($pathToComposerJson, $pathToBlacklist)
122
    {
123
        $packagePaths = [];
124
        $composer = Factory::create(new BufferIO(), $pathToComposerJson);
125
126
        $blacklistingRegExps = FileSystem::getBlacklistingRegExps($pathToBlacklist);
127
        foreach ($composer->getPackage()->getRequires() as $link) {
128
            $package = $composer->getLocker()->getLockedRepository()->findPackage($link->getTarget(), $link->getConstraint());
129
            if ($package === null) {
130
                continue;
131
            }
132
133
            $packagePath = realpath($this->getInstallPath($composer, $package));
134
            if ($this->packagePathIsBlacklisted($packagePath, $blacklistingRegExps)) {
135
                continue;
136
            }
137
138
            $packagePaths[] = $packagePath;
139
        }
140
141
        $message = 'Found ' . count($packagePaths) . ' composer packages';
142
        if (count($blacklistingRegExps) > 0) {
143
            $message .= ' not matching the ' . count($blacklistingRegExps) . ' blacklisting regular expressions';
144
        }
145
        $this->ioStyle->text($message . '.');
146
147
        return $packagePaths;
148
    }
149
150
    /**
151
     * @param string[] $files
152
     * @param string $path
153
     * @return bool
154
     */
155
    private function atLeastOneFileIsInPath(array $files, $path)
156
    {
157
        foreach ($files as $file) {
158
            if (strpos($file, $path) !== false) {
159
                return true;
160
            }
161
        }
162
163
        return false;
164
    }
165
166
    /**
167
     * @param Composer $composer
168
     * @param PackageInterface $package
169
     * @return string
170
     */
171
    private function getInstallPath(Composer $composer, PackageInterface $package)
172
    {
173
        $pathToVendorInZauberlehrling = $composer->getConfig()->get('vendor-dir');
174
175
        $pathToPackageInstallationInZauberlehrling = $composer->getInstallationManager()->getInstallPath($package);
176
        $pathToPackageInstallationInProject = str_replace($pathToVendorInZauberlehrling, $this->pathToVendor, $pathToPackageInstallationInZauberlehrling);
177
        return realpath($pathToPackageInstallationInProject);
178
    }
179
180
    /**
181
     * @param string $path
182
     * @param string[] $blacklistRegExps
183
     * @return bool
184
     */
185
    private function packagePathIsBlacklisted($path, array $blacklistRegExps)
186
    {
187
        foreach ($blacklistRegExps as $blacklistRegExp) {
188
            if (preg_match($blacklistRegExp, $path) === 1 || preg_match($blacklistRegExp, $path . '/') === 1) {
189
                return true;
190
            }
191
        }
192
193
        return false;
194
    }
195
}
196