Passed
Push — 1.0.0-dev ( a1ce08...4f1d46 )
by nguereza
10:50
created

Router::show404Error()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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