LogCommand::sort()   A
last analyzed

Complexity

Conditions 3
Paths 1

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 13
rs 9.4285
cc 3
eloc 7
nc 1
nop 1
1
<?php
2
3
namespace BZIon\Command;
4
5
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
6
use Symfony\Component\Console\Input\InputArgument;
7
use Symfony\Component\Console\Input\InputInterface;
8
use Symfony\Component\Console\Input\InputOption;
9
use Symfony\Component\Console\Output\OutputInterface;
10
use Symfony\Component\Console\Question\ChoiceQuestion;
11
use Symfony\Component\Console\Question\Question;
12
use Symfony\Component\Yaml\Yaml;
13
14
class LogCommand extends ContainerAwareCommand
15
{
16
    protected function configure()
17
    {
18
        $this
19
            ->setName('bzion:log')
20
            ->setDescription('Add a change to the changelog.yml file')
21
            ->addArgument(
22
                'type',
23
                InputArgument::OPTIONAL,
24
                'The type of the change (feature, bug or note)'
25
            )
26
            ->addArgument(
27
                'description',
28
                InputArgument::OPTIONAL,
29
                'A short description of the change'
30
            )
31
            ->addOption(
32
                'date',
33
                'd',
34
                InputOption::VALUE_OPTIONAL,
35
                'The date when the change occured',
36
                'today'
37
            )
38
            ->addOption(
39
                'changelog',
40
                'c',
41
                InputOption::VALUE_OPTIONAL,
42
                'The path to the changelog file',
43
                dirname(dirname(__DIR__)) . '/app/changelog.yml'
44
            );
45
    }
46
47
    protected function execute(InputInterface $input, OutputInterface $output)
48
    {
49
        $type = $input->getArgument('type');
50
        $date = \TimeDate::from($input->getOption('date'))->format("j F Y");
51
        $description = $input->getArgument('description');
52
53
        $helper = $this->getHelper('question');
54
55
        if (!$type) {
56
            $question = new ChoiceQuestion(
57
                '<question>Please enter the type of the change</question>: ',
58
                array('Bug', 'Feature', 'Note')
59
            );
60
            $type = $helper->ask($input, $output, $question);
61
        }
62
63
        if (!$description) {
64
            $question = new Question('<question>Please enter a short description of the change</question>: ');
65
            $description = $helper->ask($input, $output, $question);
66
        }
67
68
        $category = $this->getTypeName($type);
69
70
        // Parse the current changelog and append the new change to it
71
        $changelog = Yaml::parse(file_get_contents($input->getOption('changelog')));
72
        $this->addMissingArrays($changelog, $date, $category);
73
        $changelog[$date][$category][] = $description;
74
75
        $this->sort($changelog);
76
77
        $yaml = Yaml::dump($changelog, 3);
78
79
        // Prevent yaml from unnecessarily quoting strings
80
        $yaml = preg_replace('/^\'([[:print:]]+)\':$/m', '$1:', $yaml);
81
        $yaml = preg_replace('/^( {8}- )\'(.*)\'$/m', '$1$2', $yaml);
82
        $yaml = str_replace("''", "'", $yaml);
83
84
        if ($output->isDebug()) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Symfony\Component\Console\Output\OutputInterface as the method isDebug() does only exist in the following implementations of said interface: BZIon\Command\NullOutput, Symfony\Component\Console\Output\BufferedOutput, Symfony\Component\Console\Output\ConsoleOutput, Symfony\Component\Console\Output\NullOutput, Symfony\Component\Console\Output\Output, Symfony\Component\Console\Output\StreamOutput, Symfony\Component\Consol...ts\Fixtures\DummyOutput, Symfony\Component\Console\Tests\Output\TestOutput.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
85
            $output->writeln($yaml);
86
        }
87
88
        file_put_contents($input->getOption('changelog'), $yaml);
89
90
        $output->writeln('<info>The changelog has been updated successfully</info>');
91
    }
92
93
    /**
94
     * Get a type name to put into the YAML file from whatever the user gave us
95
     *
96
     * @param  string            $type The user's input
97
     * @throws \RuntimeException
98
     * @return string
99
     */
100
    private function getTypeName($type)
101
    {
102
        switch (strtolower($type)) {
103
            case 'b':
104
            case 'bug':
105
            case 'bugs':
106
                return 'Bugfixes';
107
            case 'f':
108
            case 'feature':
109
            case 'features':
110
                return 'Features';
111
            case 'n':
112
            case 'note':
113
            case 'notes':
114
                return 'Notes';
115
            default:
116
                throw new \RuntimeException("I don't understand what '$type' means");
117
        }
118
    }
119
120
    /**
121
     * If the changelog.yml file doesn't contain the date and the type of the
122
     * change, add them to it
123
     *
124
     * @param array  $changelog The parsed YAML file
125
     * @param string $date
126
     * @param string $category
127
     */
128
    private function addMissingArrays(&$changelog, $date, $category)
129
    {
130
        if (!is_array($changelog)) {
131
            $changelog = array();
132
        }
133
134
        if (!isset($changelog[$date])) {
135
            $changelog[$date] = array();
136
        }
137
138
        if (!isset($changelog[$date][$category])) {
139
            $changelog[$date][$category] = array();
140
        }
141
    }
142
143
    /**
144
     * Sort the parsed changelog array before saving it
145
     *
146
     * @param array $changelog The parsed changelog
147
     */
148
    public static function sort(&$changelog)
149
    {
150
        uksort($changelog, function ($first, $second) {
151
            $a = \TimeDate::from($first);
152
            $b = \TimeDate::from($second);
153
154
            if ($a == $b) {
155
                return 0;
156
            }
157
158
            return ($a > $b) ? -1 : 1;
159
        });
160
    }
161
}
162