Completed
Push — master ( 367ba0...cb743d )
by
unknown
12:47
created

getInstalledPackagesVersion()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 6.73

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 20
ccs 8
cts 11
cp 0.7272
rs 8.8571
cc 6
eloc 11
nc 5
nop 1
crap 6.73
1
<?php
2
/**
3
 * Generates the versions.yml file
4
 */
5
6
namespace Graviton\CoreBundle\Command;
7
8
use Symfony\Component\Console\Command\Command;
9
use Symfony\Component\Console\Exception\CommandNotFoundException;
10
use Symfony\Component\Console\Input\InputInterface;
11
use Symfony\Component\Console\Output\OutputInterface;
12
use Symfony\Component\Filesystem\Filesystem;
13
use Symfony\Component\Process\Process;
14
use Symfony\Component\Process\Exception\ProcessFailedException;
15
use Symfony\Component\Yaml\Dumper;
16
use Symfony\Component\Yaml\Parser;
17
use InvalidArgumentException;
18
19
/**
20
 * Reads out the used versions with composer and git and writes them in a file 'versions.yml'
21
 *
22
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
23
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
24
 * @link     http://swisscom.ch
25
 */
26
class GenerateVersionsCommand extends Command
27
{
28
    /**
29
     * @var string
30
     */
31
    private $composerCmd;
32
33
    /**
34
     * @var string
35
     */
36
    private $gitCmd;
37
38
    /**
39
     * @var string
40
     */
41
    private $contextDir;
42
43
    /*
44
     * @var \Symfony\Component\Console\Output\OutputInterface
45
     */
46
    private $output;
47
48
    /*
49
    * @var \Symfony\Component\Filesystem\Filesystem
50
    */
51
    private $filesystem;
52
53
    /*
54
    * @var |Symfony\Component\Yaml\Dumper
55
    */
56
    private $dumper;
57
58
    /*
59
    * @var |Symfony\Component\Yaml\Parser
60
    */
61
    private $parser;
62
63
    /**
64
     * {@inheritDoc}
65
     *
66
     * @return void
67
     */
68 4
    protected function configure()
69
    {
70 4
        parent::configure();
71
72 4
        $this->setName('graviton:core:generateversions')
73 4
            ->setDescription(
74 4
                'Generates the versions.yml file according to definition in app/config/version_service.yml'
75
            );
76 4
    }
77
78
79
    /**
80
     * set filesystem (in service-definition)
81
     *
82
     * @param \Symfony\Component\Filesystem\Filesystem $filesystem filesystem
83
     *
84
     * @return void
85
     */
86 4
    public function setFilesystem(Filesystem  $filesystem)
87
    {
88 4
        $this->filesystem = $filesystem;
89 4
    }
90
91
    /**
92
     * set dumper (in service-definition)
93
     *
94
     * @param \Symfony\Component\Yaml\Dumper $dumper dumper
95
     *
96
     * @return void
97
     */
98 4
    public function setDumper(Dumper $dumper)
99
    {
100 4
        $this->dumper = $dumper;
101 4
    }
102
103
    /**
104
     * set parser (in service-definition)
105
     *
106
     * @param \Symfony\Component\Yaml\Parser $parser parser
107
     *
108
     * @return void
109
     */
110 4
    public function setParser(Parser $parser)
111
    {
112 4
        $this->parser = $parser;
113 4
    }
114
115
    /**
116
     * {@inheritDoc}
117
     *
118
     * @param InputInterface  $input  input
119
     * @param OutputInterface $output output
120
     *
121
     * @return void
122
     */
123 2
    protected function execute(InputInterface $input, OutputInterface $output)
124
    {
125 2
        $container = $this->getApplication()->getKernel()->getContainer();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Symfony\Component\Console\Application as the method getKernel() does only exist in the following sub-classes of Symfony\Component\Console\Application: Symfony\Bundle\FrameworkBundle\Console\Application. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
126 2
        $rootDir = $container->getParameter('kernel.root_dir');
127 2
        $this->composerCmd = $container->getParameter('graviton.composer.cmd');
128 2
        $this->gitCmd = $container->getParameter('graviton.git.cmd');
129
130 2
        $this->contextDir = $rootDir . ((strpos($rootDir, 'vendor'))? '/../../../../' : '/../');
131
132 2
        $this->output = $output;
133
134 2
        $this->filesystem->dumpFile(
135 2
            $rootDir . '/../versions.yml',
136 2
            $this->getPackageVersions()
137
        );
138 2
    }
139
140
    /**
141
     * gets all versions
142
     *
143
     * @return array version numbers of packages
0 ignored issues
show
Documentation introduced by
Should the return type not be string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
144
     */
145 2
    public function getPackageVersions()
146
    {
147 2
        if ($this->isDesiredVersion('self')) {
148
            $versions = [
149 2
                $this->getContextVersion(),
150
            ];
151
        } else {
152
            $versions = array();
153
        }
154 2
        $versions = $this->getInstalledPackagesVersion($versions);
155
156 2
        return $this->dumper->dump($versions);
157
    }
158
159
    /**
160
     * returns the version of graviton or wrapper (the context) using git
161
     *
162
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,string>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
163
     *
164
     * @throws CommandNotFoundException
165
     */
166 2
    private function getContextVersion()
167
    {
168 2
        $wrapper = [];
169
        // git available here?
170 2
        if ($this->commandAvailable($this->gitCmd)) {
171
            // get current commit hash
172 2
            $currentHash = trim($this->runCommandInContext($this->gitCmd.' rev-parse --short HEAD'));
173
            // get version from hash:
174 2
            $version = trim($this->runCommandInContext($this->gitCmd.' tag --points-at ' . $currentHash));
175
            // if empty, set dev- and current branchname to version:
176 2
            if (!strlen($version)) {
177
                $version = 'dev-' . trim($this->runCommandInContext($this->gitCmd.' rev-parse --abbrev-ref HEAD'));
178
            }
179 2
            $wrapper['id'] = 'self';
180 2
            $wrapper['version'] = $version;
181
        } else {
182
            throw new CommandNotFoundException(
183
                'getContextVersion: '. $this->gitCmd . ' not available in ' . $this->contextDir
184
            );
185
        }
186 2
        return $wrapper;
187
    }
188
189
    /**
190
     * returns version for every installed package
191
     *
192
     * @param array $versions versions array
193
     * @throws CommandNotFoundException | \RuntimeException
194
     * @return array
195
     */
196 2
    private function getInstalledPackagesVersion($versions)
197
    {
198 2
        $composerFile = $this->contextDir . 'composer.lock';
199 2
        if (!file_exists($composerFile)) {
200
            throw new CommandNotFoundException('Composer lock file not found: '.$composerFile);
201
        }
202
203 2
        $composerJson = json_decode(file_get_contents($composerFile));
204 2
        if (!$composerJson || json_last_error()) {
205
            throw new \RuntimeException('Error on parsing json file: '.$composerFile);
206
        }
207
208 2
        foreach ($composerJson->packages as $package) {
209 2
            if ($this->isDesiredVersion($package->name)) {
210
                array_push($versions, array('id' => $package->name, 'version' => $package->version));
211
            }
212
        }
213
214 2
        return $versions;
215
    }
216
217
    /**
218
     * runs a command depending on the context
219
     *
220
     * @param string $command in this case composer or git
221
     * @return string
222
     *
223
     * @throws \RuntimeException
224
     */
225 2
    private function runCommandInContext($command)
226
    {
227 2
        $process = new Process(
228 2
            'cd ' . escapeshellarg($this->contextDir)
229 2
            . ' && ' . escapeshellcmd($command)
230
        );
231
        try {
232 2
            $process->mustRun();
233
        } catch (ProcessFailedException $pFe) {
234
            $this->output->writeln($pFe->getMessage());
235
        }
236 2
        return $process->getOutput();
237
    }
238
239
    /**
240
     * Checks if a command is available in an enviroment and in the context. The command might be as well a path
241
     * to a command.
242
     *
243
     * @param String $command the command to be checked for availability
244
     * @return bool
245
     */
246 2
    private function commandAvailable($command)
247
    {
248 2
        $process = new Process(
249 2
            'cd ' . escapeshellarg($this->contextDir)
250 2
            . ' && which ' . escapeshellcmd($command)
251
        );
252 2
        $process->run();
253 2
        return (boolean) strlen(trim($process->getOutput()));
254
    }
255
256
    /**
257
     * checks if the package version is configured
258
     *
259
     * @param string $packageName package name
260
     * @return boolean
261
     *
262
     * @throws \RuntimeException
263
     */
264 2
    private function isDesiredVersion($packageName)
265
    {
266 2
        if (empty($packageName)) {
267
            throw new \RuntimeException('Missing package name');
268
        }
269
270 2
        $config = $this->getConfiguration($this->contextDir . "/app/config/version_service.yml");
271
272 2
        if (!empty($config['desiredVersions'])) {
273 2
            foreach ($config['desiredVersions'] as $confEntry) {
274 2
                if ($confEntry == $packageName) {
275 2
                    return true;
276
                }
277
            }
278
        }
279
280 2
        return false;
281
    }
282
283
    /**
284
     * reads configuration information from the given file into an array.
285
     *
286
     * @param string $filePath Absolute path to the configuration file.
287
     *
288
     * @return array
289
     */
290 2
    private function getConfiguration($filePath)
291
    {
292 2
        $config = $this->parser->parse(file_get_contents($filePath));
293
294 2
        return is_array($config) ? $config : [];
295
    }
296
297
    /**
298
     * Returns the version out of a given version string
299
     *
300
     * @param string $versionString SemVer version string
301
     * @return string
302
     */
303
    public function getVersionNumber($versionString)
304
    {
305
        try {
306
            $version = $this->getVersionOrBranchName($versionString);
307
        } catch (InvalidArgumentException $e) {
308
            $version = $this->normalizeVersionString($versionString);
309
        }
310
311
        return empty($version) ? $versionString : $version;
312
    }
313
314
    /**
315
     * Get a version string string using a regular expression
316
     *
317
     * @param string $versionString SemVer version string
318
     * @return string
319
     */
320
    private function getVersionOrBranchName($versionString)
321
    {
322
        // Regular expression for root package ('self') on a tagged version
323
        $tag = '^(?<version>[v]?[0-9]+\.[0-9]+\.[0-9]+)(?<prerelease>-[0-9a-zA-Z.]+)?(?<build>\+[0-9a-zA-Z.]+)?$';
324
        // Regular expression for root package on a git branch
325
        $branch = '^(?<branch>(dev\-){1}[0-9a-zA-Z\.\/\-\_]+)$';
326
        $regex = sprintf('/%s|%s/', $tag, $branch);
327
328
        $matches = [];
329
        if (0 === preg_match($regex, $versionString, $matches)) {
330
            throw new InvalidArgumentException(
331
                sprintf('"%s" is not a valid SemVer', $versionString)
332
            );
333
        }
334
335
        return empty($matches['version']) ? $matches['branch'] : $matches['version'];
336
    }
337
338
    /**
339
     * Normalizing the incorrect SemVer string to a valid one
340
     *
341
     * At the moment, we are getting the version of the root package ('self') using the
342
     * 'composer show -s'-command. Unfortunately Composer is adding an unnecessary ending.
343
     *
344
     * @param string $versionString SemVer  version string
345
     * @param string $prefix        Version prefix
346
     * @return string
347
     */
348
    private function normalizeVersionString($versionString, $prefix = 'v')
349
    {
350
        if (substr_count($versionString, '.') === 3) {
351
            return sprintf(
352
                '%s%s',
353
                $prefix,
354
                implode('.', explode('.', $versionString, -1))
355
            );
356
        }
357
        return $versionString;
358
    }
359
}
360