I have this route:
Router::connect(
'/:controller/*',
array('controller'=>'con3'),
array('controller'=>'con1|con2')
);
I am trying to direct every call to
/con1/x1/x2
to
/con3/x1/x2
and
/con2/y1/y2
to
/con3/y1/y2
it is not working, why ?
If you require to route /con3/ to /con1/ and/or /con2/ based on your own constraints what you require is a Custom Route class. For this there is no better place than Mark Story's tutorial on custom Route classes.
Otherwise, you could of course just extend your controllers (and leave the body empty) like this:
<?php
class Con3Controller extends Con1Controller{
// maybe add model here if you don't have
// var $uses in Con1Controller
// otherwise, extend is just fine
}
?>
In this case you don't need to mess with connecting routes like you are right now. Object inheritance will take care of your "aliasing" for you.
Have you considered something like:
Router::connect( '/con1/:action/*', array( 'controller' => 'con3' ) );
Router::connect( '/con2/:action/*', array( 'controller' => 'con3' ) );
Related
I have a website that has users and entries, both of which are stored in a database. Every entry has its own page, using its slug, and every user has a public profile, using its username. Their respective URLs might look something like this:
Entry: https://example.com/hello-world
User: https://example.com/test-user
Refactoring said website with CakePHP 3.4 (the original being built with “vanilla” PHP), I implemented the following routes:
$routes->connect('/:slug',
['controller' => 'Entries', 'action' => 'view'],
['pass' => ['slug']]
);
$routes->connect('/:username',
['controller' => 'Users', 'action' => 'view'],
['pass' => ['username']]
);
The entry pages work like a charm — no problem there — but when I try to access a user profile, Cake throws a RecordNotFoundException. This makes sense, since it’s looking for an entry that does no exist.
I was hoping switching from firstOrFail to find in the EntriesController would allow the application to continue with the next route in line (because no exception would be thrown), but the result is actually worse: It tries to render the entry view without an object, causing PHP notices on an otherwise empty layout.
I have read the CakePHP documentation (“Book”), but could not find a solution to this (I would assume rather generic) problem. I have also tried many other (often less obvious) route setups, but no luck there either.
Now my mind keeps going to something like a EntryOrUserController, but I doubt that would be the best solution, or even a good one. Frankly, I think it’s silly. I guess I am really hoping for some controller or middleware function that does exactly what I want out of the box, but any elegant solution would do.
P.S. I do realize that the default CakePHP/MVC way of going about this would be to have URLs a bit more like this:
Entry: https://example.com/entries/hello-world
User: https://example.com/users/test-user
…but that is not an option in this case, so thanks but no. ☺
Thanks to ndm for pointing me in the right direction. After some tinkering I have a solution that works nicely. Posting it here because it might be of use to others. There are three steps.
1. Create custom routing class
/src/Routing/Route/SlugRoute.php:
namespace App\Routing\Route;
use Cake\Routing\Route\Route;
use Cake\ORM\Locator\LocatorAwareTrait;
class SlugRoute extends Route
{
use LocatorAwareTrait;
public function parse($url, $method = '')
{
$params = parent::parse($url, $method);
if (!$params ||
!isset($this->options['model']) ||
!isset($this->options['pass'][0])
) {
return false;
}
$count = $this
->tableLocator()
->get($this->options['model'])
->find()
->where([
$this->options['pass'][0] => $params['pass'][0]
])
->count();
if ($count !== 1) {
return false;
}
return $params;
}
}
2. Apply new routing class to relevant routes
$routes->connect('/:slug',
['controller' => 'Entries', 'action' => 'view'],
['pass' => ['slug', 'name'], 'routeClass' => 'SlugRoute', 'model' => 'Entries']
);
$routes->connect('/:username',
['controller' => 'Users', 'action' => 'view'],
['pass' => ['username'], 'routeClass' => 'SlugRoute', 'model' => 'Users']
);
3. Consider a few things
I’m also telling the routes which models to use. It would probably be nice if the routing class figures this out by itself.
The routing class assumes we want to do a lookup on the first value of the pass array. That’s fine in this case (with only slug and username being passed), but it’s not very transparent and easily broken.
Human readable URLs with nested categories like /category/subcategory/n-subcategories/article. I'm using CakePHP 2.2.3 and can't find a proper solution for a routing problem. Using 2 Tables:
articles (could also be products or posts or...)
have just a normal "single" view
an article belongs to one category
categories
nested (Tree behaviour with n-levels)
one category can have many articles
category-view lists all articles, that are related to this category
category view uses paginator for showing article lists
A very common example I guess. But how do I have to define the router now, to get URL-paths with the nested categories like this:
/categoryname1 (showing category view)
/categoryname1/articlename1 (showing article view)
/categoryname2/articlename2 (showing article view)
/categoryname2/subcategoryname1 (showing category view)
/categoryname2/subcategoryname2/articlename4 (showing article view)
/n-categoryname/././...n-subcategoryname (showing category view)
/n-categoryname/././...n-subcategoryname/n-articlename (article view)
I tried to make all routes fix in the routes.php, but that is not very comfortable and I think there should be a dynamic solution.
I also tried to automatically generate all routes out of category- and article-alias and save them in a separate "routes" database table - it worked, but I don't think it's really necessary to define hunderets of single routes?!
I also tried just to define all the categories fix in router, like
Router::connect(
'/any-category-name',
array('controller' => 'categories', 'action' => 'view', 1)
);
and then for the articles
Router::connect(
'/any-category-name/:slug',
array('controller' => 'articles', 'action' => 'view'),
array('pass' => array('slug'))
);
But with this method, all articles are available in all categories, which isn't a good solution. And I thought about using
Router::connect(
'/:slug', ...
but I don't know how to go on, because there are two different controllers and two different views possible (also I don't know if Pagination will still work in this case and what will happen, if I also want to use more controllers/actions in the installation).
I think it shouldn't be so difficult to get nested urls with two controllers (categories and articles) in Cake?! Thanks for any helpful advise!
I think you have to check for two things:
1) Check the number of categories in the url and
2) Check if the last parameter is a category or an article
Handle both checks within a (dynamic) route may be very difficult. I would suggest to create just one route for all these requests and do the checks for 1) and 2) in a controller.
The route may be something like this:
Router::connect(
'/*',
array('controller' => 'outputs', 'action' => 'index')
);
I called the controller for this route OutputController because this will be the controller that handles the output for all these urls.
class OutputController extends AppController
{
public $uses = array('Article', 'Category');
public function index()
{
// Get n parameters from url (1)
$args = func_get_args();
$last_arg = $args[count($args) - 1];
// Check if this is an article (2)
$article = $this->Article->find('first', array(
'conditions' => array('Article.slug' => $last_arg
));
if (!empty($article)) {
$this->set('article', $article);
$this->render('article');
}
// Check if this is an category (2)
$category = $this->Category->find('first', array(
'conditions' => array('Category.slug' => $last_arg
));
if (!empty($category)) {
$this->set('category', $category);
$this->render('category');
}
// Page not found
if (empty($article) and empty($category)) {
throw new NotFoundException();
}
}
// ...
To display an article, the view 'Output/article.ctp' is used. For a category, CakePHP renders 'Output/category.ctp'. In addition you can use the parameters in $args to fetch all the necessary data for your (sub-) categories.
First part of this question will look trivial, but the point is in second part.
So, let's say that I have next few links on my app:
http://myapp.com/cars-audi
http://myapp.com/cars-opel
http://myapp.com/cars-fiat
http://myapp.com/cars-vw
In these cases, model car is used. So, in this case, I want to escape using slash in URL.
Then I will have more pages, and URLs, where roads will be involved, like:
http://myapp.com/roads/germany
http://myapp.com/roads/austria
http://myapp.com/roads/hungary
http://myapp.com/roads/poland
So, if it starts with cars-, in that case model cars should be used, and if it starts with roads/, model roads will be in the game.
Is it possible to do with some regular expressions in routes.php, or is it better to load (use) one same model in both cases, and to work with them like that?
Also, is it possible to help parsing URL using .htaccess file?
This is a simple case of routing a URL to a controller action, it doesn't involve models at all.
Router::connect('/:carlink',
array('controller' => 'cars', 'action' => 'view'),
array('carlink' => 'cars-\w+', 'pass' => array('carlink')));
This route says any URL that matches /:carlink should be routed to the given controller and action. In the last part you're clarifying what :carlink can be with the regular expression cars-\w+ ("cars-" followed by any word). You also pass that value to your called action.
class CarsController extends AppController {
public function view($car) {
if (!preg_match('/cars-(\w+)/', $car, $matches)) {
// action was accessed with invalid URL, bail out
$this->cakeError('error404');
}
// use $matches[1], which will be 'audi', for example
…
}
}
Your road URLs would be routed to the RoadsController as usual like this:
Router::connect('/roads/*', array('controller' => 'roads', 'action' => 'view'));
I am trying to connect the next urls:
1) /food/tips
2) /happiness/tips/best_tips
To the following objects:
1) controller=tips / action=index / passed_parameters=food
2) controller=tips / action=index / passed_parameters=(happiness,best_tips)
--edit--
These routes are not fixed.
Meaning: what I try to do is to route every url that have tips as action, to the tips controller, to any fixed(index is good enough) action, and chaining the rest of the url as it was in the original call.
Something like /any_controller/tips/any_param to /tips/index/any_params
-- end edit --
Hope that now there is some sense.
How should it be done?
(please - also explain)
Thanks
the routing is all done through the call Router::connect('thing to catch', 'where to send it');
so it can be as simple as:
Router::connect('/food/tips', '/tips/index/food');
or the preferred method (using cakes built in url builder)
Router::connect('/food/tips/*', array('controller' => 'tips', 'action' => 'index', 'food');
The first method takes a string argument and passes it to another string which would be a url and you would then have to catch it in your controller, and expect a passed parameter through the url.
The second method uses cakes built in url former which takes an array with keys controller and action (there are other options: http://api.cakephp.org/class/router#method-Routerurl)
The second is preferred due to proper formatting and future flexibility (I believe).
any passed parameters in the second method are just passed as un-named items in the array. named parameters are just passed as keyed elements. So if I wanted to create a URL like this
/posts/index/find:all/page:2
I would write the url like this:
Router::connect('/url_to_catch', array('controller' => 'posts', 'action' => 'index', 'find' => 'all', 'page' => 2);
So just to finish up, I would actually pass your parameter through as named:
Router::connect('/happiness/tips/best_tips', array('controller' => 'tips', 'action' => 'index', 'items' => array('happiness', 'best_tips'));
which would need a function in your tips controller that looks like this:
function tips(){ $this->passedArgs['items']; }
I would recommend reading the chapter on Routing the the book, as it will explain things better than I can and it seems counter productive to paste it here.
http://book.cakephp.org/#!/view/948/Defining-Routes
For the sake of explanation I will try,
Router::connect('/food/tips', array('controller' => 'tips', 'action' => 'index', 'food'));
Router::connect('/happiness/tips/best_tips', array('controller' => 'tips', 'action' => 'index', 'happiness','best_tips'));
This should get things working for you. What you are essentially doing is telling the Cake Routing what url you want it to capture, as it will be doing this using Regex. Then you want to tell it which code you want it to run. So this takes a Controller and Action pair, as a set of things to do.
You also want to pass through your named paremeters afterwards. These will tack onto the function in your controller so that you can do stuff with them.
It's quite easy, just check the Router configuration in the manual. You have to use the connect method from the Router class. This accepts 2 parameters. First your desired routed (e.g. food/tips) and second an array with the actual path it should follow. So for your examples you'd do something like this:
Router::connect('/food/tips', array('controller' => 'tips', 'action' => 'index', 'food');
Router::connect('/happiness/tips/best_tips', array('controller' => 'tips', 'action' => 'index', 'happiness', 'best_tips');
This is equivalent to calling TipsController->index('food') and TipsController('happiness', 'best_tips) respectively.
However, your routes look a bit funny. The Cake convention for routes is /controller/action/param1/param2/etc where the parameters param1 etc. are optional and the index action is assumed when no other action is given.
You're taking a different approach and I would suggest (if you can) change it to the Cake conventional routes, as this will save you a lot of work later on because Cake will automatically connect these routes to the desired methods.
So my suggestion is going for tips/food and tips/happiness/best_tips instead of the routes you suggest. This way, you don't have to do any router configuration.
UPDATE
After you're edit, I think it's best to try something with defining custom routes. I can't test this for you at the moment, so you should do some testing yourself, but in that case it would be something like:
Router::connect('/:section/tips/:param',
array('action' => 'index'),
array(
'section' => '[a-z]*',
'param' => '[a-z]*'
)
);
UPDATE2
Sorry, I've tested the above and it doesn't seem to work.
I'm working on upgrading my project from CakePHP 1.2 to 1.3. In the process, it seems that the "magic" routing for plugins by which a controller name (e.g.: "ForumsController") matching the plugin name (e.g.: "forums") no longer automatically routes to the root of the plugin URL (e.g.: "www.example.com/forums" pointing to plugin "forums", controller "forums", action "index").
The error message given is as follows:
Error: ForumsController could not be found.
Error: Create the class ForumsController below in file: app/controllers/forums_controller.php
<?php
class ForumsController extends AppController {
var $name = 'Forums';
}
?>
In fact, even if I navigate to "www.example.com/forums/forums" or "www.example.com/forums/forums/index", I get the same exact error.
Do I need to explicitly set up routes to every single plugin I use? This seems to destroy a lot of the magic I like about CakePHP. I've only found that doing the following works:
Router::connect('/forums/:action/*', array('plugin' => 'forums', 'controller' => 'forums'));
Router::connect('/forums', array('plugin' => 'forums', 'controller' => 'forums', 'action' => 'index'));
Setting up 2 routes for every single plugin seems like overkill, does it not? Is there a better solution that will cover all my plugins, or at least reduce the number of routes I need to set up for each plugin?
I guess, that topic Configuration-and-application-bootstrapping covers that:
App::build(array(
'plugins' => array('/full/path/to/plugins/', '/next/full/path/to/plugins/')
));
Also take a look at this ticket: http://cakephp.lighthouseapp.com/projects/42648/tickets/750-plugin-route-problem-when-acl-and-auth-components-used#ticket-750-5 (Cake 1.3 had removed magic plugin routes).
You don't have myplugin_app_controller.php in your /app/plugins/myplugin directory.
Just create a file containing following:
<?php
class MypluginAppController extends AppController {
}
?>
And you will have all your plugin's features. :)