I have my codeigniter setup with a default controller. It's accessible as follows:
site.com/index.php
But it's actually:
site.com/project/index
Where index is the default function.
I would like to do the following:
site.com/project7362
But it actually wants:
site.com/project/index/project7362
Where project name is a variable that is pass into the index function. But by default it looks for project-name as a controller. Is there a way to avoid this?
Essentially what I'm hoping to accomplish is to pass a variable directly after the domain name. A user may create a project, and I want that project to be accessible at domain.com/project_id
"Essentially what I'm hoping to accomplish is to pass a variable
directly after the domain name. A user may create a project, and I
want that project to be accessible at domain.com/project_id"
so another way to do this would be to have it like.
domain.com/project/id
this will give you much more flexibility later in your routes for adding different features.
in config/routes:
$route['project/(:any)'] = 'project/view/$1';
in your project controller
function view($id) {
// clean it
$id = htmlspecialchars($id) ;
if ( ! $project = $this->members->returnProjectBy($id) {
$this->showNoResultsFor($id) ; }
else { $this->show($project) ; }
}
OR -- another way to do this would be to put your defined routes first, and then have project be last (because it requires searching on whatever is there)
$route['home'] = 'page/home';
$route['contact'] = 'contact';
// etc etc so you first define your hard coded routes, and then if its not any of those
// you do a search on whatever the value is to find a project
$route['(:any)'] = 'project/view/$1';
so then your link could be
domain.com/id
I have 2 sites.
www.xxx.com & www.yyy.com
I have a CakePHP solution on www.xxx.com and it works fine.
I want to now make site www.yyy.com use the same controllers and DB as www.xxx.com. BUT it is to have totally different CTPs and CSS. I can of course just copy the whole lot over to www.yyy.com but then I need to maintain 2 sets of code. I only want to maintain 2 different sets of CTPS and CSS.
So the questions are:
Can you split controllers and views across 2 different URLs?
Assuming the answer to 1 is yes. Can you then have 2 different sites using the same controllers and DB but showing a totally different view & CSS?
What should I think about before doing this from your experience?
Thanks.
Your question is quite similar to a site that needs a desktop and a mobile version. You could research the solutions used for that.
We built something like this by creating an AppController that sets a variable in the beforeFilter() method (check the request domain in your case), and use this variable in a the render method to choose which layout and view files to use:
function render($action = null, $layout = null, $file = null) {
$view_file = false;
$layout_file = false;
// test for availability of layout does not work for null so set to default setting of null.
if ($layout === null) {
$layout = $this->layout;
}
// if not mobile: show desktop version.
if (!$this->is_mobile) {
$view_file = file_exists(VIEWS . $this -> name . DS . 'desktop/' . $file . '.ctp');
$layout_file = file_exists(LAYOUTS . 'desktop/' . $layout . '.ctp');
}
return parent::render($action, ($layout_file ? 'desktop/' : '') . $layout, ($view_file ? 'desktop/' : '') . $file);
}
This way if is_mobile was false the views/layouts in a subdir 'desktop' were used instead of the default ones. You can use the same system to create separate views/layouts for 2 domains.
We do this by overriding render() instead of using afterFilter() which is sometimes also suggested. I forgot the real reason but know that it was because we couldn't get it to work like we wanted using afterFilter() ;-)
I'm trying to create my own xml sitemap. Everything is done except for the part that I thought was going to be the easiest. How do you get a list of all the pages on the site? I have a bunch of views in a /site folder and a few others. Is there a way to explicitly request their URLs or perhaps via the controllers?
I do not want to make use of an extension
You can use reflection to iterate through all methods of all your controllers:
Yii::import('application.controllers.*');
$urls = array();
$directory = Yii::getPathOfAlias('application.controllers');
$iterator = new DirectoryIterator($directory);
foreach ($iterator as $fileinfo)
{
if ($fileinfo->isFile() and $fileinfo->getExtension() == 'php')
{
$className = substr($fileinfo->getFilename(), 0, -4); //strip extension
$class = new ReflectionClass($className);
foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method)
{
$methodName = $method->getName();
//only take methods that begin with 'action', but skip actions() method
if (strpos($methodName, 'action') === 0 and $methodName != 'actions')
{
$controller = lcfirst(substr($className, 0, strrpos($className, 'Controller')));
$action = lcfirst(substr($methodName, 6));
$urls[] = Yii::app()->createAbsoluteUrl("$controller/$action");
}
}
}
}
You need to know what content you want to include in your sitemap.xml, I don't really think you want to include ALL pages in your sitemap.xml, or do you really want to include something like site.com/article/edit/1 ?
That said, you may only want the result from the view action in your controllers. truth is, you need to know what you want to indexed.
Do not think in terms of controllers/actions/views, but rather think of the resources in your system that you want indexed, be them articles, or pages, they are all in your database or stored somehow, so you can list them, and they have a URI that identifies them, getting the URI is a matter of invoking a couple functions.
There are two possiblities -
Case 1:
You are running a static website then you can find all your HTML inside 1 folder - protected/views/site/pages
http://www.yiiframework.com/wiki/22/how-to-display-static-pages-in-yii/
Case 2:
Website is dynamic. Tasks such as generating and regenerating Sitemaps can be classified into background tasks.
Running background taks can be achieved by emulating the browser which is possible in linux using - WGET, GET or lynx commands
Or, You can create a CronController as a CConsoleCommand. How to use Commands in YII is shown in link below -
http://tariffstreet.com/yii/2012/04/implementing-cron-jobs-with-yii-and-cconsolecommand/
Sitemap is an XML which lists your site's URL. But it does more than that.
It helps you visualize the structure of a website , you may have
category
subcategories.
While making a useful extension, above points can be kept into consideration before design.
Frameworks like Wordpress provide way to generate categorical sitemap.
So the metadata for each page is stored from before and using that metadata it discovers and group pages.
Solution by Reflection suggested by #Pavle is good and should be the way to go.
Consider there may be partial views and you may or may not want to list them as separate links.
So how much effort you want to put into creating the extension is subject to some of these as well.
You may either ask user to list down all variables in config fie and go from there which is not bad or you have to group pages and list using some techniques like reflection and parsing pages and looking for regex.
For ex - Based on module names you can group them first and controllers inside a module can form sub-group.
One first approach could be to iterate over the view files, but then you have to take into account that in some cases, views are not page destinations, but page sections included in another pages by using CController::renderPartial() method. By exploring CController's Class Reference I came upon the CController::actions() method.
So, I have not found any Yii way to iterate over all the actions of a CController, but I used php to iterate over all the methods of a SiteController in one of my projects and filter them to these with the prefix 'action', which is my action prefix, here's the sample
class SiteController extends Controller{
public function actionTest(){
echo '<h1>Test Page!</h1></p>';
$methods = get_class_methods(get_class($this));
// The action prefix is strlen('action') = 6
$actionPrefix = 'action';
$reversedActionPrefix = strrev($actionPrefix);
$actionPrefixLength = strlen($actionPrefix);
foreach ($methods as $index=>$methodName){
//Always unset actions(), since it is not a controller action itself and it has the prefix 'action'
if ($methodName==='actions') {
unset($methods[$index]);
continue;
}
$reversedMethod = strrev($methodName);
/* if the last 6 characters of the reversed substring === 'noitca',
* it means that $method Name corresponds to a Controller Action,
* otherwise it is an inherited method and must be unset.
*/
if (substr($reversedMethod, -$actionPrefixLength)!==$reversedActionPrefix){
unset($methods[$index]);
} else $methods[$index] = strrev(str_replace($reversedActionPrefix, '', $reversedMethod,$replace=1));
}
echo 'Actions '.CHtml::listBox('methods', NULL, $methods);
}
...
}
And the output I got was..
I'm sure it can be furtherly refined, but this method should work for any of the controllers you have...
So what you have to do is:
For each Controller: Filter out all the not-action methods of the class, using the above method. You can build an associative array like
array(
'controllerName1'=>array(
'action1_1',
'action1_2'),
'controllerName2'=>array(
'action2_1',
'action2_2'),
);
I would add a static method getAllActions() in my SiteController for this.
get_class_methods, get_class, strrev and strlen are all PHP functions.
Based on your question:
1. How do you get a list of all the pages on the site?
Based on Yii's way of module/controller/action/action_params and your need to construct a sitemap for SEO.
It will be difficult to parse automatically to get all the urls as your action params varies indefinitely. Though you could simply get controller/action easily as constructed by
Pavle Predic. The complexity comes along when you have customized (SERF) URL rules meant for SEO.
The next best solution is to have a database of contents and you know how to get each content via url rules, then a cron console job to create all the urls to be saved as sitemap.xml.
Hope this helps!
I have the following code:
public function _setHelpers() {
Zend_Controller_Action_HelperBroker::addPrefix('My_Controller_Action_Helpers');
Zend_Controller_Action_HelperBroker::addPath ( APPLICATION_PATH . '/controllers/helpers' );
}
With this I can add a helper wether in My/Controller/Action/Helpers/Helper.php or /application/controllers/helpers/Helper.php. That is ok, however I need to gave priority to the one in /application/, that is: if I have both helpers load the one in /application and not the one in My/Controller/...
EDITING
I was able to fix this by changing the code to this:
public function _setHelpers() {
$prefix = 'My_Controller_Action_Helpers';
Zend_Controller_Action_HelperBroker::addPrefix ( $prefix );
Zend_Controller_Action_HelperBroker::addPath ( APPLICATION_PATH . '/controllers/helpers', 'My_Controller_Action_Helper' );
return $this;
}
As you may notice, its almost the same, with the difference that when adding the path I added also the prefix. Thanks to all for your help.
Plugin paths added to the Helper Broker use Zend_Loader_PluginLoader which uses a LIFO stack for paths, so that is the last path added is the first one checked. Given your code, since you are adding the application path second, it should have priority over the files in My/Controller (both addPrefix and addPath call the same method in Zend_Loader_PluginLoader so they are effectively the same thing.
Is this not what you are finding?
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?