How to load classes based on pretty URLs in MVC-like page?

I would like to ask for some tips, on how solve this problem. I'm trying to build my own MVC website. I learned the basics of the URL.

http://example.com/blog/cosplay/cosplayer-expo-today

blog -> the controller
cosplay -> the method in controller
cosplayer-expo-today ->variable in method

What if i dynamically extend the category in my blog controller? Will I need to create the method, or is there some trick to do that automatically? I mean... i have these categories now: cosplay,game,movie,series. So I need to create these methods in controller, but they all do the same thing, namely select other category from database.

  • function cosplay() = example.com/blog/cosplay/
  • function game() = example.com/blog/game/
  • function movie() = example.com/blog/movie/
  • function series() = example.com/blog/series/

Is there any good advice on how can i write my controller to do that automatically? I mean if I upload a new category in my database, but i don't want to modify the controller. Is it possible? Thanks for the help!

UPDATE

Here is my URL exploder class

class Autoload
{
    var $url;
    var $controller;
    function __construct()
    {
        $this->url = $_GET['url'];
        //HNEM ÜRES AZ URL
        if($this->url!='' && !empty($this->url))
        {
            require 'application/config/routes.php';
            //URL VIZSGÁLATA
            $this->rewrite_url($this->url);

            //URL SZÉTBONTÁSA
            $this->url = explode('/', $this->url);

            $file = 'application/controllers/'.$this->url[0].'.php';
            //LÉTEZIK A CONTROLLER?
            if(file_exists($file))
            {
                require $file;
                $this->controller = new $this->url[0];

                //KÉRELEM ALATT VAN AZ ALOLDAL?
                if(isset($this->url[1]))
                {
                    //LÉTEZIK A METÓDUS? ENGEDÉLYEZVE VAN?
                    if(method_exists($this->controller, $this->url[1]) && in_array($this->url[1], $route[$this->url[0]]))
                    {
                        if(isset($this->url[2]))
                        {
                            $this->controller->{$this->url[1]}($this->url[2]);
                        }
                        else
                        {
                            $this->controller->{$this->url[1]}();
                        }
                    }
                    else
                    {
                        header('location:'.SITE.$this->url[0]);
                        die();
                    }
                }
            }
            else
            {
                header('location:'.SITE);
                die();
            }
        }
        else
        {
            header('location:'.SITE.'blog');
            die();
        }
    }

    /**
     * Első lépésben megvizsgáljuk, hogy a kapott szöveg tartalmaz-e nagybetűt. Amennyiben igen átalakítjuk kisbetűsre.<br/>
     * Második lépésben megnézzük, hogy a kapott szöveg '/'-re végződik-e. Amennyiben igen levágjuk azt.<br/>
     * Harmadik lépésben újra töltjük az oldalt a formázott szöveggel.
     * 
     * @param string $url Korábban beolvasott URL.
     */
    private function rewrite_url($url)
    {
        //HA NAGYBETŰ VAN AZ URL-BEN VAGY '/'-RE VÉGZŐDIK
        if(preg_match('/[A-Z]/', $url) || substr($url, -1)=='/')
        {
            //NAGYBETŰS AZ URL KICSIRE ALAKÍTJUK
            if(preg_match('/[A-Z]/', $url))
            {
                $url = strtolower($url);
            }
            //HA '/'-RE VÉGZŐDIK LEVÁGJUK
            if(substr($url, -1)=='/')
            {
                $url = substr($url, 0, strlen($url)-1);
            }
            header('location:'.SITE.$url);
            die();
        }
    }




}

And here is my .htacces

Options +FollowSymLinks
RewriteEngine On

RewriteCond %{REQUEST_FILENAME} !-d  
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-l

RewriteRule ^(.+)$ index.php?url=$1 [QSA,L]

Solution 1:

FYI: There are several things that you are doing wrong. I will try to go through each of them and explain the problems, the misconceptions and the possible solution(s).

The autoloading and routing are separate things.

From the look of you posted code, it's obvious, that you have a single class, which is responsible the following tasks:

  • routing: it splits the URL in parts that have some significance for the rest of applications
  • autoloading: class takes the separated URL segments and attempts to include related code
  • factory: new instances are initialized and some method are called on them
  • response: in some cases, the class sends a response to user in form of HTTP header

In OOP there is this thing, called: Single Responsibility Principle [short version]. Basically it means that a class should be handling one specific are thing. The list above constitutes at least 4 different responsibilities for your Autoload class.

Instead of what you have now, each of these general tasks should be handled by a separate class. And in case of autoloader, you could get away with a single function.

How to do write your own autoloading code ?

Part of the problem that I see is the confusion about how autoload actually works in PHP. The call of include or require doesn't need to be done where the instance will be created. Instead you register a handler (using spl_autoload_register() function), which then is **automatically* called, when you try to use previously-undefined class.

The simplest example for it is:

spl_autoload_register( function( $name ) use ( $path ) {
    $filename = $path . '/' . $name . '.php';
    if ( file_exists( $filename ) === true ) {
        require $filename;
        return true;
    }
    return false;
});

This particular example uses anonymous function, which is one of features that was introduced in PHP 5.3, but the manual page for the spl_autoload_register() will also show you examples how to achieve the same with objects or ordinary functions.

Another new feature that is closely related to autoloading is namespaces. In this context the namespaces would give you two immediate benefits: ability to have multiple classes with same name and options to load class file from multiple directories.

For example, you can have code like this:

$controller = new \Controllers\Overview;
$view = new \Views\Overview;

$controller->doSomething( $request );

.. in this case you can have autoloader fetching classes from /project/controllers/overview.php and /project/views/overview.php files respectively. Because the spl_autoload_register() will pass "\Controllers\Overview" and "\Views\Overview" to the handler function.

There is also a FIG recommendation for how to implement autoloaders. You can find it here. While it has some significant problems, it should provide you with good base on which to build upon.

How to parse pretty URLs ?

It is no secret, that Apache's mod_rewrite is quite limited in what it can do with pretty URLs. And, while it's a widespread server, it is not the only option for webservers. This is why for maximum flexibility PHP developers opt to handle URLs on the PHP end.

And the first thing any newbie will do is explode('/', ... ). It is a natural choice, but you will soon notice that it is also extremely limited in what it can really do. The routing mechanism will start to grow. At first based on count of segments, later - adding different conditional values in segments, that require different behavior.

Essentially, this will turn in huge, fragile and uncontrollable mess. Bad idea.

Instead what you should do is have a list of regular expressions, that you match against given pretty URL. For example:

'#/(?P<resource>[^/\\\\.,;?\n]+)/foobar#'

The above defined pattern would match all the URL that have two segments, with some text in first segment and "foobar" in the second ... like "/testme/foobar".

Additionally you can link each pattern with corresponding default values for each match. When you put this all together, you might end up with configuration like this (uses 5.4+ array syntax, because that's how I like to write .. deal with it):

$routes = [
    'primary' => [
        'pattern'   => '#/(?P<resource>[^/\\\\.,;?\n]+)/foobar#',
        'default'   => [
            'action'    => 'standard',
        ],
    ],
    'secundary' => [
        'pattern'   => '#^/(?P<id>[0-9]+)(?:/(?P<resource>[^/\\\\.,;?\n]+)(?:/(?P<action>[^/\\\\.,;?\n]+))?)?$#',
        'default'   => [
            'resource'  => 'catalog',
            'action'    => 'view',
        ]
    ],
    'fallback'  => [
        'pattern'   => '#^.*$#',
        'default'   => [
            'resource'  => 'main',
            'action'    => 'landing',
        ],
    ],
]; 

Which you could handle using following code:

// CHANGE THIS
$url = '/12345/product';

$current = null;

// matching the route
foreach ($routes as $name => $route) {
    $matches = [];
    if ( preg_match( $route['pattern'], $url, $matches ) ) {
        $current = $name;
        $matches = $matches + $route['default'];
        break;
    }
}


// cleaning up results
foreach ( array_keys($matches) as $key ) {
    if ( is_numeric($key) ) {
        unset( $matches[$key] );
    }
}


// view results
var_dump( $current, $matches );

Live code: here or here

Note:
If you use '(?P<name> .... )' notation, the matches will return array with 'name' as a key. Useful trick for more then routing.

You probably will want to generate the regular expressions for the matching from some more-readable notations. For example, in configuration file, this expression:

'#^/(?P<id>[0-9]+)(?:/(?P<resource>[^/\\\\.,;?\n]+)(?:/(?P<action>[^/\\\\.,;?\n]+))?)?$#'

.. should probably look something like

'/:id[[/:resource]/:action]'

Where the :param would indication an URL segment and [...] would signify an optional part of URL.

Based on this you should be able to flesh out your own routing system. The code fragments above is just example of simplified core functionality. To get some perspective on how it might look when fully implemented, you could look at code in this answer. It should give you some ideas for your own API version.

Calling the stuff on controllers ..

It is quite common mistake to bury the execution of controllers somewhere deep in the routing class (or classes).This causes two problems:

  • confusion: it makes harder to find where the "real work" begins in the application
  • coupling: your router ends up chained to that specific interpretation of MVC-like architecture

Routing is a task which even in custom-written application will naturally gravitate toward the "framework-ish" part of codebase.

The (really) simplified versions would look like:

$matches = $router->parse( $url );

$controller = new {'\\Controller\\'.$matches['controller']};
$controller->{$matches['action']( $matches );

This way there is nothing that requires your routing results to be used in some MVC-like architecture. Maybe you just need a glorified fetching mechanism for serving static HTML files.

What about those dynamically extend categories?

You are looking at it the wrong way. There is no need for dynamically adding methods to controller. In your example there is actually one controller method ... something along the lines of:

public function getCategory( $request ) {
    $category = $request->getParameter('category');

    // ... rest of your controller method's code
}

Where $category would end up containing "cosplay", "game", "movie", "series" or any other category that you have added. It is something that your controller would pass to the model layer, to filter out articles.

What people actually use professionally?

These days, since everyone (well .. everyone with some clue) uses composer, for autoloading the best option is to use the loader that is comes bundled with composer.

You simply add require __DIR__ . '/vendor/autoload.php' and with some configuration it will just work.

As for routing, there are two major "standalone" solutions: FastRoute or Symfony's Routing Component. These ones can be included in you project without additional headaches.

But since some of people will be using frameworks, each of those will also contain capability of routing the requests.

Some further reading ..

If you want to learn more about MVC architectural pattern, I would strongly recommend for you to go though all the materials listed in this post. Think of it as mandatory reading/watching list. You also might find somewhat beneficial these old posts of mine on the MVC related subjects: here, here and here

P.S.: since PHP 5.0 was released (some time in 2004th), class's variables should be defined using public, private or protected instead of var.