LaravelEnvScanner::storeResult()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 28
rs 9.472
c 0
b 0
f 0
cc 3
nc 3
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
        'rows' => [],
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 = basename($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
    private function storeResult()
269
    {
270
        $resultData = [
271
            'location' => $this->location,
272
            'defined' => '-',
273
            'depending_on_default' => '-',
274
            'undefined' => '-',
275
        ];
276
277
        $this->results['locations']++;
278
279
        if (env($this->parameters->variable) !== null) {
280
            $resultData['defined'] = $this->parameters->variable;
281
            $this->results['defined']++;
282
        } else if ($this->parameters->default) {
283
            $resultData['depending_on_default'] = $this->parameters->variable;
284
            $this->results['depending_on_default']++;
285
        } else {
286
            $resultData['undefined'] = $this->parameters->variable;
287
            $this->results['undefined']++;
288
            $this->undefined[] = [
289
                'filename' => $this->location,
290
                'variable' => $this->parameters->variable,
291
            ];
292
        }
293
294
        $this->results['rows'][] = $resultData;
295
    }
296
297
    private function getFiles(): array
298
    {
299
        if (!file_exists($this->dir)) {
300
            return [];
301
        }
302
303
        $files = new RegexIterator(
304
            new RecursiveIteratorIterator(
305
                new RecursiveDirectoryIterator($this->dir)
306
            ),
307
            '/.*?.php/', RegexIterator::GET_MATCH
308
        );
309
310
        $list = [[]];
311
312
        foreach ($files as $file) {
313
            $list[] = $file;
314
        }
315
316
        $list = array_merge(...$list);
317
318
        return $list;
319
    }
320
}
321