Completed
Push — feature/EVO-7007-selfversion-w... ( 015eef...16ac37 )
by
unknown
102:16 queued 95:52
created

GenerateVersionsCommand::configure()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 9
ccs 7
cts 7
cp 1
rs 9.6666
cc 1
eloc 5
nc 1
nop 0
crap 1
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->runGitInContext('rev-parse --short HEAD'));
123
            // get version from hash:
124 2
            $version = trim($this->runGitInContext('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->runGitInContext('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->gitCmdCmd . ' 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->runComposerInContext('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 composer command depending on the context
170
     *
171
     * @param string $command composer args
172
     * @return string
173
     *
174
     * @throws \RuntimeException
175
     */
176 2
    private function runComposerInContext($command)
177
    {
178 2
        $process = new Process(
179 2
            'cd ' . escapeshellarg($this->contextDir)
180 2
            . ' && ' . escapeshellcmd($this->composerCmd)
181 2
            . ' ' . $command
182 1
        );
183
        try {
184 2
            $process->mustRun();
185 1
        } catch (ProcessFailedException $pFe) {
186
            $this->output->writeln($pFe->getMessage());
187
        }
188 2
        return $process->getOutput();
189
    }
190
191
    /**
192
     * Checks if a command is available in an enviroment and in the context. The command might be as well a path
193
     * to a command.
194
     *
195
     * @param String $command the command to be checked for availability
196
     * @return bool
197
     */
198 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...
199
    {
200 2
        $process = new Process(
201 2
            'cd ' . escapeshellarg($this->contextDir)
202 2
            . ' && which ' . escapeshellcmd($command)
203 1
        );
204 2
        $process->run();
205 2
        return (boolean) strlen(trim($process->getOutput()));
206
    }
207
208
209
    /**
210
     * runs a git command depending on the context
211
     *
212
     * @param string $command git args
213
     * @return string
214
     *
215
     * @throws \RuntimeException
216
     */
217 2
    private function runGitInContext($command)
218
    {
219 2
        $process = new Process(
220 2
            'cd ' . escapeshellarg($this->contextDir)
221 2
            . ' && ' . escapeshellcmd($this->gitCmd)
222 2
            . ' ' . $command
223 1
        );
224
        try {
225 2
            $process->mustRun();
226 1
        } catch (ProcessFailedException $pFe) {
227
            $this->output->writeln($pFe->getMessage());
228
        }
229 2
        return $process->getOutput();
230
    }
231
232
    /**
233
     * checks if the package version is configured
234
     *
235
     * @param string $packageName package name
236
     * @return boolean
237
     *
238
     * @throws \RuntimeException
239
     */
240 2
    private function isDesiredVersion($packageName)
241
    {
242 2
        if (empty($packageName)) {
243
            throw new \RuntimeException('Missing package name');
244
        }
245
246 2
        $config = $this->getConfiguration($this->contextDir . "/app/config/version_service.yml");
247
248 2
        if (!empty($config['desiredVersions'])) {
249 2
            foreach ($config['desiredVersions'] as $confEntry) {
250 2
                if ($confEntry == $packageName) {
251 2
                    return true;
252
                }
253 1
            }
254 1
        }
255
256 2
        return false;
257
    }
258
259
    /**
260
     * reads configuration information from the given file into an array.
261
     *
262
     * @param string $filePath Absolute path to the configuration file.
263
     *
264
     * @return array
265
     */
266 2
    private function getConfiguration($filePath)
267
    {
268 2
        $parser = new Parser();
269 2
        $config = $parser->parse(file_get_contents($filePath));
270
271 2
        return is_array($config) ? $config : [];
272
    }
273
274
    /**
275
     * Returns the version out of a given version string
276
     *
277
     * @param string $versionString SemVer version string
278
     * @return string
279
     */
280
    public function getVersionNumber($versionString)
281
    {
282
        try {
283
            $version = $this->getVersionOrBranchName($versionString);
284
        } catch (InvalidArgumentException $e) {
285
            $version = $this->normalizeVersionString($versionString);
286
        }
287
288
        return empty($version) ? $versionString : $version;
289
    }
290
291
    /**
292
     * Get a version string string using a regular expression
293
     *
294
     * @param string $versionString SemVer version string
295
     * @return string
296
     */
297
    private function getVersionOrBranchName($versionString)
298
    {
299
        // Regular expression for root package ('self') on a tagged version
300
        $tag = '^(?<version>[v]?[0-9]+\.[0-9]+\.[0-9]+)(?<prerelease>-[0-9a-zA-Z.]+)?(?<build>\+[0-9a-zA-Z.]+)?$';
301
        // Regular expression for root package on a git branch
302
        $branch = '^(?<branch>(dev\-){1}[0-9a-zA-Z\.\/\-\_]+)$';
303
        $regex = sprintf('/%s|%s/', $tag, $branch);
304
305
        $matches = [];
306
        if (0 === preg_match($regex, $versionString, $matches)) {
307
            throw new InvalidArgumentException(
308
                sprintf('"%s" is not a valid SemVer', $versionString)
309
            );
310
        }
311
312
        return empty($matches['version']) ? $matches['branch'] : $matches['version'];
313
    }
314
315
    /**
316
     * Normalizing the incorrect SemVer string to a valid one
317
     *
318
     * At the moment, we are getting the version of the root package ('self') using the
319
     * 'composer show -s'-command. Unfortunately Composer is adding an unnecessary ending.
320
     *
321
     * @param string $versionString SemVer  version string
322
     * @param string $prefix        Version prefix
323
     * @return string
324
     */
325
    private function normalizeVersionString($versionString, $prefix = 'v')
326
    {
327
        if (substr_count($versionString, '.') === 3) {
328
            return sprintf(
329
                '%s%s',
330
                $prefix,
331
                implode('.', explode('.', $versionString, -1))
332
            );
333
        }
334
        return $versionString;
335
    }
336
}
337