Completed
Push — master ( 13dbcb...3ebc3a )
by Iurii
02:03
created

Extractor::parse()   C

Complexity

Conditions 10
Paths 18

Size

Total Lines 60
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 60
rs 6.5333
c 0
b 0
f 0
cc 10
eloc 33
nc 18
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * @package Skeleton module
5
 * @author Iurii Makukh <[email protected]>
6
 * @copyright Copyright (c) 2015, Iurii Makukh
7
 * @license https://www.gnu.org/licenses/gpl.html GNU/GPLv3
8
 */
9
10
namespace gplcart\modules\skeleton\models;
11
12
use gplcart\core\Model;
13
use gplcart\core\models\Language as LanguageModel;
14
15
/**
16
 * Methods to extract hooks from source files
17
 */
18
class Extractor extends Model
19
{
20
21
    /**
22
     * Max number of rows to parse
23
     */
24
    const MAX_LINES = 5000;
25
26
    /**
27
     * Max columns in a single line to parse
28
     */
29
    const MAX_COLS = 500;
30
31
    /**
32
     * Pattern to extract hook arguments
33
     */
34
    const PATTERN_HOOK = '/->fire\s*\(\s*(.+?)\s*\)/s';
35
36
    /**
37
     * Pattern to extract method names
38
     */
39
    const PATTERN_FUNCTION = '/function\s+(\w+)\s*\(/';
40
41
    /**
42
     * Pattern to extract class namespaces
43
     */
44
    const PATTERN_NAMESPACE = '/^namespace\s+(.+?)\s*;/';
45
46
    /**
47
     * Pattern to extract class names
48
     */
49
    const PATTERN_CLASS = '/(?:^| )class\s+(.+?)(\s+|\n\r|$)/';
50
51
    /**
52
     * The current method while parsing hooks
53
     * @var string
54
     */
55
    protected $current_function;
56
57
    /**
58
     * The current class namespace while parsing hooks
59
     * @var string
60
     */
61
    protected $current_namespace;
62
63
    /**
64
     * The current class while parsing hooks
65
     * @var string
66
     */
67
    protected $current_class;
68
69
    /**
70
     * Language model instance
71
     * @var \gplcart\core\models\Language $language
72
     */
73
    protected $language;
74
75
    /**
76
     * Constructor
77
     * @param LanguageModel $language
78
     */
79
    public function __construct(LanguageModel $language)
80
    {
81
        parent::__construct();
82
83
        $this->language = $language;
84
    }
85
86
    /**
87
     * Returns an array of hook scopes/types
88
     * @return array
89
     */
90
    public function getHookScopes()
91
    {
92
        $items = array(
93
            'construct' => $this->language->text('Class construction'),
94
            'destruct' => $this->language->text('Class destruction'),
95
            'cli' => $this->language->text('Command line'),
96
            'template' => $this->language->text('Templates'),
97
            'theme' => $this->language->text('Theme'),
98
            'library' => $this->language->text('Libraries'),
99
            'route' => $this->language->text('URL'),
100
            'cron' => $this->language->text('CRON'),
101
            'address' => $this->language->text('User addresses'),
102
            'backup' => $this->language->text('Backup'),
103
            'cart' => $this->language->text('Shopping cart'),
104
            'category' => $this->language->text('Categories'),
105
            'city' => $this->language->text('Cities'),
106
            'collection' => $this->language->text('Collections'),
107
            'compare' => $this->language->text('Product comparison'),
108
            'condition' => $this->language->text('Trigger conditions'),
109
            'country' => $this->language->text('Countries'),
110
            'currency' => $this->language->text('Currencies'),
111
            'dashboard' => $this->language->text('Dashboard'),
112
            'field' => $this->language->text('Product fields'),
113
            'file' => $this->language->text('Files'),
114
            'filter' => $this->language->text('Filters'),
115
            'imagestyle' => $this->language->text('Images'),
116
            'install' => $this->language->text('Installation'),
117
            'job' => $this->language->text('Bulk jobs'),
118
            'language' => $this->language->text('Localization'),
119
            'mail' => $this->language->text('E-mail'),
120
            'order' => $this->language->text('Orders'),
121
            'page' => $this->language->text('Pages'),
122
            'payment' => $this->language->text('Payment methods'),
123
            'price' => $this->language->text('Prices'),
124
            'product' => $this->language->text('Products'),
125
            'rating' => $this->language->text('Ratings'),
126
            'report' => $this->language->text('Reporting'),
127
            'review' => $this->language->text('Reviews'),
128
            'search' => $this->language->text('Searching'),
129
            'shipping' => $this->language->text('Shipping methods'),
130
            'sku' => $this->language->text('SKU'),
131
            'state' => $this->language->text('Country states'),
132
            'store' => $this->language->text('Stores'),
133
            'transaction' => $this->language->text('Payment transactions'),
134
            'trigger' => $this->language->text('Triggers'),
135
            'user' => $this->language->text('Users'),
136
            'validator' => $this->language->text('Validating'),
137
            'wishlist' => $this->language->text('Wishlists'),
138
            'zone' => $this->language->text('Geo zones'),
139
            'module' => $this->language->text('Modules'),
140
            'oauth' => $this->language->text('Oauth')
141
        );
142
143
        asort($items);
144
        return $items;
145
    }
146
147
    /**
148
     * Prepares extracted hook data
149
     * @param array $data
150
     * @return array
151
     */
152
    public function prepareHook(array $data)
153
    {
154
        $data['namespaced_class'] = $data['namespace'] . '\\' . $data['class'];
155
156
        $exploded = $this->prepareHookArguments(explode(',', $data['hook']), $data);
157
158
        // Shift hook name and trim single/double quotes from it
159
        // Use strtok() to get everything before | which separates hook name and module ID
160
        $name = strtok(preg_replace('/^(\'(.*)\'|"(.*)")$/', '$2$3', array_shift($exploded)), '|');
161
162
        array_walk($exploded, function(&$param) {
163
            if (strpos($param, '$') === 0) {
164
                $param = "&$param";
165
            }
166
        });
167
168
        $data['hook'] = array(
169
            'name' => $name,
170
            'arguments' => $exploded,
171
            'uppercase_name' => implode('', array_map('ucfirst', explode('.', $name)))
172
        );
173
174
        return $data;
175
    }
176
177
    /**
178
     * Returns an array of prepared hook arguments
179
     * @param array $arguments
180
     * @param array $data
181
     * @return array
182
     */
183
    protected function prepareHookArguments(array $arguments, array $data)
184
    {
185
        $i = 0;
186
        foreach ($arguments as &$argument) {
187
            $argument = trim($argument);
188
            // Replace arguments which aren't plain variables, e.g $data['key']
189
            // TODO: check uniqueness
190
            if (strpos($argument, '$') === 0 && preg_match('/^[A-Za-z0-9_\$]+$/', $argument) !== 1) {
191
                $argument = '$param' . $i;
192
            } else if ($argument === '$this') {
193
                // Replace $this argument with type hinting $object
194
                $argument = '\\' . $data['namespaced_class'] . ' $object';
195
            }
196
197
            $i++;
198
        }
199
        return $arguments;
200
    }
201
202
    /**
203
     * Returns an array of extracted hooks
204
     * @param array $options
205
     * @return array
206
     */
207
    public function getHooks(array $options)
208
    {
209
        $scanned = $this->scan($options['directory']);
210
211
        // Pager
212
        if (!empty($options['limit'])) {
213
            list($start, $length) = $options['limit'];
214
            $scanned = array_slice($scanned, $start, $length, true);
215
        }
216
217
        if (empty($scanned)) {
218
            return array();
219
        }
220
221
        $success = $errors = array();
222
223
        foreach ($scanned as $file) {
0 ignored issues
show
Bug introduced by
The expression $scanned of type integer|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
224
            foreach ($this->parse($file) as $extracted) {
225
226
                $extracted['file'] = gplcart_relative_path($file);
227
                $prepared = $this->prepareHook($extracted);
228
229
                // Filter by scopes
230
                if (!empty($options['scopes']) && !$this->inScope($prepared['hook']['name'], $options['scopes'])) {
231
                    continue;
232
                }
233
234
                // Test extracted class and method
235
                if (!method_exists($prepared['namespaced_class'], $prepared['function'])) {
236
                    trigger_error("Extracted method {$prepared['namespaced_class']}::{$prepared['function']} does not exist");
237
                    $errors[$prepared['hook']['name']] = $prepared;
238
                    continue;
239
                }
240
241
                $success[$prepared['hook']['name']] = $prepared;
242
            }
243
        }
244
245
        return array('success' => $success, 'errors' => $errors, 'files' => $scanned);
246
    }
247
248
    /**
249
     * Whether a given hook name is in array of scopes
250
     * @param string $hook
251
     * @param array $scopes
252
     * @return boolean
253
     */
254
    protected function inScope($hook, array $scopes)
255
    {
256
        foreach ($scopes as $scope) {
257
            $parts = explode('.', $hook);
258
            if (reset($parts) === $scope) {
259
                return true;
260
            }
261
        }
262
        return false;
263
    }
264
265
    /**
266
     * Returns an array of extracted hooks from a single file
267
     * @param string $file
268
     * @return array
269
     */
270
    public function parse($file)
271
    {
272
        $this->current_class = $this->current_namespace = null;
273
274
        $handle = fopen($file, 'r');
275
276
        if (!is_resource($handle)) {
277
            trigger_error("Failed to open file $file");
278
            return array();
279
        }
280
281
        $row = 0;
282
        $extracted = array();
283
        $lines = self::MAX_LINES;
284
285
        while ($lines && $line = fgets($handle, self::MAX_COLS)) {
286
287
            $row++;
288
            $lines--;
289
290
            $namespace = $class = $function = $hook = array();
291
292
            preg_match(self::PATTERN_NAMESPACE, $line, $namespace);
293
294
            // Namespace and class name should occur once per file
295
            // If it has been set, skip others to avoid errors
296
            if (!isset($this->current_namespace) && !empty($namespace[1])) {
297
                $this->current_namespace = $namespace[1];
298
            }
299
300
            preg_match(self::PATTERN_CLASS, $line, $class);
301
302
            if (!isset($this->current_class) && !empty($class[1])) {
303
                $this->current_class = $class[1];
304
            }
305
306
            preg_match(self::PATTERN_FUNCTION, $line, $function);
307
308
            if (!empty($function[1])) {
309
                $this->current_function = $function[1];
310
            }
311
312
            preg_match(self::PATTERN_HOOK, $line, $hook);
313
314
            if (empty($hook[1])) {
315
                continue;
316
            }
317
318
            $extracted[] = array(
319
                'row' => $row,
320
                'hook' => $hook[1],
321
                'class' => $this->current_class,
322
                'function' => $this->current_function,
323
                'namespace' => $this->current_namespace
324
            );
325
        }
326
327
        fclose($handle);
328
        return $extracted;
329
    }
330
331
    /**
332
     * Returns an array of scanned files to extract hooks from or counts extracted items
333
     * @param string $directory
334
     * @return integer|array
335
     */
336
    public function scan($directory, $count = false)
337
    {
338
        $files = array_filter(gplcart_file_scan_recursive($directory), function($file) {
339
            return pathinfo($file, PATHINFO_EXTENSION) === 'php';
340
        });
341
342
        if ($count) {
343
            return count($files);
344
        }
345
346
        sort($files);
347
        return $files;
348
    }
349
350
}
351