I have a problem building a front controller for a project I have at school.
Here is the thing, i created several controllers per entities i.e. One to select, another one to delete etc... and this for posts and comments. So i have 6 controllers and then I'm being asked to build front controller above these controllers but i keep on adding layers (route all to index.php then from there I instantiate the front controller which then will send the request to the related controller with header(location:...) and it gets messy and very complicated because for some of the request i need to pass some ids in the url for the db request...
I can't use any framework so is there a cleaner way to build a front controller that can handle request with parameters in the url and some without?
Do i have to route everything to a single page (with htaccess i created a simple rule to send everything to index.php)?
And then from there again instantiate the front controller?
If you need some code I'll share this with you but it's a mess :)
Thanks guys, i need advise I'm kind of lost at this point
You will want to build a basic router, what it does.
A router will parse the path and take parts of it and locate a controller file. And then run a method in that file with arguments. We will use this format for urls
www.yoursite.com/index.php/{controller}/{method}/{args}
So for this example our url will be
www.yoursite.com/index.php/home/index/hello/world
the index.php can be hidden using some basic .htaccess ( Mod Rewrite )
So lets define some things, first we will have folders
-public_html
index.php
--app
---controller
home.php
So the main folder is public_html with the index.php and a folder named app. Inside app is a folder named controller and inside that is our home.php contoller
Now for the Code ( yea )
index.php (basic router )
<?php
//GET URI segment from server, everything after index.php ( defaults to home )
$path_info = isset( $_SERVER['PATH_INFO'] ) ? $_SERVER['PATH_INFO'] : '/home';
//explode into an array - array_filter removes empty items such as this would cause '/home//index/' leading /, trailing / and // double slashes.
$args = array_filter( explode('/', $path_info) );
$controller_class = array_shift($args); //remove first item ( contoller )
$method = count( $args ) > 0 ? array_shift($args) : 'index'; //remove second item or default to index ( method )
$basepath = __DIR__.'/app/controller/'; //base path to controllers
if(!file_exists($basepath.$controller_class.".php") ){
echo "SHOW 404";
exit();
}
//instantiate controller class
require_once $basepath.$controller_class.".php";
$Controller = new $controller_class;
//check if method exists in controller
if(!method_exists( $Controller, $method ) ){
echo "Method not found in controller / or 404";
exit();
}
//call methods with any remaining args
call_user_func_array( [$Controller, $method], $args);
home.php ( controller )
<?php
class home{
public function index( $arg1="", $arg2=""){
echo "Arg1: ".$arg1 . "\n";
echo "Arg2: ".$arg2 . "\n";
}
public function test( $arg1 = "" ){
echo "Arg1: ".$arg1 . "\n";
}
}
Now if you put in any of these urls
www.yoursite.com/index.php
www.yoursite.com/index.php/home
www.yoursite.com/index.php/home/index
It should print ( defaults )
Arg1:
Arg2:
If you do this url
www.yoursite.com/index.php/home/index/hello/world
It should print
Arg1: hello
Arg2: world
And if you do this one
www.yoursite.com/index.php/home/test/hello_world
it would print
Arg1: hello_world
The last one, running in the second method test ( no echo arg2 ), this way you can see how we can add more controllers and methods with only having to code them into a controller.
This method still allows you to use the $_GET part of the url, as well as the URI part to pass info into the controller. So this is still valid
www.yoursite.com/index.php/home/test/hello_world?a=1
and you could ( in home::test() ) output the contents of $_GET with no issues, this is useful for search forms etc. Some pretty url methods prevent this which is just ... well ... crap.
In .htaccess with mod rewrite you would do this to remove index.php from the urls
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?/$1 [L]
</IfModule>
Then you can use urls without the index.php such as this
www.yoursite.com/home/index/hello/world
This is the simplest Router I could come up with on such short notice, yes I just created it. But this is a very similar ( although simplified ) implementation used in many MVC frameworks
PS. please understand how this is all done, so you actually learn something...
Many improvements could be made, such as allowing these urls
www.yoursite.com/hello/world
www.yoursite.com/home/hello/world
www.yoursite.com/index/hello/world
Which would all fall back to the a default of home controller and index method, but that would take some additional checks (for not found files and methods ) I cant be bothered with right now...
Here's a simple demo process (won't work, just an example).
index.php
//Say you called /index.php/do/some/stuff
$route = $_SERVER["PATH_INFO"]; //This will contain /do/some/stuff
$router = new Router();
$router->handle($route);
router.php
class Router {
private static $routeTable = [
"do/some/stuff" => [ "DoingSomeController" => "stuff" ]
];
public function handle($route) {
if (isset(self::$routeTable[trim($route,"/"])) {
$controller = new self::$routeTable[trim($route,"/"][0];
call_user_func([$controller,self::$routeTable[trim($route,"/"][1]]);
}
}
}
Basic demo, what some frameworks do.
Related
I am trying to figure out the best approach when linking to static pages using a loosely followed MVC design pattern.
I begin by rewriting all requests to the index.php which handles all request and break them down the url into the controller, action and parameters. However if i don't want to follow this url structure and just want to visit a static page such as 'http://example.com/home/' without having to call some action how would i achieve this without getting a php error caused by my router/dispatcher trying to request a file that does not exist?
I thought about setting up some switch statement or a if statement as shown below that checks if the url is set to something then uses a custom defined controller and action, or i wasn't sure whether to take the static resources out of the MVC directory and have it seperate and link to it that way?
<?php
class Router
{
static public function parse($url, $request)
{
$url = trim($url);
if ($url == "/")
{
$request->controller = "tasks";
$request->action = "index";
$request->params = [];
}
else
{
$explode_url = explode('/', $url);
$explode_url = array_slice($explode_url, 2);
$request->controller = $explode_url[0];
$request->action = $explode_url[1];
$request->params = array_slice($explode_url, 2);
}
}
}
?>
This works, but i'd rather not have a huge router setup for many different static resources as it feels tacky and that i am just patching together code. Would putting static pages in its own directory outside of MVC and linking to them in the views be a valid option? i'm relatively new to MVC so any guidance would be great.
Your application shouldn't receive request it is not supposed to handle, you can solve this on a webserver level:
if you are using apache for example, you can setup in the .htaccess file that the request should be directed to your front controller (ex: index.php) only if the requested resource does not exist
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^.*$ /index.php [L]
I am trying to make website where users are able to post content using an WYSIWYG editor. In my .htaccess file i have
FallbackResource index.php
which is successfully sending all requests to the index.php file. I am then using PHP to require the desired file
$path = explode('/', $_SERVER['REQUEST_URI']);
//[SCRIPT_FILENAME] => C:/xampp/htdocs/tests/index.php
if($path[2] == 'notifications'){
require_once 'notifications.php';
}
All this works fine. The problem arises when i try to redirect to an external link like google.com. The link itself gets redirected to the index page instead of being redirected.
This is the key because users can insert tags for other websites as reference in their text with the WYSIWYG editors. So my question is, Am i doing this right or do i need a different approach? and if so, which approach?. I would greatly appreciate similar approaches to those used by sites like facebook or twitter.
AS for an approach I would use a method similar to CodeIgniter or other MVC frameworks ( but simpler ).
So first you have to devise a schema, what I mean by that is something like this
www.yoursite.com/index.php/{controller}/{method}/args ...
Then you would build a router to go parse the url. I can write you one but it will take a minute.
UPDATE
You can find it on my github page here
But for refrence here is the code:
SimpleRouter.php
/**
* A simple 1 level router
*
* URL schema is http://example.com/{controller}/{method}/{args ... }
*
* #author ArtisticPhoenix
* #package SimpleRouter
*/
class SimpleRouter{
/**
* should be the same as rewrite base in .htaccess
* #var string
*/
const REWRITE_BASE = '/MISC/Router/';
/**
* path to controller files
*
* #var string
*/
const CONTOLLER_PATH = __DIR__.'/Controllers/';
/**
* route a url to a controller
*/
public static function route(){
//normalize
if(self::REWRITE_BASE != '/'){
$uri = preg_replace('~^'.self::REWRITE_BASE.'~i', '',$_SERVER['REQUEST_URI']);
}
$uri = preg_replace('~^index\.php~i', '',$uri);
$uri = trim($uri,'/');
//empty url, like www.example.com
if(empty($uri)) $uri = 'home/index';
//empty method like www.example.com/home
if(!substr_count($uri, '/')) $uri .= '/index';
$arrPath = explode('/', $uri);
$contollerName = array_shift($arrPath);
$methodName = array_shift($arrPath);;
$contollerFile = self::CONTOLLER_PATH.$contollerName.'.php';
if(!file_exists($contollerFile)){
//send to error page
self::error404($uri);
return;
}
require_once $contollerFile;
if(!class_exists($contollerName)){
self::error404($uri);
return;
}
$Controller = new $contollerName();
if(!method_exists($Controller, $methodName)){
self::error404($uri);
return;
}
if(!count($arrPath)){
call_user_func([$Controller, $methodName]);
}else{
call_user_func_array([$Controller, $methodName], $arrPath);
}
}
/**
* call error 404
*
* #param string $uri
*/
protected static function error404($uri){
require_once self::CONTOLLER_PATH.'home.php';
$Controller = new home();
$Controller->error404($uri);
}
}
Default Controller home.php
/**
*
* The default controller
*
* #author ArtisticPhoenix
* #package SimpleRouter
*/
class home{
public function index($arg=false){
echo "<h3>".__METHOD__."</h3>";
echo "<pre>";
print_r(func_get_args());
}
public function otherpage($arg){
echo "<h3>".__METHOD__."</h3>";
echo "<pre>";
print_r(func_get_args());
}
public function error404($uri){
header('HTTP/1.0 404 Not Found');
echo "<h3>Error 404 page {$uri} not found</h3>";
}
}
2nd Controller (example) user.php
/**
*
* An example users router
*
* #author ArtisticPhoenix
* #package SimpleRouter
*/
class user{
public function index(){
echo "<h3>".__METHOD__."</h3>";
}
public function login(){
echo "<h3>".__METHOD__."</h3>";
}
}
Rewrite (remove index.php) .htaccess
RewriteEngine On
# For sub-foder installs set your RewriteBase including trailing and leading slashes
# your rewrite base will vary, possibly even being / if no sub-foder are involved
RewriteBase /MISC/Router/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php/$1 [L]
Finally index.php
require_once __DIR__.'/SimpleRouter.php';
SimpleRouter::route();
I went with a static call to the router just to keep things simple, this way no instance is needed of the class which only has 2 methods (1 public) anyway.
I built it as a single level router, what I mean by that is that you cannot put controllers in sub-folders, such as Controllers/user/home.php instead of just Controllers/user.php. This should be fine for small to medium sized sites, but for a vary large site it may be desirable to nest the controllers. However this adds a bunch of complexity to it.
The advantage of this way, instead of individually creating the routes should be obvious. You shouldn't have to touch the code as long as the route (the url) follows the simple schema I outlined. This also allows you organize the sections of your site in their own files by functionality. For example take the user.php controller. Would let you put all that functionality in one place, such as the login, profile, logout pages etc...
If you were to add a member.php controller you can put all the stuff that you only want to show to logged in users there. And then in the __construct method of that controller you check if the current session is logged in and it covers all methods for that file.
The url schema is this http://example.com/{controller}/{method}/{args ... }. But you can also use urls like this http://example.com/index.php/{controller}/{method}/{args ... }. I'm using Mod Rewrite to allow for the removal of the index.php in the URL, so if you don't have that then you have to put index.php in the URL ( or remove it by some other means )
REWRITE_BASE On my local test server I just place everything in it's own folder as I am to lazy to setup virtual hosts. So you would want to change this constant, and the matching .htaccess, value to be whatever your sub folder is ( maybe nothing ). I just left it here for example of how to use it within a sub-folder.
Lastly, this is the C in traditional MVC architecture. You should avoid doing what is called "Business logic" in the controllers, you should also avoid outputting HTML directly from the controller (although I did this in the examples). Instead of outputting the HTML, I would suggest using a template engine like Blade or Twig there are many of these freely available. Think of a Controller like the "Glue" that joins the "Model (business logic)" to the "View (template)". Business logic would be like a User class (uppercase U like a noun, not lowercase as in our user controller) that handles the Database functionality for users. So instead of putting that code right in the Controller you would build a "User Model" and import that into the controller. This is the "Right Way" to do it, but of course it adds some complexity up front, but you will find later on the organization of it to far outweigh that in time savings.
Now some examples
404 errors (and missed routes):
http://example.com/foo routes to home::error404() and outputs <h3>Error 404 page foo/index not found</h3>
http://example.com/home/foo routes to home::error404() and outputs <h3>Error 404 page home/foo not found</h3>
Default Home page:
http://example.com/ routes to home::index()
http://example.com/index.php routes to home::index()
http://example.com/home routes to home::index()
http://example.com/home/index routes to home::index()
http://example.com/home/index/hello routes to home::index('hello')
http://example.com/home/index/hello/world routes to home::index('hello','world')
As it stands you have to put the full path in for arguments for home, the above code could be changed to account for this but it has some repercussions for missing pages. Basically any missing page would become an argument of the controller. So if you had http://example.com/user/foo it would call user::index('foo') if that was done. Of course this too can be accounted for but the complexity starts to stack up.
http://example.com/home/otherpage routes to home::otherpage() but issues a warning for missing argument.
http://example.com/home/otherpage/hello routes to home::otherpage('hello')
http://example.com/index.php/home/otherpage/hello routes to home::otherpage('hello')
If you look: home::index($arg=false) defines a default, where this method does not. So this method requires an argument. The rest of this should be pretty self explanatory.
Second Controller user example:
http://example.com/user routes to user::index()
http://example.com/user/index routes to user::index()
http://example.com/user/login routes to user::login()
The rest of this is pretty much the same, I just wanted to show how to organize the controllers by functionally for the site.
I should note that this can handle an unlimited number of arguments following the {method} in the url.
Enjoy!
I have built my own PHP MVC Framework after following some tutorials online. I have this all working using an entry script in .htaccess as follows:
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?rt=$1 [L,QSA]
I have a Router class which basically translates the URL and divides it in to action / controller segments:
$route = (empty($_GET['rt'])) ? '' : $_GET['rt'];
if (empty($route))
{
$route = 'index';
}
else
{
/*** get the parts of the route ***/
$parts = explode('/', $route);
$this->controller = $parts[0];
if(isset( $parts[1]))
{
$this->action = $parts[1];
}
}
What I want to do is take this one step further and actually define URL rewriting rules that work in addition to the automatic routing. So basically I want to be able to do something like this:
$rules = array(
'directory/listing/<id>' => 'listing/index',
'directory/<category>/<location>' => 'directory/view',
);
In the above, the array key is the entered URL - the parts in the angle brackets are dynamic variables (like GET variables). The array value is where the request needs to be routed to (controller/action). So for the above two rules we have the following two actions:
public function actionIndex($id) {
}
public function actionView($category, $location) {
}
So essentially the URL needs to checked against the array keys first, if it matches one of the keys then it needs to use the array value as the controller/action pair.
Anybody have any ideas how to go about this?
I'm not sure of the exact code you would need to implement this in your environment, but I can mention that this is exactly how Magento's URL rewrites function. Their URLs are structured as <controller>/<action>/<arguments>. They even take it one step further and define key/value pairs as URL parameters so ../id/20/meta/25 turns into an array like so:
array(
'id' => 20,
'meta' => 25
)
You could download their source and check it out. It may help you head in the right direction:
I'm trying to learn MVC design pattern for web applications so I decided to write my own PHP MVC framework. Before writing this post I read a lot of tutorials and forums about MVC. Now I pretty well understanding the MVC idea, and how communicate controller-model-view. I have write router and few modules (login, categories, ...) - seems it's working.
Now I'm confused a bit:
If I call localhost/LogIn I get only login form, if I call localhost/categories I get category list. Everything OK, but I want to create index controller and when calling localhost/index I want see login form, categories and a lot more modules.
Should I call controllers (login, categories) from indexController.php?
I need advice how to concatenate needed modules in one page.
No, controllers shouldn't be calling each other's functions. Some frameworks introduce "helpers" to implement what you need.
Controllers can use the same models, and views anyway are going to be different, so you can use your Categories model to provide you categories to display (e.g. $categories->getCategoriesList()) and then using it in category controller view and also in index controller view.
A legitimate method of calling one controller from another is by forming an HTTP request - e.g. receiving an HTML snippet (another controller rendered view) to display in your view via AJAX or using an iframe with a source pointing to your another controller (which is a clumsy solution, mostly for idea illustration).
You need several things:
You need a .htaccess file that will cause all requests to go through your index file, here is a simple one:
RewriteEngine On
RewriteBase /demo
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-l
RewriteRule ^(.+)$ index.php?url=$1 [QSA,L]
In the index.php file you need to set the include path, so you won't have to explicitly include the modules/controllers/views or any other class you choose:
define("APPLICATION_PATH", realpath('.'));
$paths = array(
APPLICATION_PATH.'/controllers',
APPLICATION_PATH.'/models',
APPLICATION_PATH.'/views',
APPLICATION_PATH.'/libs',
APPLICATION_PATH.'/includes',
get_include_path()
);
set_include_path(implode(PATH_SEPARATOR, $paths));
now add the 'magic method' for autoloading classes (called automatically) and initialize your Bootsrap class:
function __autoload($className){
$fileName = str_replace('\\','/', $className);
require_once "$fileName.php";
}
new Bootstrap();
Bootstrap.php:
<?php
class Bootstrap {
public function __construct() {
$url = $_GET['url'];
$params = explode('/', $url);
//if controller exist - call it, else call login controller
if (isset($params[0]) && $params[0]){
$controller = new $params[0]();
}
else{
$controller = new login();
}
//if method exist - call it, else call index method
if (isset($params[1]) && $params[1]){
//if parameter exit - call method with param, else call witout param
if (isset($params[2]) && $params[2]){
$controller->$params[1]($params[2]);
}
else{
$controller->$params[1]();
}
}
else{
$controller->index();
}
}
}
That should give you a basic MVC Framework.
Use your controller (index.php) to centralized code that would be used on every page (request validators, error handles, exception handlers, session stuff).
Create a Router class to get the correct models. Allow the models to get the correct views. I have included some UML diagrams from my other answer (https://stackoverflow.com/questions/42172228/is-this-how-an-mvc-router-class-typically-works) to help out. Remember, try to program to an abstract interface, not to a concrete implementation.
I am setting up a simple routing system for my new custom MVC framework that I am making.
Currently my router class views the URL as such:
www.example.com/controller/controller_action/some/other/params
So, essentially...I've been reserving the first two segments of the URI for controller routing. However, what if I just want to run the following?
www.example.com/controller/some/other/params
...which would attempt to just run the default controller action and send the extra parameters to it?
Here is the simple router I'm using:
\* --- htaccess --- *\
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?rt=$1 [L,QSA]
\* --- index.php --- *\
if (array_key_exists('rt',$_GET)) {
$path = $_GET['rt'];
$uri = explode('/',$this->path);
if(empty($uri[0])) {
$load->ctrl('home');
}
elseif(empty($uri[1])) {
$load->ctrl($uri[0]);
}
else {
$load->ctrl($uri[0],$uri[1]);
}
}
else {
$load->ctrl('index');
}
\* --- loader class --- *\
public function ctrl($ctrl,$action=null) {
$ctrl_name = 'Ctrl_'.ucfirst(strtolower($ctrl));
$ctrl_path = ABS_PATH . 'ctrl/' . strtolower($ctrl) . '.php';
if(file_exists($ctrl_path)) { require_once $ctrl_path;}
$ctrl = new $ctrl_name();
is_null($action) ? $action = "__default" : $action = strtolower($action);
$ctrl->$action();
}
How can I do this?
You could handle this within your controller. Typically, MVC frameworks will call a default method when the requested method isn't available. Simply overwrite this fallback-method to call your desired method and pass the parameter list in as parameters.
For instance, KohanaPHP has the __call($method, $params) method that is called when the requested method doesn't exist. You could handle the logic within this, or its functional equivalent in your MVC framework..
This would let you keep the logic internal to the controller itself rather than having it blasted out between various files.