Completed
Push — master ( 5cf2d9...00c11c )
by Iurii
01:59
created

Extractor::prepareHook()   B

Complexity

Conditions 2
Paths 1

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.9713
c 0
b 0
f 0
cc 2
eloc 12
nc 1
nop 1
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|$)/';
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('Bootstraping, class construction'),
94
            'cli' => $this->language->text('Command line interface'),
95
            'template' => $this->language->text('Rendering templates'),
96
            'theme' => $this->language->text('Theme setup, JS/CSS assets'),
97
            'library' => $this->language->text('3-d party libraries'),
98
            'route' => $this->language->text('URL routing'),
99
            'cron' => $this->language->text('Scheduled operations'),
100
            'address' => $this->language->text('User addresses'),
101
            'backup' => $this->language->text('Backup'),
102
            'cart' => $this->language->text('Shopping cart'),
103
            'category' => $this->language->text('Categories'),
104
            'city' => $this->language->text('Cities'),
105
            'collection' => $this->language->text('Collections'),
106
            'compare' => $this->language->text('Product comparison'),
107
            'condition' => $this->language->text('Trigger conditions'),
108
            'country' => $this->language->text('Countries'),
109
            'currency' => $this->language->text('Currencies'),
110
            'editor' => $this->language->text('Editing theme files'),
111
            'export' => $this->language->text('Export (products, categories etc.)'),
112
            'field' => $this->language->text('Product fields'),
113
            'file' => $this->language->text('Files'),
114
            'filter' => $this->language->text('HTML filters'),
115
            'imagestyle' => $this->language->text('Processing images'),
116
            'import' => $this->language->text('Import (products, categories etc.)'),
117
            'install' => $this->language->text('Installation'),
118
            'job' => $this->language->text('Bulk jobs'),
119
            'language' => $this->language->text('Languages, localization'),
120
            'mail' => $this->language->text('Sending E-mail'),
121
            'order' => $this->language->text('Ordering products'),
122
            'page' => $this->language->text('Pages'),
123
            'payment' => $this->language->text('Payment methods'),
124
            'price' => $this->language->text('Prices'),
125
            'product' => $this->language->text('Products'),
126
            'rating' => $this->language->text('Ratings'),
127
            'report' => $this->language->text('Reporting PHP errors, system events'),
128
            'review' => $this->language->text('Reviews'),
129
            'search' => $this->language->text('Searching'),
130
            'shipping' => $this->language->text('Shipping methods'),
131
            'sku' => $this->language->text('Product SKU'),
132
            'state' => $this->language->text('Country states'),
133
            'store' => $this->language->text('Stores'),
134
            'transaction' => $this->language->text('Payment transactions'),
135
            'trigger' => $this->language->text('Triggers'),
136
            'user' => $this->language->text('Users'),
137
            'validator' => $this->language->text('Validating'),
138
            'wishlist' => $this->language->text('Wishlists'),
139
            'zone' => $this->language->text('Geo zones'),
140
            'module' => $this->language->text('Modules'),
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
                    $errors[$prepared['hook']['name']] = $prepared;
237
                    continue;
238
                }
239
240
                $success[$prepared['hook']['name']] = $prepared;
241
            }
242
        }
243
244
        return array('success' => $success, 'errors' => $errors, 'files' => $scanned);
245
    }
246
247
    /**
248
     * Whether a given hook name is in array of scopes
249
     * @param string $hook
250
     * @param array $scopes
251
     * @return boolean
252
     */
253
    protected function inScope($hook, array $scopes)
254
    {
255
        foreach ($scopes as $scope) {
256
            if (strpos($hook, "$scope.") === 0) {
257
                return true;
258
            }
259
        }
260
        return false;
261
    }
262
263
    /**
264
     * Returns an array of extracted hooks from a single file
265
     * @param string $file
266
     * @return array
267
     */
268
    public function parse($file)
269
    {
270
        $this->current_class = $this->current_namespace = '';
271
272
        $handle = fopen($file, 'r');
273
274
        if (!is_resource($handle)) {
275
            trigger_error("Failed to open file $file");
276
            return array();
277
        }
278
279
        $row = 0;
280
        $extracted = array();
281
        $lines = self::MAX_LINES;
282
283
        while ($lines && $line = fgets($handle, self::MAX_COLS)) {
284
285
            $row++;
286
            $lines--;
287
288
            $namespace = $class = $function = $hook = array();
289
290
            preg_match(self::PATTERN_NAMESPACE, $line, $namespace);
291
292
            if (!empty($namespace[1])) {
293
                $this->current_namespace = $namespace[1];
294
            }
295
296
            preg_match(self::PATTERN_CLASS, $line, $class);
297
298
            if (!empty($class[1])) {
299
                $this->current_class = $class[1];
300
            }
301
302
            preg_match(self::PATTERN_FUNCTION, $line, $function);
303
304
            if (!empty($function[1])) {
305
                $this->current_function = $function[1];
306
            }
307
308
            preg_match(self::PATTERN_HOOK, $line, $hook);
309
310
            if (empty($hook[1])) {
311
                continue;
312
            }
313
314
            $extracted[] = array(
315
                'row' => $row,
316
                'hook' => $hook[1],
317
                'class' => $this->current_class,
318
                'function' => $this->current_function,
319
                'namespace' => $this->current_namespace
320
            );
321
        }
322
323
        fclose($handle);
324
        return $extracted;
325
    }
326
327
    /**
328
     * Returns an array of scanned files to extract hooks from or counts extracted items
329
     * @param string $directory
330
     * @return integer|array
331
     */
332
    public function scan($directory, $count = false)
333
    {
334
        $files = array_filter(gplcart_file_scan_recursive("$directory/*"), function($file) {
335
            return pathinfo($file, PATHINFO_EXTENSION) === 'php';
336
        });
337
338
        if ($count) {
339
            return count($files);
340
        }
341
342
        sort($files);
343
        return $files;
344
    }
345
346
}
347