Routing, Navigation and State in MVC

I am trying to refactor my application with the MVC paradigm.

My site displays charts. URLs are of the form

  • app.com/category1/chart1
  • app.com/category1/chart2
  • app.com/category2/chart1
  • app.com/category2/chart2

I am using Apache Rewrite to route all requests to index.php and so I am doing URL handling in PHP.

I am working on the constant task of adding a class active

to my navigation links when a certain page is selected. Specifically, I have both category level navigation and chart level navigation. My question is, what's the best way to do this while staying in the spirit of MVC?

Before my refactoring, as the navigator was getting relatively complex, I decided to put it in an array:

$nav = array(
  '25th_monitoring' => array(
    'title'    => '25th Monitoring',
    'charts' => array(
      'month_over_month' => array(
        'default' => 'month_over_month?who=total&deal=loan&prev='.date('MY', strtotime('-1 month')).'&cur='.date('MY'),
        'title'   => 'Month over Month'),
      'cdu_tracker' => array(
        'default' => 'cdu_tracker',
        'title'   => 'CDU Tracker')
    )
  ),
  'internet_connectivity' => array(
    'title'   => 'Internet Connectivity',
    'default' => 'calc_end_to_end',
    'charts' => array(
      'calc_end_to_end' => array(
        'default' => 'calc_end_to_end',
        'title' => 'calc End to End'),
      'quickcontent_requests' => array(
        'default' => 'quickcontent_requests',
        'title' => 'Quickcontent Requests')
    )
  )
);

      

Again, I need to know both the current category and the current chart. My main navigator was

<nav>
  <ul>
    <?php foreach ($nav as $category => $category_details): ?>
    <li class='<?php echo ($current_category == $category) ? null : 'active'; ?>'>
      <a href="<?php echo 'http://' . $_SERVER['SERVER_NAME'] . '/' . $category . '/' . reset(reset($category_details['charts'])); ?>"><?php echo $category_details['title']; ?></a>
    </li>
    <?php endforeach; ?>
  </ul>
</nav>

      

and the sub-navigator was something similar, checking current_chart instead of current_category.

Before, during the parsing, I exploded $_SERVER['REQUEST_URI']

on /

and smashed the pieces into $current_category

and $current_chart

. I was doing this in index.php. Now I feel like this is not in the spirit of the font controller. From links like Symfony 2 docs , it looks like each route should have its own controller. But then I have to define the current category and diagram multiple times, either in the template files themselves (which doesn't seem to be in the spirit of MVC) or in an arbitrary function in the model (which then has to be called by multiple controllers, which is apparently redundant ).

What's the best practice here?

Update: My front controller looks like here:

// index.php
<?php
// Load libraries
require_once 'model.php';
require_once 'controllers.php';

// Route the request
$uri = str_replace('?'.$_SERVER['QUERY_STRING'], '', $_SERVER['REQUEST_URI']);
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && (!empty($_GET)) && $_GET['action'] == 'get_data') {

  $function = $_GET['chart'] . "_data";
  $dataJSON = call_user_func($function);
  header('Content-type: application/json');
  echo $dataJSON;

} elseif ( $uri == '/' ) {
  index_action();

} elseif ( $uri == '/25th_monitoring/month_over_month' ) {
  month_over_month_action();

} elseif ( $uri == '/25th_monitoring/cdu_tracker' ) {
  cdu_tracker_action();

} elseif ( $uri == '/internet_connectivity/intexcalc_end_to_end' ) {
  intexcalc_end_to_end_action();

} elseif ( $uri == '/internet_connectivity/quickcontent_requests' ) {
  quickcontent_requests_action();

} else {
  header('Status: 404 Not Found');
  echo '<html><body><h1>Page Not Found</h1></body></html>';   
}

?>

      

It looks like when month_over_month_action () is called, for example, since the controller knows current_chart is month_over_month, it should just pass that. This is where I worked.

+3


source to share


2 answers


Well, I had almost the same problem when I was writing a CMS-like product. So I spent some time figuring out how to make this work and keep the code more usable and cleaner. Both CakePHP and Symfony route-mecanisms inspired me a bit, but it wasn't enough for me. So I will try to give you an example of how I am doing it now.

My question is, what's the best way to do this while staying in the MVC spirit?

First, in general, it is best practice to DO NOT use a procedural approach with MVC in web development at all. Second, save the SRP.

From links similar to the Symfony 2 docs, it seems that each route should have its own controller.

Yes, this is the correct approach, but that doesn't mean that another route match cannot have the same controller, but a different action.

The main flaw in your approach (the code you posted) is that you are mixing responsibilities and not using the MVC pattern. Anyway, MVC in PHP with a procedural approach is just a terrible thing.

So what exactly are you mixing:

  • The logic of the route engine (this must be a different class), not in the "controller" and route map.
  • Request response and response (I can see this is not obvious to you)
  • Class autoloading
  • Controller logic

All these "parts" must be of the same class. Basically, they should be included in the index or boot files.

Also, by doing this:

require_once 'controllers.php';

You automatically enable ALL controllers for every match (even if there is no match). This actually has nothing to do with MVC and leads to memory leaks. Instead, you should ONLY include and instantiate the controller that matches the URI strings. Also, be careful with include()

and require()

as they can lead to duplicate code if you include the same file twice.

And

} elseif ( $uri == '/' ) {
  index_action();

} elseif ( $uri == '/25th_monitoring/month_over_month' ) {
  month_over_month_action();

} elseif ( $uri == '/25th_monitoring/cdu_tracker' ) {
  cdu_tracker_action();

} elseif ( $uri == '/internet_connectivity/intexcalc_end_to_end' ) {
  intexcalc_end_to_end_action();

      

It is very difficult to make a match with control structures if/else/elseif

. Okay, what if you have 50 matches? or even 100? Then you need to write 50 or 100 times to write else/elseif

accordingly. Instead, you should have a map and (like an array) iterate over each HTTP request.

The general approach to using MVC with a routing engine boils down to this:

  • Matching the request with a route map (and store the parameters somewhere if any)
  • Then create the appropriate controller
  • Then pass parameters if any

In PHP, the implementation will look like this:



File: index.php

<?php

//.....

// -> Load classes here via SPL autoloader or smth like this

// .......

// Then -> define or (better include route map from config dir)

$routes = array(

    // -> This should default one
    '/' => array('controller' => 'Path_To_home_Controller', 'action' => 'indexAction'),

    '/user/:id' => array('controller' => 'Path_to_user_controller', 'action' => 'ViewAction'),   

    // -> Define the same controller
    '/user/:id/edit' => array('controller' => 'Path_to_user_controller', 'action' => 'editAction'),


    // -> This match we are going to hanlde in example below:
    '/article/:id/:user' => array('controller' => 'SomeArticleController', 'action' => )

);

// -> Also, note you can differently handle this: array('controller' => 'SomeArticleController', 'action' => )
// -> Generally controller key should point to the path of a matched controller, and action should be a method of the controller instance
// -> But if you're still on your own, you can define it the way you want.


// -> Then instantiate common classes

$request  = new Request();
$response = new Response();

$router = new Router();

$router->setMap( $routes );

// -> getURI() should return $_SERVER['REQUEST_URI']
$router->setURI( $request->getURI() ); 


if ( $router->match() !== FALSE ) {

  // -> So, let assume that URI was:  '/article/1/foo'     

  $info = $router->getAll();

  print_r ( $info );

  /**
   * Array( 'parameters'  =>  Array(':id' => '1', ':user' => 'foo'))
   *        'controller'  => 'Path_To_Controller.php'
   *        'action'      => 'indexAction'
   */

   // -> The next things we are going to do are:

   // -> 1. Instantiate the controller
   // -> 2. Pass those parameters we got to the indexAction method   

   $controller =  $info['controller'];

   // -> Assume that the name of the controller is User_Controller
   require ( $controller ); 

   // -> The name of class should also be dynamic, not like this, thats just an example
   $controller = new User_Controller(); 

   $arguments = array_values( $info['parameters'] );

   call_user_func_array( array($controller, $info['action']), $arguments );  

   // -> i.e we just called $controller->indexAction('1', 'foo') "dynamically" according to the matched URI string

   // -> idealy this should be done like: $response->send( $content ), however

} else {

   // -> In order not to show any error
   // -> redirect back to "default" controller
   $request->redirect('/');

}

      

In my MVC built apps I do the following:

(Where i use Dependecy Injection and save SRP)

<?php

require (__DIR__ . '/core/System/Auload/Autoloader.php');

Autoloader::boot(); // one method includes all required classes

$map = require(__DIR__ . '/core/System/Route/map.php');

$request    = new Request();
$response   = new Response();

$mvc        = new MVC();
$mvc->setMap( array_values($map) ); 
// -> array_values($map) isn't accurate here, it'd be a map of controllers
// -> take this as a quick example


$router     = new Router();

$router->setMap( $map );
$router->setURI( $request()->getURI() );


if ( $router->match() !== FALSE ) {

    // -> Internally, it would automatically find both model and view instances
    // -> then do instantiate and invoke appropriate action
    $router->run( $mvc );

} else {

    // No matches handle here
    $request->redirect('/');
}

      

I found this to be more appropriate for me after poking around Cake and Symfony.

I want to note:

It is not easy to find good articles about MVC in PHP. Most of them are simply wrong. (I know how it feels because the first time I started learning from them, like many people)

So my thought is here:

Don't make the same mistake as before. If you want to learn MVC, get started by reading Zend Framework or Symfony Tutorials. Even the ones that are slightly different from each other, the idea behind the scene is the same.

Return to another part of the question

Again, I need to know both the current category and the current schedule to access them. My main navigator was

<nav>
  <ul>
    <?php foreach($nav as $category => $category_details): ?>
    <li class='<?php echo ($current_category == $category) ? null : 'active'; ?>'>
      <a href="<?php echo 'http://' . $_SERVER['SERVER_NAME'] . '/' . $category . '/' . reset(reset($category_details['charts'])); ?>"><?php echo $category_details['title']; ?></a>
    </li>
    <?php endforeach; ?>
  </ul>
</nav>

      

First of all, don't concatenate the string, but use printf()

like:

<a href="<?php printf('http://%s/%s/%s', $_SERVER['SERVER_NAME'], $category, reset(reset($category_details['charts']))); ?>"><?php echo $category_details['title']; ?></a> 

      

If you need this to be all over the place (or at least in many different templates), then I would suggest it be in the general paragraph of the View class.

For example,

abstract class View
{
    // -> bunch of view reusable methods here...

    // -> Including this one
    final protected function getCategories()
    {
        return array(

            //....
        );
    }
}

class Customers_View extends View
{
    public function render()
    {
        $categories =& $this->getCategories();

        // -> include HTML template and then interate over $categories
    }

}

      

+2


source


There are no "best practices" in this area. Although, there are some that are most often used by others, and some are very bad ideas (unfortunately, these two groups tend to overlap).

Routing in MVC

While not technically part of the MVC design pattern when applied to the Web, your application needs to know which controller to initialize and which method to call on it.

Doing it explode()

to collect this kind of information is a bad idea. It is difficult to debug and maintain. A much better solution is to use regular expressions.

Basically you get a list of routes that contain a regex and some fallback values. You go through that list and on fists - fetch the data and apply the defaults where the data was missing.

This approach also frees you from much broader options for parameter ordering.

To make the solution easier to use, you can also add functionality that turns the comment string into a regular expression.

For example (taken from some unit test I have):



  • notation:   test[/:id]


    expression:#^/test(:?/(?P<id>[^/\.,;?\n]+))?$#

  • notation:   [[/:minor]/:major]


    expression:#^(:?(:?/(?P<minor>[^/\.,;?\n]+))?/(?P<major>[^/\.,;?\n]+))?$#

  • notation:   user/:id/:nickname


    expression:#^/user/(?P<id>[^/\.,;?\n]+)/(?P<nickname>[^/\.,;?\n]+)$#

When creating such a generator, everything will not be so simple, it would be quite reusable. IMHO the time spent creating it would be well spent. Also, using the construct (?P<key>expression)

in regular expressions gives you a very useful array of key-value pairs from the matched route.

Menu and MVC

The decision about which menu item to highlight as active

should always be up to the current instance of the view.

A more complex issue is where the information needed to make such a decision comes from. There are two sources if data is available to a view instance: information that was passed to the controller and data for the view that is requested at the model level.

A controller in MVC takes user input and, based on that input, changes the state of the current view and model level by passing in the specified values. The controller should not extract information from the model level.

IMHO, the best approach in this case is to relay at the model level for information about the contents of the menu and the currently active item. While both the hard code and the currently active element are in sight and the relays on the controllers transmit information, MVC is typically used in a large-scale application where this practice could hurt you.

The view in the MVC design pattern is not a dumb pattern. This is the structure that is responsible for the logic of the user interface. In the context of the Web, this would mean generating a response from multiple templates when needed, or simply simply sending an HTTP location header.

+3


source







All Articles