Test Failed
Push — develop ( fe93c2...a526c9 )
by nguereza
05:50
created

Router::setRouteParamsIfAppHasModuleOrFound()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 26
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 17
c 1
b 0
f 0
nc 7
nop 0
dl 0
loc 26
rs 9.3888
1
<?php
2
    defined('ROOT_PATH') or exit('Access denied');
3
    /**
4
     * TNH Framework
5
     *
6
     * A simple PHP framework using HMVC architecture
7
     *
8
     * This content is released under the MIT License (MIT)
9
     *
10
     * Copyright (c) 2017 TNH Framework
11
     *
12
     * Permission is hereby granted, free of charge, to any person obtaining a copy
13
     * of this software and associated documentation files (the "Software"), to deal
14
     * in the Software without restriction, including without limitation the rights
15
     * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
     * copies of the Software, and to permit persons to whom the Software is
17
     * furnished to do so, subject to the following conditions:
18
     *
19
     * The above copyright notice and this permission notice shall be included in all
20
     * copies or substantial portions of the Software.
21
     *
22
     * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
     * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
     * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
     * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
     * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
     * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
     * SOFTWARE.
29
     */
30
31
    class Router extends BaseClass {
32
        /**
33
         * @var array $pattern: The list of URIs to validate against
34
         */
35
        private $pattern = array();
36
37
        /**
38
         * @var array $callback: The list of callback to call
39
         */
40
        private $callback = array();
41
42
        /**
43
         * @var string $uriTrim: The char to remove from the URIs
44
         */
45
        protected $uriTrim = '/\^$';
46
47
        /**
48
         * @var string $uri: The route URI to use
49
         */
50
        protected $uri = '';
51
52
        /**
53
         * The module name of the current request
54
         * @var string
55
         */
56
        protected $module = null;
57
		
58
        /**
59
         * The controller name of the current request
60
         * @var string
61
         */
62
        protected $controller = null;
63
64
        /**
65
         * The controller path
66
         * @var string
67
         */
68
        protected $controllerPath = null;
69
70
        /**
71
         * The method name. The default value is "index"
72
         * @var string
73
         */
74
        protected $method = 'index';
75
76
        /**
77
         * List of argument to pass to the method
78
         * @var array
79
         */
80
        protected $args = array();
81
82
        /**
83
         * List of routes configurations
84
         * @var array
85
         */
86
        protected $routes = array();
87
88
        /**
89
         * The segments array for the current request
90
         * @var array
91
         */
92
        protected $segments = array();
93
94
        /**
95
         * Whether the current request generate 404 error
96
         * @var boolean
97
         */
98
        protected $error404 = false;
99
100
        /**
101
         * The instance of module to use
102
         * @var object
103
         */
104
        protected $moduleInstance = null;
105
106
        /**
107
         * Construct the new Router instance
108
         *
109
         * @param object $module the instance of module to use
110
         */
111
        public function __construct(Module $module = null) {
112
            parent::__construct();
113
            $this->setModuleInstance($module);
114
115
            //loading routes for module
116
            $moduleRouteList = array();
117
            $modulesRoutes = $this->moduleInstance->getModulesRoutesConfig();
118
            if (is_array($modulesRoutes)) {
119
                $moduleRouteList = $modulesRoutes;
120
                unset($modulesRoutes);
121
            }
122
            $this->setRouteConfiguration($moduleRouteList);
123
            $this->logger->info('The routes configuration are listed below: ' 
124
                                . stringify_vars($this->routes));
125
        }
126
127
        /**
128
         * Return the module instance to use
129
         * @return object
130
         */
131
        public function getModuleInstance() {
132
            return $this->moduleInstance;
133
        }
134
135
        /**
136
         * Set the module instance to use
137
         *
138
         * @param object $module the new module instance
139
         * 
140
         * @return object the current instance
141
         */
142
        public function setModuleInstance(Module $module = null) {
143
            $this->moduleInstance = $module;
144
            return $this;
145
        }
146
147
        /**
148
         * Return the 404 error or not
149
         * @return boolean
150
         */
151
        public function is404() {
152
            return $this->error404;
153
        }
154
155
        /**
156
         * Get the module name
157
         * @return string
158
         */
159
        public function getModule() {
160
            return $this->module;
161
        }
162
		
163
        /**
164
         * Get the controller name
165
         * @return string
166
         */
167
        public function getController() {
168
            return $this->controller;
169
        }
170
171
        /**
172
         * Get the controller file path
173
         * @return string
174
         */
175
        public function getControllerPath() {
176
            return $this->controllerPath;
177
        }
178
179
        /**
180
         * Get the controller method
181
         * @return string
182
         */
183
        public function getMethod() {
184
            return $this->method;
185
        }
186
187
        /**
188
         * Get the request arguments
189
         * @return array
190
         */
191
        public function getArgs() {
192
            return $this->args;
193
        }
194
195
        /**
196
         * Get the URL segments array
197
         * @return array
198
         */
199
        public function getSegments() {
200
            return $this->segments;
201
        }
202
203
        /**
204
         * Get the route URI
205
         * @return string
206
         */
207
        public function getRouteUri() {
208
            return $this->uri;
209
        }
210
211
        /**
212
         * Add the URI and callback to the list of URIs to validate
213
         *
214
         * @param string $uri the request URI
215
         * @param string $callback the callback function
216
         *
217
         * @return object the current instance
218
         */
219
        public function add($uri, $callback) {
220
            $uri = trim($uri, $this->uriTrim);
221
            if (in_array($uri, $this->pattern)) {
222
                $this->logger->warning('The route [' . $uri . '] already added, '
223
                                        . 'may be adding again can have route conflict');
224
            }
225
            $this->pattern[]  = $uri;
226
            $this->callback[] = $callback;
227
            return $this;
228
        }
229
230
        /**
231
         * Remove the route configuration
232
         *
233
         * @param string $uri the URI
234
         *
235
         * @return object the current instance
236
         */
237
        public function removeRoute($uri) {
238
            $uri = trim($uri, $this->uriTrim);
239
            $index = array_search($uri, $this->pattern, true);
240
            if ($index !== false) {
241
                $this->logger->info('Remove route for uri [' . $uri . '] from the configuration');
242
                unset($this->pattern[$index]);
243
                unset($this->callback[$index]);
244
            }
245
            $this->logger->warning('Cannot found route uri [' . $uri . '] from the configuration');
246
            return $this;
247
        }
248
249
250
        /**
251
         * Remove all the routes from the configuration
252
         *
253
         * @return object the current instance
254
         */
255
        public function removeAllRoute() {
256
            $this->logger->info('Remove all routes from the configuration');
257
            $this->pattern  = array();
258
            $this->callback = array();
259
            $this->routes   = array();
260
            return $this;
261
        }
262
263
        /**
264
        * Setting the route configuration using the configuration file 
265
        * and additional configuration from param
266
        * @param array $overwriteConfig the additional configuration 
267
        * to overwrite with the existing one
268
        * @param boolean $useConfigFile whether to use route configuration file
269
        * 
270
        * @return object
271
        */
272
        public function setRouteConfiguration(array $overwriteConfig = array(), $useConfigFile = true) {
273
            $route = array();
274
            if ($useConfigFile && file_exists(CONFIG_PATH . 'routes.php')) {
275
                require_once CONFIG_PATH . 'routes.php';
276
            }
277
            $route = array_merge($route, $overwriteConfig);
278
            $this->routes = $route;
279
            //if route is empty remove all configuration
280
            if (empty($route)) {
281
                $this->removeAllRoute();
282
            }
283
            //Set route informations
284
            $this->setRouteConfigurationInfos();
285
            return $this;
286
        }
287
288
        /**
289
         * Get the route configuration
290
         * 
291
         * @return array
292
         */
293
        public function getRouteConfiguration() {
294
            return $this->routes;
295
        }
296
297
        /**
298
         * Set the route URI to use later
299
         * @param string $uri the route URI, if is empty will 
300
         * determine automatically
301
         * @return object
302
         */
303
        public function setRouteUri($uri = '') {
304
            $routeUri = '';
305
            $globals = & class_loader('GlobalVar', 'classes');
306
            $cliArgs = $globals->server('argv');
307
            if (!empty($uri)) {
308
                $routeUri = $uri;
309
            } else if ($globals->server('REQUEST_URI')) {
310
                $routeUri = $globals->server('REQUEST_URI');
311
            }
312
            //if the application is running in CLI mode use the first argument
313
            else if (IS_CLI && isset($cliArgs[1])) {
314
                $routeUri = $cliArgs[1];
315
            } 
316
            $routeUri = $this->removeSuffixAndQueryStringFromUri($routeUri);
317
            $this->uri = trim($routeUri, $this->uriTrim);
318
            return $this;
319
        }
320
321
        /**
322
         * Set the route segments informations
323
         * @param array $segements the route segments information
324
         * 
325
         * @return object
326
         */
327
        public function setRouteSegments(array $segments = array()) {
328
            if (!empty($segments)) {
329
                $this->segments = $segments;
330
            } else if (!empty($this->uri)) {
331
                $this->segments = explode('/', $this->uri);
332
            }
333
            $this->removeDocumentRootFrontControllerFromSegments();
334
            return $this;
335
        }
336
337
        /**
338
         * Setting the route parameters like module, controller, method, argument
339
         * @return object the current instance
340
         */
341
        public function determineRouteParamsInformation() {
342
            $this->logger->debug('Routing process start ...');
343
			
344
            //determine route parameters using the config
345
            $this->determineRouteParamsFromConfig();
346
			
347
            //if can not determine the module/controller/method via the 
348
            //defined routes configuration we will use
349
            //the URL like http://domain.com/module/controller/method/arg1/arg2/argn 
350
            if (!$this->controller) {
351
                $this->logger->info('Cannot determine the routing information ' 
352
                       . 'using the predefined routes configuration, will use the request URI parameters');
353
                //determine route parameters using the route URI param
354
                $this->determineRouteParamsFromRequestUri();
355
            }
356
            //Set the controller file path if not yet set
357
            $this->setControllerFilePath();
358
            $this->logger->debug('Routing process end.');
359
360
            return $this;
361
        }
362
        
363
        /**
364
         * Routing the request to the correspondant module/controller/method if exists
365
         * otherwise send 404 error.
366
         */
367
        public function processRequest() {
368
            //Setting the route URI
369
            $this->setRouteUri();
370
371
            //setting route segments
372
            $this->setRouteSegments();
373
374
            $this->logger->info('The final Request URI is [' . implode('/', $this->segments) . ']');
375
376
            //determine the route parameters information
377
            $this->determineRouteParamsInformation();
378
379
            //Now load the controller if exists
380
            $this->loadControllerIfExist();
381
            
382
            //if we have 404 error show it
383
            if ($this->error404) {
384
                $this->show404Error();
385
            } else {
386
                //render the final page to user
387
                $this->logger->info('Render the final output to the browser');
388
                get_instance()->response->renderFinalPage();
389
            }
390
        }
391
	    
392
        /**
393
         * Set the controller file path if is not set
394
         * @param string $path the file path if is null will using the route 
395
         * information
396
         *
397
         * @return object the current instance
398
         */
399
        protected function setControllerFilePath($path = null) {
400
            if ($path !== null) {
401
                $this->controllerPath = $path;
402
                return $this;
403
            }
404
            //did we set the controller, so set the controller path
405
            //if not yet set before 
406
            if ($this->controller && !$this->controllerPath) {
407
                $this->logger->debug('Setting the file path for the controller [' . $this->controller . ']');
408
                $controllerPath = APPS_CONTROLLER_PATH . ucfirst($this->controller) . '.php';
409
                //if the controller is in module
410
                if ($this->module) {
411
                    $path = $this->moduleInstance->findControllerFullPath(ucfirst($this->controller), $this->module);
412
                    if ($path !== false) {
413
                        $controllerPath = $path;
414
                    }
415
                }
416
                $this->controllerPath = $controllerPath;
417
            }
418
            return $this;
419
        }
420
421
        /**
422
         * Set the route informations using the configuration
423
         *
424
         * @return object the current instance
425
         */
426
        protected function setRouteConfigurationInfos() {
427
            //adding route
428
            foreach ($this->routes as $pattern => $callback) {
429
                $this->add($pattern, $callback);
430
            }
431
            return $this;
432
        }
433
434
        /**
435
         * Remove the DOCUMENT_ROOT and front controller from segments if exists
436
         * @return void
437
         */
438
        protected function removeDocumentRootFrontControllerFromSegments(){
439
            $segment = $this->segments;
440
            $globals = & class_loader('GlobalVar', 'classes');
441
            $rootFolder = substr($globals->server('SCRIPT_NAME'), 0, strpos(
442
                                                                    $globals->server('SCRIPT_NAME'), 
443
                                                                    basename($globals->server('SCRIPT_FILENAME'))
444
                                                                ));
445
            //Remove "/" at the first or folder root
446
            $rootFolder = trim($rootFolder, $this->uriTrim);
447
            $segmentString = implode('/', $segment);
448
            $segmentString = str_ireplace($rootFolder, '', $segmentString);
449
            //Remove the "/" after replace like "root_folder/foo" => "/foo"
450
            $segmentString = trim($segmentString, $this->uriTrim);
451
            if (empty($segmentString)) {
452
                //So means we are on the home page
453
                $segment = array();
454
            } else {
455
                $segment = explode('/', $segmentString);
456
            }
457
            $this->segments = $segment;
458
            $this->logger->debug('Check if the request URI contains the front controller');
459
            if (isset($segment[0]) && $segment[0] == SELF) {
0 ignored issues
show
Bug introduced by
The constant self was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
460
                $this->logger->info('The request URI contains the front controller');
461
                array_shift($segment);
462
                $this->segments = $segment;
463
            }
464
        }
465
466
         /**
467
         * Remove the URL suffix and query string values if exists
468
         * @param  string $uri the route URI to process
469
         * @return string      the final route uri after processed
470
         */
471
        protected function removeSuffixAndQueryStringFromUri($uri) {
472
            $this->logger->debug('Check if URL suffix is enabled in the configuration');
473
            //remove url suffix from the request URI
474
            $suffix = get_config('url_suffix');
475
            if ($suffix) {
476
                $this->logger->info('URL suffix is enabled in the configuration, the value is [' . $suffix . ']');
477
                $uri = str_ireplace($suffix, '', $uri);
478
            } 
479
            if (strpos($uri, '?') !== false) {
480
                $uri = substr($uri, 0, strpos($uri, '?'));
481
            }
482
            return $uri;
483
        }
484
485
        /**
486
         * Set the route params using the predefined config
487
         * @param int $findIndex the index in $this->callback
488
         */
489
        protected function setRouteParamsUsingPredefinedConfig($findIndex) {
490
            $callback = $this->callback[$findIndex];
491
            //only one
492
            if (preg_match('/^([a-z0-9_]+)$/i', $callback)) {
493
                $this->logger->info('Callback [' . $callback . '] does not have module or ' 
494
                    . 'controller definition try to check if is an module or controller');
495
                //get the module list
496
                $modules = $this->moduleInstance->getModuleList();
497
                if (in_array($callback, $modules)) {
498
                    $this->logger->info('Callback [' . $callback . '] found in module use it as an module');
499
                    $this->module = $callback;
500
                } else {
501
                    $this->logger->info('Callback [' . $callback . '] not found in module use it as an controller');
502
                    $this->controller = $callback;
503
                }
504
                return;
505
            }
506
           
507
            //Check for module
508
            if (strpos($callback, '#') !== false) {
509
                $part = explode('#', $callback);
510
                $this->logger->info('The current request use the module [' . $part[0] . ']');
511
                $this->module = $part[0];
512
                array_shift($part);
513
                //if the second part exists and not empty and don't have @
514
                //so use it as controller
515
                if (!empty($part[0]) && strpos($part[0], '@') === false) {
516
                    $this->controller = $part[0];
517
                    array_shift($part);
518
                }
519
                $callback = implode('', $part);
520
            }
521
            
522
            //Check for controller
523
            if (strpos($callback, '@') !== false) {
524
                $part = explode('@', $callback);
525
                $this->controller = $part[0];
526
                array_shift($part);
527
                $callback = implode('', $part);
528
            }
529
530
            //Check for method
531
            //the remaining will be the method if is not empty
532
            if (!empty($callback)) {
533
                $this->method = $callback;
534
            }
535
        }
536
537
        /**
538
         * Determine the route parameters from route configuration
539
         * @return void
540
         */
541
        protected function determineRouteParamsFromConfig() {
542
            $uri = implode('/', $this->segments);
543
            /*
544
	    * Generics routes patterns
545
	    */
546
            $pattern = array(':num', ':alpha', ':alnum', ':any');
547
            $replace = array('[0-9]+', '[a-zA-Z]+', '[a-zA-Z0-9]+', '.*');
548
549
            $this->logger->debug(
550
                                    'Begin to loop in the predefined routes configuration ' 
551
                                    . 'to check if the current request match'
552
                                    );
553
            $args = array();
554
            $findIndex = -1;
555
            //Cycle through the URIs stored in the array
556
            foreach ($this->pattern as $index => $uriList) {
557
                $uriList = str_ireplace($pattern, $replace, $uriList);
558
                // Check for an existant matching URI
559
                if (preg_match("#^$uriList$#", $uri, $args)) {
560
                    $this->logger->info(
561
                                        'Route found for request URI [' . $uri . '] using the predefined configuration '
562
                                        . ' [' . $this->pattern[$index] . '] --> [' . $this->callback[$index] . ']'
563
                                    );
564
                    $findIndex = $index;
565
                    //stop here
566
                    break;
567
                }
568
            }
569
            if($findIndex !== -1){
570
                /*
571
                * $args[0] => full string captured by preg_match
572
                * $args[1], $args[2], $args[n] => contains the value of 
573
                * (:num), (:alpha), (:alnum), (:any)
574
                * so need remove the first value $args[0]
575
                */
576
                array_shift($args);
577
                $this->args = $args;
578
                $this->setRouteParamsUsingPredefinedConfig($findIndex);
579
            }
580
581
            //first if the controller is not set and the module is set use the module name as the controller
582
            if (!$this->controller && $this->module) {
583
                $this->logger->info(
584
                                    'After loop in predefined routes configuration,'
585
                                    . 'the module name is set but the controller is not set,' 
586
									. 'so we will use module as the controller'
587
                                );
588
                $this->controller = $this->module;
589
            }
590
        }
591
592
        /**
593
         * Find file path of the current controller using the current module
594
         * @return boolean true if the file path is found otherwise false.
595
         */
596
        protected function findControllerFullPathUsingCurrentModule(){
597
            $path = $this->moduleInstance->findControllerFullPath(ucfirst($this->controller), $this->module);
598
            if (!$path) {
599
                $this->logger->info('The controller [' . $this->controller . '] not ' 
600
                                    . 'found in the module, may be will use the module [' . $this->module . '] as controller');
601
                $this->controller = $this->module;
602
                return false;
603
            }
604
            $this->controllerPath = $path;
605
            return true;
606
        }
607
608
        /**
609
         * Set the route information if application does not have modules,
610
         * or the current request does not use module
611
         * @return void
612
         */
613
        protected function setRouteParamsIfNoModuleOrNotFound(){
614
            $segment = $this->segments;
615
            //controller
616
            if (isset($segment[0])) {
617
                $this->controller = $segment[0];
618
                array_shift($segment);
619
            }
620
            //method
621
            if (isset($segment[0])) {
622
                $this->method = $segment[0];
623
                array_shift($segment);
624
            }
625
            //args
626
            $this->args = $segment;
627
        }
628
629
        /**
630
         * Set the route information if application have modules,
631
         * or the current request use module
632
         * @return void
633
         */
634
        protected function setRouteParamsIfAppHasModuleOrFound(){
635
            //get the module list
636
            $modules = $this->moduleInstance->getModuleList();
637
            $segment = $this->segments;
638
            if (in_array($segment[0], $modules)) {
639
                $this->logger->info('Found, the current request use the module [' . $segment[0] . ']');
640
                $this->module = $segment[0];
641
                array_shift($segment);
642
                //check if the second arg is the controller from module
643
                if (isset($segment[0])) {
644
                    $this->controller = $segment[0];
645
                    //check if the request use the same module name and controller
646
                    if($this->findControllerFullPathUsingCurrentModule()){
647
                        array_shift($segment);
648
                    }
649
                }
650
                //check for method
651
                if (isset($segment[0])) {
652
                    $this->method = $segment[0];
653
                    array_shift($segment);
654
                }
655
                //the remaining is for args
656
                $this->args = $segment;
657
            } else {
658
                $this->logger->info('The current request information is not found in the module list');
659
                $this->setRouteParamsIfNoModuleOrNotFound();
660
            }
661
        }
662
663
        /**
664
         * Determine the route parameters using the server variable "REQUEST_URI"
665
         * @return void
666
         */
667
        protected function determineRouteParamsFromRequestUri() {
668
            $segment = $this->segments;
669
            $nbSegment = count($segment);
670
            //if segment is null so means no need to perform
671
            if ($nbSegment > 0) {
672
                //get the module list
673
                $modules = $this->moduleInstance->getModuleList();
674
                //first check if no module
675
                if (empty($modules)) {
676
                    $this->logger->info('No module was loaded will skip the module checking');
677
                    $this->setRouteParamsIfNoModuleOrNotFound();
678
                } else {
679
                    $this->logger->info('The application contains a loaded module will check if the current request is found in the module list');
680
                    $this->setRouteParamsIfAppHasModuleOrFound();
681
                }
682
                if (!$this->controller && $this->module) {
683
                    $this->logger->info('After using the request URI the module name is set but the controller is not set so we will use module as the controller');
684
                    $this->controller = $this->module;
685
                }
686
            }
687
        }
688
689
        /**
690
         * Show error 404 if can not found route for the current request
691
         * @return void
692
         */
693
        protected function show404Error() {
694
            if (IS_CLI) {
695
                set_http_status_header(404);
696
                echo 'Error 404: page not found.';
697
            } else {
698
                //create the instance of Controller
699
                $c404 = new Controller();
700
                //remove other content set to prevent duplicate view
701
                $c404->response->setFinalPageContent(null);
702
                $c404->response->render('404');
703
                $c404->response->send404();
704
            }
705
        }
706
707
        /**
708
         * Load the controller and call it method based on the routing information
709
         * @return void
710
         */
711
        protected function loadControllerIfExist() {
712
            $e404 = false;
713
            $classFilePath = $this->controllerPath;
714
            $controller = ucfirst($this->controller);
715
            $this->logger->info('The routing information are: module [' . $this->module . '], controller [' . $controller . '], method [' . $this->method . '], args [' . stringify_vars($this->args) . ']');
716
            $this->logger->debug('Loading controller [' . $controller . '], the file path is [' . $classFilePath . ']...');
717
            
718
            if (file_exists($classFilePath)) {
719
                require_once $classFilePath;
720
                if (!class_exists($controller, false)) {
721
                    $e404 = true;
722
                    $this->logger->warning('The controller file [' . $classFilePath . '] exists but does not contain the class [' . $controller . ']');
723
                } else {
724
                    $controllerInstance = new $controller();
725
                    $controllerMethod = $this->getMethod();
726
                    if (!method_exists($controllerInstance, $controllerMethod)) {
727
                        $e404 = true;
728
                        $this->logger->warning('The controller [' . $controller . '] exist but does not contain the method [' . $controllerMethod . ']');
729
                    } else {
730
                        $this->logger->info('Routing data is set correctly now GO!');
731
                        call_user_func_array(array($controllerInstance, $controllerMethod), $this->args);
732
                    }
733
                }
734
            } else {
735
                $this->logger->info('The controller file path [' . $classFilePath . '] does not exist');
736
                $e404 = true;
737
            }
738
            $this->error404 = $e404;
739
        }
740
    }
741