Completed
Push — master ( e19d62...f054f0 )
by Maarten
59s
created

LaravelEnvScanner::needsWarning()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
cc 2
nc 2
nop 0
1
<?php
2
3
namespace Mtolhuys\LaravelEnvScanner;
4
5
use RecursiveDirectoryIterator;
6
use RecursiveIteratorIterator;
7
use RegexIterator;
8
9
class LaravelEnvScanner
10
{
11
    /**
12
     * The results of performed scan
13
     *
14
     * @var array
15
     */
16
    public $results = [
17
        'locations' => 0,
18
        'defined' => 0,
19
        'undefined' => 0,
20
        'depending_on_default' => 0,
21
        'columns' => [],
22
    ];
23
24
    /**
25
     * Stores processed file and var names
26
     *
27
     * @var array
28
     */
29
    private $processed = [
30
        'variables' => [],
31
    ];
32
33
    /**
34
     * Stores undefined var names
35
     *
36
     * @var array
37
     */
38
    public $undefined = [];
39
40
    /**
41
     * Stores warnings for vars not passing validation
42
     *
43
     * @var array
44
     */
45
    public $warnings = [];
46
47
    /**
48
     * Current file being processed
49
     *
50
     * @var string
51
     */
52
    private $file;
53
54
    /**
55
     * Current location a found invocation
56
     *
57
     * @var string
58
     */
59
    private $location;
60
61
    /**
62
     * Current invocation being processed
63
     *
64
     * @var string
65
     */
66
    private $invocation;
67
68
    /**
69
     * Current parameters being processed
70
     *
71
     * @var object
72
     */
73
    private $parameters;
74
75
    /**
76
     * Root directory to start recursive search for env()'s from
77
     * Defaults to config_path()
78
     *
79
     * @var string $dir
80
     */
81
    public $dir;
82
83
    public function __construct(string $dir = null)
84
    {
85
        $this->dir = $dir ?? config_path();
86
    }
87
88
    /**
89
     * Run the scan
90
     *
91
     * @return mixed
92
     * @throws \Exception
93
     */
94
    public function scan()
95
    {
96
        foreach ($this->getFiles() as $file) {
97
            $lines = explode(PHP_EOL, file_get_contents($file));
98
99
            $this->file = $file;
100
101
            foreach ($lines as $index => $line) {
102
103
                if (preg_match('# env\(| getenv\(#', $line)) {
104
                    if (! $this->setInvocationDetails($lines, $line, $index)) {
105
                        continue;
106
                    }
107
108
                    $this->storeResult();
109
                }
110
            }
111
        }
112
113
        return $this;
114
    }
115
116
    /**
117
     * Search for possible matches and make something usable out of it
118
     *
119
     * @param array $lines
120
     * @param string $line
121
     * @param int $index
122
     * @return bool
123
     */
124
    private function setInvocationDetails(array $lines, string $line, int $index): bool
125
    {
126
        $matches = $this->search($lines, $line, $index);
127
128
        if (empty(array_filter($matches))) {
129
            return false;
130
        }
131
132
        $this->setInvocation($matches);
0 ignored issues
show
Bug introduced by
It seems like $matches defined by $this->search($lines, $line, $index) on line 126 can also be of type null; however, Mtolhuys\LaravelEnvScann...canner::setInvocation() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
133
        $this->setParameters($matches);
0 ignored issues
show
Bug introduced by
It seems like $matches defined by $this->search($lines, $line, $index) on line 126 can also be of type null; however, Mtolhuys\LaravelEnvScann...canner::setParameters() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
134
        $this->setLocation($index + 1);
135
136
        if ($this->needsWarning()) {
137
            return false;
138
        }
139
140
        if ($this->alreadyProcessed()) {
141
            return false;
142
        }
143
144
        $this->processed['variables'][] = $this->parameters->variable;
145
146
        return true;
147
    }
148
149
    /**
150
     * Search for single and multi-lined env and getenv invocations
151
     *
152
     * @param array $lines
153
     * @param string $line
154
     * @param int $number
155
     * @return mixed
156
     */
157
    private function search(array $lines, string $line, int $number)
158
    {
159
        preg_match_all(
160
            '# env\((.*?)\)| getenv\((.*?)\)#',
161
            $line,
162
            $matches
163
        );
164
165
        $line = str_replace(' ', '', $line);
166
167
        if ($line === 'env(' || $line === 'getenv(') {
168
            $matches = $this->searchMultiLine($lines, $number);
169
        }
170
171
        return $matches;
172
    }
173
174
    /**
175
     * For multi-line invocation f.e.
176
     * env(
177
     *   'MULTI',
178
     *   'lined'
179
     * );
180
     *
181
     * @param array $lines
182
     * @param int $number
183
     * @return mixed
184
     */
185
    private function searchMultiLine(array $lines, int $number)
186
    {
187
        $search = $lines[$number];
188
        $search .= $lines[$number + 1];
189
        $search .= $lines[$number + 2];
190
        $search .= $lines[$number + 3];
191
192
        preg_match_all(
193
            '# env\((.*?)\)| getenv\((.*?)\)#',
194
            $search,
195
            $matches
196
        );
197
198
        return $matches;
199
    }
200
201
    /**
202
     * Set invocation based on first index in preg_match_all result
203
     *
204
     * @param array $matches
205
     */
206
    private function setInvocation(array $matches)
207
    {
208
        $this->invocation = str_replace(' ', '', str_replace(
209
            ' ', '', $matches[0]
210
        )[0]);
211
    }
212
213
    /**
214
     * Sets parameters based on comma exploding
215
     * 1 of last indexes in preg_match_all result
216
     *
217
     * @param array $matches
218
     */
219
    private function setParameters(array $matches)
220
    {
221
        $parameters = empty($matches[1][0]) ? $matches[2][0] : $matches[1][0];
222
        $parameters = explode(',', str_replace(["'", '"', ' ',], '', $parameters));
223
224
        $this->parameters = (object)[
225
            'variable' => $parameters[0],
226
            'default' => $parameters[1] ?? null,
227
        ];
228
    }
229
230
    /**
231
     * Sets location as filename + linenumber
232
     *
233
     * @param int $linenumber
234
     */
235
    private function setLocation(int $linenumber)
236
    {
237
        $this->location = "{$this->file}:$linenumber";
238
    }
239
240
    /**
241
     * Only warn about risky and unreadable invocations
242
     *
243
     * @return bool
244
     */
245
    private function needsWarning(): bool
246
    {
247
248
        if (!preg_match('/^\w+$/', $this->parameters->variable)) {
249
            $this->warnings[] =  (object)[
250
                'invocation' => $this->invocation,
251
                'location' => $this->location,
252
            ];
253
254
            return true;
255
        }
256
257
        return false;
258
    }
259
260
    /**
261
     * @return bool
262
     */
263
    private function alreadyProcessed(): bool
264
    {
265
        return in_array($this->parameters->variable, $this->processed['variables'], true);
266
    }
267
268
    /**
269
     * Store result and optional runtime output
270
     */
271
    private function storeResult()
272
    {
273
        $resultData = [
274
            'location' => $this->location,
275
            'defined' => '-',
276
            'depending_on_default' => '-',
277
            'undefined' => '-',
278
        ];
279
280
        $this->results['locations']++;
281
282
        if (env($this->parameters->variable) !== null) {
283
            $resultData['defined'] = $this->parameters->variable;
284
            $this->results['defined']++;
285
        } else if ($this->parameters->default) {
286
            $resultData['depending_on_default'] = $this->parameters->variable;
287
            $this->results['depending_on_default']++;
288
        } else {
289
            $resultData['undefined'] = $this->parameters->variable;
290
            $this->results['undefined']++;
291
            $this->undefined[] = [
292
                'filename' => $this->location,
293
                'variable' => $this->parameters->variable,
294
            ];
295
        }
296
297
        $this->results['columns'][] = $resultData;
298
    }
299
300
    private function getFiles(): array
301
    {
302
        if (!file_exists($this->dir)) {
303
            return [];
304
        }
305
306
        $files = new RegexIterator(
307
            new RecursiveIteratorIterator(
308
                new RecursiveDirectoryIterator($this->dir)
309
            ),
310
            '/.*?.php/', RegexIterator::GET_MATCH
311
        );
312
313
        $list = [[]];
314
315
        foreach ($files as $file) {
316
            $list[] = $file;
317
        }
318
319
        $list = array_merge(...$list);
320
321
        return $list;
322
    }
323
}
324