Completed
Push — 4.0 ( b44693...cafdfc )
by Marco
02:39
created

Parser::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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