Completed
Push — master ( d75c00...042f84 )
by Narcotic
21:29 queued 08:56
created

GenerateVersionsCommand::getVersionOrBranchName()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 0
cts 0
cp 0
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 9
nc 3
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
    * @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 1
                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)
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...
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