My desired URL structure for a section of a web application is as follows:
/user/FooBar42/edit/privacy, and I would like this to route to controller: user, function: edit, with FooBar42 and privacy as arguments (in that order). How should I accomplish this with CodeIgniter?
Defining this route in application/config/routes.php should work:
$route['user/(:any)/edit/(:any)'] = "user/edit/$1/$2";
However, be aware that (:any) in the above route would match multiple segments. For example, user/one/two/edit/three would call the edit function in the user controller but only pass one as the fist parameter and two as the second.
Replacing the (:any) with the regex ([a-zA-Z0-9]+) will only allow one only alphanumeric values of length at least 1. This mitigates the issue above, where a / would be permitted allowing multiple segments to be allowed. Now, if user/one/two/edit/three was used, a 404 page would be shown.
$route['user/([a-zA-Z0-9]+)/edit/([a-zA-Z0-9]+)'] = "user/edit/$1/$2";
You can also use the remapping option of the CI controller
http://ellislab.com/codeigniter/user-guide/general/controllers.html#remapping
and doing something like this:
public function _remap($method, $params = array())
{
// check if the method exists
if (method_exists($this, $method))
{
// run the method
return call_user_func_array(array($this, $method), $params);
}
else
{
// method does not exists so you can call nay other method you want
$this->edit($params);
}
}
So I have a page:
http://www.mysite.com/controller/function
The function is defined in the controller as:
function ()
{
//some stuff here
}
However it is possible to resolve the URL:
http://www.mysite.com/controller/function/blablabla
i.e. "blablabla" can be passed to the function and forms an additional URI segment, but still brings up the same page. I have the same issue with a number of controllers / functions - how do I prevent parameters being passed to the function (or appearing as a URI segment)?
I've been working with Codeigniter and PHP for around 6 months very part time, so forgive me if the answer is obvious but my searches haven't been fruitful on this.
My goal is optimised SEO - unsure whether better to redirect the page with the extra URI segment to the correct page or to the 404 page.
You can't prevent that without changing how CI handles URI parsing.
You could force a redirect like so:
function my_happy_function($redirect=null) {
if($redirect) {
redirect('/controller/my_happy_function/');
}
}
That would strip out any variables that are given in the URI, at the cost of a page redirect.
Sounds like you want a generic catch all for pages. You can do this using routes.
For example:
$route['my_happy_function(/:any)*'] = "my_happy_function";
then in your my_happy_function index method you check the URI segments there...
public function index()
{
$something = $this->uri->segment(1);
$something_else = $this->uri->segment(2);
// etc
}
this way all calls to my_happy_function get pushed to the index method...
wait, did I understand your question correctly? If I missed the point let me know and I can update.
I was browsing Symfony's website. I didn't really feel like I need all the functionality the framework offers, but I did like the routing part. It allows you to specify URL pattern like this
/some/path/{info}
Now for URL like www.somewebsite.com/app.php/some/path/ANYTHING it would allow you to send client a response specific for this URL. You could also use string ANYTHING and use it similar as GET parameter.
There is also option to hide app.php part of the URL which leaves us URL like www.somewebsite.com/some/path/ANYTHING.
My question is what's best approach to do this without a complex framework?
I have made my own mini-framework with the same routing syntax. Here's what I do:
Use MOD_REWRITE to store the parameters (like /some/path/{info}) in a $_GET variable I call params:
RewriteRule ^(.+)(\?.+)?$ index.php?params=$1 [L,QSA]
Parse the parameters and store them globally using this function:
public static function parseAndGetParams() {
// get the original query string
$params = !empty($_GET['params']) ? $_GET['params'] : false;
// if there are no params, set to false and return
if(empty($params)) {
return false;
}
// append '/' if none found
if(strrpos($params, '/') === false) $params .= '/';
$params = explode('/', $params);
// take out the empty element at the end
if(empty($params[count($params) - 1])) array_pop($params);
return $params;
}
Route to the proper page dynamically:
// get the base page string, must be done after params are parsed
public static function getCurPage() {
global $params;
// default is home
if(empty($params))
return self::PAGE_HOME;
// see if it is an ajax request
else if($params[0] == self::PAGE_AJAX)
return self::PAGE_AJAX;
// see if it is a multi param page, and if not, return error
else {
// store this, as we are going to use it in the loop condition
$numParams = count($params);
// initialize to full params array
$testParams = $params;
// $i = number of params to include in the current page name being checked, {1, .., n}
for($i = $numParams; $i > 0; $i--) {
// get test page name
$page = strtolower(implode('/', $testParams));
// if the page exists, return it
if(self::pageExists($page))
return $page;
// pop the last param off
array_pop($testParams);
}
// page DNE go to error page
return self::PAGE_ERROR;
}
}
The value here is that it looks for the most specific page to the least specific page. Also, workout outside of a framework gives you complete control so if there's a bug somewhere, you know you can fix it - you don't have to look up some weird workaround in your framework.
So now that $params is global, any page that uses a parameter simply calls $params[X] to work with it. Friendly URLs without a framework.
The way I add pages then is I put them into an array that is looked at in the pageExists($page) call.
For AJAX calls, I put in a special IF:
// if ajax, include it and finish
if($page == PageHelper::PAGE_AJAX) {
PageHelper::includeAjaxPage();
$db->disconnect();
exit;
}
And voila - your own micro routing framework.
I recommend this article http://net.tutsplus.com/tutorials/other/a-deeper-look-at-mod_rewrite-for-apache/ to understand url rewrite using apache mod_rewrite you do not need any framework just php. Also this is what in the depth any framework implements
The problem is that a routing is a complex thing in a framework.
Perhaps you take a look at Silex. Its a micro-framework based on the Symfony2 Components. Its not so big as Symfony2 but have some of the features.
I have some questions concerning the Zend Framework. I am attempting to route all static pages through the default controller using the now default displayAction() method. The intention is to have the displayAction() process the request by looking at the page param, determine if the script page exists, if it does render the view otherwise throw a 404 page not found error. Additionally, a test is done to see if a method with the same name as the param exists, if so, call on that action.
Listed here is the routing configuration from the application.ini
resources.router.routes.static-pages.route = /:page
resources.router.routes.static-pages.defaults.module = default
resources.router.routes.static-pages.defaults.controller = index
resources.router.routes.static-pages.defaults.action = display
Here is the controller actions:
public function someAction() {
// do something
}
public function displayAction() {
// extract page param, e.g. 'some'
$page = $this->getRequest()->getParam('page');
// create zend styled action method, e.g. 'someAction'
$page_action = $page.'Action';
// if a method within this controller exists, call on it
if (method_exists($this, $page_action)) {
$this->$page_action();
}
// if nothing was passed in page param, set to 'home'
if (empty($page)) {
$page = 'home';
}
// if script exists, render, otherwise, throw exception.
if (file_exists($this->view->getScriptPath(null)."/".$this->getRequest()->getControllerName()."/$page.".$this->viewSuffix)) {
$this->render($page);
} else {
throw new Zend_Controller_Action_Exception('Page not found', 404);
}
}
Now, here are my questions: Is there a better way of doing this? I'm relatively new to this framework, so are there best practices which apply? Is there a better way of calling on an action from within a controller? I have done A LOT of looking around through the documentation, however, quite a bit of it seems to contradict itself.
Update 1:
After having a think and a read, I've managed to simplify the solution and include a few things which were mentioned. NOTE: I use PagesController as my default static-content controller.
Listed here is the routing configuration from the application.ini. For calls to the home page i.e. "/", I pass "home" as the action param, for all other requests, the user defined / url link param is sent in action.
resources.router.routes.home.route = "/"
resources.router.routes.home.defaults.module = "default"
resources.router.routes.home.defaults.controller = "pages"
resources.router.routes.home.defaults.action = "home"
resources.router.routes.pages.route = "/:action"
resources.router.routes.pages.defaults.module = "default"
resources.router.routes.pages.defaults.controller = "pages"
Here is the controller actions. If user define parameter exists as an action, it will be called, else it falls to the php magic function __call.
public function someAction()
{
// Do something
}
public function __call($method, $args)
{
// extract action param, e.g. "home"
$page = $title = $this->getRequest()->getParam('action');
// test if script exists
if (file_exists($this->view->getScriptPath(null) . "/"
. $this->getRequest()->getControllerName() . "/$page . " . $this->viewSuffix))
{
// pass title to layout
$this->view->assign(compact('title'));
// render script
$this->render($page);
} else {
throw new Zend_Controller_Action_Exception('Page not found', 404);
}
}
It works. So, here are my questions: Would you consider standardising on using this method to manage static content? If not, why not? How would you improve it? Also, considering this is a GET request, would it be a wise move to use Zend_Filter_input to cleanse input or is that just overkill?
Your approach seems reasonable to me. However, perhaps you should take advantage of the __call method instead, which would allow you to more easily route your actions...
Setup your route like this:
resources.router.routes.static-pages.route = /:action
resources.router.routes.static-pages.defaults.module = default
resources.router.routes.static-pages.defaults.controller = index
And your controller like so:
public function someAction() {
//going to URL /some will now go into this method
}
public function __call($name, $args) {
//all URLs which don't have a matching action method will go to this one
}
I think your on the right track however here are some other ideas.
Break up your routing per sections in your INI:
ie a blog router, a static page router a forum router etc.. (I think you are already doing this)
Use the various router classes to handle routing per section rather than sending it to a controller.
Static:
http://framework.zend.com/manual/en/zend.controller.router.html#zend.controller.router.routes.static
All:
http://framework.zend.com/manual/en/zend.controller.router.html
Some links that may help:
codeutopia.net/blog/2007/11/16/routing-and-complex-urls-in-zend-framework/
www.vayanis.com/2009/03/20/intro-to-zend-framework-routing/
I am new to MVC so this is my first attempt and I am sure you guys can give me improvement on this, thanks for any tips or help!
Below is what I have come up with for a router/dispatcher system for my personal framework I am working on, it is my first attempt at using the MVC pattern.
The first block of code is just my .htaccess file which routes all request through my index.php file.
The second block of code is my array of "Routes" which will tell the Router object, which class and method to call as well as any ID or paging numbers if they exists.
Third block of code is the router class.
Fourth block is just running the class
So the router class has to use regex to match the URI with a route in the route map, in theory, this just sounds like bad performance when there is a list of 50+ routes that the regex has to run on, should I be doing this differently? The main reason I use the regex is to match page numbers and ID numbers when they exists in the route.
Also please do not just tell me to use a framework, I am doing this to learn it better, I learn better this way and just prefer to not use an existing framework at this time, I have studies all the main ones and some less common ones for ideas already.
1) So the main question, does anything just not look right?
2) Is there a better way to detect what is in the URI than using the regex on an array like I am doing, consider it on a high traffic site?
3) Since everything is routed through the index.php file with this, how would I go about handling AJAX requests?
Sorry if this is confusing, I am a little confused mtyself!
.htaccess file
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?uri=$1 [NC,L,QSA]
Map 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
);
Router class that reads and matches 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();
?>
1) Look alright to me. The code looks a bit messy though.
2) Yes there is a better way. You're doing the regex because you want to match parts of the URL that you don't know. Why not do $parts = explode("/", $uri) then see if you can find the page you're looking for? You will need to define how many parameters you're expecting for each page or you wont know whether to pick forums with parameters array("viewform", 123) or forums/viewforum with parameters array(123).
explode feels loads better than a regex. It also adds the benefit of improved error handling. What if the argument passed to viewforum is not a number? Surely you can do better than "404" ;)
3) Make a seperate ajax handler. Ajax is hidden from view anyway so you don't need to bother with 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);
(untested)
Plugins
Because of $uri_route_map you can't 'dynamically' register more plugins or pages or 'routes'. I'd add a function to add more routes dynamically to the Router.
Additionally you could consider some auto-discovery scheme that, for instance, will check the folder plugins/ for folders with a file called "manifest.php" that, when called, will optionally add more routes to Router.
1),2) I dont think that is good idea to put id_number and page_number in Router, because in future you can encounter many other parameters for url. Better just use controller and method and define in controller what to do with other parameters or create other class Request which deals with request info.
3) For ajax use url like ajax/module/action. And create ajax controller which do basic ajax security stuff, like checking against XSRF and then decides which controllers to run and action to call.
1) & 2) I will not say ,it's not right but why not using default routes ? Most of the time a route like
controller/action/param1/param2
is good enought for a majority of your page.
You probably could do something like that 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 whatever you want. In your routeryou just have to check if the current URI match a specific routes or the default routes.
By doing that you will decrease the number of routes to match and increase the speed of your router.
3) Your router is probably instancied by your index, without index no root, so unfortunately you probably can't avoid using it.
That why it's very important to avoid expensive action in your index. Typically don't init your database connection in index if all your pages don't need it.
Also please do not just tell me to use a framework
Don't forget to download some famous framework and look their code. It's the better way to learn. By doing that you will probably find a lot of good practices and answers.
1) So the main question, does anything just not look right?
Personally, I see this becoming more complicated as your site grows. An MVC framework, as I was taught, should pretty much be "Set it and forget it" – you're separating the request handler (controller) from the database querying and business end (model) and from the display elements (view).
[NB: You may need other core aspects. My standard framework includes some core elements that carry the session through the various parts, as well as handle fundamental aspects of the site work – For instance, while the models are responsible for making the right database calls as directed by the controller, there are core functions in a sql.class.php file that give me a standardized set of methods for making those calls and delivering or caching the results as needed.]
Your dispatch method is on the right track with this – you're extracting from the URI the name of the controller (Forums, Profiles, etc.). Do you need a uri map? I feel you're creating an unnecessary situation in which you have to update this map each time, rather than simply creating a new controller when you need new functionality, and registering it with the database. I'm not saying you're wrong per se, I just don't feel I'd have done it that way.
2) Is there a better way to detect what is in the URI than using the regex on an array like I am doing, consider it on a high traffic site?
Control the outcome (no pun intended, since it's the controller that 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 "/" into bits. bit[0] is the controller ID – this says "I want to use the controller named bit[0]=>value". This is done as:
require_once( dirname( __FILE__ )."/controllers/".$bit[0]."controller.php" );
Personally, being a bit of a neat freak when it comes to directory structures, I use bit[0] to identify the directory in which controller.php is located, as I might have sub controllers.
It's this controller file that I use to parse other bits. For this, I'll use an example:
Assume that bit[0] carried the value "forums". I might pass, if it's set, bit[1] to a switch statement. By default, I always want to list, but I might specifically direct it to "list", "view", or "post" in bit[1]. This will tell me in the controller class which method to call. The method will then tell me to call the associated "forums" model if I need to perform queries and cache the forum listing, for instance.
The extraneous "bits" may do one of two things: they may be passed as simple arguments to the method as to what data to request from the model, or bit[1] may be complex enough to warrant a sub controller, and the subsequent bits will be passed to that controller to determine the appropriate action, as was done with the forums controller.
Regex, being slow, should be avoided when possible. Since we may have a URI of /forums/view/102305 we can assume that the forums controller will be passing 102305 to the method associated with the view argument (the method being something like private function displayPost( $id ) where $id is 102305). No regex is needed since we can simply explode the values along a common anticipated delimiter.
3) Since everything is routed through the index.php file with this, how would I go about handling AJAX requests?
Not terribly difficult. If the controller is set to, say, AJAX, you could rebuild the URL and direct access it. You could write exclusions in the .htaccess file (RewriteRule ^(AJAX)($|/) - [L]). Or (not ideal, but a sneaky workaround) is to append ../ to your AJAX URI to push the URI back to root – it's no longer attempting to access index.php so the rewrite rule doesn't apply.
Edit
Let's assume that we're using a URI of /forums/id-1234/page-4 per your example. Again, let's assume as I mentioned above that forums refers to the controller to be used, and every other / delimits arguments (what I like to call "drill downs"). So, in our forum controller file (let's call it 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's 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 point is that you're in control of what is being passed and how it gets handled. Control the URIs and you can simplify their processing.
Edit 2
Reading through again, there's a few concepts that I think will help you and that you should familiarize yourself with:
View the "Factory" pattern here (My $registry is, at it's heart, a set of factories):
http://php.net/manual/en/language.oop5.patterns.php
A 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/
One other note, and this is a personal observation after having worked with Joomla, Drupal, Wordpress, and various different enterprise CMS and BBS solutions – Design solely with you in mind. When you start trying to become "something for everybody", you end up with a lot of unnecessary bloat that's getting loaded with each page and used 1 time out of 100. MVC is a design pattern, and using it as a pattern will tell you to get rid of excess in every aspect, including URIs. Processing /controller/arg1-Identifier/arg2-offset is unnecessary, and you can easily get away with /controller/id/offset (e.g. /forums/1234/4). If you want to make it SEO friendly, add the thread title, not a tag identifying the ID (e.g. /forums/1234-This-Is-A-Topic/4).
Now, let's also point out the obvious about my edit above – This is a controller intended solely for the forum element. Each element of your site (i.e. forums, galleries, profiles, etc.) should have it's own controller. Why? Because each is doing completely different things on its pages. So make use of that – you don't need to use a URI map so long as you understand that you're directing to the controller, and the controller is delegating responsibilities to the model and any sub controllers that might be needed.
I really hope that helps.
1) Is it works? If yes, then yes. Since above code only contain array, regex and validation for that, i dont think theres a problem(s) with your code. As long as it works. But if you ask, 'is that code is scalable?' then the answer would be various, and its all depend at your MVC Framework purposes (for example, is that framework for general uses, eg : blog, or its particullary for REST API provider. And so on...)
2) Yes. Kohana, Zend, CI and other popular (and highly optimized) PHP framework use that(array+regex on router).
3) I think you could just give it a flag in route block/section, and make that flag available as a global variable. So that, in your controller, you can decide which response to send for different request type (ajax/non-ajax) by checking that flag (for example you can provide $this->is_ajax as global method which available in Controller scope).
If I may add couple of points:
Remove id_number and page_number from the router - just pass everything that was matched to a controller, after all, it's a controller job to process that data, not router's
Don't pass $uri to a constructor, pass it to a dispatch() instead.
Why those isset()-s in $uri_route_map? Obviously they would be false, since $uri_route_map is defined before Router() object is instantiated.
Would recommend adding more logic to matching routine - in your current case sitename/forums will not match anything resulting in 404 (no trailing slash)
You can also define default parameters in your $uri_route_map, and then array_merge them with parameters matched. So, for example, when no page number is specified page_number will be equal to 1
If you are worried about perfomance on high traffic website, 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's the problem with that?