Completed
Push — feature/EVO-7007-selfversion-w... ( 16ac37...18cf44 )
by
unknown
89:42 queued 83:26
created

GenerateVersionsCommand::getVersionNumber()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 10
ccs 0
cts 5
cp 0
rs 9.4285
cc 3
eloc 6
nc 4
nop 1
crap 12
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
     * {@inheritDoc}
50
     *
51
     * @return void
52
     */
53 4
    protected function configure()
54
    {
55 4
        parent::configure();
56
57 4
        $this->setName('graviton:core:generateversions')
58 4
            ->setDescription(
59 2
                'Generates the versions.yml file according to definition in app/config/version_service.yml'
60 2
            );
61 4
    }
62
63
    /**
64
     * {@inheritDoc}
65
     *
66
     * @param InputInterface  $input  input
67
     * @param OutputInterface $output output
68
     *
69
     * @return void
70
     */
71 2
    protected function execute(InputInterface $input, OutputInterface $output)
72
    {
73 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...
74 2
        $rootDir = $container->getParameter('kernel.root_dir');
75 2
        $this->composerCmd = $container->getParameter('graviton.composer.cmd');
76 2
        $this->gitCmd = $container->getParameter('graviton.git.cmd');
77
78 2
        $this->contextDir = $rootDir . ((strpos($rootDir, 'vendor'))? '/../../../../' : '/../');
79
80 2
        $this->output = $output;
81
82 2
        $filesystem = new Filesystem();
83 2
        $filesystem->dumpFile(
84 2
            $rootDir . '/../versions.yml',
85 2
            $this->getPackageVersions()
86 1
        );
87 2
    }
88
89
    /**
90
     * gets all versions
91
     *
92
     * @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...
93
     */
94 2
    public function getPackageVersions()
95
    {
96 2
        if ($this->isDesiredVersion('self')) {
97
            $versions = [
98 2
                $this->getContextVersion(),
99 1
            ];
100 1
        } else {
101
            $versions = array();
102
        }
103 2
        $versions = $this->getInstalledPackagesVersion($versions);
104
105 2
        $yamlDumper = new Dumper();
106 2
        return $yamlDumper->dump($versions);
107
    }
108
109
    /**
110
     * returns the version of graviton or wrapper (the context) using git
111
     *
112
     * @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...
113
     *
114
     * @throws CommandNotFoundException
115
     */
116 2
    private function getContextVersion()
117
    {
118 2
        $wrapper = [];
119
        // git available here?
120 2
        if ($this->commandAvailable($this->gitCmd)) {
121
            // get current commit hash
122 2
            $currentHash = trim($this->runCommandInContext($this->gitCmd.' rev-parse --short HEAD'));
123
            // get version from hash:
124 2
            $version = trim($this->runCommandInContext($this->gitCmd.' tag --points-at ' . $currentHash));
125
            // if empty, set dev- and current branchname to version:
126 2
            if (!strlen($version)) {
127 2
                $version = 'dev-' . trim($this->runCommandInContext($this->gitCmd.' rev-parse --abbrev-ref HEAD'));
128 1
            }
129 2
            $wrapper['id'] = 'self';
130 2
            $wrapper['version'] = $version;
131 1
        } else {
132
            throw new CommandNotFoundException(
133
                'getContextVersion: '. $this->gitCmd . ' not available in ' . $this->contextDir
134
            );
135
        }
136 2
        return $wrapper;
137
    }
138
139
    /**
140
     * returns version for every installed package
141
     *
142
     * @param array $versions versions array
143
     * @return array
144
     */
145 2
    private function getInstalledPackagesVersion($versions)
146
    {
147
        // composer available here?
148 2
        if ($this->commandAvailable($this->composerCmd)) {
149 2
            $output = $this->runCommandInContext($this->composerCmd.' show --installed');
150 2
            $packages = explode(PHP_EOL, $output);
151
            //last index is always empty
152 2
            array_pop($packages);
153
154 2
            foreach ($packages as $package) {
155 2
                $content = preg_split('/([\s]+)/', $package);
156 2
                if ($this->isDesiredVersion($content[0])) {
157 1
                    array_push($versions, array('id' => $content[0], 'version' => $content[1]));
158
                }
159 1
            }
160 1
        } else {
161
            throw new CommandNotFoundException(
162
                'getInstalledPackagesVersion: '. $this->composerCmd . ' not available in ' . $this->contextDir
163
            );
164
        }
165 2
        return $versions;
166
    }
167
168
    /**
169
     * runs a command depending on the context
170
     *
171
     * @param string $command in this case composer or git
172
     * @return string
173
     *
174
     * @throws \RuntimeException
175
     */
176 2
    private function runCommandInContext($command)
177
    {
178 2
        $process = new Process(
179 2
            'cd ' . escapeshellarg($this->contextDir)
180 2
            . ' && ' . escapeshellcmd($command)
181 1
        );
182
        try {
183 2
            $process->mustRun();
184 1
        } catch (ProcessFailedException $pFe) {
185
            $this->output->writeln($pFe->getMessage());
186
        }
187 2
        return $process->getOutput();
188
    }
189
190
    /**
191
     * Checks if a command is available in an enviroment and in the context. The command might be as well a path
192
     * to a command.
193
     *
194
     * @param String $command the command to be checked for availability
195
     * @return bool
196
     */
197 2
    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...
198
    {
199 2
        $process = new Process(
200 2
            'cd ' . escapeshellarg($this->contextDir)
201 2
            . ' && which ' . escapeshellcmd($command)
202 1
        );
203 2
        $process->run();
204 2
        return (boolean) strlen(trim($process->getOutput()));
205
    }
206
207
    /**
208
     * checks if the package version is configured
209
     *
210
     * @param string $packageName package name
211
     * @return boolean
212
     *
213
     * @throws \RuntimeException
214
     */
215 2
    private function isDesiredVersion($packageName)
216
    {
217 2
        if (empty($packageName)) {
218
            throw new \RuntimeException('Missing package name');
219
        }
220
221 2
        $config = $this->getConfiguration($this->contextDir . "/app/config/version_service.yml");
222
223 2
        if (!empty($config['desiredVersions'])) {
224 2
            foreach ($config['desiredVersions'] as $confEntry) {
225 2
                if ($confEntry == $packageName) {
226 2
                    return true;
227
                }
228 1
            }
229 1
        }
230
231 2
        return false;
232
    }
233
234
    /**
235
     * reads configuration information from the given file into an array.
236
     *
237
     * @param string $filePath Absolute path to the configuration file.
238
     *
239
     * @return array
240
     */
241 2
    private function getConfiguration($filePath)
242
    {
243 2
        $parser = new Parser();
244 2
        $config = $parser->parse(file_get_contents($filePath));
245
246 2
        return is_array($config) ? $config : [];
247
    }
248
249
    /**
250
     * Returns the version out of a given version string
251
     *
252
     * @param string $versionString SemVer version string
253
     * @return string
254
     */
255
    public function getVersionNumber($versionString)
256
    {
257
        try {
258
            $version = $this->getVersionOrBranchName($versionString);
259
        } catch (InvalidArgumentException $e) {
260
            $version = $this->normalizeVersionString($versionString);
261
        }
262
263
        return empty($version) ? $versionString : $version;
264
    }
265
266
    /**
267
     * Get a version string string using a regular expression
268
     *
269
     * @param string $versionString SemVer version string
270
     * @return string
271
     */
272
    private function getVersionOrBranchName($versionString)
273
    {
274
        // Regular expression for root package ('self') on a tagged version
275
        $tag = '^(?<version>[v]?[0-9]+\.[0-9]+\.[0-9]+)(?<prerelease>-[0-9a-zA-Z.]+)?(?<build>\+[0-9a-zA-Z.]+)?$';
276
        // Regular expression for root package on a git branch
277
        $branch = '^(?<branch>(dev\-){1}[0-9a-zA-Z\.\/\-\_]+)$';
278
        $regex = sprintf('/%s|%s/', $tag, $branch);
279
280
        $matches = [];
281
        if (0 === preg_match($regex, $versionString, $matches)) {
282
            throw new InvalidArgumentException(
283
                sprintf('"%s" is not a valid SemVer', $versionString)
284
            );
285
        }
286
287
        return empty($matches['version']) ? $matches['branch'] : $matches['version'];
288
    }
289
290
    /**
291
     * Normalizing the incorrect SemVer string to a valid one
292
     *
293
     * At the moment, we are getting the version of the root package ('self') using the
294
     * 'composer show -s'-command. Unfortunately Composer is adding an unnecessary ending.
295
     *
296
     * @param string $versionString SemVer  version string
297
     * @param string $prefix        Version prefix
298
     * @return string
299
     */
300
    private function normalizeVersionString($versionString, $prefix = 'v')
301
    {
302
        if (substr_count($versionString, '.') === 3) {
303
            return sprintf(
304
                '%s%s',
305
                $prefix,
306
                implode('.', explode('.', $versionString, -1))
307
            );
308
        }
309
        return $versionString;
310
    }
311
}
312