Is this a good way to map a URI to a class / method in PHP for MVC

I am new to MVC, so this is my first attempt, and I'm sure you guys can improve me, thanks for any advice or help!

The following is what I came up with for the router / dispatcher system for my personal structure that I am working on, this is my first attempt to use the MVC pattern.

The first block of code is just the .htaccess file, which routes the entire request through my index.php file.

The second block of code is my "Routes" array, which will point to the Router object, which class and method to call, as well as any identifiers or swap numbers, if they exist.

The third block of code is the router class.

The fourth block just starts the class

So the router class should use regular expression to match the URI with the route in the route map, theoretically it just sounds like poor performance when there is a list of 50+ routes that the regular expression should run on, should I do it differently? The main reason I use regex is to match page numbers and ID numbers when they exist on the route.

Also, please, do not just tell me how to use the framework, I do it in order to get to know it better, I will learn better and just prefer not to use the existing structure at this time, I study all the basic and some less general ideas already.

1) So, the main question is, does something just not look right?

2) Is there a better way to determine what is in the URI than using a regular expression in an array, as I do, consider it on a site with high traffic?

3) Since everything is routed through the index.php file with this, how will I handle AJAX request processing?

Sorry if this is confusing, I'm a little confused!


.htaccess file

RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*)$ index.php?uri=$1 [NC,L,QSA] 

Array Array ()

 /** * Map URI to class/method and ID and Page numbers * Must be an array */ $uri_route_map = array( //forums 'forums/' => array( 'controller' => 'forums', 'method' => 'index', 'id_number' => '', 'page_number' => ''), 'forums/viewforum/(?<id_number>\d+)' => array( 'controller' => 'forums', 'method' => 'viewforum', 'id_number' => isset($id_number), 'page_number' => ''), 'forums/viewthread/(?<id_number>\d+)' => array( 'controller' => 'forums', 'method' => 'viewthread', 'id_number' => isset($id_number), 'page_number' => ''), 'forums/viewthread/(?<id_number>\d+)/page-(?<page_number>\d+)' => array( 'controller' => 'forums', 'method' => 'viewthread', 'id_number' => isset($id_number), 'page_number' => isset($page_number)), // user routes // account routes // blog routes // mail routes // various other routes ); 

The class of the router that reads and maps the map array above

 /** * Run URI against our Map array to get class/method/id-page numbers */ class Router { private $_controller = ''; private $_method = ''; public $page_number = ''; public $id_number = ''; public function __construct($uri, array $uri_route_map) { foreach ($uri_route_map as $rUri => $rRoute) { if (preg_match("#^{$rUri}$#Ui", $uri, $uri_digits)) { //if page number and ID number in uri then set it locally $this->page_number = (isset($uri_digits['page_number']) ? $uri_digits['page_number'] : null); $this->id_number = (isset($uri_digits['id_number']) ? $uri_digits['id_number'] : null); $this->_controller = $rRoute['controller']; $this->_method = $rRoute['method']; // just for debug and testing while working on it / will be removed from final code echo '<hr> $page_number = ' . $this->page_number . '<br><br>'; echo '<hr> $id_number = ' . $this->id_number . '<br><br>'; echo '<hr> $controller = ' . $this->_controller . '<br><br>'; echo '<hr> $method = ' . $this->_method . '<br><br>'; break; }else{ $this->page_number = ''; $this->id_number = ''; $this->_controller = '404'; $this->_method = '404'; } } } public function getController() { return $this->_controller; } public function getMethod() { return $this->_method; } public function getPageNumber() { return $this->page_number; } public function getIDNumber() { return $this->id_number; } /** * Call our class and method from values in the URI */ public function dispatch() { if (file_exists('controller' . $this->_controller . '.php')) { include ('controller' . $this->_controller . '.php'); $controllerName = 'Controller' . $this->_controller; $controller = new $controllerName($this->getIDNumber(),$this->getPageNumber()); $method = $this->_method; if (method_exists($this->_controller, $this->_method)) { return $controller->$method(); } else { // method does not exist } } else { // Controller does not exist } } } 

Run it

 /** * Testing the class */ $uri = isset($_GET['uri']) ? $_GET['uri'] : null; $router = new Router($uri, $uri_route_map); $router->dispatch(); ?> 
+6
source share
6 answers

1) Look good to me. However, the code looks a little dirty.

2) Yes, there is a better way. You are performing regex because you want to match parts of the url that you don't know. Why not do $parts = explode("/", $uri) and then see if you can find the page you want? You will need to determine how many parameters you expect for each page, or don’t know whether to choose forums with array("viewform", 123) parameters array("viewform", 123) or forums/viewforum with array(123) parameters array(123) .

explode feels better than regular expression. It also adds the benefits of improved error handling. What if the argument passed to viewforum is not a number? Of course you can do better than "404" ;)

3) Create a separate ajax handler. Ajax is hidden from view anyway, so you don’t have to worry about providing semantic URLs.

Example:

 function find_route($parts) { foreach ($uri_route_map as $route => $route_data) { $route_check = implode("/", array_slice($parts, 0, count($parts) - $route_data['num_arguments'])); if ($route_check === $route) { return $route_data; } } throw new Exception("404?"); } $uri = "forum/viewforum/522"; $parts = explode("/", $uri); $route = find_route($parts); $arguments = array_slice($parts, count($parts) - $route['num_arguments']); $controller = $rRoute['controller']; $method = $rRoute['method']; $controller_instance = new $controller(); call_user_func_array(array($controller_instance, $method), $arguments); 

(unverified)

Plugins

Because of $ uri_route_map, you cannot dynamically register more plugins or pages or routes. I would add a function to add dynamic routes to the Router .

In addition, you can consider an automatic discovery scheme that, for example, checks the plugins/ folder for folders with a file called manifest.php, which, when called, will additionally add more routes to the router.

+4
source

1), 2) I do not think it is a good idea to put id_number and page_number in the Router, because in the future you may encounter many other parameters for url. It’s better to just use the controller and method and determine in the controller what to do with other parameters or create another class request that processes the request information.

3) For ajax, use url as ajax / module / action. And create an ajax controller that performs basic ajax security functions, for example, checks the XSRF, and then decides which controllers should start, and the actions to invoke.

+4
source

1) and 2) I will not say that this is wrong, but why not use the default routes? In most cases, a route such as

controller / action / param1 / param2

good enough for most of your pages.

Perhaps you could do something similar to define default routes:

 $this->controller = 'index'; $this->action = 'index'; private function getDefaultRoutes() { $url = $_SERVER['REQUEST_URI']; $tabUrl = explode('/',$url); if(!empty($tabUrl)) { $this->controller = array_shift($tabUrl); $this->action = array_shift($tabUrl); $this->params = $tabUrl; } } 

And then if you need more specific routes, you can define them in an array or anywhere. In your route, you only need to check if the current URIs match specific routes or the default routes. By doing this, you will reduce the number of routes to match and increase the speed of your router.

3) Your router is probably independent of your index, without the no root index, so unfortunately you probably won't be able to use it. Therefore, it is very important to avoid costly actions in your index. As a rule, do not start connecting to the database in the index if all your pages do not need it.

Also please don't just tell me to use the framework

Remember to download some well-known frameworks and see their code. This is the best way to learn. By doing this, you are likely to find many good practices and answers.

+2
source

1) So, the main question is, does something just not look right?

Personally, I see this becoming more complex as your site grows. The MVC structure, as I was taught, to a large extent should be “Set and Forget” - you separate the request handler (controller) from the database requests and the business end (model) and from the display elements (view).

[ Note: You may need other basic aspects. My standard structure includes some basic elements that carry the session through various parts, and also process the fundamental aspects of the site. For example, while models are responsible for the correct database calls as instructed by the controller, there are basic functions in the sql.class.php file that give me a standardized set of methods for making these calls and delivering or caching the results as needed.]

Your submit method is on the right track with this - you are retrieving the controller name from the URI (forums, profiles, etc.). Do you need uri map? I feel that you are creating an unnecessary situation when you have to update this card every time, and not just create a new controller, when you need new functions, and register it in the database. I’m not saying that you did wrong, I just don’t feel that I would have done so.

2) Is there a better way to determine what is in the URI than using a regular expression in an array, as I do, consider it on a site with high traffic?

Control the result (pun intended, as it does the work here). Consider this approach and see how it works for you:

Your index.php file (aka "Main Controller") grabs the URI and explodes the values ​​along the "/" into bits. bit [0] is the identifier of the controller - it says: "I want to use a controller with the name bit [0] => value." This is done as:

 require_once( dirname( __FILE__ )."/controllers/".$bit[0]."controller.php" ); 

Personally, I, being a little neat when it comes to directory structures, I use the [0] bit to identify the directory where controller.php is located, since I may have subcontrollers.

This controller file that I use to parse other bits. For this, I will give an example:

Suppose bit [0] is set to forums. I can pass, if it is set, bit [1] to the switch statement. By default, I always want to list, but I could send it to a "list", "view" or "message" in bit [1]. This will tell me in the controller class which method to call. Then the method will prompt me to invoke the associated model of "forums" if I need to fulfill requests and cache the list of forums, for example.

Extraneous “bits” can do one of two things: they can be passed as simple arguments to the method, what data is requested from the model, or bit [1] can be complex enough to guarantee a subcontroller, and subsequent bits will be passed to this controller to determine the appropriate actions as was done with the forums controller.

Regular expression, being slow, should be avoided whenever possible. Since we may have a URI /forums/view/102305 , we can assume that the forums controller will pass 102305 method associated with the view argument (a method similar to private function displayPost( $id ) , where $id is 102305 ). There is no regex, as we can just blow values ​​along the common expected delimiter.

3) Since everything is routed through the index.php file with this, how will I handle AJAX request processing?

Not very difficult. If the controller is configured for, say, AJAX, you can rebuild the URL and gain direct access to it. You can write exceptions in the .htaccess file ( RewriteRule ^(AJAX)($|/) - [L] ). Or (not an ideal but ill-conceived workaround) is to add ../ to your AJAX URI to return the URI to the root directory - it no longer tries to access index.php , so the rewrite rule does not apply.

Edit

Suppose we use the URI from /forums/id-1234/page-4 for your example. Again, suppose that, as I mentioned above, forums refers to the controller that will be used, and all the rest / limit the arguments (which I like to call "drilling"). So, in our forum manager file (call him forumcontroller.php , we might have something like this (extremely simplified) constructor:

 // $registry is a class containing fundamental methods, and is meant to exemplify all // classes tied to the main controller "index.php". Keep in mind, I'm assuming we've // found the right controller by exploding the URI, and passed the remainder as bits // to the constructor. public function __construct( registry $registry ) { $this->registry = $registry; //tying this controller to main controller. // For ease and clarity, we're assuming there no case in which you wouldn't have // bits set. Error checking is easy. $bits = $this->registry->getURLBits; switch( $bits[0] ) { case 'view': $this->showForumEntry( $bits[1], (isset( $bits[2] ) ? $bits[2] : '' ); break; case 'edit': $this->editForumEntry( $bits[1] ); break; case 'post': $this->postForumEntry(); break; default: $this->listForumEntries(); break; } } private function showForumEntry( $thread, $offset ) { // Because you wanted to prepend id to the id element, we can use this for // cheekiness in the query if our DB is well designed. $data = explode('-', $thread); // Select all from forums where id = 1234 $sql = "SELECT * FROM forums WHERE $data[0] = $data[1]"; if( $offset != '' ) { $page = explode('-', $offset); $offset = $page[1] * 25; // Or whatever your max per page is. Make it dynamic. $max = $offset+25; $sql .= " LIMIT $offset, $max"; } // You see where I'm going with this... } 

The fact is that you control what is transferred and processed. Manage URIs and you can simplify their handling.

Edit 2 Repeating again, there are several concepts that I think will help you and you should familiarize yourself with:

Check out the "Factory" template here (My $ registry, in its heart, factory set): http://php.net/manual/en/language.oop5.patterns.php

Good breakdown of MVC graphically: http://best-practice-software-engineering.ifs.tuwien.ac.at/patterns/images/mvc3.jpg

More on Factory Methods: http://www.devshed.com/c/a/PHP/Design-Patterns-in-PHP-Factory-Method-and-Abstract-Factory/

Another point, and this is a personal observation after working with Joomla, Drupal, Wordpress and various corporate solutions CMS and BBS. Design exclusively with you. When you start trying to become "something for everyone", you get a lot of extra bloat, which is downloaded from each page and used 1 time out of 100. MVC is a design template, and using it as a template will tell you to get rid of excess in all aspects, including the URI. Handling /controller/arg1-Identifier/arg2-offset not needed, and you can easily get away with /controller/id/offset (e.g. /forums/1234/4 ). If you want to make it SEO friendly, add a thread name and not a tag identifying the identifier (e.g. /forums/1234-This-Is-A-Topic/4 ).

Now let's also point out the obvious about my editing above. This is a controller designed exclusively for a forum item. Each element of your site (for example, forums, galleries, profiles, etc.) must have its own controller. What for? Because everyone does completely different things on their pages. Therefore, use this - you do not need to use a URI card if you understand that you are going to the controller, and the controller delegates the responsibilities of the model to any additional controllers that may be needed.

I really hope this helps.

+2
source

1) Does it work? If yes, then yes. Since the above code only contains an array, regular expression and validation for this, I don't think the problem is with your code. While this is working. But if you ask: "is this code scalable?" then the answer will be different, and it all depends on your goals of the MVC Framework (for example, this environment for general purposes, for example: a blog or its features for the REST API provider. And so on ...)

2) Yes. Kohana, Zend, CI and other popular (and optimized) PHP frameworks use this (array + regular expression on the router).

3) I think you could just give it a flag in the block / section of the route and make this flag available as a global variable. Thus, in your controller, you can decide which response to send for another type of request (ajax / non-ajax) by checking this flag (for example, you can provide $this->is_ajax as a global method available in the controller area).

+1
source

If I can add a couple of points:

  • Remove id_number and page_number from the router - just transfer everything that was agreed with the controller, in the end, this is the task of the controller to process this data, not the router

  • Do not pass $uri to the constructor, translate it instead of dispatch() .

  • Why are those isset() -s in $uri_route_map ? Obviously, they would be false , since $uri_route_map is defined before the Router() object is created.

  • Would recommend adding more logic to the matching subroutine - in your current case, sitename/forums will not match anything that will result in 404 (without a slash)

  • You can also define default parameters in your $uri_route_map and then array_merge with the corresponding parameters. So, for example, when the page number is not specified, page_number will be 1

  • If you are concerned about the performance of a site with high traffic, you can cache routes. After all, forums/viewforum/100 will always point to the same controller / method.

And why are you worried about sending AJAX requests to your index.php file? What is the problem with this?

+1
source

Source: https://habr.com/ru/post/894882/


All Articles