How should I prepare my routes to deal with it, instead of addictional parts in url?
$routes = array(
/**
* Static
*/
'news' => new Zend_Controller_Router_Route('news/:page',
array('controller' => 'news', 'action' => 'index', 'page' => 1 )
),
/**
* Dynamic
*/
'thread' => new Zend_Controller_Router_Route(':slug/:page',
array('controller' => 'Thread', 'action' => 'index', 'page' => 1 )
),
e.g. example.com/thread-name-slug it shows thread with slug thread-name-slug but when I visit example.com/news it wants to show thread with slug news. I want static page here.
Thanks in advance.
The router matches routes in reverse order of their declaration. Given the request url /news, the router will attempt to match first against the route :slug/:page and, of course, finds a match, so it never gets to examine your news/:page route.
The solution is to reverse the order in which you declare the routes. Generally speaking, one wants to add generic routes before specific ones.
As the latest version of zendframework is 3.x I'll post a sample solution for Zf3, because it's not easy a complete article on zend routes.
Supouse you wanna centralize your admin requests by using only one controller; so you can check permisions, roles, etc in order to serve your site's admin pages.
We'll perform the next tasks:
Edit the "module.config.php" file to have a easy to read code.
Create a DefineRoutes.php file
Write a simple regular expression to set wildcard matching places for all posible admin tasks.
I'll supouse we creates an admin module properly registered in "modules.config.php" file
Editing the module.config.php file:
<?php
/**
* #Filename: zendframework/module/Admin/config/module.config.php
* The module required settings.
* #author: your name here
*/
return [
'controllers' => [
'factories' => include __DIR__ . '/ControllerFactories.php'
],
'router' => [
'routes' => include __DIR__ . '/DefineRoutes.php',
],
'view_manager' => ['template_path_stack' => [__DIR__ . '/../view',],],
];
Note: we do not use the close tag ?> in our files
Creating the "DefineRoutes.php" file.
<?php
/**
* #Filename: zendframework/module/Admin/config/DefineRoutes.php
* Declares site's admin routes
* #author: your name here
*/
namespace Admin;
use Zend\Router\Http\Segment;
// first a couple of useful functions to make our life easy:
// Creates a regexp to match all case-variants of a word
function freeCaseExt($toCase){
$len = strlen($toCase);
$out = '';
if($len < 1){ return $out; }
for ($i=0; $i<$len; $i++){
$s = strtolower(substr($toCase, $i, 1));
$out .= '['.$s.strtoupper($s).']';
}
return $out;
}
// To append slash child routes elsewhere
function SlashUri($controller, $action){
return [
'type' => \Zend\Router\Http\Literal::class,
'options' => [
'route' => '/',
'defaults' => ['controller' => $controller, 'action' => $action ]]];
}
$adminvariants = freeCaseExt('admin'); // to constrain our main route
// Our route family tree:
'admin' => [
'type' => Segment::class,
'options' => [
'route' => '/:admin[/:case][/:casea][/:caseb][/:casec][/:cased][/:casee][/:casef][/:caseg][/:caseh]',
'constraints' => [
'admin' => $adminvariants,
'case' => '[a-zA-Z0-9][a-zA-Z0-9_-]*',
'casea' => '[a-zA-Z0-9][a-zA-Z0-9_-]*',
'caseb' => '[a-zA-Z0-9][a-zA-Z0-9_-]*',
'casec' => '[a-zA-Z0-9][a-zA-Z0-9_-]*',
'cased' => '[a-zA-Z0-9][a-zA-Z0-9_-]*',
'casee' => '[a-zA-Z0-9][a-zA-Z0-9_-]*',
'casef' => '[a-zA-Z0-9][a-zA-Z0-9_-]*',
'caseg' => '[a-zA-Z0-9][a-zA-Z0-9_-]*',
'caseh' => '[a-zA-Z0-9][a-zA-Z0-9_-]*'
],
'defaults' => [
'controller' => Controller\AdminController::class,
'action' => 'index'
]
],
'may_terminate' => TRUE,
'child_routes' => [
'adminslash' => SlashUri(Controller\AdminController::class, 'index'),
]
],
// Now you have declared all the posible admin routes with or without
// slaches '/' at 9 deep levels using the AdminController::Index() method
// to decide wath to do.
IMPORTANT: As we defined a first level wildcard :admin a proper constraint is required or it overlaps other first level routes.
The controllers logics is a few out of skope.
Hope this idea helps somebody.
Luis
Related
I have a site that needs to allow multiple URL structures. For example:
www.examplesite.com/people/add // <-- example company
www.otherexample.com/xyz/people/add // <-- "xyz" company (not location based)
www.otherexample.com/florida/abc/people/add //<-- "abc" company (location based)
Each URL should be able to detect which company it is based on the URL.
So far, I've been able to parse out the URL just fine to determine which company it is, but how to I add these extra /florida/abc/ parts to the routes to allow the rest of the app to work?
I've tried a number of things including setting a variable to the '/florida/abc' (or whatever it is) at the top of the routes file, then adding that before each route, but that doesn't handle every controller/action and seems very hit or miss/buggy.
I also use the admin prefix, so for example, it would also need to work like this:
www.otherexample.com/admin/florida/abc/people/add
My assumption is that I need to use the routes.php file, but I can't determine within that how I can make this happen.
I used that approach in the web application farm.ba (not more maintained by the owner).
What I did:
Create table "nodes" with fields id, slug, model, foreign_key, type, ..
Create custom route (1),(2) class that handles Node model
After save post, store and cache slug in Node Model
After deleting the post, delete the cache and node records
This works much like wordpress routing, allows you to enter custom slug, etc.
EDIT:
Create custom route class in App/Lib/Routing/Router/MultiRoute.php like:
<?php
App::uses('CakeRoute', 'Routing/Route');
/**
* MultiRoute
*/
class MultiRoute extends CakeRoute
{
public function parse($url)
{
// debug($url); '/florida/abc/people/add'
// Add custom params
$params = array(
'location' => null,
'company' => null,
'controller' => 'peoples',
);
$params += parent::parse($url);
// debug($params);
/**
* array(
* 'location' => null,
* 'company' => null,
* 'controller' => 'peoples',
* 'named' => array(),
* 'pass' => array(
* (int) 0 => 'florida', // location
* (int) 1 => 'abc', //company
* (int) 2 => 'people', // controller
* (int) 3 => 'add' // action, default index
* ),
* 'action' => 'index',
* 'plugin' => null
* )
*
*/
// reverse passed params
$pass = array_reverse($params['pass']);
// debug($pass);
/**
* array(
* (int) 0 => 'add',
* (int) 1 => 'people',
* (int) 2 => 'abc',
* (int) 3 => 'florida'
* )
*/
if(isset($pass[3])) { $params['location'] = $pass[3]; }
if(isset($pass[2])) { $params['company'] = $pass[2]; }
// if you need load model and find by slug, etc...
return $params;
}
public function match($url)
{
// implement your code
$params = parent::match($url);
return $params;
}
}
in routes.php
App::uses('MultiRoute', 'Lib/Routing/Route');
Router::connect('/admin/*',
array('admin' => true),// we set controller name in MultiRoute class
array('routeClass' => 'MultiRoute')
);
Router::connect('/*',
array(),// we set controller name in MultiRoute class
array('routeClass' => 'MultiRoute')
);
In your controller find results using extra request params, like:
$this->request->location;
$this->request->company;
I hope this is helpful.
Creating a route for each case seems the way to go:
Router::connect('/people/add', array('controller' => 'people', 'action' => 'add'));
Router::connect('/:company/people/add', array('controller' => 'people', 'action' => 'add'), array('pass' => array('company'), 'company' => '[a-z]+'));
Router::connect('/:location/:company/people/add', array('controller' => 'people', 'action' => 'add'), array('pass' => array('company', 'location'), 'company' => '[a-z]+', 'location' => '[a-z]+'));
Then the controller can receive these values:
public function add($company = '', $location = '') {
var_dump($company, $location); exit;
}
Mind the regex in routes and amend to match your incoming data.
Working with Yii 2.0.4, I'm trying to use urlManager Rule to preload an object based on a given ID in the URL.
config/web.php
'components' => [
'urlManager' => [
[
'pattern' => 'view/<id:\d+>',
'route' => 'site/view',
'defaults' => ['client' => Client::findOne($id)],
],
[
'pattern' => 'update/<id:\d+>',
'route' => 'site/update',
'defaults' => ['client' => Client::findOne($id)],
],
]
]
If this works, it will not be necessary to manually find and object each time, for some CRUD actions:
class SiteController extends Controller {
public function actionView() {
// Using the $client from the urlManager Rule
// Instead of using $client = Client::findOne($id);
return $this->render('view', ['client' => $client]);
}
public function actionUpdate() {
// Using $client from urlManager Rule
// Instead of using $client = Client::findOne($id);
if ($client->load(Yii::$app->request->post()) && $client->save()) {
return $this->redirect(['view', 'id' => $client->id]);
} else {
return $this->render('edit', ['client' => $client]);
}
}
}
NOTE: The above snippets are not working. They're the idea of what I want to get
Is it possible? Is there any way to achieve this?
If you look closer: nothing actually changes. You still call Client::findOne($id); but now doing it in an unexpected and inappropriate place, and if you look at the comment about default parameter it says:
array the default GET parameters (name => value) that this rule provides.
When this rule is used to parse the incoming request, the values declared in this property will be injected into $_GET.
default parameter is needed when you want to specify some $_GET parameters for your rule. E.g.
[
'pattern' => '/',
'route' => 'article/view',
'defaults' => ['id' => 1],
]
Here we specify article with id = 1 as default article when you open main page of site e.g. http://example.com/ will be handled as http://example.com/article/view?id=1
I can suggest to you add property clientModel in to your controller and then in beforeAction() method check if its update or view action then set
$this->clientModel = Client::findOne($id);
and in your action:
return $this->render('view', ['client' => $this->clientModel]);
I would like to make navigation buttons in my view, for example index.phtml but it's not working. I did know how to do it in Zend1 but in Zend2 I have a problem. My code looks like this (file index.phtml):
$container = new \Zend\Navigation\Navigation($tableActions);
var_dump($container);
echo '<div class="table-column">';
echo $this->navigation($container)->menu();
echo '</div>';
Variable $tableAction looks like this:
public $tableActions = array(
array(
'label' => 'On/Off',
'module' => 'import',
'controller' => 'import',
'action' => 'setstatus',
'params' => array('id' => null),
),
);
I did not get any error, just whole site die on this line. var_dump returns object(Zend\Navigation\Navigation) so it's fine so far. Problem is, how to show it...
The navigation pages have dependencies which aren't being met by just creating a new container class in a view. The Mvc page needs a RouteStackInterface (Router) instance and a RouteMatch instance. Similarly Uri pages need the current Request instance.
You can see this clearly if you take a look at the Zend\Navigation\Service\AbstractNavigationFactory and its preparePages and injectComponents methods.
The view is not the right place to be instantiating menus, instead put the menu configuration spec in your module.config.php...
<?php
return array(
'navigation' => array(
'table_actions' => array(
array(
'label' => 'On/Off',
'module' => 'import',
'controller' => 'import',
'action' => 'setstatus',
'params' => array('id' => null),
),
),
),
);
Write a factory extending the AbstractNavigationFactory class and implement the getName() method which returns the name of your menu spec key (table_actions in this example)
<?php
namespace Application\Navigation\Service;
use Zend\Navigation\Service\AbstractNavigationFactory;
class TableActionsFactory extends AbstractNavigationFactory
{
/**
* #return string
*/
protected function getName()
{
return 'table_actions';
}
}
Map the factory to a service name in the service_manager spec of module.config.php ...
<?php
return array(
'navigation' => array(// as above ... )
'service_manager' => array(
'factories' => array(
'TableActionsMenu' => 'Application\Navigation\Service\TableActionsFactory',
),
),
);
Now you can call the view helper using the service name TableActionsMenu you just mapped
<div class="table-column">
<?php echo $this->navigation('TableActionsMenu')->menu(); ?>
</div>
Finally, if, as I suspect, you need to change an attribute of the page depending on the view, you can do that too, navigation containers have find* methods which can be accessed from the navigation helper and used to retrieve pages.
Here's an example looking for the page with a matching page label, then changing it before rendering (obviously not an ideal search param, but it gives you the idea)
$page = $this->navigation('TableActionsMenu')->findOneByLabel('On/Off');
$page->setLabel('Off/On');
// and then render ...
echo $this->navigation('TableActionsMenu')->menu();
I've got two or more routes that will be going to the same controller and action. This is fine until I want to use a helper such as the form helper or pagination on the page.
What happens is that the current url changes to whatever is declared first in my routes.php file.
I see there is a way to promote a router with Router::promote but I'm not sure if I can do it based on the current url or router being used or if there's a bett way to do this.
Here's an example of what my router.php looks like:
Router::connect('/cars-for-sale/results/*', array('controller' => 'listings', 'action' => 'results'));
Router::connect('/new-cars/results/*', array('controller' => 'listings', 'action' => 'results'));
Router::connect('/used-cars/results/*', array('controller' => 'listings', 'action' => 'results'));
Let's say for example that I'm at the url domain.com/used-cars/results/ and I'm using the form helper or pagination helper, the url that is being put in the action or href is domain.com/cars-for-sale/results/.
Any help or info would be appreciated.
Routes should be unique and identifiable!
The problem with these Routes is that, basically, you created duplicate URLs not only does this cause problems with CakePHP picking the right route, Google doesn't like that as well; duplicated content will have a negative effect on your SEO ranking!
In order to pick the right URL (Route), CakePHP should be able to do so, based on its parameters; your current Routes do not offer any way to distinguish them.
And neither does your application!
All these URLs will present the same data;
/cars-for-sale/results/
/new-cars/results/
/used-cars/results/
Solution 1 - separate actions
If your application is limited to these three categories, the easiest solution is to create three actions, one per category;
Controller:
class ListingsController extends AppController
{
const CATEGORY_NEW = 1;
const CATEGORY_USED = 2;
const CATEGORY_FOR_SALE = 3;
public $uses = array('Car');
public function forSaleCars()
{
$this->set('cars', $this->Paginator->paginate('Car', array('Car.category_id' => self::CATEGORY_FOR_SALE)));
}
public function newCars()
{
$this->set('cars', $this->Paginator->paginate('Car', array('Car.category_id' => self::CATEGORY_NEW)));
}
public function usedCars()
{
$this->set('cars', $this->Paginator->paginate('Car', array('Car.category_id' => self::CATEGORY_USED)));
}
}
Routes.php
Router::connect(
'/cars-for-sale/results/*',
array('controller' => 'listings', 'action' => 'forSaleCars')
);
Router::connect(
'/new-cars/results/*',
array('controller' => 'listings', 'action' => 'newCars')
);
Router::connect(
'/used-cars/results/*',
array('controller' => 'listings', 'action' => 'usedCars')
);
Solution 2 - Pass the 'category' as parameter
If the list of URLs to be used for the 'listings' will not be fixed and will expand, it may be better to pass the 'filter' as a parameter and include that in your routes;
routes.php
Router::connect(
'/:category/results/*',
array(
'controller' => 'listings',
'action' => 'results',
),
array(
// category: lowercase alphanumeric and dashes, but NO leading/trailing dash
'category' => '[a-z0-9]{1}([a-z0-9\-]{2,}[a-z0-9]{1})?',
// Mark category as 'persistent' so that the Html/PaginatorHelper
// will automatically use the current category to generate links
'persist' => array('category'),
// pass the category as parameter for the 'results' action
'pass' => array('category'),
)
);
Read about the Router API
In your controller:
class ListingsController extends AppController
{
public $uses = array('Car');
/**
* Shows results for the specified category
*
* #param string $category
*
* #throws NotFoundException
*/
public function results($category = null)
{
$categoryId = $this->Car->Category->field('id', array('name' => $category));
if (!$categoryId) {
throw new NotFoundException(__('Unknown category'));
}
$this->set('cars', $this->Paginator->paginate('Car', array('Car.category_id' => $categoryId)));
}
}
And, to create a link to a certain category;
$this->Html->link('New Cars',
array(
'controller' => 'listings',
'action' => 'results',
'category' => 'new-cars'
)
);
Firstly, Kohana's documentation is terrible, before people go "read the docs" I have read the docs and they don't seem to make much sense, even copying and pasting some of the code doesn't work for some things in the documentation.
With that in mind, I have a route like so:
//(enables the user to view the profile / photos / blog, default is profile)
Route::set('profile', '<userid>(/<action>)(/)', array( // (/) for trailing slash
"userid" => "[a-zA-Z0-9_]+",
"action" => "(photos|blog)"
))->defaults(array(
'controller' => 'profile',
'action' => 'view'
))
This enables me to go http://example.com/username and be taken to the users profile, http://example.com/username/photos to be taken to view the users photos and http://example.com/username/blog to view the blog.
If somebody goes http://example.com/username/something_else I want it to default to the action view for the user specified in <userid> but I can't seem to find any way of doing this.
I could do it like this:
Route::set('profile', '<userid>(/<useraction>)(/)', array(
"userid" => "[a-zA-Z0-9_]+",
"useraction" => "(photos|blog)"
))->defaults(array(
'controller' => 'profile',
'action' => 'index'
))
then in the controller do this:
public function action_index(){
$method = $this->request->param('useraction');
if ($method && method_exists($this, "action_{$method}")) {
$this->{"action_{$method}"}();
} else if ($method) {
// redirect to remove erroneous method from url
} else {
$this->action_view(); // view profile
}
}
(it might be better in the __construct() function but you get the gist of it.)
I'd rather not do that though if there is a better method available (which there really should be)
I think the answer might be in the regex but the following does not work:
$profile_functions = "blog|images";
//(enables the user to view the images / blog)
Route::set('profile', '<id>/<action>(/)', array(
"id" => "[a-zA-Z0-9_]+",
"action" => "($profile_functions)",
))->defaults(array(
'controller' => 'profile'
));
Route::set('profile_2', '<id>(<useraction>)', array(
"id" => "[a-zA-Z0-9_]+",
"useraction" => "(?!({$profile_functions}))",
))->defaults(array(
'controller' => 'profile',
'action' => 'view'
));
although it does match when nothing is after the ID.
I would set up the route like this:
Route::set('profile', '<userid>(/<action>)(/)', array(
"userid" => "[a-zA-Z0-9_]+",
"action" => "[a-zA-Z]+"
))->defaults(array(
'controller' => 'profile',
'action' => 'index'
))
And then in the controllers before() method:
if(!in_array($this->request->_action, array('photos', 'blog', 'index')){
$this->request->_action = 'view';
}
Or somethig similiar, just validate the action in the controller...
EDIT:
This could also work:
if(!is_callable(array($this, 'action_' . $this->request->_action))){
$this->request->_action = 'view';
}