How to Load Classes Based on Pretty Urls in MVC-Like Page

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

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.

How to include model/view and call classes in simple php mvc example

As it is already say you can have a look to use the spl_autoload_register which will require all your files in the given path. You can change this function to improve the load.

Concerning the Model with your current code you can implement it as follow:

$controllerPath = "/controllers/{$cont}Controller.php";
$modelPath = "/model/{$cont}Model.php";
if (FILE_EXISTS($controllerPath)) {
require $controllerPath;
if (FILE_EXISTS($modelPath)) {
require $modelPath;
}
else {
throw new \LogicException(
sprintf("Your controller must implement a model. No model found: %s", $modelPath)
);
}
} else {
$cont = "home";
require "/controllers/{$cont}Controller.php";
require "/model/{$cont}Model.php";
}

//===============================================
// Start the controller
//===============================================
$controller = new $cont($cont);

In this sample of code, $cont is the name of the page like home. Which require the homeController and homeModel. Then in your __construct($modelName) just set the model.

However, I don't recommand you tu use this because your controller can load many Models. Then, your controller could look like this:

<?php
namespace \App\Controller; // If you use namespace

use App\Model\homeModel, // If you use namespace
App\Model\productModel; // If you use namespace

Class HomeController extends Controller
{

private $model;

/* If this function is common to all controllers just move it
in the parent Controller class( one more time, I don't recommend to set
the model using this way). */
public function __construct($model) {
$this->model= $model;
}

public function login() {
$homeModel = new homeModel(); // If you use namespace

// Example to call the view
$myValue = 'Hello world';
$this->render(array("myVariableName" => $myValue))
}
}

In this second example $this->render can be a method in your Controller class (which by the way should be an abstract class). I'll give a last sample of code for the logic of the abstract controller.

<?php

namespace \App\Controller;

abstract class AbstractController {

/* Common code for all controllers */
public function __construct() {

}

/* View renderer */
protected function render($parameters) {
/* Call the view here maybe an another class which manage the View*/
}
}

To conclude, you can implement this MVC in many way, this code is just a suggestion and maybe its not the best way. I advise you to have a look with the spl_autoload that I put at the beginning of my answer.

URL is not working when it's trying it for routing

two things straight away: "/" is not legal in a url param as part of a get string. you need to encapsulate it with URL encodeing

EG:

  http://localhost/mvc/index.php?url=Index%2Fcategory

it's also the fact that "$ctlr->$url[1]" simply doesn't have a function called it.. eg: whatever "$ctlr->$url[1]" resolves to ??category()?? doesn't exist, you need to MAKE it.

add this to your code

 function category() {
Index tmp = new Index();
tmp->category();
}

EDIT : I've just noticed, it's even more idiotic than I thought.. your string says Index/category doesn't it?.. make the class method static.. (this code is dreadful by the way it displays almost no sound knowledge of design whatsoever) there is no Index/category because you can't call category inside a class except if it's a static method.

Learn to code.



Related Topics



Leave a reply



Submit