I have to support url friendly structure for a project.
There is multiple tables with a slug column, in cakephp how can I route the slug to a controller in the most efficient way.
At first I was checking if slug exist in a table, if slug exist use the route:
$c = TableRegistry::get('cateogories');
$result= $c->find()->select(['id'])->where(['url'=>$slug])->toArray();
if(count($result) > 0) {
$routes->connect(
'/:slug',
['controller' => 'Categories', 'action' => 'index', 'id' => $result[0]['id']]
);
}
The problem being that I have multiple checks like the one above and each one is being ran even if a route prior matches (doesn't need to be ran so extra querys are being called).
So how can I add a conditional statement of some sort so that it only checks if the route matches if none of the prior ones have.
I'd suggest to go for a custom route class that handles this. While you could query the data in your routes files, this is
not overly test friendly
not very DRY
not safe for reverse routing
The latter point means that when not connecting all routes, trying to generate a URL from a route array for a non-connected route might trigger an exception, or match the wrong route.
With a custom route class you could simply pass the model in the options when connecting the routes, and in the route class after parsing the URL, query that model for the given slug, and return false or the parsed data accordingly.It's really simple, just have a look at what the existing route classes do.
Here's a very basic example which should be pretty self-explantory.
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)
{
$params = parent::parse($url);
if (!$params ||
!isset($this->options['model'])
) {
return false;
}
$count = $this
->tableLocator()
->get($this->options['model'])
->find()
->where([
'slug' => $params['slug']
])
->count();
if ($count !== 1) {
return false;
}
return $params;
}
}
This example assumes that in the controller, you'd use the slug to retrieve the record. If you'd wanted to have the ID passed, then instead of using count(), you could fetch the ID and pass it along in the parsed data, like:
$params['pass'][] = $id;
It would then end up being passed as the second argument of the controller action.
routes.php
$routes->connect(
'/:slug',
['controller' => 'Articles', 'action' => 'view'],
[
'pass' => ['slug'],
'routeClass' => 'SlugRoute',
'model' => 'Articles'
]
);
$routes->connect(
'/:slug',
['controller' => 'Categories', 'action' => 'view'],
[
'pass' => ['slug'],
'routeClass' => 'SlugRoute',
'model' => 'Categories'
]
);
// ...
This would first check the Articles model, then the Categories model, etc., and stop once one of the routes finds a record for the given slug.
See also
Cookbook > Routing > Custom Route Classes
API > \Cake\Routing\Route::parse()
Source > \Cake\Routing\Route
Related
I'm using CakePHP 3 and I want to paginate my users.
However when I click on the second page, the URL looks like /users?page=2 and I expect : /users/2.
I created this route in routes.php :
$routes->connect('/users/:page', ['controller' => 'users', 'action' => 'index'], ['page' => '[0-9]+']);
And in Users/index.ctp before the "prev" button I put :
<?php
$this->Paginator->options([
'url' => [
'controller' => 'users',
'action' => 'index'
]
]);
?>
Now when I click on page 2 for example, /users/2 opens and I got this error message (RuntimeException) :
Unable to locate an object compatible with paginate.
Did I miss something or where I made a mistake ?
Thanks for your help.
The PaginatorHelper has built in the url format, i.e. to use ?page=n. It will also do sorting such as users?page=2&sort=user_id&direction=asc. Your format of /users/{page} does not handle sorting.
If your REALLY want to stick to /users/{page} you'll have to override PaginatorHelper.
try this
in side your controller with paginator component . It works for me
$this->Paginator->paginate('Users')
for custom urlenter code here
u need to implement index action as
public function index($page = null){
$this->Paginator->settings = ['limit' => 15, 'page' => $page];
$this->set('users', $this->Paginator->paginate('Users'));
}
So I've been working on an app that lived on the domain root and now has to work on /admin. So URLs like domain.com/[resource] should now be domain.com/admin/[resource]. I didn't thought this very well before since I assumed that this had to be a very easy fix on Laravel. After all, that's one of the main reasons for not hardcoding routes, right?
So my routes.php file looked something like:
Route::group(['before' => 'auth'], function() {
Route::resource('books', 'BooksController');
... more resources here ...
});
Going through the docs I found that 'prefix' => 'admin' would do the trick:
Route::group(['prefix' => 'admin', 'before' => 'auth'], function() {
Route::resource('books', 'BooksController');
... more resources here ...
});
But it turns out that every route name get's changed from books.{action} to admin.books.{action} which requires me to change the whole app. Regexing would be dangerous and doing it manually would be annoying. Laravel was supposed to help with this! Or am I missing something?
This is untested, but after looking on the documentation for resource controllers it seems as though you can manually set their names. I'm assuming Laravel automatically namespaces grouped resource controller routes to avoid name collision, but you can override this to avoid going back through the rest of your app (just beware of future name collision):
Route::resource(
'books',
'BooksController',
array(
'names' => array(
'index' => 'photo.index',
'create' => 'photo.create',
'store' => 'photo.store',
'show' => 'photo.show',
'edit' => 'photo.edit',
'update' => 'photo.update',
'destroy' => 'photo.destroy',
)
)
);
Shorter Method:
Just define a quick method at the top of your routes.php file to shorten up this repetitive task of creating an array of route names. Still not the greatest solution, but I believe its the only thing you can do with how Laravel has this set up.
function createRouteNames($resource) {
$names = array();
$types = ['index', 'create', 'store', 'show', 'edit', 'update', 'destroy'];
foreach($types as $type) {
$names[$type] = $resource . '.' . $type;
}
return $names;
}
Route::resource('books', 'BooksController', ['names' => createRouteNames('books')]);
Note: [] === array() and may not be supported on older PHP's, meaning you may need to replace them with the old syntax.
I want want dispatch another controller action when Zf2 raise route not found exception to display my custom page ( i dont want to display a custom error page). I am working on dynamic url from database in Zf2 which will occur only when route not found.
i added a event in bootstrap function
$event->attach('route', array($this, 'loadConfiguration'), 2);
in loadConfiguration function i added to load
public function loadConfiguration(MvcEvent $e){
$application = $e->getApplication();
$sm = $application->getServiceManager();
$router = $sm->get('router');
$request = $sm->get('request');
$matchedRoute = $router->match($request);
if (null == $matchedRoute) {
$request_uri = $e->getRequest()->getrequestUri();
$dbAdaptor = $e->getApplication()->getServiceManager()->get('Zend\Db\Adapter\Adapter');
$url_table = new UrlMappingTable($dbAdaptor);
$url_data = $url_table->find($request_uri);
$controller = $url_data['controller'];
$action = $url_data['action'];
$id = $url_data['post_id'];
$original_url = $url_data['original_url'];
$alias = $sm->get('Application\Router\Alias');
$alias->setNavigation($original_url);
if(isset($url_data)){
$url = $e->getRouter ()->assemble (array('controller' => $controller,
'action' => $action ,
'id' => $id,
), array (
'name' => 'myurl'
) );
}
print 'no route match';
}
}
after getting the controller and action i just want the dispatcher to forward this controller.
I needed something similar for my project. I ended up just adding a 'catchall' rule in module.config.php
i.e.
'router' => array(
'routes' => array(
'catchAll' => array(
'type' => 'regex',
'options' => array(
'regex' => '/(?<page>.+)',
'defaults' => array(
'controller' => 'Project\Controller\MyController',
'action' => 'customPage',
),
'spec' => '/%page%',
),
),
//Other route items ...
),
//Other stuff ...
)
Place this as the first item in the routes array so it has the lowest precedence. Then you can have your customePageAction to do whatever you want!
Just something not really answering your question:
I am working on dynamic url from database in Zf2 which will occur only when route not found.
There are two ways you can achieve this much more efficiently. If there aren't much routes, you can load them before the route event. For example, you query on bootstrap all your database routes and inject them into the route stack.
Another way is creating a "catch all" route which always will match after all routes failed. Then you don't have a "route not found" but your default route is matched. This will then look for a matching database record. If none found, you just return a 404 response.
In case #1, the controller mapped by your database route is directly dispatched. In the second case, you are in your "database controller" and want to dispatch your controller mapped by the database route, you use the forward plugin:
public function matchAction()
{
// Fetch route from db here
if (!$match) {
$this->getResponse()->setStatusCode(404);
return;
}
return $this->forward($match->getController, array(
'action' => $match->getAction()
));
}
I decided to add some extra data about the controllers and actions in some model beforeSave as follows:
//in the model
public function beforeSave() {
$this->data[$this->alias]['path'] = 'blah blan';
debug($this->params);
die(); //for debugging!
}
The printout of debug returns null! The model I uses is the Comment model of the comments plugin. I need to access params to get the current controller, actions and some url parameters.
Indeed, I plan to change the way that comments plugin lists the comments from model based to be path based to solve the issue of need comments for more than one action depend on the same model.
Finally I found the solution: It is in Router object method getParams();
//in the model
public function beforeSave() {
$this->data[$this->alias]['path'] = 'blah blan';
debug(Router::getParams());
die(); //for debugging!
}
it prints out something like:
array(
'plugin' => null,
'controller' => 'qurans',
'action' => 'view',
'named' => array(
'comment' => '0'
),
'pass' => array(
(int) 0 => '8'
)
)
I have one route that looks like this:
Router::connect('/Album/:slug/:id',array('controller' => 'albums', 'action' => 'photo'),array('pass' => array('slug','id'),'id' => '[0-9]+'));
and another like this:
Router::connect('/Album/:slug/*',array('controller' => 'albums','action' => 'contents'),array('pass' => array('slug')));
for what doesn't match the first. In the 'contents' action of the 'albums' controller, I take care of pagination myself - meaning I retrieve the named parameter 'page'.
A URL for the second route would look like this:
http://somesite.com/Album/foo-bar/page:2
The Above URL indeed works, but when I try to use the HTML Helper (url,link) to output a url like this, it appends the controller and action to the beginning, like this:
http://somesite.com/albums/contents/Album/foo-bar/page:2
Which i don't like.
The code that uses the HtmlHelper is as such:
$html->url(array('/Album/' . $album['Album']['slug'] . '/page:' . $next))
See below url it is very help full to you
http://book.cakephp.org/2.0/en/development/routing.html
Or read it
Passing parameters to action
When connecting routes using Route elements you may want to have routed elements be passed arguments instead. By using the 3rd argument of Router::connect() you can define which route elements should also be made available as passed arguments:
<?php
// SomeController.php
public function view($articleId = null, $slug = null) {
// some code here...
}
// routes.php
Router::connect(
'/blog/:id-:slug', // E.g. /blog/3-CakePHP_Rocks
array('controller' => 'blog', 'action' => 'view'),
array(
// order matters since this will simply map ":id" to $articleId in your action
'pass' => array('id', 'slug'),
'id' => '[0-9]+'
)
);
And now, thanks to the reverse routing capabilities, you can pass in the url array like below and Cake will know how to form the URL as defined in the routes:
// view.ctp
// this will return a link to /blog/3-CakePHP_Rocks
<?php
echo $this->Html->link('CakePHP Rocks', array(
'controller' => 'blog',
'action' => 'view',
'id' => 3,
'slug' => 'CakePHP_Rocks'
));