Completed
Push — 4.0 ( 6a2270...a7534e )
by Marco
12:55
created

Parser::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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