Completed
Push — 4.0 ( 25fc97...6a2270 )
by Marco
16:06
created

Parser::param()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 47
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 47
rs 8.6845
cc 4
eloc 15
nc 8
nop 4
1
<?php namespace Comodojo\Dispatcher\Router;
2
3
use \Comodojo\Dispatcher\Components\Model as DispatcherClassModel;
4
use \Monolog\Logger;
5
use \Comodojo\Dispatcher\Components\Configuration;
6
use \Comodojo\Exception\DispatcherException;
7
use \Exception;
8
9
/**
10
 * @package     Comodojo Dispatcher
11
 * @author      Marco Giovinazzi <[email protected]>
12
 * @author      Marco Castiello <[email protected]>
13
 * @license     GPL-3.0+
14
 *
15
 * LICENSE:
16
 *
17
 * This program is free software: you can redistribute it and/or modify
18
 * it under the terms of the GNU Affero General Public License as
19
 * published by the Free Software Foundation, either version 3 of the
20
 * License, or (at your option) any later version.
21
 *
22
 * This program is distributed in the hope that it will be useful,
23
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25
 * GNU Affero General Public License for more details.
26
 *
27
 * You should have received a copy of the GNU Affero General Public License
28
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
29
 */
30
31
class Parser extends DispatcherClassModel {
32
    
33
    // This method read the route (folder by folder recursively) and build 
34
    // the global regular expression against which all the request URI will be compared
35
    public function read($folders = array(), &$value = array(), $regex = '') {
36
        
37
        // if the first 'folder' is empty is removed
38
        while (!empty($folders) && empty($folders[0])) {
39
40
            array_shift($folders);
41
42
        }
43
44
        // if the 'folder' array is empty, the route has been fully analyzed
45
        // this is the exit condition from the recursive loop.
46
        if (empty($folders)) {
47
48
            return '^'.$regex.'[\/]?$';
49
50
        } else {
51
52
            // The first element of the array 'folders' is taken in order to be analyzed
53
            $folder  = array_shift($folders);
54
            
55
            // All the parameters of the route must be json strings
56
            $decoded = json_decode($folder, true);
57
58
            if (!is_null($decoded) && is_array($decoded)) {
59
60
                $param_regex    = '';
61
62
                $param_required = false;
63
64
                /* All the folders can include more than one parameter
65
                 * Eg: /service_name/{'param1': 'regex1', 'param2': 'regex2'}/
66
                 *     /calendar/{'ux_timestamp*': '\d{10}', 'microseconds': '\d{4}'}/
67
                 *
68
                 * The '*' at the end of the paramerter name implies that the parameter is required
69
                 * This example can be read as a calendar service that accepts both 
70
                 * timestamps in unix or javascript format.
71
                 *
72
                 * This is the reason of the following 'foreach'
73
                 */
74
                foreach ($decoded as $key => $string) {
75
76
                    $this->logger->debug("PARAMETER KEY: " . $key);
77
78
                    $this->logger->debug("PARAMETER STRING: " . $string);
79
                    
80
                    /* The key and the regex of every paramater is passed to the 'param'
81
                     * method which will build an appropriate regular expression and will understand 
82
                     * if the parameter is required and will build the $value['query'] object
83
                     */
84
                    $param_regex .= $this->param($key, $string, $param_required, $value);
85
86
                    $this->logger->debug("PARAMETER REGEX: " . $param_regex);
87
88
                }
89
                // Once the parameter is analyzed, the result is passed to the next iteration
90
                return $this->read(
91
                    $folders,
92
                    $value,
93
                    $regex.'(?:\/'.$param_regex.')'. (($param_required)?'{1}':'?')
94
                );
95
96
            } else {
97
                // if the element is not a json string, I assume it's the service name
98
                if (!isset($value['service'])) $value['service'] = array();
99
                array_push($value['service'], $folder);
100
101
                return $this->read(
102
                    $folders,
103
                    $value,
104
                    $regex.'\/'.$folder
105
                );
106
107
            }
108
109
        }
110
111
    }
112
113
    // This method read a single parameter and build the regular expression
114
    private function param($key, $string, &$param_required, &$value) {
115
116
        $field_required = false;
117
118
        // If the field name ends with a '*', the parameter is considered as required
119
        if (preg_match('/^(.+)\*$/', $key, $bits)) {
120
121
            $key            = $bits[1];
122
            $field_required = true;
123
            $param_required = true;
124
125
        }
126
127
        // The $value['query'] field contains all regex which will be used by the collector to parse the route fields
128
        if (!is_null($value)) {
129
130
            $value['query'][$key] = array(
131
                'regex'    => $string,
132
                'required' => $field_required
133
            );
134
135
        }
136
137
        /* Every parameter can include it's own logic into the regular expression,
138
         * it can use backreferences and it's expected to be used against a single parameter.
139
         * This means that it can't be used as is to build the route regular expression,
140
         * Backreferences are not useful at this point and can make the regular expression more time consuming
141
         * and resource hungry. This is why they are replaced with the grouping parenthesis.
142
         * Eg: (value) changes in (?:value)
143
         *
144
         * Delimiting characters like '^' and '$' are also meaningless in the complete regular expression and
145
         * need to be removed. Contrariwise, wildcards must be delimited in order to keet the whole regular
146
         * expression consistent, hence a '?' is added to all the '.*' or '.+' that don't already have one.
147
         */
148
        $string = preg_replace("/(?<!\\\\)\\((?!\\?)/", '(?:', $string);
149
        $string = preg_replace("/\\.([\\*\\+])(?!\\?)/", '.${1}?', $string);
150
        $string = preg_replace("/^[\\^]/", '', $string);
151
        $string = preg_replace("/[\\$]$/", '', $string);
152
153
        /* The produced regular expression is grouped and associated with its key (this means that the 'preg_match'
154
         * function will generate an associative array where the key/value association is preserved).
155
         * If the field is required, the regular expression is completed with a '{1}' (which make it compulsory),
156
         * otherwise a '?' is added.
157
         */
158
        return '(?P<' . $key . '>' . $string . ')' . (($field_required)?'{1}':'?');
159
160
    }
161
162
}
163