Passed
Push — 1.0.0-dev ( 024223...5d8234 )
by nguereza
02:46
created

Router::getModuleInstance()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 2
rs 10
c 0
b 0
f 0
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 ($modulesRoutes && is_array($modulesRoutes)) {
119
                $moduleRouteList = $modulesRoutes;
120
                unset($modulesRoutes);
121
            }
122
            $this->setRouteConfiguration($moduleRouteList);
123
            $this->logger->info('The routes configuration are listed below: ' . stringfy_vars($this->routes));
124
        }
125
126
        /**
127
         * Return the module instance to use
128
         * @return object
129
         */
130
        public function getModuleInstance() {
131
            return $this->moduleInstance;
132
        }
133
134
        /**
135
         * Set the module instance to use
136
         *
137
         * @param object $module the new module instance
138
         * 
139
         * @return object the current instance
140
         */
141
        public function setModuleInstance(Module $module = null) {
142
            $this->moduleInstance = $module;
143
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 route patterns
157
         * @return array
158
         */
159
        public function getPattern() {
160
            return $this->pattern;
161
        }
162
163
        /**
164
         * Get the route callbacks
165
         * @return array
166
         */
167
        public function getCallback() {
168
            return $this->callback;
169
        }
170
171
        /**
172
         * Get the module name
173
         * @return string
174
         */
175
        public function getModule() {
176
            return $this->module;
177
        }
178
		
179
        /**
180
         * Get the controller name
181
         * @return string
182
         */
183
        public function getController() {
184
            return $this->controller;
185
        }
186
187
        /**
188
         * Get the controller file path
189
         * @return string
190
         */
191
        public function getControllerPath() {
192
            return $this->controllerPath;
193
        }
194
195
        /**
196
         * Get the controller method
197
         * @return string
198
         */
199
        public function getMethod() {
200
            return $this->method;
201
        }
202
203
        /**
204
         * Get the request arguments
205
         * @return array
206
         */
207
        public function getArgs() {
208
            return $this->args;
209
        }
210
211
        /**
212
         * Get the URL segments array
213
         * @return array
214
         */
215
        public function getSegments() {
216
            return $this->segments;
217
        }
218
219
        /**
220
         * Get the route URI
221
         * @return string
222
         */
223
        public function getRouteUri() {
224
            return $this->uri;
225
        }
226
227
        /**
228
         * Add the URI and callback to the list of URIs to validate
229
         *
230
         * @param string $uri the request URI
231
         * @param string $callback the callback function
232
         *
233
         * @return object the current instance
234
         */
235
        public function add($uri, $callback) {
236
            $uri = trim($uri, $this->uriTrim);
237
            if (in_array($uri, $this->pattern)) {
238
                $this->logger->warning('The route [' . $uri . '] already added, may be adding again can have route conflict');
239
            }
240
            $this->pattern[] = $uri;
241
            $this->callback[] = $callback;
242
            return $this;
243
        }
244
245
        /**
246
         * Remove the route configuration
247
         *
248
         * @param string $uri the URI
249
         *
250
         * @return object the current instance
251
         */
252
        public function removeRoute($uri) {
253
            $uri = trim($uri, $this->uriTrim);
254
            $index = array_search($uri, $this->pattern, true);
255
            if ($index !== false) {
256
                $this->logger->info('Remove route for uri [' . $uri . '] from the configuration');
257
                unset($this->pattern[$index]);
258
                unset($this->callback[$index]);
259
            }
260
            return $this;
261
        }
262
263
264
        /**
265
         * Remove all the routes from the configuration
266
         *
267
         * @return object the current instance
268
         */
269
        public function removeAllRoute() {
270
            $this->logger->info('Remove all routes from the configuration');
271
            $this->pattern  = array();
272
            $this->callback = array();
273
            $this->routes = array();
274
            return $this;
275
        }
276
277
        /**
278
        * Setting the route configuration using the configuration file and additional configuration from param
279
        * @param array $overwriteConfig the additional configuration to overwrite with the existing one
280
        * @param boolean $useConfigFile whether to use route configuration file
281
        * 
282
        * @return object
283
        */
284
        public function setRouteConfiguration(array $overwriteConfig = array(), $useConfigFile = true) {
285
            $route = array();
286
            if ($useConfigFile && file_exists(CONFIG_PATH . 'routes.php')) {
287
                require_once CONFIG_PATH . 'routes.php';
288
            }
289
            $route = array_merge($route, $overwriteConfig);
290
            $this->routes = $route;
291
            //if route is empty remove all configuration
292
            if (empty($route)) {
293
                $this->removeAllRoute();
294
            }
295
            //Set route informations
296
            $this->setRouteConfigurationInfos();
297
            return $this;
298
        }
299
300
        /**
301
         * Get the route configuration
302
         * 
303
         * @return array
304
         */
305
        public function getRouteConfiguration() {
306
            return $this->routes;
307
        }
308
309
        /**
310
         * Set the route URI to use later
311
         * @param string $uri the route URI, if is empty will determine automatically
312
         * @return object
313
         */
314
        public function setRouteUri($uri = '') {
315
            $routeUri = '';
316
            $globals = & class_loader('GlobalVar', 'classes');
317
            $cliArgs = $globals->server('argv');
318
            if (!empty($uri)) {
319
                $routeUri = $uri;
320
            } else if ($globals->server('REQUEST_URI')) {
321
                $routeUri = $globals->server('REQUEST_URI');
322
            }
323
            //if the application is running in CLI mode use the first argument
324
            else if (IS_CLI && isset($cliArgs[1])) {
325
                $routeUri = $cliArgs[1];
326
            } 
327
            $routeUri = $this->removeSuffixAndQueryStringFromUri($routeUri);
328
            $this->uri = trim($routeUri, $this->uriTrim);
329
            return $this;
330
        }
331
332
        /**
333
         * Set the route segments informations
334
         * @param array $segements the route segments information
335
         * 
336
         * @return object
337
         */
338
        public function setRouteSegments(array $segments = array()) {
339
            if (!empty($segments)) {
340
                $this->segments = $segments;
341
            } else if (!empty($this->uri)) {
342
                $this->segments = explode('/', $this->uri);
343
            }
344
            $this->removeDocumentRootFrontControllerFromSegments();
345
            return $this;
346
        }
347
348
        /**
349
         * Setting the route parameters like module, controller, method, argument
350
         * @return object the current instance
351
         */
352
        public function determineRouteParamsInformation() {
353
            $this->logger->debug('Routing process start ...');
354
			
355
            //determine route parameters using the config
356
            $this->determineRouteParamsFromConfig();
357
			
358
            //if can not determine the module/controller/method via the defined routes configuration we will use
359
            //the URL like http://domain.com/module/controller/method/arg1/arg2
360
            if (!$this->controller) {
361
                $this->logger->info('Cannot determine the routing information using the predefined routes configuration, will use the request URI parameters');
362
                //determine route parameters using the route URI param
363
                $this->determineRouteParamsFromRequestUri();
364
            }
365
            //Set the controller file path if not yet set
366
            $this->setControllerFilePath();
367
            $this->logger->debug('Routing process end.');
368
369
            return $this;
370
        }
371
        
372
        /**
373
         * Routing the request to the correspondant module/controller/method if exists
374
         * otherwise send 404 error.
375
         */
376
        public function processRequest() {
377
            //Setting the route URI
378
            $this->setRouteUri();
379
380
            //setting route segments
381
            $this->setRouteSegments();
382
383
            $this->logger->info('The final Request URI is [' . implode('/', $this->segments) . ']');
384
385
            //determine the route parameters information
386
            $this->determineRouteParamsInformation();
387
388
            //Now load the controller if exists
389
            $this->loadControllerIfExist();
390
            
391
            //if we have 404 error show it
392
            if ($this->error404) {
393
                $this->show404Error();
394
            } else {
395
                //render the final page to user
396
                $this->logger->info('Render the final output to the browser');
397
                get_instance()->response->renderFinalPage();
398
            }
399
        }
400
	    
401
        /**
402
         * Set the controller file path if is not set
403
         * @param string $path the file path if is null will using the route 
404
         * information
405
         *
406
         * @return object the current instance
407
         */
408
        public function setControllerFilePath($path = null) {
409
            if ($path !== null) {
410
                $this->controllerPath = $path;
411
                return $this;
412
            }
413
            //did we set the controller, so set the controller path
414
            if ($this->controller && !$this->controllerPath) {
415
                $this->logger->debug('Setting the file path for the controller [' . $this->controller . ']');
416
                $controllerPath = APPS_CONTROLLER_PATH . ucfirst($this->controller) . '.php';
417
                //if the controller is in module
418
                if ($this->module) {
419
                    $path = $this->moduleInstance->findControllerFullPath(ucfirst($this->controller), $this->module);
420
                    if ($path !== false) {
421
                        $controllerPath = $path;
422
                    }
423
                }
424
                $this->controllerPath = $controllerPath;
425
            }
426
            return $this;
427
        }
428
429
        /**
430
         * Set the route informations using the configuration
431
         *
432
         * @return object the current instance
433
         */
434
        protected function setRouteConfigurationInfos() {
435
            //adding route
436
            foreach ($this->routes as $pattern => $callback) {
437
                $this->add($pattern, $callback);
438
            }
439
            return $this;
440
        }
441
442
        /**
443
         * Remove the DOCUMENT_ROOT and front controller from segments if exists
444
         * @return void
445
         */
446
        protected function removeDocumentRootFrontControllerFromSegments(){
447
            $segment = $this->segments;
448
            $baseUrl = get_config('base_url');
449
            //check if the app is not in DOCUMENT_ROOT
450
            if (isset($segment[0]) && stripos($baseUrl, $segment[0]) !== false) {
451
                array_shift($segment);
452
                $this->segments = $segment;
453
            }
454
            $this->logger->debug('Check if the request URI contains the front controller');
455
            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...
456
                $this->logger->info('The request URI contains the front controller');
457
                array_shift($segment);
458
                $this->segments = $segment;
459
            }
460
        }
461
462
         /**
463
         * Remove the URL suffix and query string values if exists
464
         * @param  string $uri the route URI to process
465
         * @return string      the final route uri after processed
466
         */
467
        protected function removeSuffixAndQueryStringFromUri($uri) {
468
            $this->logger->debug('Check if URL suffix is enabled in the configuration');
469
            //remove url suffix from the request URI
470
            $suffix = get_config('url_suffix');
471
            if ($suffix) {
472
                $this->logger->info('URL suffix is enabled in the configuration, the value is [' . $suffix . ']');
473
                $uri = str_ireplace($suffix, '', $uri);
474
            } 
475
            if (strpos($uri, '?') !== false) {
476
                $uri = substr($uri, 0, strpos($uri, '?'));
477
            }
478
            return $uri;
479
        }
480
481
        /**
482
         * Set the route params using the predefined config
483
         * @param int $findIndex the index in $this->callback
484
         */
485
        protected function setRouteParamsUsingPredefinedConfig($findIndex) {
486
            $callback = $this->callback[$findIndex];
487
            //only one
488
            if (strpos($callback, '#') === false && strpos($callback, '@') === false) {
489
                $this->logger->info('Callback [' . $callback . '] does not have module or controller definition try to check if is an module or controller');
490
                //get the module list
491
                $modules = $this->moduleInstance->getModuleList();
492
                if (in_array($callback, $modules)) {
493
                    $this->logger->info('Callback [' . $callback . '] found in module use it as an module');
494
                    $this->module = $callback;
495
                } else {
496
                    $this->logger->info('Callback [' . $callback . '] not found in module use it as an controller');
497
                    $this->controller = $callback;
498
                }
499
                return;
500
            }
501
           
502
            //Check for module
503
            if (strpos($callback, '#') !== false) {
504
                $part = explode('#', $callback);
505
                $this->logger->info('The current request use the module [' . $part[0] . ']');
506
                $this->module = $part[0];
507
                array_shift($part);
508
                //if the second part exists and not empty and don't have @
509
                //so use it as controller
510
                if (!empty($part[0]) && strpos($part[0], '@') === false) {
511
                    $this->controller = $part[0];
512
                    array_shift($part);
513
                }
514
                $callback = implode('', $part);
515
            }
516
            
517
            //Check for controller
518
            if (strpos($callback, '@') !== false) {
519
                $part = explode('@', $callback);
520
                $this->controller = $part[0];
521
                array_shift($part);
522
                $callback = implode('', $part);
523
            }
524
525
            //check for method
526
            //the remaining will be the method if is not empty
527
            if (!empty($callback)) {
528
                $this->method = $callback;
529
            }
530
        }
531
532
        /**
533
         * Determine the route parameters from route configuration
534
         * @return void
535
         */
536
        protected function determineRouteParamsFromConfig() {
537
            $uri = implode('/', $this->segments);
538
            /*
539
	   		* Generics routes patterns
540
	    	*/
541
            $pattern = array(':num', ':alpha', ':alnum', ':any');
542
            $replace = array('[0-9]+', '[a-zA-Z]+', '[a-zA-Z0-9]+', '.*');
543
544
            $this->logger->debug(
545
                                    'Begin to loop in the predefined routes configuration ' 
546
                                    . 'to check if the current request match'
547
                                    );
548
            $args = array();
549
            $findIndex = -1;
550
            // Cycle through the URIs stored in the array
551
            foreach ($this->pattern as $index => $uriList) {
552
                $uriList = str_ireplace($pattern, $replace, $uriList);
553
                // Check for an existant matching URI
554
                if (preg_match("#^$uriList$#", $uri, $args)) {
555
                    $this->logger->info(
556
                                        'Route found for request URI [' . $uri . '] using the predefined configuration '
557
                                        . ' [' . $this->pattern[$index] . '] --> [' . $this->callback[$index] . ']'
558
                                    );
559
                    $findIndex = $index;
560
                    // stop here
561
                    break;
562
                }
563
            }
564
            if($findIndex !== -1){
565
                //$args[0] => full string captured by preg_match
566
                //$args[1], $args[2], $args[n] => contains the value of 
567
                //(:num), (:alpha), (:alnum), (:any)
568
                //so need remove the first value $args[0]
569
                array_shift($args);
570
                $this->args = $args;
571
                $this->setRouteParamsUsingPredefinedConfig($findIndex);
572
            }
573
574
            //first if the controller is not set and the module is set use the module name as the controller
575
            if (!$this->controller && $this->module) {
576
                $this->logger->info(
577
                                    'After loop in predefined routes configuration,'
578
                                    . 'the module name is set but the controller is not set,' 
579
									. 'so we will use module as the controller'
580
                                );
581
                $this->controller = $this->module;
582
            }
583
        }
584
585
        /**
586
         * Find file path of the current controller using the current module
587
         * @return boolean true if the file path is found otherwise false.
588
         */
589
        protected function findControllerFullPathUsingCurrentModule(){
590
            $path = $this->moduleInstance->findControllerFullPath(ucfirst($this->controller), $this->module);
591
            if (!$path) {
592
                $this->logger->info('The controller [' . $this->controller . '] not found in the module, may be will use the module [' . $this->module . '] as controller');
593
                $this->controller = $this->module;
594
                return false;
595
            }
596
            $this->controllerPath = $path;
597
            return true;
598
        }
599
600
        /**
601
         * Set the route information if application does not have modules,
602
         * or the current request does not use module
603
         * @return void
604
         */
605
        protected function setRouteParamsIfNoModuleOrNotFound(){
606
            $segment = $this->segments;
607
            //controller
608
            if (isset($segment[0])) {
609
                $this->controller = $segment[0];
610
                array_shift($segment);
611
            }
612
            //method
613
            if (isset($segment[0])) {
614
                $this->method = $segment[0];
615
                array_shift($segment);
616
            }
617
            //args
618
            $this->args = $segment;
619
        }
620
621
        /**
622
         * Set the route information if application have modules,
623
         * or the current request use module
624
         * @return void
625
         */
626
        protected function setRouteParamsIfAppHasModuleOrFound(){
627
            //get the module list
628
            $modules = $this->moduleInstance->getModuleList();
629
            $segment = $this->segments;
630
            if (in_array($segment[0], $modules)) {
631
                $this->logger->info('Found, the current request use the module [' . $segment[0] . ']');
632
                $this->module = $segment[0];
633
                array_shift($segment);
634
                //check if the second arg is the controller from module
635
                if (isset($segment[0])) {
636
                    $this->controller = $segment[0];
637
638
                    //check if the request use the same module name and controller
639
                    if($this->findControllerFullPathUsingCurrentModule()){
640
                        array_shift($segment);
641
                    }
642
                }
643
                //check for method
644
                if (isset($segment[0])) {
645
                    $this->method = $segment[0];
646
                    array_shift($segment);
647
                }
648
                //the remaining is for args
649
                $this->args = $segment;
650
            } else {
651
                $this->logger->info('The current request information is not found in the module list');
652
                $this->setRouteParamsIfNoModuleOrNotFound();
653
            }
654
        }
655
656
        /**
657
         * Determine the route parameters using the server variable "REQUEST_URI"
658
         * @return void
659
         */
660
        protected function determineRouteParamsFromRequestUri() {
661
            $segment = $this->segments;
662
            $nbSegment = count($segment);
663
            //if segment is null so means no need to perform
664
            if ($nbSegment > 0) {
665
                //get the module list
666
                $modules = $this->moduleInstance->getModuleList();
667
668
                //first check if no module
669
                if (empty($modules)) {
670
                    $this->logger->info('No module was loaded will skip the module checking');
671
                    $this->setRouteParamsIfNoModuleOrNotFound();
672
                } else {
673
                    $this->logger->info('The application contains a loaded module will check if the current request is found in the module list');
674
                    $this->setRouteParamsIfAppHasModuleOrFound();
675
                }
676
                if (!$this->controller && $this->module) {
677
                    $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');
678
                    $this->controller = $this->module;
679
                }
680
            }
681
        }
682
683
        /**
684
         * Show error 404 if can not found route for the current request
685
         * @return void
686
         */
687
        protected function show404Error() {
688
            if (IS_CLI) {
689
                set_http_status_header(404);
690
                echo 'Error 404: page not found.';
691
            } else {
692
                //create the instance of Controller
693
                $c404 = new Controller();
694
                //remove other content set to prevent duplicate view
695
                $c404->response->setFinalPageContent(null);
696
                $c404->response->render('404');
697
                $c404->response->send404();
698
            }
699
        }
700
701
        /**
702
         * Load the controller and call it method based on the routing information
703
         * @return void
704
         */
705
        protected function loadControllerIfExist() {
706
            $e404 = false;
707
            $classFilePath = $this->controllerPath;
708
            $controller = ucfirst($this->controller);
709
            $this->logger->info('The routing information are: module [' . $this->module . '], controller [' . $controller . '], method [' . $this->method . '], args [' . stringfy_vars($this->args) . ']');
710
            $this->logger->debug('Loading controller [' . $controller . '], the file path is [' . $classFilePath . ']...');
711
            
712
            if (file_exists($classFilePath)) {
713
                require_once $classFilePath;
714
                if (!class_exists($controller, false)) {
715
                    $e404 = true;
716
                    $this->logger->warning('The controller file [' . $classFilePath . '] exists but does not contain the class [' . $controller . ']');
717
                } else {
718
                    $controllerInstance = new $controller();
719
                    $controllerMethod = $this->getMethod();
720
                    if (!method_exists($controllerInstance, $controllerMethod)) {
721
                        $e404 = true;
722
                        $this->logger->warning('The controller [' . $controller . '] exist but does not contain the method [' . $controllerMethod . ']');
723
                    } else {
724
                        $this->logger->info('Routing data is set correctly now GO!');
725
                        call_user_func_array(array($controllerInstance, $controllerMethod), $this->args);
726
                    }
727
                }
728
            } else {
729
                $this->logger->info('The controller file path [' . $classFilePath . '] does not exist');
730
                $e404 = true;
731
            }
732
            $this->error404 = $e404;
733
        }
734
    }
735