Completed
Push — master ( 6abda5...b08bdd )
by Narcotic
56:44 queued 45:19
created

GenerateVersionsCommand::getContextVersion()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3.3332

Importance

Changes 4
Bugs 2 Features 0
Metric Value
c 4
b 2
f 0
dl 0
loc 22
ccs 10
cts 15
cp 0.6667
rs 9.2
cc 3
eloc 13
nc 3
nop 0
crap 3.3332
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 2
                'Generates the versions.yml file according to definition in app/config/version_service.yml'
75 2
            );
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 1
        );
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 1
            ];
151 1
        } 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 1
        } 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
     * @return array
194
     */
195 2
    private function getInstalledPackagesVersion($versions)
196
    {
197
        // composer available here?
198 2
        if ($this->commandAvailable($this->composerCmd)) {
199 2
            $output = $this->runCommandInContext($this->composerCmd.' show --installed');
200 2
            $packages = explode(PHP_EOL, $output);
201
            //last index is always empty
202 2
            array_pop($packages);
203
204 2
            foreach ($packages as $package) {
205 2
                $content = preg_split('/([\s]+)/', $package);
206 2
                if ($this->isDesiredVersion($content[0])) {
207 1
                    array_push($versions, array('id' => $content[0], 'version' => $content[1]));
208
                }
209 1
            }
210 1
        } else {
211
            throw new CommandNotFoundException(
212
                'getInstalledPackagesVersion: '. $this->composerCmd . ' not available in ' . $this->contextDir
213
            );
214
        }
215 2
        return $versions;
216
    }
217
218
    /**
219
     * runs a command depending on the context
220
     *
221
     * @param string $command in this case composer or git
222
     * @return string
223
     *
224
     * @throws \RuntimeException
225
     */
226 2
    private function runCommandInContext($command)
227
    {
228 2
        $process = new Process(
229 2
            'cd ' . escapeshellarg($this->contextDir)
230 2
            . ' && ' . escapeshellcmd($command)
231 1
        );
232
        try {
233 2
            $process->mustRun();
234 1
        } catch (ProcessFailedException $pFe) {
235
            $this->output->writeln($pFe->getMessage());
236
        }
237 2
        return $process->getOutput();
238
    }
239
240
    /**
241
     * Checks if a command is available in an enviroment and in the context. The command might be as well a path
242
     * to a command.
243
     *
244
     * @param String $command the command to be checked for availability
245
     * @return bool
246
     */
247 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...
248
    {
249 2
        $process = new Process(
250 2
            'cd ' . escapeshellarg($this->contextDir)
251 2
            . ' && which ' . escapeshellcmd($command)
252 1
        );
253 2
        $process->run();
254 2
        return (boolean) strlen(trim($process->getOutput()));
255
    }
256
257
    /**
258
     * checks if the package version is configured
259
     *
260
     * @param string $packageName package name
261
     * @return boolean
262
     *
263
     * @throws \RuntimeException
264
     */
265 2
    private function isDesiredVersion($packageName)
266
    {
267 2
        if (empty($packageName)) {
268
            throw new \RuntimeException('Missing package name');
269
        }
270
271 2
        $config = $this->getConfiguration($this->contextDir . "/app/config/version_service.yml");
272
273 2
        if (!empty($config['desiredVersions'])) {
274 2
            foreach ($config['desiredVersions'] as $confEntry) {
275 2
                if ($confEntry == $packageName) {
276 2
                    return true;
277
                }
278 1
            }
279 1
        }
280
281 2
        return false;
282
    }
283
284
    /**
285
     * reads configuration information from the given file into an array.
286
     *
287
     * @param string $filePath Absolute path to the configuration file.
288
     *
289
     * @return array
290
     */
291 2
    private function getConfiguration($filePath)
292
    {
293 2
        $config = $this->parser->parse(file_get_contents($filePath));
294
295 2
        return is_array($config) ? $config : [];
296
    }
297
298
    /**
299
     * Returns the version out of a given version string
300
     *
301
     * @param string $versionString SemVer version string
302
     * @return string
303
     */
304
    public function getVersionNumber($versionString)
305
    {
306
        try {
307
            $version = $this->getVersionOrBranchName($versionString);
308
        } catch (InvalidArgumentException $e) {
309
            $version = $this->normalizeVersionString($versionString);
310
        }
311
312
        return empty($version) ? $versionString : $version;
313
    }
314
315
    /**
316
     * Get a version string string using a regular expression
317
     *
318
     * @param string $versionString SemVer version string
319
     * @return string
320
     */
321
    private function getVersionOrBranchName($versionString)
322
    {
323
        // Regular expression for root package ('self') on a tagged version
324
        $tag = '^(?<version>[v]?[0-9]+\.[0-9]+\.[0-9]+)(?<prerelease>-[0-9a-zA-Z.]+)?(?<build>\+[0-9a-zA-Z.]+)?$';
325
        // Regular expression for root package on a git branch
326
        $branch = '^(?<branch>(dev\-){1}[0-9a-zA-Z\.\/\-\_]+)$';
327
        $regex = sprintf('/%s|%s/', $tag, $branch);
328
329
        $matches = [];
330
        if (0 === preg_match($regex, $versionString, $matches)) {
331
            throw new InvalidArgumentException(
332
                sprintf('"%s" is not a valid SemVer', $versionString)
333
            );
334
        }
335
336
        return empty($matches['version']) ? $matches['branch'] : $matches['version'];
337
    }
338
339
    /**
340
     * Normalizing the incorrect SemVer string to a valid one
341
     *
342
     * At the moment, we are getting the version of the root package ('self') using the
343
     * 'composer show -s'-command. Unfortunately Composer is adding an unnecessary ending.
344
     *
345
     * @param string $versionString SemVer  version string
346
     * @param string $prefix        Version prefix
347
     * @return string
348
     */
349
    private function normalizeVersionString($versionString, $prefix = 'v')
350
    {
351
        if (substr_count($versionString, '.') === 3) {
352
            return sprintf(
353
                '%s%s',
354
                $prefix,
355
                implode('.', explode('.', $versionString, -1))
356
            );
357
        }
358
        return $versionString;
359
    }
360
}
361