Codeigniter Dynamic Routing - php

Hi i wont to make something like that.
http:// example.com/ - Main Controller
http:// example.com/rules/ - Main Controller where content get from database, but if not exist
return 404 page. (It's ok, isn't problem.)
But if i have subfolder in application/controlles/rules/
I want to redirect it to Main Contorller at Rules folder.
This follow code can solve problem, but i don't know how it right realise.
At routes.php:
$route['default_controller'] = "main";
$route['404_override'] = '';
$dirtest = $route['(:any)'];
if (is_dir(APPPATH.'controllers/'.$dirtest)) {
$route['(:any)'] = $dirtest.'/$1';
} else {
$route['(:any)'] = 'main/index/$1';
}
Ok, what I have:
controllers/main.php
class Main extends CI_Controller {
public function __construct()
{
parent::__construct();
$this->load->model('main_model');
}
public function index($method = null)
{
if (is_dir(APPPATH.'controllers/'.$method)) {
// Need re-rout to the application/controllers/$method/
} else {
if ($query = $this->main_model->get_content($method)) {
$data['content'] = $query[0]->text;
// it shows at views/main.php
} else {
show_404($method);
}
}
$data['main_content'] = 'main';
$this->load->view('includes/template', $data);
}
}
Updated Again (routes.php):
So, seem's like what i search (work example):
$route['default_controller'] = "main";
$route['404_override'] = '';
$subfolders = glob(APPPATH.'controllers/*', GLOB_ONLYDIR);
foreach ($subfolders as $folder) {
$folder = preg_replace('/application\/controllers\//', '', $folder);
$route[$folder] = $folder.'/main/index/';
$route[$folder.'/(:any)'] = $folder.'/main/$1';
}
$route['(:any)'] = 'main/index/$1';
But, in perfectly need some like this:
http:// example.com/1/2/3/4/5/6/...
Folder "controllers" has subfolder "1"?
YES: Folder "1" has subfolder "2"?
NO: Folder "1" has controller "2.php"?
NO: Controller "controllers/1/main.php" has function "2"?
YES: redirect to http:// example.com/1/2/ - where 3,4,5 - parameters..
It is realy nice, when you have structure like:
http://example.com/blog/ - recent blog's posts
http://example.com/blog/2007/ - recent from 2007 year blog's posts
http://example.com/blog/2007/06/ - same with month number
http://example.com/blog/2007/06/29/ - same with day number
http://example.com/blog/web-design/ - recent blog's post's about web design
http://example.com/blog/web-design/2007/ - blog' posts about web design from 2007 years.
http://example.com/blog/current-post-title/ - current post
Same interesting i find http://codeigniter.com/forums/viewthread/97024/#490613

I didn't thoroughly read your question, but this immediately caught my attention:
if (is_dir($path . '/' . $folder)) {
echo "$route['$folder/(:any)'] = '$folder/index/$1';"; //<---- why echo ???
}
Honestly I'm not sure why this didn't cause serious issues for you in addition to not working.
You don't want to echo the route here, that will just try to print the string to screen, it's not even interpreted as PHP this way. There are also some issues with quotes that need to be remedied so the variables can be read as variables, not strings. Try this instead:
if (is_dir($path . '/' . $folder)) {
$route[$folder.'/(:any)'] = $folder.'/index/$1';
}
Aside: I'd like to offer some additional resources that are not directly related to your problem, but should help you nonetheless with a solution:
Preferred way to remap calls to controllers: http://codeigniter.com/user_guide/general/controllers.html#remapping
Easier way to scan directories: http://php.net/manual/en/function.glob.php

It's hard to say why the registering of your routes fails. From your code I can see that you're not registering the routes (you just echo them), additionally I see that the usage of variables in strings are used inconsistently. Probably you mixed this a bit, the codeigniter documentation about routes is not precise on it either (in some minor points, their code examples are not really syntactically correct, but overall it's good).
I suggest you first move the logic to register your dynamic routes into a function of it's own. This will keep things a bit more modular and you can more easily change things and you don't pollute the global namespace with variables.
Based on this, I've refactored your code a bit. It does not mean that this works (not tested), however it might make things more clear when you read it:
function register_dynamic_routes($path, array &$route)
{
$nodes = scandir($path);
if (false === $nodes)
{
throw new InvalidArgumentException(sprintf('Path parameter invalid. scandir("$path") failed.', $path));
}
foreach ($nodes as $node)
{
if ($node === '.' or $node === '..')
continue
;
if (!is_dir("{$path}/{$node}")
continue
;
$routeDef = "{$folder}/(:any)";
$routeResolve = "{$folder}/index/\$1";
$route[$routeDef] = $routeResolve;
# FIXME debug output
echo "\$route['{$routeDef}'] = '{$routeResolve}';";
}
}
$path = APPPATH.'controllers/';
register_dynamic_routes($path, $route);
$route['(:any)'] = 'main/index/$1';
Next to this you probably might not want to shift everything onto the index action, but a dynamic action instead. Furthermore, you might want to have a base controller that is delegating everything into the sub-controllers instead of adding the routes per controller. But that's up to you. The example above is based on the directory approach you outlined in your question.
Edit: Additional information is available in the Controllers section next to the URI Routing section

All this seems kind of complicated.
Plus, if you have hundreds (or thousands or more?) of possible routes in a database you may not want to load all of them into the "$routes" array every time any page loads in your application.
Instead, why not just do this?
last line of routes.php:
$route['404_override'] = 'vanity';
Controller file: Vanity.php:
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Vanity extends MY_Controller {
/**
* Vanity Page controller.
*
*/
public function __construct() {
parent::__construct();
}
public function index()
{
$url = $_SERVER['PHP_SELF'];
$url = str_replace("/index.php/", "", $url);
// you could check here if $url is valid. If not, then load 404 via:
//
// show_404();
//
// or, if it is valid then load the appropriate view, redirect, or
// whatever else it is you needed to do!
echo "Hello from page " . $url;
exit;
}
}
?>

Related

Codeigniter URL Routing - Capture Everything for Tree Structure

OK, I think I am probably (hopefully) going to be told I am going about this in the wrong way.
Currently, if I go to root of CI web I call a function which reads a predefined location with map_directory(). I then iterate it out in a view as a simple directory listing.
I then want to click on one of these directories to see what's inside. When I do that I call a different controller function called browse.
So if I click on one link I go to
www.mysite.com/dir1
(which is Routed to www.mysite.com/controller/browse/$1 - where $1 in this instance = dir1).
Now I am presented with a dir listing of dir1. I have configured the links of the displayed listings to now go to:
www.mysite.com/dir1/subdir1 etc.
What I want to do and this might be the bit where I am cheating/going wrong, is capture everything after
www.mysite.com/
and pass it to
www.mysite.com/controller/browser/$1
So example:
www.mysite.com/dir1/subdir1/ => www.mysite.com/controller/browse/"dir1/subdir1"
I know I can't have '"' in there, but that's the bit I am trying to pass to the map_directory() function...so it goes /.$var (where $var would = $1 = "dir1/subdir1".
So far I have tried in CI Routes.php:
$route['(.+)$'] = 'controller/browse/$1';
$route['([a-zA-Z0-9\/]*)'] = 'controller/browse/$1';
$route['(:any)'] = 'controller/browse/$1';
...but they all only ever seem to capture "dir1" never anything beyond that.
I hope that makes sense to someone....
You can just use the special _remap() method in your controller to override the default routing behavior:
class Controller extends CI_Controller
{
protected function browse($path)
{
// ... do something with $path here
}
public function _remap($method, $params = array())
{
if ($method === 'browse')
{
$params = implode('/', $params);
return $this->browse($params);
}
elseif (method_exists($this, $method))
{
return call_user_func_array(array($this, $method), $params);
}
show_404();
}
}

Manage URL routes in own php framework

I'm creating a PHP Framework and I have some doubts...
The framework takes the url in this way:
http:/web.com/site/index
It takes the first parameter to load controller (site) and then loads the specific action (index).
If you've installed the framework in a base URL works ok, but if you install it in a subfolder like this:
http://web.com/mysubfolder/controller/action
My script parses it as controller = mysubfolder and action = controller.
If you have more subfolders the results will be worst.
This is my Route code:
Class Route
{
private $_htaccess = TRUE;
private $_suffix = ".jsp";
public function params()
{
$url='';
//nombre del directorio actual del script ejecutandose.
//basename(dirname($_SERVER['SCRIPT_FILENAME']));
if($this->_htaccess !== FALSE):
//no está funcionando bien si está en un subdirectorio web, por ej stynat.dyndns.org/subdir/
// muestra el "subdir" como primer parámetro
$url = $_SERVER['REQUEST_URI'];
if(isset($_SERVER['QUERY_STRING']) && !empty($_SERVER['QUERY_STRING'])):
$url = str_replace("?" . $_SERVER['QUERY_STRING'], '',$url);
endif;
else:
if(isset($_SERVER['PATH_INFO'])):
$url = $_SERVER['PATH_INFO'];
endif;
endif;
$url = explode('/',preg_replace('/^(\/)/','',$url));
var_dump($url);
var_dump($_GET);
}
}
Thanks for any help you can give.
You are missing a base path. The routing script must now where to start when detecting a pattern or route detected.
Pseudo code:
//set the base URI
$base_uri = '/base';
//remove the base URI from the original uri. You can also REGEX or string match against it if you want
$route_uri = str_replace($base_uri,'',$uri);
//perform route matching $route_uri, in your code a simple explode
$url = explode('/',preg_replace('/^(\/)/','',$route_uri));
You can use this with or without RewriteBase for your .htaccess so long as they use the same harness - index.php.
Additionally, you can improve your route match procedure using Regular Expressions function like preg_match and preg_match_all. They let you define a pattern to match against and results to an array of matching strings - see http://php.net/manual/en/function.preg-match.php.
Even if you are creating your own framework, there is no reason not to reuse robust, well tested and documented components, like this Routing component.
Just use Composer, which has become the standard for dependency management in PHP, and you'll be fine. Add as many components as you want to your stack.
And here you have a must read guide on how to make your own framework.
Yes, I think I know how to fix that.
(Disclaimer: I know that you know most of this but I am going to explain everything for others who may not know some of the gotchas)
Using PATH_INFO and .htaccess
There is a trick in php where if you go to a path like:
http://web.com/mysubfolder/index.php/controller/action
you will get "/controller/action" in the $_SERVER['PATH_INFO'] variable
Now what you need to do is take a .htaccess file (or equivalent) and make it tell your php script the current folder depth.
To do this, put the .htaccess file into the "mysubfolder"
mysubfolder
.htaccess
index.php
.htaccess should contain:
RewriteEngine on
# if a directory or a file exists, use it directly
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# otherwise forward it to index.php
RewriteRule (.*) index.php/$1
(I used the yii framework manual as reference, I also recommend using the html5 boilerplate)
Basically you set it up to redirect everything to index.php at a specific point in the url.
Now if you visit: http://web.com/mysubfolder/index.php/controller/action
Now you can get the right path "/controller/action" from $_SERVER['PATH_INFO']
Except it's not going to have any value if you visit http://web.com/mysubfolder/ because the .htaccess file will ignore the rewrite because the http://web.com/mysubfolder/ path requests the mysubfolder/index.php which actually exists and gets denied thank yo RewriteCond %{REQUEST_FILENAME} !-f.
ifsetor function
For this you can use this super handy function called ifsetor (I don't remember where I got it)
function ifsetor(&$variable, $default = null) {
return isset($variable)? $variable: $default;
}
What it does is take a reference to a variable that might or might not exist and provide a default if it does not exist without throwing any notices or errors
Now you can use it to take the PATH_INFO variable safely like so
In your index.php
function ifsetor(&$variable, $default = null) {
return isset($variable)? $variable: $default;
}
$path = ifsetor($_SERVER['PATH_INFO'],'/');
var_dump($path);
php 5.4 also has this new shorter ternary format which you can use if you don't care about notices (I do)
$path = $_SERVER['PATH_INFO']?:'/';
Handling the QUERY_STRING
Now tecnically you are not getting a URL, it is merely its path part and will not contain the query_string, for example when visiting
http://web.com/mysubfolder/index.php/test?param=val
you will only get '/test' in the $path variable, to get the query string use the $_SERVER['QUERY_STRING'] variable
index.php
function ifsetor(&$variable, $default = null) {
return isset($variable)? $variable: $default;
}
$path = ifsetor($_SERVER['PATH_INFO'],'/');
$fullpath = ($_SERVER['QUERY_STRING'])? $path.'?'.$_SERVER['QUERY_STRING']:$path;
var_dump($fullpath);
But that might depend on your needs
$_SERVER['QUERY_STRING'] vs $_GET
Also keep in mind that the $_SERVER['QUERY_STRING'] variable is different from the $_GET and $_REQUEST variables because it keeps duplicate parameters from the query string, for example:
Visiting this page
http://web.com/mysubfolder/controller/action?foo=1&foo=2&foo=3
if going to give you a $_SERVER['QUERY_STRING'] that looks like
?foo=1&foo=2&foo=3
While the $_GET variable is going to be an array like this:
array(
'foo'=>'3'
);
If you don't have .htaccess
You can try using the SCRIPT_NAME to your advantage
list($url) = explode('?',$_SERVER['REQUEST_URI']);
list($basepath) = explode('index.php',$_SERVER['SCRIPT_NAME']);
$url = substr($url,strlen($basepath));
If you like to blow up stuff like me :)
Your case
Class Route
{
private $_htaccess = TRUE;
private $_suffix = ".jsp";
public function params()
{
$url='';
//nombre del directorio actual del script ejecutandose.
//basename(dirname($_SERVER['SCRIPT_FILENAME']));
if($this->_htaccess !== FALSE):
//no está funcionando bien si está en un subdirectorio web, por ej stynat.dyndns.org/subdir/
// muestra el "subdir" como primer parámetro
list($url) = explode('?',$_SERVER['REQUEST_URI']);
$basepath = dirname($_SERVER['SCRIPT_NAME']);
$basepath = ($basepath==='/')? $basepath: $basepath.'/';
$url = substr($url,strlen($basepath));
else:
if(isset($_SERVER['PATH_INFO'])):
$url = $_SERVER['PATH_INFO'];
$url = preg_replace('|^/|','',$url);
endif;
endif;
$url = explode('/',$url);
var_dump($url);
var_dump($_GET);
}
}
I hope this helps
P.S. Sorry for the late reply :(
At some point you will have to check the $_SERVER ['HTTP_HOST'] and a config var defined by the programmer/user wich indicates the subfolder(s) where the app is located, and remove the portion you are not interested in from the rest of the URL.
You can check this forum post on the codeigniter formus for some hints.
CodeIgniter uses another different way to route the controller/method internally.
You do the routing by the $_SERVER['PATH_INFO'] value. You use the urls like this: myapp.com/index.php/controller/method .
To avoid showing index.php on the uri you must rely on an Apache rewrite rule, but even with that I think that the CI one is a nice solution, once you have your index file location, you can avoid all the hassle of parsing the URL.
If I am understanding what you are after correctly, then one solution may be to carry on doing what you are doing, but also get the path of the main routing script (using realpath() for example).
If the last folder (or folder before that etc) matches the beginning URL item (or another section), you ignore it and go for the next one.
Just my 2 cents :-).
Within the application configuration script place a variable which will be set to the path the application runs at.
An alternative is to dynamically set that path.
Before the part
$url = explode('/',preg_replace('/^(\/)/','',$url));
strip the location (sub-folder) path out of the $url string using the predefined application path.
This is how i implemented loader.php
<?php
/*#author arun ak
auto load controller class and function from url*/
class loader
{
private $request;
private $className;
private $funcName;
function __construct($folder = array())
{
$parse_res = parse_url($this->createUrl());
if(!empty($folder) && trim($folder['path'],DIRECTORY_SEPARATOR)!='')
{
$temp_path = explode(DIRECTORY_SEPARATOR,trim($parse_res['path'],DIRECTORY_SEPARATOR));
$folder_path = explode(DIRECTORY_SEPARATOR,trim($folder['path'],DIRECTORY_SEPARATOR));
$temp_path = array_diff($temp_path,$folder_path);
if(empty($temp_path))
{
$temp_path = '';
}
}else
{
if(trim($parse_res['path'],DIRECTORY_SEPARATOR)!='')
{
$temp_path = explode(DIRECTORY_SEPARATOR,trim($parse_res['path'],DIRECTORY_SEPARATOR));
}
else
$temp_path ='';
}
if(is_array($temp_path))
{
if(count($temp_path) ==1)
{
array_push($temp_path,'index');
}
foreach($temp_path as $pathname)
{
$this->request .= $pathname.':';
}
}
else $this->request = 'index'.':'.'index';
}
private function createUrl()
{
$pageURL = (#$_SERVER["HTTPS"] == "on") ? "https://" : "http://";
$pageURL .= $_SERVER["SERVER_NAME"].$_SERVER["REQUEST_URI"];
return $pageURL;
}
public function autolLoad()
{
if($this->request)
{
$parsedPath = explode(':',rtrim($this->request,':'));
if(is_file(APPLICATION_PATH.DIRECTORY_SEPARATOR.'controllers'.DIRECTORY_SEPARATOR.$parsedPath[0].'_controller'.'.php'))
{
include_once(APPLICATION_PATH.DIRECTORY_SEPARATOR.'controllers'.DIRECTORY_SEPARATOR.$parsedPath[0].'_controller'.'.php');
if(class_exists($parsedPath[0].'_controller'))
{
$class = $parsedPath[0].'_controller';
$obj = new $class();
//$config = new config('localhost','Winkstore','nCdyQyEdqDbBFpay','mawinkcms');
//$connect = connectdb::getinstance();
//$connect->setConfig($config);
//$connection_obj = $connect->connect();
//$db = $connect->getconnection();//mysql link object
//$obj->setDb($db);
$method = $parsedPath[1];
if(method_exists ($obj ,$parsedPath[1] ))
{
$obj->$method();
}else die('class method '.$method.' not defined');
}else die('class '.$parsedPath[0]. ' has not been defined' );
} else die('controller not found plz define one b4 u proceed'.APPLICATION_PATH.DIRECTORY_SEPARATOR.'controllers'.DIRECTORY_SEPARATOR.$parsedPath[0].'.php');
}else{ die('oops request not set,i know tis is not the correct way to error :)'); }
}
}
Now in my index file
//include_once('config.php');
include_once('connectdb.php');
require_once('../../../includes/db_connect.php');
include_once('view.php');
include_once('abstractController.php');
include_once('controller.php');
include_once('loader.php');
$loader = new loader(array('path'=>DIRECTORY_SEPARATOR.'magsonwink'.DIRECTORY_SEPARATOR.'modules'.DIRECTORY_SEPARATOR.'admin'.DIRECTORY_SEPARATOR.'atom'.DIRECTORY_SEPARATOR));
$loader->autolLoad();
I haven't used the concept of modules.only controller action and view are rendered.
Are you sure you have your htaccess correctly?
I guess if you're placing your framework on subfolder, then you have to change your RewriteBase in htaccess file from / to /subfolder/. it would be something like this:
# on root
RewriteBase /
#on subfolder
RewriteBase /subfolder/
that's only thing I could wonder of that in your case ...
I don't use OOP, but could show you some snippets of how I do things to dynamically detect if I'm in a subdirectory. Too keep it short and to the point I'll only describe parts of it instead of posting all the code.
So I start out with a .htaccess that send every request to redirect.php in which I splice up the $_SERVER['REQUEST_URI'] variable. I use regex to decide what parts should be keys and what should be values (I do this by assigning anything beginning with 0-9 as a value, and anything beginning with a-z as key) and build the array $GET[].
I then check the path to redirect.php and compare that to my $GET array, to decide where the actual URL begins - or in your case, which key is the controller. Would look something like this for you:
$controller = keyname($GET, count(explode('/', dirname($_SERVER['SCRIPT_NAME']))));
And that's it, I have the first part of the URL. The keyname() function looks like this:
/*************************************
* get key name from array position
*************************************/
function keyname ($arr, $pos)
{
$pos--;
if ( ($pos < 0) || ( $pos >= count($arr) ) )
return ""; // set this any way you like
reset($arr);
for($i = 0;$i < $pos; $i++) next($arr);
return key($arr);
}
To get the links pointing right I use a function called fixpath() like this:
print 'link';
And this is how that function looks:
/*************************************
* relative to absolute path
*************************************/
function fixpath ($path)
{
$root = dirname($_SERVER['SCRIPT_NAME']);
if ($root == "\\" || $root == ".") {
$root = "";
}
$newpath = explode('/', $path);
$newpath[0] .= $root;
return implode('/', $newpath);
}
I hope that helps and can give you some inspiration.
Forget about "reinventing the wheel is wrong" claims. They don't have to use our wheels. I walked on the same road a while ago and i'm totally grateful what i get... i hope you will too
When it comes to the answer to your specific problem -which if faced too- there is a very easy solution. it's a new line in .htaccess at root folder...
Just add line below to your root .htaccess file ; (if your subfoler is "subfolder" )
RewriteRule subfolder/ - [L]
This will leave apart this folder from rewriting directives
By using this way you can install as many instances of your framework as you wish. But if you want this to be framework driven then you have to create/change .htaccess within your framework.
Create /myBaseDirectory/public directory and put your files there - like index.php.
This works because Apache sees this directory like base directory.
basically grab the url string after your first slash, and then explode it into an array (i use '/' as a delimiter).
then carefully array_shift off your elements and store them as variables
item 0: controller
item 1: the action / method in that controller
item 2 thru n: the remaining array is your params

Codeigniter URL remapping strategy

I'm working on a project built in codeigniter that makes heavy use of routes and the remap function to rewrite urls. The current implementation is confusing and messy.
Essentially this is what the designer was trying to accomplish:
www.example.com/controller/method/arg1/
TO
www.example.com/arg1/controller/method/
Can anyone suggest a clean way of accomplishing this?
This actually only needs to happen for one specific controller. It's fine if all other controllers need to simply follow the normal /controller/model/arg1... pattern
Just to give you an idea of how the current code looks here is the 'routes' file: (not really looking into any insight into this code, just want to give you an idea of how cluttered this current setup is that I'm dealing with. I want to just throw this away and replace it with something better)
// we need to specify admin controller and functions so they are not treated as a contest
$route['admin/users'] = 'admin/users';
$route['admin/users/(:any)'] = 'admin/users/$1';
$route['admin'] = 'admin/index/';
$route['admin/(:any)'] = 'admin/$1';
// same goes for sessions and any other controllers
$route['session'] = 'session/index/';
$route['session/(:any)'] = 'session/$1';
// forward http://localhost/ball/contests to controller contests method index
$route['(:any)/contests'] = 'contests/index/$1';
// forward http://localhost/ball/contests/vote (example) to controller contests method $2 (variable)
$route['(:any)/contests/(:any)'] = 'contests/index/$1/$2';
// forward http://localhost/ball/contests/users/login (example) to controller users method $2 (variable)
$route['(:any)/users/(:any)'] = 'users/index/$1/$2';
// if in doubt forward to contests to see if its a contest
// this controller will 404 any invalid requests
$route['(:any)'] = 'contests/index/$1';
$route['testing/'] = 'testing/';
And the remap function that goes with it:
public function _remap($method, $params = array()){
// example $params = array('ball', 'vote')
// params[0] = 'ball', params[1] = 'vote'
/*
* Write a detailed explanation for why this method is used and that it's attempting to accomplish.
* Currently there is no documentation detailing what you're trying to accomplish with the url here.
* Explain how this moves the contest name url segment infront of the controller url segment. Also
* explain how this works with the routing class.
* */
$count = count($params);
if($count == 0){ // no contest specified
redirect('http://messageamp.com');
return;
}
$contest_name = $params[0];
unset($params[0]); //remove the contest name from params array because we are feeding this to codeigniter
if($count < 2) // no method specified
$method = 'index';
else{
$method = $params[1];
unset($params[1]);
}
//We need to scrap this, lazy-loading is a best-practice we should be following
$this->init(); //load models
//make sure contest is valid or 404 it
if(!$this->central->_check_contest($contest_name)){
show_404();
return;
}
$this->data['controller'] = 'contests';
$this->data['method'] = $method;
$this->data['params'] = $params;
// call the function if exists
if(method_exists($this, $method)){
return call_user_func_array(array($this, $method), $params);
}
show_404(); // this will only be reached if method doesn't exist
}
To get something like this:
www.example.com/controller/method/arg1/ TO www.example.com/arg1/controller/method/
You could do this in your routes.php config:
$route['(:any)/(:any)/(:any)'] = "$2/$3/$1";
However, if you want to have all of your other classes stick to the default routing, you would need to create routes for each of them to overwrite this default route:
$route['controller_name/(:any)'] = "controller_name/$1";

Current URI Segment in CodeIgniter

What would be the best way to check for the current URI segment in a CodeIgniter view? What I am trying to do is use the current URI segment [i.e. $this->uri->segment(1)] in order to highlight the current page on the navigation bar.
The best that I have figured out is to do
$data['current_uri'] = $this->uri->segment(1);
$this->load->view('header', $data);
in each of my controllers and then in the header.php file, I check the $current_uri variable to determine which part of the navigation should be highlighted. As you know, this is a very tedious way of doing it, but I'm at a loss of a better way to do this.
It may even be possible to extend the default Controller class to pass the current URI segment, but I'm not sure if this would work, or even how to go about doing it.
I myself use an extra function similar to anchor(). I call it active_anchor(), and it takes all the same parameters plus another (the uri). The function then adds the class 'active' if the uri string passed matches the active_anchor() url paramter.
Then the function returns using the anchor function (all that the function did was determine if the link needed the class 'active' or not.
EDIT:
I just put this code in a file called 'MY_url_helper.php'. That way, when the url helper is loaded (I actually auto load that one since pretty much all of my views use it anyway.) This is just some quick code too, pretty sure it would work. Basically it takes the same arguments as the anchor() function, but also the $key variable. It appends a class of "active" to the anchor tag if the key and url match.
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
if ( ! function_exists('active_anchor'))
{
function active_anchor($url = NULL, $title = NULL, $key = NULL, $params = array())
{
if ($url && $key)
{
if($key == $url)
{
if (array_key_exists ('class' , $params))
{
$params['class'] .= ' active';
}
else
{
$params['class'] = 'active';
}
}
}
return anchor($url, $title, $params);
}
}
Simple way to check the uri segment in view,
Add some code if matches found.
<li class="<?php echo (strcmp($this->uri->segment(2),'test')==0)?'active':''; ?>"><li>
<li class="<?php echo (strcmp($this->uri->segment(2),'test1')==0)?'active':''; ?>"><li>
<li class="<?php echo (strcmp($this->uri->segment(2),'test2')==0)?'active':''; ?>"><li>
I also had the same problem when I was building a customer website in Cakephp, passing those strings for every menu item from controller to view, then checking again in view for implementing the highlighting to tedious to say the least.
For some of my projects now, I have been implementing the same by storing the page information for each of the navigation menu pages in database, things like page name, url, title, position in navigation menu etc.
Then at the start of controller, I store all this data in an array say $pageinfo.
I handle the navigation functionality via a single controller that checks the URI segment and loads the content based on that.
The highlighting part is left to an if statement when generating the view, where I compare each page name to the information I dumped in $pageinfo.
Something like this...
foreach ($navi_menu as $links) {
if ( $links['title'] == $pageinfo['title'] ) {
// Highlight here
}
else {
// No highlight
}
}
This way I don't need to pass the string constants (uri segments in your case) to the view. This CMS-kinda-approach allows me to flexible in adding further items to my menu, without adding more code.
I remember getting this from a codeigniter wiki, can't find the link to it right now.
this simple way and running well for me..
<li class="<?=($this->uri->segment(2)==='test')?'current-menu-item':''?>"><?php echo anchor ('home/index','HOME'); ?></li>
<li class="<?=($this->uri->segment(2)==='blog')?'current-menu-item':''?>"><?php echo anchor ('home/blog','BLOG'); ?></li>
<li class="<?=($this->uri->segment(2)==='bla..bla')?'current-menu-item':''?>"><?php echo anchor ('home/blog','bla..bla'); ?></li>
uri_segment(2) that mean function in ur controller.
but have a weakness, i have trouble if i put view in index controller, so im not use function index ( toruble in uri segment, make 2 current page in same time... read this http://ellislab.com/codeigniter/user-guide/libraries/uri.html
I'll probably get flamed for suggesting a client-side approach, but this is something I've used in the past to mark the current page as highlighted:
var path = location.pathname.substring(1);
if ( path )
$('#navigation a[href$="' + path + '"]').parents('li').attr('class', 'current');
In every CodeIgniter project I gather some basic information about the request in MY_Controller.
I extend the core controller and put in some initialization logic that needs to happen on every page. This includes getting the information about the controller and method, which is passed to the view. As a short example:
class MY_Controller extends CI_Controller
{
protected $_response_type = 'html';
protected $_secure;
protected $_dir;
protected $_controller;
protected $_method;
protected $_template;
protected $_view;
protected $_user;
public function __construct()
{
parent::__construct();
// Gather info about the request
$this->_secure = ! empty($_SERVER['HTTPS']);
$this->_dir = $this->router->fetch_directory();
$this->_controller = $this->router->fetch_class();
$this->_method = $this->router->fetch_method();
// set the default template and view
$this->_template = 'default';
$this->_view = $this->_dir . $this->_controller . '/' . $this->_method;
}
/**
* Performs operations right before data is displayed.
*
* #access public
* #return void
*/
public function _pre_display()
{
if ($this->_response_type === 'html') {
$this->load->vars(array(
'user' => $this->_user
));
}
elseif ($this->_response_type === 'json') {
$this->_template = 'json';
$this->_view = NULL;
}
else {
show_error('Invalid response type.');
}
$this->load->vars(array(
'is_secure' => $this->_secure,
'controller' => $this->_controller,
'method' => $this->_method
));
}
}
Now in a view such as the navigation you can use that information like this:
<a href="<?php echo site_url('products') ?>"<?php echo ($controller === 'products') ? ' class="selected"' : ''; ?>>Products</a>
I like it because with routes or rewrites you may access controllers and methods from different URLs. This way you set whether the link is active based on the controller/method that is serving up the content and not based on the URL.
Also, this information can be reused within your controllers or views for any reason and you are not continually asking the router or uri to calculate the information (which not only is inefficient, but is cumbersome to write over and over).

A Generic, catch-all action in Zend Framework... can it be done?

This situation arises from someone wanting to create their own "pages" in their web site without having to get into creating the corresponding actions.
So say they have a URL like mysite.com/index/books... they want to be able to create mysite.com/index/booksmore or mysite.com/index/pancakes but not have to create any actions in the index controller. They (a non-technical person who can do simple html) basically want to create a simple, static page without having to use an action.
Like there would be some generic action in the index controller that handles requests for a non-existent action. How do you do this or is it even possible?
edit: One problem with using __call is the lack of a view file. The lack of an action becomes moot but now you have to deal with the missing view file. The framework will throw an exception if it cannot find one (though if there were a way to get it to redirect to a 404 on a missing view file __call would be doable.)
Using the magic __call method works fine, all you have to do is check if the view file exists and throw the right exception (or do enything else) if not.
public function __call($methodName, $params)
{
// An action method is called
if ('Action' == substr($methodName, -6)) {
$action = substr($methodName, 0, -6);
// We want to render scripts in the index directory, right?
$script = 'index/' . $action . '.' . $this->viewSuffix;
// Script file does not exist, throw exception that will render /error/error.phtml in 404 context
if (false === $this->view->getScriptPath($script)) {
require_once 'Zend/Controller/Action/Exception.php';
throw new Zend_Controller_Action_Exception(
sprintf('Page "%s" does not exist.', $action), 404);
}
$this->renderScript($script);
}
// no action is called? Let the parent __call handle things.
else {
parent::__call($methodName, $params);
}
}
You have to play with the router
http://framework.zend.com/manual/en/zend.controller.router.html
I think you can specify a wildcard to catch every action on a specific module (the default one to reduce the url) and define an action that will take care of render the view according to the url (or even action called)
new Zend_Controller_Router_Route('index/*',
array('controller' => 'index', 'action' => 'custom', 'module'=>'index')
in you customAction function just retrieve the params and display the right block.
I haven't tried so you might have to hack the code a little bit
If you want to use gabriel1836's _call() method you should be able to disable the layout and view and then render whatever you want.
$this->_helper->layout()->disableLayout();
$this->_helper->viewRenderer->setNoRender(true);
I needed to have existing module/controller/actions working as normal in a Zend Framework app, but then have a catchall route that sent anything unknown to a PageController that could pick user specified urls out of a database table and display the page. I didn't want to have a controller name in front of the user specified urls. I wanted /my/custom/url not /page/my/custom/url to go via the PageController. So none of the above solutions worked for me.
I ended up extending Zend_Controller_Router_Route_Module: using almost all the default behaviour, and just tweaking the controller name a little so if the controller file exists, we route to it as normal. If it does not exist then the url must be a weird custom one, so it gets sent to the PageController with the whole url intact as a parameter.
class UDC_Controller_Router_Route_Catchall extends Zend_Controller_Router_Route_Module
{
private $_catchallController = 'page';
private $_catchallAction = 'index';
private $_paramName = 'name';
//-------------------------------------------------------------------------
/*! \brief takes most of the default behaviour from Zend_Controller_Router_Route_Module
with the following changes:
- if the path includes a valid module, then use it
- if the path includes a valid controller (file_exists) then use that
- otherwise use the catchall
*/
public function match($path, $partial = false)
{
$this->_setRequestKeys();
$values = array();
$params = array();
if (!$partial) {
$path = trim($path, self::URI_DELIMITER);
} else {
$matchedPath = $path;
}
if ($path != '') {
$path = explode(self::URI_DELIMITER, $path);
if ($this->_dispatcher && $this->_dispatcher->isValidModule($path[0])) {
$values[$this->_moduleKey] = array_shift($path);
$this->_moduleValid = true;
}
if (count($path) && !empty($path[0])) {
$module = $this->_moduleValid ? $values[$this->_moduleKey] : $this->_defaults[$this->_moduleKey];
$file = $this->_dispatcher->getControllerDirectory( $module ) . '/' . $this->_dispatcher->formatControllerName( $path[0] ) . '.php';
if (file_exists( $file ))
{
$values[$this->_controllerKey] = array_shift($path);
}
else
{
$values[$this->_controllerKey] = $this->_catchallController;
$values[$this->_actionKey] = $this->_catchallAction;
$params[$this->_paramName] = join( self::URI_DELIMITER, $path );
$path = array();
}
}
if (count($path) && !empty($path[0])) {
$values[$this->_actionKey] = array_shift($path);
}
if ($numSegs = count($path)) {
for ($i = 0; $i < $numSegs; $i = $i + 2) {
$key = urldecode($path[$i]);
$val = isset($path[$i + 1]) ? urldecode($path[$i + 1]) : null;
$params[$key] = (isset($params[$key]) ? (array_merge((array) $params[$key], array($val))): $val);
}
}
}
if ($partial) {
$this->setMatchedPath($matchedPath);
}
$this->_values = $values + $params;
return $this->_values + $this->_defaults;
}
}
So my MemberController will work fine as /member/login, /member/preferences etc, and other controllers can be added at will. The ErrorController is still needed: it catches invalid actions on existing controllers.
I implemented a catch-all by overriding the dispatch method and handling the exception that is thrown when the action is not found:
public function dispatch($action)
{
try {
parent::dispatch($action);
}
catch (Zend_Controller_Action_Exception $e) {
$uristub = $this->getRequest()->getActionName();
$this->getRequest()->setActionName('index');
$this->getRequest()->setParam('uristub', $uristub);
parent::dispatch('indexAction');
}
}
You could use the magic __call() function. For example:
public function __call($name, $arguments)
{
// Render Simple HTML View
}
stunti's suggestion was the way I went with this. My particular solution is as follows (this uses indexAction() of whichever controller you specify. In my case every action was using indexAction and pulling content from a database based on the url):
Get an instance of the router (everything is in your bootstrap file, btw):
$router = $frontController->getRouter();
Create the custom route:
$router->addRoute('controllername', new Zend_Controller_Router_Route('controllername/*', array('controller'=>'controllername')));
Pass the new route to the front controller:
$frontController->setRouter($router);
I did not go with gabriel's __call method (which does work for missing methods as long as you don't need a view file) because that still throws an error about the missing corresponding view file.
For future reference, building on gabriel1836 & ejunker's thoughts, I dug up an option that gets more to the point (and upholds the MVC paradigm). Besides, it makes more sense to read "use specialized view" than "don't use any view".
// 1. Catch & process overloaded actions.
public function __call($name, $arguments)
{
// 2. Provide an appropriate renderer.
$this->_helper->viewRenderer->setRender('overload');
// 3. Bonus: give your view script a clue about what "action" was requested.
$this->view->action = $this->getFrontController()->getRequest()->getActionName();
}
#Steve as above - your solution sounds ideal for me but I am unsure how you implmeented it in the bootstrap?

Categories