Completed
Push — feature/EVO-7007-selfversion-w... ( b73673...015eef )
by
unknown
126:26 queued 61:32
created

GenerateVersionsCommand   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 316
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 71.43%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 32
c 3
b 0
f 0
lcom 1
cbo 8
dl 0
loc 316
ccs 75
cts 105
cp 0.7143
rs 9.6

13 Methods

Rating   Name   Duplication   Size   Complexity  
A configure() 0 9 1
A execute() 0 20 2
A getPackageVersions() 0 14 2
A getContextVersion() 0 23 3
B getInstalledPackagesVersion() 0 23 4
A runComposerInContext() 0 14 2
A commandAvailable() 0 9 1
A runGitInContext() 0 14 2
B isDesiredVersion() 0 18 5
A getConfiguration() 0 7 2
A getVersionNumber() 0 10 3
A getVersionOrBranchName() 0 17 3
A normalizeVersionString() 0 11 2
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 4
48
    /**
49 4
     * {@inheritDoc}
50
     *
51 4
     * @return void
52 4
     */
53 2
    protected function configure()
54 2
    {
55 4
        parent::configure();
56
57
        $this->setName('graviton:core:generateversions')
58
            ->setDescription(
59
                'Generates the versions.yml file according to definition in app/config/version_service.yml'
60
            );
61
    }
62
63
    /**
64
     * {@inheritDoc}
65 2
     *
66
     * @param InputInterface  $input  input
67 2
     * @param OutputInterface $output output
68 2
     *
69 2
     * @return void
70
     */
71 2
    protected function execute(InputInterface $input, OutputInterface $output)
72 2
    {
73
        $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...
74
        $rootDir = $container->getParameter('kernel.root_dir');
75
        $this->composerCmd = $container->getParameter('graviton.composer.cmd');
76 2
        $this->gitCmd = $container->getParameter('graviton.git.cmd');
77
78 2
        $this->contextDir = $rootDir . '/../';
79 2
        if (strpos($rootDir, 'vendor')) {
80 2
            $this->contextDir = $rootDir . '/../../../../';
81 2
        }
82 1
83 2
        $this->output = $output;
84
85
        $filesystem = new Filesystem();
86
        $filesystem->dumpFile(
87
            $rootDir . '/../versions.yml',
88
            $this->getPackageVersions()
89
        );
90 2
    }
91
92 2
    /**
93
     * gets all versions
94 2
     *
95 1
     * @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...
96 1
     */
97
    public function getPackageVersions()
98
    {
99 2
        if ($this->isDesiredVersion('self')) {
100
            $versions = [
101 2
                $this->getContextVersion(),
102 2
            ];
103
        } else {
104
            $versions = array();
105
        }
106
        $versions = $this->getInstalledPackagesVersion($versions);
107
108
        $yamlDumper = new Dumper();
109
        return $yamlDumper->dump($versions);
110 2
    }
111
112
    /**
113 2
     * returns the version of graviton or wrapper (the context) using git
114
     *
115 2
     * @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...
116
     *
117 2
     * @throws CommandNotFoundException
118 2
     */
119 1
    private function getContextVersion()
120
    {
121 2
        $wrapper = [];
122 2
        // git available here?
123
        if ($this->commandAvailable($this->gitCmd)) {
124 2
            // get current commit hash
125
            $currentHash = trim($this->runGitInContext('rev-parse --short HEAD'));
126
            // get version from hash:
127
            $version = trim($this->runGitInContext('tag --points-at ' . $currentHash));
128
            // if empty, set dev- and current branchname to version:
129
            if (!strlen($version)) {
130
                $version = 'dev-' . trim($this->runGitInContext('rev-parse --abbrev-ref HEAD'));
131
            }
132
133 2
            $wrapper['id'] = 'self';
134
            $wrapper['version'] = $version;
135 2
        } else {
136 2
            throw new CommandNotFoundException(
137
                'getContextVersion: '. $this->gitCmdCmd . ' not available in ' . $this->contextDir
138 2
            );
139
        }
140 2
        return $wrapper;
141 2
    }
142 2
143 1
    /**
144
     * returns version for every installed package
145 1
     *
146
     * @param array $versions versions array
147 2
     * @return array
148
     */
149
    private function getInstalledPackagesVersion($versions)
0 ignored issues
show
Unused Code introduced by
The parameter $versions is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
150
    {
151
        $versions = [];
152
        // composer available here?
153
        if ($this->commandAvailable($this->composerCmd)) {
154
            $output = $this->runComposerInContext('show --installed');
155
            $packages = explode(PHP_EOL, $output);
156
            //last index is always empty
157
            array_pop($packages);
158 2
159
            foreach ($packages as $package) {
160 2
                $content = preg_split('/([\s]+)/', $package);
161 2
                if ($this->isDesiredVersion($content[0])) {
162 2
                    array_push($versions, array('id' => $content[0], 'version' => $content[1]));
163 2
                }
164 1
            }
165
        } else {
166 2
            throw new CommandNotFoundException(
167 1
                'getInstalledPackagesVersion: '. $this->composerCmd . ' not available in ' . $this->contextDir
168
            );
169
        }
170 2
        return $versions;
171
    }
172
173
    /**
174
     * runs a composer command depending on the context
175
     *
176
     * @param string $command composer args
177
     * @return string
178
     *
179
     * @throws \RuntimeException
180
     */
181 2
    private function runComposerInContext($command)
182
    {
183 2
        $process = new Process(
184
            'cd ' . escapeshellarg($this->contextDir)
185 2
            . ' && ' . escapeshellcmd($this->composerCmd)
186 1
            . ' ' . $command
187
        );
188
        try {
189 2
            $process->mustRun();
190
        } catch (ProcessFailedException $pFe) {
191
            $this->output->writeln($pFe->getMessage());
192
        }
193
        return $process->getOutput();
194
    }
195
196
    /**
197
     * Checks if a command is available in an enviroment and in the context. The command might be as well a path
198
     * to a command.
199
     *
200 2
     * @param String $command the command to be checked for availability
201
     * @return bool
202 2
     */
203
    private function commandAvailable($command)
0 ignored issues
show
Coding Style introduced by
function commandAvailable() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
204
    {
205
        $process = new Process(
206 2
            'cd ' . escapeshellarg($this->contextDir)
207
            . ' && which ' . escapeshellcmd($command)
208 2
        );
209 2
        $process->run();
210 2
        return (boolean) strlen(trim($process->getOutput()));
211 2
    }
212
213 1
214 1
    /**
215
     * runs a git command depending on the context
216 2
     *
217
     * @param string $command git args
218
     * @return string
219
     *
220
     * @throws \RuntimeException
221
     */
222
    private function runGitInContext($command)
223
    {
224
        $process = new Process(
225
            'cd ' . escapeshellarg($this->contextDir)
226 2
            . ' && ' . escapeshellcmd($this->gitCmd)
227
            . ' ' . $command
228 2
        );
229 2
        try {
230
            $process->mustRun();
231 2
        } catch (ProcessFailedException $pFe) {
232
            $this->output->writeln($pFe->getMessage());
233
        }
234
        return $process->getOutput();
235
    }
236
237
    /**
238
     * checks if the package version is configured
239
     *
240
     * @param string $packageName package name
241
     * @return boolean
242
     *
243
     * @throws \RuntimeException
244
     */
245
    private function isDesiredVersion($packageName)
246
    {
247
        if (empty($packageName)) {
248
            throw new \RuntimeException('Missing package name');
249
        }
250
251
        $config = $this->getConfiguration($this->contextDir . "/app/config/version_service.yml");
252
253
        if (!empty($config['desiredVersions'])) {
254
            foreach ($config['desiredVersions'] as $confEntry) {
255
                if ($confEntry == $packageName) {
256
                    return true;
257
                }
258
            }
259
        }
260
261
        return false;
262
    }
263
264
    /**
265
     * reads configuration information from the given file into an array.
266
     *
267
     * @param string $filePath Absolute path to the configuration file.
268
     *
269
     * @return array
270
     */
271
    private function getConfiguration($filePath)
272
    {
273
        $parser = new Parser();
274
        $config = $parser->parse(file_get_contents($filePath));
275
276
        return is_array($config) ? $config : [];
277
    }
278
279
    /**
280
     * Returns the version out of a given version string
281
     *
282
     * @param string $versionString SemVer version string
283
     * @return string
284
     */
285
    public function getVersionNumber($versionString)
286
    {
287
        try {
288
            $version = $this->getVersionOrBranchName($versionString);
289
        } catch (InvalidArgumentException $e) {
290
            $version = $this->normalizeVersionString($versionString);
291
        }
292
293
        return empty($version) ? $versionString : $version;
294
    }
295
296
    /**
297
     * Get a version string string using a regular expression
298
     *
299
     * @param string $versionString SemVer version string
300
     * @return string
301
     */
302
    private function getVersionOrBranchName($versionString)
303
    {
304
        // Regular expression for root package ('self') on a tagged version
305
        $tag = '^(?<version>[v]?[0-9]+\.[0-9]+\.[0-9]+)(?<prerelease>-[0-9a-zA-Z.]+)?(?<build>\+[0-9a-zA-Z.]+)?$';
306
        // Regular expression for root package on a git branch
307
        $branch = '^(?<branch>(dev\-){1}[0-9a-zA-Z\.\/\-\_]+)$';
308
        $regex = sprintf('/%s|%s/', $tag, $branch);
309
310
        $matches = [];
311
        if (0 === preg_match($regex, $versionString, $matches)) {
312
            throw new InvalidArgumentException(
313
                sprintf('"%s" is not a valid SemVer', $versionString)
314
            );
315
        }
316
317
        return empty($matches['version']) ? $matches['branch'] : $matches['version'];
318
    }
319
320
    /**
321
     * Normalizing the incorrect SemVer string to a valid one
322
     *
323
     * At the moment, we are getting the version of the root package ('self') using the
324
     * 'composer show -s'-command. Unfortunately Composer is adding an unnecessary ending.
325
     *
326
     * @param string $versionString SemVer  version string
327
     * @param string $prefix        Version prefix
328
     * @return string
329
     */
330
    private function normalizeVersionString($versionString, $prefix = 'v')
331
    {
332
        if (substr_count($versionString, '.') === 3) {
333
            return sprintf(
334
                '%s%s',
335
                $prefix,
336
                implode('.', explode('.', $versionString, -1))
337
            );
338
        }
339
        return $versionString;
340
    }
341
}
342