I have a route as:
((?<directory>\w+)/?)?((?<controller>\w+)/?)?((?<action>\w+)/?)?((?<id>\d+))?
It works fine but it causes my system to have to include the default controller (index) for all routes to the sub routes. For example, if my page URI is /blog/post (where blog is the directory and post would be the action), my actual URI would have to be blog/index/post - I'd like to be able to fall back to just using blog/post instead.
So, I would like it to be routed to:
directory = blog
controller = index
action = post
Obviously this causes issues when the second parameter is actually a controller. For example directory/controller/action would be routed incorrectly.
Is there a routing method to detect that there are three word parameters, possibly followed by a numeric parameter, which can do what I need?
For claification:
param/param/param(?/id) would be: directory/controller/action(/id)
param/param(?/id) would be: directory/default_controller/action(/id)
i'd actually think that you want to alias blog/index/post with blog/post; insert it as a route before the "catch-all" route that you have; the "one big shoe fits all" approach is not always the best. Especially, if you only have 1 such particular use case.
edit:
"kohana's routing system" is daunting; can't make sense of the elephant they're trying to give birth to there... here are some other suggestions:
Take this issue to the manufacturer; this is definetely an FAQ question
Mess around with the regex patterns. Here's a snippet that might be useful (i put it inside a PHP test case, but you could easily decouple it)
public function testRoutePatterns(){
$data = array(
array(
//most specific: word/word/word/id
'~^(?P<directory>\w+)/(?P<controller>\w+)/(?P<action>\w+)/(?P<id>.*)$~i',
'myModule/blog/post/some-id',
array('directory'=>'myModule', 'controller'=>'blog', 'action'=>'post', 'id'=>'some-id'),
true
),
array(
//less specific: word/word/id
'~^(?P<directory>\w+)/(?P<action>\w+)/(?P<id>.*)$~i',
'blog/post/some-id',
array('directory'=>'blog', 'action'=>'post'), //need to inject "index" controller via "defaults()" here i guess
true
),
);
foreach ($data as $d) {
$matches = array();
list($pattern, $subject, $expected, $bool) = $d;
$actual = (bool) preg_match($pattern, $subject, $matches);
$this->assertEquals($bool, $actual); //assert matching
$this->assertEquals(array(), array_diff($expected, $matches)); //$expected contained in $matches
}
}
As explained on this answer, if you have some route like this:
Route::set('route_name', 'directory/controller/action')
->defaults(array(
'directory' => 'biz',
'controller' => 'foo',
'action' => 'bar',
));
You should have the directory structure like this:
/application/classes/controller/biz/foo.php
Related
OK, I dont know if I am taking the wrong approach or not but am stuck here...
We have developed our website and we have many controllers expecting ids and special variables, links already redirecting to the controllers passing what is expected.
The new requirement is to use friendlyUrls and the idea is that instead of having:
http://domain.com/search/advanced/term:head/city:/set:show-all/sort:basic-relevance
it now reads
http://domain.com/search/head
or passing options.
http://domain.com/search/in-edinburgh-scotland/by-rating/head
My idea was to, at the beginning of the Routes.php have a simple if such as:
$friendlyUrl = $_SERVER['REQUEST_URI'];
$friendlyUrl = split('/', $friendlyUrl);
foreach ($friendlyUrl as $key => $params) {
if(empty($params)){
unset($friendlyUrl[$key]);
}
if($params == 'search'){
Router::connect('/search/*', array('plugin'=>'Search','controller' => 'Search', 'action' => 'advancedSearch', 'term'=>'head));
}elseif ($params == 'employers') {
# code...
}elseif ($params == 'employer-reviews') {
# code...
}elseif ($params == 'jobs') {
# code...
}
}
That didn't work, then I tried adding something similar in my AppController and nothing.
All in all the the thing that has to do is:
Url be in the format of: /search/{term}
Actually be redirecting to: /search/advanced/{term}/city:{optional}/set:show-all/sort:basic-relevance
URL bar to keep reading: /search/{term}
Anyone has an idea?! Thank you
You definitely want to have a look at the routing page in the book
http://book.cakephp.org/2.0/en/development/routing.html
There are tons of options there to match url patterns to pass parameters to the controllers.
Router::connect(
'/search/:term',
array('controller' => 'search', 'action' => 'advanced'),
array(
'pass' => array( 'term')
)
);
You should probably set the defaults for city & set & sort in the actions function parameters definitions:
public function advanced($term, $city='optional', $sort = 'basic'){
// your codes
}
The great thing about doing it this way, is that your $this->Html->link's will reflect the routes in the paths they generate. (reverse routing)
The routes in cake are quite powerful, you should be able to get some decent friendly urls with them. One extra thing I've used is to use a behaviour - sluggable - to generate a searchable field from the content items title - for pages / content types in the cms.
good luck!
I am attempting to refactor my app using the MVC paradigm.
My site displays charts. The URLs are of the form
app.com/category1/chart1
app.com/category1/chart2
app.com/category2/chart1
app.com/category2/chart2
I am using Apache Rewrite to route all requests to index.php, and so am doing my URL parsing in PHP.
I am working on the enduring task of adding an active class to my navigation links when a certain page is selected. Specifically, I have both category-level navigation, and chart-level sub-navigation. My question is, what is the best way to do this while staying in the spirit of MVC?
Before my refactoring, since the nav was getting relatively complicated, I decided to put it into an array:
$nav = array(
'25th_monitoring' => array(
'title' => '25th Monitoring',
'charts' => array(
'month_over_month' => array(
'default' => 'month_over_month?who=total&deal=loan&prev='.date('MY', strtotime('-1 month')).'&cur='.date('MY'),
'title' => 'Month over Month'),
'cdu_tracker' => array(
'default' => 'cdu_tracker',
'title' => 'CDU Tracker')
)
),
'internet_connectivity' => array(
'title' => 'Internet Connectivity',
'default' => 'calc_end_to_end',
'charts' => array(
'calc_end_to_end' => array(
'default' => 'calc_end_to_end',
'title' => 'calc End to End'),
'quickcontent_requests' => array(
'default' => 'quickcontent_requests',
'title' => 'Quickcontent Requests')
)
)
);
Again, I need to know both the current category and current chart being accessed. My main nav was
<nav>
<ul>
<?php foreach ($nav as $category => $category_details): ?>
<li class='<?php echo ($current_category == $category) ? null : 'active'; ?>'>
<?php echo $category_details['title']; ?>
</li>
<?php endforeach; ?>
</ul>
</nav>
and the sub-nav was something similar, checking for current_chart instead of current_category.
Before, during parsing, I was exploding $_SERVER['REQUEST_URI'] by /, and breaking the pieces up into $current_category and $current_chart. I was doing this in index.php. Now, I feel this is not in the spirit of the font controller. From references like Symfony 2's docs, it seems like each route should have its own controller. But then, I find myself having to define the current category & chart multiple times, either within the template files themselves (which doesn't seem to be in the spirit of MVC), or in an arbitrary function in the model (which would then have to be called by multiple controllers, which is seemingly redundant).
What is the best practice here?
Update: Here's what my front controller looks like:
// index.php
<?php
// Load libraries
require_once 'model.php';
require_once 'controllers.php';
// Route the request
$uri = str_replace('?'.$_SERVER['QUERY_STRING'], '', $_SERVER['REQUEST_URI']);
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && (!empty($_GET)) && $_GET['action'] == 'get_data') {
$function = $_GET['chart'] . "_data";
$dataJSON = call_user_func($function);
header('Content-type: application/json');
echo $dataJSON;
} elseif ( $uri == '/' ) {
index_action();
} elseif ( $uri == '/25th_monitoring/month_over_month' ) {
month_over_month_action();
} elseif ( $uri == '/25th_monitoring/cdu_tracker' ) {
cdu_tracker_action();
} elseif ( $uri == '/internet_connectivity/intexcalc_end_to_end' ) {
intexcalc_end_to_end_action();
} elseif ( $uri == '/internet_connectivity/quickcontent_requests' ) {
quickcontent_requests_action();
} else {
header('Status: 404 Not Found');
echo '<html><body><h1>Page Not Found</h1></body></html>';
}
?>
It seems like when month_over_month_action() is called, for instance, since the controller knows the current_chart is month_over_month, it should just pass that along. This is where I'm getting tripped up.
There are not "best practices" in this area. Though, there are some, that are more often used then others, and some, that are extremely bad ideas (unfortunately, these two groups tend to overlap).
Routing in MVC
While technically not a part of MVC design pattern, when applied to Web, your application needs to know which controller to initialize and what method(s) to call on it.
Doing explode() to gather this sort of information is a bad idea. It is both hard to debug and maintain. A much better solution is to use regular expressions.
Basically you end up having a list of routes, that contain a regular expression and some fallback values. You loop through that list and on fists match extract the data and apply default values, where data was missing.
This approach also frees you to have much wider possibilities for order of parameters.
To make the solution easier to use, you can also add functionality, that turns a notation string into a regular expression.
For example (taken from some unit-test, that I have):
notation: test[/:id]
expression: #^/test(:?/(?P<id>[^/\.,;?\n]+))?$#
notation: [[/:minor]/:major]
expression: #^(:?(:?/(?P<minor>[^/\.,;?\n]+))?/(?P<major>[^/\.,;?\n]+))?$#
notation: user/:id/:nickname
expression: #^/user/(?P<id>[^/\.,;?\n]+)/(?P<nickname>[^/\.,;?\n]+)$#
While creating such a generator will not be all that easy, it would be quite reusable. IMHO the time invested in making it would be well spent. Also, the use of (?P<key>expression) construct in regular expressions provides you with a very useful array of key-value pairs from the matched route.
Menus and MVC
The decision about which menu item to highlight as active should always be the responsibility of current view instance.
More complicated issue is where the information, that is necessary for making such decision, comes from. There are two source if data that are available to a view instance: information that was passed to view by controller and data, that view requested from model layer.
The controller in MVC takes the user's input and, based on this input, it changes the state of current view and model layer, by passing said values. Controller should not be extracting information from model layer.
IMHO, the better approach in this case is to relay on model layer for information about both menu content and the currently active element in it. While it's possible to both hardcode the currently active element in view and relay on controllers passed informations, MVC is usually used in large scale application, where such practices would end up hurting you.
The view in MVC design pattern is not a dumb template. It's a structure, that is responsible for UI logic. In context of Web that would mean creating a response from multiple template, when necessary, or sometimes just simply sending an HTTP location header.
Well, I had almost the same trouble when was writing CMS-like product.
So I've spend some time trying to figure out how to make this work and keep the code more maintainable and clean as well.
Both CakePHP and Symfony route-mecanisms have a bit inspired me but it wasn't good enough for me.
So I'll try to give you an example of how I do this now.
My question is, what is the best way to do this while staying in the
spirit of MVC?
First, In general, best practice is NOT TO USE procedural approach with MVC in web development at all.
Second, keep the SRP.
From references like Symfony 2's docs, it seems like each route should
have its own controller.
Yeah, that's right approach, but it doesn't mean that another route match can't have the same controller, but different action.
The main disadvantage of your approach (code that you have posted) is that you mix responsibilities and you're not implementing MVC-inspired pattern.
Anyway, MVC in PHP with procedural approach is just a horrible thing.
So, what exactly you are mixing is:
Route mechanism logic (It should be another class) not in a "controller" and route map as well
Request and Response responsibilites (I see that it isn't obvious to you)
Class autoloading
Controller logic
All those "parts" should have one class. Basically, they have to be included in index or bootstrap files.
Also, by doing so:
require_once 'controllers.php';
You automatically include ALL controllers per match (even on no-match). It actually has nothing to do with MVC and leads to memory leaks.
Instead, you should ONLY include and instantiate the controller that matches against URI string.
Also, be careful with include() and require() as they may lead to code duplication if you include the same file somewhere twice.
And also,
} elseif ( $uri == '/' ) {
index_action();
} elseif ( $uri == '/25th_monitoring/month_over_month' ) {
month_over_month_action();
} elseif ( $uri == '/25th_monitoring/cdu_tracker' ) {
cdu_tracker_action();
} elseif ( $uri == '/internet_connectivity/intexcalc_end_to_end' ) {
intexcalc_end_to_end_action();
It's extremely unwise to do a match using if/else/elseif control structures.
Okay, what if you have 50 matches? or even 100? Then you need to write 50 or 100 times to write else/elseif accordingly.
Instead, you should have a map and (an array for example) iterate over it on each HTTP request.
The general approach of using MVC with routing mechanism comes down to:
Matching the request against route map (and keep somewhere parameters if we have them)
Then instantiate appropriate controller
Then pass parameters if we have them
In PHP, the implementation would look like:
File: index.php
<?php
//.....
// -> Load classes here via SPL autoloader or smth like this
// .......
// Then -> define or (better include route map from config dir)
$routes = array(
// -> This should default one
'/' => array('controller' => 'Path_To_home_Controller', 'action' => 'indexAction'),
'/user/:id' => array('controller' => 'Path_to_user_controller', 'action' => 'ViewAction'),
// -> Define the same controller
'/user/:id/edit' => array('controller' => 'Path_to_user_controller', 'action' => 'editAction'),
// -> This match we are going to hanlde in example below:
'/article/:id/:user' => array('controller' => 'SomeArticleController', 'action' => )
);
// -> Also, note you can differently handle this: array('controller' => 'SomeArticleController', 'action' => )
// -> Generally controller key should point to the path of a matched controller, and action should be a method of the controller instance
// -> But if you're still on your own, you can define it the way you want.
// -> Then instantiate common classes
$request = new Request();
$response = new Response();
$router = new Router();
$router->setMap( $routes );
// -> getURI() should return $_SERVER['REQUEST_URI']
$router->setURI( $request->getURI() );
if ( $router->match() !== FALSE ) {
// -> So, let's assume that URI was: '/article/1/foo'
$info = $router->getAll();
print_r ( $info );
/**
* Array( 'parameters' => Array(':id' => '1', ':user' => 'foo'))
* 'controller' => 'Path_To_Controller.php'
* 'action' => 'indexAction'
*/
// -> The next things we are going to do are:
// -> 1. Instantiate the controller
// -> 2. Pass those parameters we got to the indexAction method
$controller = $info['controller'];
// -> Assume that the name of the controller is User_Controller
require ( $controller );
// -> The name of class should also be dynamic, not like this, thats just an example
$controller = new User_Controller();
$arguments = array_values( $info['parameters'] );
call_user_func_array( array($controller, $info['action']), $arguments );
// -> i.e we just called $controller->indexAction('1', 'foo') "dynamically" according to the matched URI string
// -> idealy this should be done like: $response->send( $content ), however
} else {
// -> In order not to show any error
// -> redirect back to "default" controller
$request->redirect('/');
}
In my MVC-inspired applications I do route like this:
(Where I use Dependecy Injection and keep the SRP)
<?php
require (__DIR__ . '/core/System/Auload/Autoloader.php');
Autoloader::boot(); // one method includes all required classes
$map = require(__DIR__ . '/core/System/Route/map.php');
$request = new Request();
$response = new Response();
$mvc = new MVC();
$mvc->setMap( array_values($map) );
// -> array_values($map) isn't accurate here, it'd be a map of controllers
// -> take this as a quick example
$router = new Router();
$router->setMap( $map );
$router->setURI( $request()->getURI() );
if ( $router->match() !== FALSE ) {
// -> Internally, it would automatically find both model and view instances
// -> then do instantiate and invoke appropriate action
$router->run( $mvc );
} else {
// No matches handle here
$request->redirect('/');
}
I found this to be more appropriate for me, after poking around Cake and Symfony.
One thing I want to note:
It's not that easy to find good articles about MVC in PHP. Most of them are just wrong.
(I know how it feels, because first time I've started to learn from them, like so many people do)
So my point here is:
Don't make the same mistake like I did before. If you want to learn MVC, start doing this by reading
Zend Framework or Symfony Tutorials. Even the ones are bit different, the idea behing the scene is the same.
Back to the another part of the question
Again, I need to know both the current category and current chart
being accessed. My main nav was
<nav>
<ul>
<?php foreach($nav as $category => $category_details): ?>
<li class='<?php echo ($current_category == $category) ? null : 'active'; ?>'>
<?php echo $category_details['title']; ?>
</li>
<?php endforeach; ?>
</ul>
</nav>
First of all, don't concatenate the string, instead use printf() like:
<?php echo $category_details['title']; ?>
If you need this to be everywhere (or at least in many different templates), I'd suggest to this to have in a common abstact View class.
For example,
abstract class View
{
// -> bunch of view reusable methods here...
// -> Including this one
final protected function getCategories()
{
return array(
//....
);
}
}
class Customers_View extends View
{
public function render()
{
$categories =& $this->getCategories();
// -> include HTML template and then interate over $categories
}
}
In my layout-script I wish to create a link, appending [?|&]lang=en to the current url, using the url view helper. So, I want this to happen:
http://localhost/user/edit/1 => http://localhost/user/edit/1?lang=en
http://localhost/index?page=2 => http://localhost/index?page=2&lang=en
I have tried the solution suggested Zend Framework: Append query strings to current page url, accessing the router directly, but that does work.
My layout-script contains:
English
If the default route is used, it will append /lang/en, but when another route is used, nothing is appended at all.
So, is there any way to do this with in Zend without me having to parse the url?
Edit
Sorry for my faulty explanation. No, I haven't made a custom router. I have just added other routes. My bad. One route that doesn't work is:
$Routes = array(
'user' => array(
'route' => 'admin/user/:mode/:id',
'defaults' => array('controller' => 'admin', 'action' => 'user', 'mode'=>'', 'id' => 0)
)
);
foreach( $Routes as $k=>$v ) {
$route = new Zend_Controller_Router_Route($v['route'], $v['defaults']);
$router->addRoute($k, $route);
}
Upd:
You must add wildcard to your route or define 'lang' parameter explicitly.
'admin/user/:mode/:id/*'
Additionally, according to your comment, you can do something like this:
class controllerplugin extends Zend_Controller_Plugin_Abstract
{
function routeShutdown($request)
{
if($request->getParam('lang', false) {
//store lang
Zend_Controller_Front::getInstance()->getRouter()->setGlobalParam('lang', NULL); //check if this will remove lang from wildcard parameters, have no working zf installation here to check.
}
}
}
I want to make my /pages/about become just `/about
I tried doing it with the routing in routes.php but couldn't get it working e.g. Router::connect('/pages/about', array('controller' => 'pages', 'action' => 'display'));
can anyone help?
ALSO
for my portfolio controller I have it currently showing work like /portfolio/view/102 but I would like to display it something like /portfolio/view/Paperview_Magazine-102 where Paperview Magazine is the title of the post and 102 is the ID of the post. I have looked at the Cake Book but if someone could post up some code that'd be awesome.
Thanks
To make a route for /about, you need to make a route for /about, not /pages/about:
Router::connect('/about',
array('controller' => 'pages', 'action' => 'display', 'about'));
To use URLs like /portfolio/view/Paperview_Magazine-102, you can use standard routes, but you'll have to do a little work in the controller:
// PortfolioController
// $identifier == "Paperview_Magazine-102"
public function view($identifier) {
if (!preg_match('/^(.+)-(\d+)$/', $identifier, $matches)) {
// $identifier is not in format 'Title-Id'
$this->cakeError('error404');
}
// $matches[1] == Paperview_Magazine
// $matches[2] == 102
$post = $this->Portfolio->read(null, $matches[2]);
$this->set(compact('post'));
}
Maybe a bit of a long shot, but in the first one you could try Router::connect('/:controller/about', array('action'=>'display'));? I seem to remember having some similar problems with my routes and this maybe helping, though I can't remember why.
With the second question, can't you just rework the view function so it accepts a $slug parameter instead of $id, and find the right portfolio record by searching on the slug instead? Or check if the parameter is_numeric, and search for an id if it is, or a slug if it isn't...
I'm new to cakephp...and I have a page with a url this:
http://localhost/books/filteredByAuthor/John-Doe
so the controller is ´books´, the action is ´filteredByAuthor´ and ´John-Doe´ is a parameter.. but the url looks ugly so i've added a Route like this:
Router::connect('/author/:name', array( 'controller' => 'books','action' => 'filteredByAuthor'), array('pass'=>array('name'),'name'=>".*"));
and now my link is:
http://localhost/author/John-Doe
the problem is that the view has a paginator and when i change the page (by clicking on the next or prev button).. the paginator won't consider my routing... and will change the url to this
http://localhost/books/filteredByAuthor/John-Doe/page:2
the code on my view is just:
<?php echo $this->Paginator->prev('<< ' . __('previous', true), array(), null, array('class'=>'disabled'));?>
the documentation doesn't say anything about avoiding this and i've spent hours reading the paginators source code and api.. and in the end i just want my links to be something like this: (with the sort and direction included on the url)
http://localhost/author/John-Doe/1/name/asc
Is it possible to do that and how?
hate to answer my own question... but this might save some time to another developper =) (is all about getting good karma)
i found out that you can pass an "options" array to the paginator, and inside that array you can specify the url (an array of: controller, action and parameters) that the paginator will use to create the links.. so you have to write all the possible routes in the routes.php file. Basically there are 3 possibilities:
when the "page" is not defined
For example:
http://localhost/author/John-Doe
the paginator will assume that the it's the first page. The corresponding route would be:
Router::connect('/author/:name', array( 'controller' => 'books','action' => 'filteredByAuthor'),array('pass'=>array('name'),'name'=>'[a-zA-Z\-]+'));
when the "page" is defined
For example:
http://localhost/author/John-Doe/3 (page 3)
The route would be:
Router::connect('/author/:name/:page', array( 'controller' => 'books','action' => 'filteredByAuthor'),array('pass'=>array('name','page'),'name'=>'[a-zA-Z\-]+','page'=>'[0-9]+'));
finally when the page and the sort is defined on the url (by clicking on the sort links created by the paginator).
For example:
http://localhost/author/John-Doe/3/title/desc (John Doe's books ordered desc by title)
The route is:
Router::connect('/author/:name/:page/:sort/:direction', array( 'controller' => 'books','action' => 'filteredByAuthor'),
array('pass'=>array('name','page','sort','direction'),
'name'=>"[a-zA-Z\-]+",
'page'=>'[0-9]*',
'sort'=>'[a-zA-Z\.]+',
'direction'=>'[a-z]+',
));
on the view you have to unset the url created by the paginator, cause you'll specify your own url array on the controller:
Controller:
function filteredByAuthor($name = null,$page = null , $sort = null , $direction = null){
$option_url = array('controller'=>'books','action'=>'filteredByAuthor','name'=>$name);
if($sort){
$this->passedArgs['sort'] = $sort;
$options_url['sort'] = $sort;
}
if($direction){
$this->passedArgs['direction'] = $direction;
$options_url['direction'] = $direction;
}
Send the variable $options_url to the view using set()... so in the view you'll need to do this:
View:
unset($this->Paginator->options['url']);
echo $this->Paginator->prev(__('« Précédente', true), array('url'=>$options_url), null, array('class'=>'disabled'));
echo $this->Paginator->numbers(array('separator'=>'','url'=>$options_url));
echo $this->Paginator->next(__('Suivante »', true), array('url'=>$options_url), null, array('class' => 'disabled'));
Now, on the sort links you'll need to unset the variables 'sort' and 'direction'. We already used them to create the links on the paginator, but if we dont delete them, then the sort() function will use them... and we wont be able to sort =)
$options_sort = $options_url;
unset($options_sort['direction']);
unset($options_sort['sort']);
echo $this->Paginator->sort('Produit <span> </span>', 'title',array('escape'=>false,'url'=>$options_sort));
hope this helps =)