Extractor::prepareHookArguments()   B
last analyzed

Complexity

Conditions 5
Paths 4

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 10
nc 4
nop 2
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 RuntimeException;
13
14
/**
15
 * Methods to extract hooks from source files
16
 */
17
class Extractor
18
{
19
20
    /**
21
     * Max number of rows to parse
22
     */
23
    const MAX_LINES = 5000;
24
25
    /**
26
     * Max columns in a single line to parse
27
     */
28
    const MAX_COLS = 500;
29
30
    /**
31
     * Pattern to extract hook arguments
32
     */
33
    const PATTERN_HOOK = '/->attach\(\s*(.+?)\s*\)/s';
34
35
    /**
36
     * Pattern to extract method names
37
     */
38
    const PATTERN_FUNCTION = '/function +(\w+)\s*\(/';
39
40
    /**
41
     * Pattern to extract class namespaces
42
     */
43
    const PATTERN_NAMESPACE = '/^namespace +(.+?)\s*;/';
44
45
    /**
46
     * Pattern to extract class names
47
     */
48
    const PATTERN_CLASS = '/(?:^abstract +|^)class +(.+?)(\s+|\n\r|$)/';
49
50
    /**
51
     * The current method while parsing hooks
52
     * @var string
53
     */
54
    protected $current_function;
55
56
    /**
57
     * The current class name space while parsing hooks
58
     * @var string
59
     */
60
    protected $current_namespace;
61
62
    /**
63
     * The current class while parsing hooks
64
     * @var string
65
     */
66
    protected $current_class;
67
68
    /**
69
     * Returns an array of hook scopes/types
70
     * @return array
71
     */
72
    public function getHookScopes()
73
    {
74
        $scopes = gplcart_config_get(__DIR__ . '/../config/scopes.php');
75
        asort($scopes);
76
        return $scopes;
77
    }
78
79
    /**
80
     * Prepares extracted hook data
81
     * @param array $data
82
     * @return array
83
     */
84
    public function prepareHook(array $data)
85
    {
86
        $data['namespaced_class'] = $data['namespace'] . '\\' . $data['class'];
87
88
        $exploded = $this->prepareHookArguments(explode(',', $data['hook']), $data);
89
90
        // Shift hook name and trim single/double quotes from it
91
        // Use strtok() to get everything before | which separates hook name and module ID
92
        $name = strtok(preg_replace('/^(\'(.*)\'|"(.*)")$/', '$2$3', array_shift($exploded)), '|');
93
94
        array_walk($exploded, function (&$param) {
95
            if (strpos($param, '$') === 0) {
96
                $param = "&$param";
97
            }
98
        });
99
100
        $data['hook'] = array(
101
            'name' => $name,
102
            'arguments' => $exploded,
103
            'uppercase_name' => implode('', array_map('ucfirst', explode('.', $name)))
104
        );
105
106
        return $data;
107
    }
108
109
    /**
110
     * Returns an array of prepared hook arguments
111
     * @param array $arguments
112
     * @param array $data
113
     * @return array
114
     */
115
    protected function prepareHookArguments(array $arguments, array $data)
116
    {
117
        $i = 0;
118
        foreach ($arguments as &$argument) {
119
            $argument = trim($argument);
120
            // Replace arguments which aren't plain variables, e.g $data['key']
121
            // TODO: check uniqueness
122
            if (strpos($argument, '$') === 0 && preg_match('/^[A-Za-z0-9_\$]+$/', $argument) !== 1) {
123
                $argument = '$param' . $i;
124
            } else if ($argument === '$this') {
125
                // Replace $this argument with type hinting $object
126
                $argument = '\\' . $data['namespaced_class'] . ' $object';
127
            }
128
129
            $i++;
130
        }
131
        return $arguments;
132
    }
133
134
    /**
135
     * Returns an array of extracted hooks
136
     * @param array $options
137
     * @return array
138
     */
139
    public function getHooks(array $options)
140
    {
141
        $scanned = (array) $this->scan($options['directory']);
142
143
        // Pager
144
        if (!empty($options['limit'])) {
145
            list($start, $length) = $options['limit'];
146
            $scanned = array_slice($scanned, $start, $length, true);
147
        }
148
149
        if (empty($scanned)) {
150
            return array();
151
        }
152
153
        $success = $errors = array();
154
155
        foreach ($scanned as $file) {
156
            foreach ($this->parse($file) as $extracted) {
157
158
                $extracted['file'] = gplcart_path_relative($file);
159
                $prepared = $this->prepareHook($extracted);
160
161
                if (!empty($options['scopes']) && !$this->inScope($prepared['hook']['name'], $options['scopes'])) {
162
                    continue;
163
                }
164
165
                if (!method_exists($prepared['namespaced_class'], $prepared['function'])) {
166
                    $errors[$prepared['hook']['name']] = $prepared;
167
                    continue;
168
                }
169
170
                $success[$prepared['hook']['name']] = $prepared;
171
            }
172
        }
173
174
        return array(
175
            'success' => $success,
176
            'errors' => $errors,
177
            'files' => $scanned
178
        );
179
    }
180
181
    /**
182
     * Whether a given hook name is in array of scopes
183
     * @param string $hook
184
     * @param array $scopes
185
     * @return boolean
186
     */
187
    protected function inScope($hook, array $scopes)
188
    {
189
        foreach ($scopes as $scope) {
190
            $parts = explode('.', $hook);
191
            if (reset($parts) === $scope) {
192
                return true;
193
            }
194
        }
195
        return false;
196
    }
197
198
    /**
199
     * Returns an array of extracted hooks from a single file
200
     * @param string $file
201
     * @return array
202
     * @throws RuntimeException
203
     */
204
    public function parse($file)
205
    {
206
        $this->current_class = $this->current_namespace = null;
207
208
        $handle = fopen($file, 'r');
209
210
        if (!is_resource($handle)) {
211
            throw new RuntimeException("Failed to open file $file");
212
        }
213
214
        $row = 0;
215
        $extracted = array();
216
        $lines = self::MAX_LINES;
217
218
        while ($lines && $line = fgets($handle, self::MAX_COLS)) {
219
220
            $row++;
221
            $lines--;
222
223
            $namespace = $class = $function = $hook = array();
224
225
            preg_match(self::PATTERN_NAMESPACE, $line, $namespace);
226
227
            // Namespace and class name should occur once per file
228
            // If it has been set, skip others to avoid errors
229
            if (!isset($this->current_namespace) && !empty($namespace[1])) {
230
                $this->current_namespace = $namespace[1];
231
            }
232
233
            preg_match(self::PATTERN_CLASS, $line, $class);
234
235
            if (!isset($this->current_class) && !empty($class[1])) {
236
                $this->current_class = $class[1];
237
            }
238
239
            preg_match(self::PATTERN_FUNCTION, $line, $function);
240
241
            if (!empty($function[1])) {
242
                $this->current_function = $function[1];
243
            }
244
245
            preg_match(self::PATTERN_HOOK, $line, $hook);
246
247
            if (empty($hook[1])) {
248
                continue;
249
            }
250
251
            $extracted[] = array(
252
                'row' => $row,
253
                'hook' => $hook[1],
254
                'class' => $this->current_class,
255
                'function' => $this->current_function,
256
                'namespace' => $this->current_namespace
257
            );
258
        }
259
260
        fclose($handle);
261
        return $extracted;
262
    }
263
264
    /**
265
     * Returns an array of scanned files to extract hooks from or counts extracted items
266
     * @param string $directory
267
     * @param bool $count
268
     * @return integer|array
269
     */
270
    public function scan($directory, $count = false)
271
    {
272
        $files = array_filter(gplcart_file_scan_recursive($directory), function ($file) {
273
            return pathinfo($file, PATHINFO_EXTENSION) === 'php';
274
        });
275
276
        if ($count) {
277
            return count($files);
278
        }
279
280
        sort($files);
281
        return $files;
282
    }
283
284
}
285