SignatureParser::parseOption()   A
last analyzed

Complexity

Conditions 5
Paths 8

Size

Total Lines 35
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 25
c 1
b 0
f 0
dl 0
loc 35
rs 9.2088
cc 5
nc 8
nop 1
1
<?php
2
3
namespace PortlandLabs\Slackbot\Command;
4
5
use Illuminate\Support\Str;
6
use InvalidArgumentException;
7
use PortlandLabs\Slackbot\Command\Argument\Manager;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, PortlandLabs\Slackbot\Command\Manager. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
8
9
/**
10
 * Largely copied / inspired by Laravel's console component: https://github.com/illuminate/console/blob/master/Parser.php
11
 */
12
class SignatureParser
13
{
14
15
    /**
16
     * Parse the given console command definition into an array.
17
     *
18
     * @param  string $expression
19
     * @param Manager $arguments
20
     * @return Manager
21
     *
22
     * @throws \Exception
23
     */
24
    public static function parse($expression, Manager $arguments)
25
    {
26
        $arguments->setCommand(static::name($expression));
27
28
        if (preg_match_all('/\{\s*(.*?)\s*\}/', $expression, $matches)) {
29
            if (count($matches[1])) {
30
                $arguments->add(static::parameters($matches[1], $arguments));
0 ignored issues
show
Unused Code introduced by
The call to PortlandLabs\Slackbot\Co...ureParser::parameters() has too many arguments starting with $arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

30
                $arguments->add(static::/** @scrutinizer ignore-call */ parameters($matches[1], $arguments));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
31
            }
32
        }
33
34
        return $arguments;
35
    }
36
37
    /**
38
     * Extract the name of the command from the expression.
39
     *
40
     * @param  string  $expression
41
     * @return string
42
     *
43
     * @throws \InvalidArgumentException
44
     */
45
    protected static function name($expression)
46
    {
47
        if (trim($expression) === '') {
48
            throw new InvalidArgumentException('Console command definition is empty.');
49
        }
50
51
        if (! preg_match('/[^\s]+/', $expression, $matches)) {
52
            throw new InvalidArgumentException('Unable to determine command name from signature.');
53
        }
54
55
        return $matches[0];
56
    }
57
58
    /**
59
     * Extract all of the parameters from the tokens.
60
     *
61
     * @param  array  $tokens
62
     * @return array
63
     */
64
    protected static function parameters(array $tokens)
65
    {
66
        $data = [];
67
        foreach ($tokens as $token) {
68
            if (preg_match('/-{2,}(.*)/', $token, $matches)) {
69
                $argument = static::parseOption($matches[1]);
70
                $data[$argument['longPrefix']] = $argument;
71
            } else {
72
                $argument = static::parseArgument($token);
73
                $data[$argument['token']] = $argument;
74
            }
75
        }
76
77
        return $data;
78
    }
79
80
    /**
81
     * Parse an argument expression.
82
     *
83
     *
84
     * @param  string  $token
85
     * @return array Options are prefix, longprefix, descriptions, defaultValue, required, noValue
86
     */
87
    protected static function parseArgument($token)
88
    {
89
        list($token, $description) = static::extractDescription($token);
90
91
        $argument = [
92
            'token' => $token,
93
            'description' => $description
94
        ];
95
96
97
        switch (true) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/(.+)\=\*(.+)/', $token, $matches) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/(.+)\=(.+)/', $token, $matches) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
98
            case Str::endsWith($token, '?*'):
99
            case Str::endsWith($token, '?'):
100
                $argument['token'] = trim($token, '?*');
101
                $argument['required'] = false;
102
                break;
103
            case Str::endsWith($token, '*'):
104
                $argument['token'] = trim($token, '*');
105
                $argument['required'] = true;
106
                break;
107
            case preg_match('/(.+)\=\*(.+)/', $token, $matches):
108
            case preg_match('/(.+)\=(.+)/', $token, $matches):
109
                $argument['token'] = $matches[1];
110
                $argument['defaultValue'] = $matches[2];
111
                $argument['required'] = false;
112
                break;
113
            default:
114
                $argument['token'] = $token;
115
                $argument['required'] = true;
116
                break;
117
        }
118
119
        return $argument;
120
    }
121
122
    /**
123
     * Parse an option expression.
124
     *
125
     * @param  string  $token
126
     * @return array Options are prefix, longprefix, descriptions, defaultValue, required, noValue
127
     */
128
    protected static function parseOption($token)
129
    {
130
        list($token, $description) = static::extractDescription($token);
131
132
        $matches = preg_split('/\s*\|\s*/', $token, 2);
133
134
        if (isset($matches[1])) {
135
            $shortcut = $matches[0];
136
            $token = $matches[1];
137
        } else {
138
            $shortcut = null;
139
        }
140
141
        $argument = [
142
            'prefix' => $shortcut,
143
            'longPrefix' => $token,
144
            'description' => $description,
145
            'noValue' => true,
146
            'required' => false,
147
        ];
148
149
        switch (true) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/(.+)\=(.+)/', $token, $matches) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
150
            case Str::endsWith($token, '='):
151
            case Str::endsWith($token, '=*'):
152
                $argument['noValue'] = false;
153
                $argument['longPrefix'] = trim($token, '=*');
154
                break;
155
            case preg_match('/(.+)\=(.+)/', $token, $matches):
156
                $argument['noValue'] = false;
157
                $argument['longPrefix'] = $matches[1];
158
                $argument['defaultValue'] = $matches[2];
159
                break;
160
        }
161
162
        return $argument;
163
    }
164
165
    /**
166
     * Parse the token into its token and description segments.
167
     *
168
     * @param  string  $token
169
     * @return array
170
     */
171
    protected static function extractDescription($token)
172
    {
173
        $parts = preg_split('/\s+:\s+/', trim($token), 2);
174
175
        return count($parts) === 2 ? $parts : [$token, ''];
176
    }
177
}
178