I am using CakePHP 1.3. I have a Product model. on the DB table among others there are id and slug fields.
If I have a product that is id:37 and slug:My-Product-Title I want the URL for the product to be:
products/37/My-Product-Title
Instead of the standard:
products/view/37
I created a route that looks like this:
Router::connect(
'/products/:id/:slug',
array('controller' => 'products', 'action' => 'view'),
array('pass' => array('id'), 'id' => '[0-9]+')
);
Now I can go to http://server/products/37/My-Product-Title and it takes me to the right place.
But How do I get reverse routing to automatically build the correct URL in $HtmlHelper->link?
When I use:
echo $html->link(
'Product 37',
array('controller'=>'products', 'action' => 'view', 37)
);
It still outputs the standard products/view/37 url.
I don't believe that it's possible to be done auto-magically. The helper is just an "helper" who builds the link from the given parameters.
So the easiest method is to add another parameter in your link like so:
echo $html->link(
'Product 37',
array('controller'=>'products', 'action' => 'view', 37, $slug)
);
where the $slug is the data from the slug field.
Probably it could be done your idea, but you need to break the MVC pattern very badly :)
Edit:
Reading your question again I understood it well. See how should be done:
in your router.php add the following rule:
Router::connect(
'/product/*',
array('controller' => 'products', 'action' => 'view')
);
Please note that it's /product/* rather than /products/*
Your link should be done like this:
echo $html->link(
'Product 37',
array('controller'=>'products', 'action' => 'view', 37, 'my-product-title')
);
and the link would look like:
http://yourdomain.com/product/37/my-product-title
For me doing your suggestion is bad practice. Also I don't think it's good from SEO point of view redirecting always the user.
For routing:
Router::connect(
'/products/:id/:slug',
array('controller' => 'products', 'action' => 'view'),
array('pass' => array('id'), 'id' => '[0-9]+')
);
Your links should look like this:
echo $html->link(
'Product 37',
array('controller'=>'products', 'action' => 'view', 'id' => 37, 'slug' => 'my-product-title')
);
You have to add additional (key => value) to your array for each :param in your routing. Then magic will work
You should look at the following post regarding custom route classes.
The slug data doesn't need to be involved with the database at all - the field is a fake field used to simplify logic and lookups. This solution allows you to reverse route slugs, without needing a slug field in the models table.
http://42pixels.com/blog/slugs-ugly-bugs-pretty-urls
I am not sure how bad this is but with the following code in the ProductsController:
function view($id)
{
if( isset($_SERVER) && stristr($_SERVER["REQUEST_URI"],'view/') )
{
$this->Product->id = $id;
$slug = $this->Product->field('slug');
$this->redirect($id.'/'.$slug);
}
$data = $this->Product->find('first', array('conditions' => array('Product.id' => $id)));
$this->set("data", $data);
}
If the page is accesses via /view/id it automatically redirects them to the current page using /id/slug
Now I can just use the default link scheme:
echo $html->link(
'Product 37',
array('controller'=>'products', 'action' => 'view', 37)
);
and they will be redirected to the right URL.
Only problem is I am not sure how bad it is to have a redirect happening every time a user visits a product page?
Related
I want to make my url seo friendly. www.example.com/posts/view/1 change for www.example.pl/:slug-:id. Everything works fine, but probably I'm doing something wrong with routing, because when after clicking the urls in paginator, the url is correct, it looks like www.example.pl/:slug-:id , but it appears an error
"The requested address 'www.example.pl/:slug-:id' was not found on this server."
I don't know what's wrong. Here's my code:
Router::connect(
'/:slug-:id',
array(
'controller' => 'posts',
'action' => 'view'
),
array(
'pass' => array('slug' , 'id'),
'id' => '[0-9]+'
)
);
in paginator view:
echo $this->Html->link($ad['Post']['title'], array(
'controller' => 'posts',
'action' => 'view',
'slug' => Inflector::slug($post['Post']['title'],'-'),
'id'=>$post['Post']['id'])
);
I solved the problem.
Its too simple i'll give you an example from my project ..
in your routes.php
Router::connect(
'/:slug-:id',
array('controller'=>'posts','action'=>'view'),
array('pass'=>array('slug','id'),'slug'=>'[a-zA-Z0-9 -]+','id'=>'[0-9]+')
);
your link in views should be like .
$this->Html->link(__('link desu'),array('controller'=>'posts','action'=>'view','id'=>$post['Post']['id'],'slug'=>$post['Post']['slug']));
and your PostsController.php
public function view($slug,$id){
$this->Post->id = $id;
// ....
}
Quick tip : try to create an array in your PostModel to avoid creating it every time in your view .
example :
Post.php
class Post extends AppModel{
// ....
public function afterFind($results,$primary = false){
foreach ($results as $key => $value) {
if(isset($value[$this->alias]['id'])){
$results[$key][$this->alias]['url'] = array(
'controller'=>'posts',
'action'=>'view',
'id'=>$results[$key][$this->alias]['id'],
'slug'=>$results[$key][$this->alias]['slug']
);
}
// ....
}
return $results;
}
}
}
so you can call it in your view simply like that
$this->Html->link(__('link desu'),$post['Post']['url']);
It's probably a problem with the regex on the route. Your slug contain hyphens - which you also use to separate between the slug and the id. i.e.:
example.com/my-slug-has-hyphens-1
The regex is not smart enough to know that the "last" hyphen separates the slug from the id.
To test if this is the problem, try using a route like this '/:slug__:id', just to see if it works.
I solved the problem. In the posts controller my view function was wrong. Here's right correct:
function view($id = null, $slug = null) {
$this->Post->id = $this->params['post'];
$this->set('post', $this->Post->read());
Pass is order sensitive
In the question the route is as follows:
Router::connect(
'/:slug-:id',
array(
'controller' => 'posts',
'action' => 'view'
),
array(
'pass' => array('slug' , 'id'), # <-
'id' => '[0-9]+'
)
);
That means the post function will recieve:
public function view($slug, $id)
As indicated by the edited question, the code is expecting the id to be the first parameter. The easiest solution is simply to specify the passed parameters in the order that they are expected:
...
'pass' => array('id', 'slug'), # <-
Router::connect(
'/:slug/:id',
array(
'controller' => 'posts',
'action' => 'view'
),
array(
'pass' => array('slug' , 'id'),
'id' => '[0-9]+'
)
);
the above code will create correct link as www.example.com/posts/view/title/1
echo $this->Html->link($post['Post']['title'], array('controller' => 'posts', 'action' => 'view', Inflector::slug($post['Post']['title'],'-'),$post['Post']['id']));
I am having problems trying to get my cake application to display items from the database on a single page based on the url of the page... For example I want to display items that are in the "Party" category and I type "mydomain.com/All/party" in the address bar and I get a list of all the items in that category on that page.
This is my code but the pages I am getting from the route is blank:
My routes.php:
Router::connect('/categories/:name', array(
'controller' => 'All', 'action' => 'categories'
),
array(
'pass' => 'catname',
'catname' => '[a-zA-Z0-9]+'
));
my AllController.php:
function categories($catname = null) {
$options = array(
'conditions' => array('Category.name'=>$catname)
);
$Category = $this->Category->find('all', $options);
$this->set('Category', $Category);
$this ->render('/All/categories');
}
Any help would be appreciated.
I don't get at all the code you just posted, but what you want to achieve is quite trivial:
Router::connect(
'/products/:category',
['controller' => 'products', 'action' => 'categorized'],
['pass' => ['category']]
);
In ProductsController:
function categorized($category) {
$conditions = ['Category.name' => $category];
$this->set('products', $this->Prodcut->find('all', compact('conditions')));
}
That's it. You may want to reuse the index action if you don't need a separate view template for that, in that case just set the conditions (for example to the Paginator component) and call $this->setAction('index')
I found a much easier way to do what I wanted. I just remember that I had a database search option that allows persons to search for products and categories. What I did was simply do a search for the categories and then copy the URL of the page I got and set that as a link to the category in my application.
Example code:
<ul class="filter-options">
<li><a style="color:#fff" href="http://mydomain.com/products/search?q=parties">Parties</a></li>
<li><a style="color:#fff" href="http://mydomain.com/products/search?q=festivals">Festivals</a></li>
<li><a style="color:#fff" href="http://mydomain.com/products/search?q=theatres">Cinemas/Theatres</a></li>
Everything looks good on your side just change your routes.php as follows
Router::connect('/categories/:catname', array(
'controller' => 'all',
'action' => 'categories'),
array('pass' => array('catname'),
'catname' => '[a-zA-Z0-9]+'));
I have some simple pagination set up in my controller like so:
public function index() {
$this->set('items', $this->paginate());
}
In my view I'm using the Paginator helper to output numbered links:
echo $this->Paginator->numbers(array(
'separator' => '',
'tag' => 'li',
'currentTag' => 'a',
'currentClass' => 'active'
));
This all works fine, however I want to use a custom URL for the paginated links. I added this to my routes.php file:
Router::connect('/things/:page', array('controller' => 'things', 'action' => 'index'), array('page' => '[0-9]+'));
Now the links outputted by the Paginator helper are the way I want. They look like http://mysite.com/things/2, http://mysite.com/things/3 etc.
But when I click the links the Paginator in my controller doesn't seem to recognize it's on a certain page, as I just get the first page's results shown. I'm guessing I need to somehow pass the page number to $this->paginate(), but I don't know what the best method is. Is there a way for Cake to get the page number automatically if I modify my route?
Thanks!
Since the Pagination component by default expect named parameter 'page:1' you need somehow to pass same variable in index.
if you make print of $this->request->params in your controller, you will see that it's missing.
See that example how you can pass named parameters in the Router:
Router::connect(
'/:controller/:action/*',
array(),
array(
'named' => array(
'wibble',
'fish' => array('action' => 'index'),
'fizz' => array('controller' => array('comments', 'other')),
'buzz' => 'val-[\d]+'
)
)
);
For more info see this section in Cakephp book
I obviously have a fundamental misunderstanding of how pagination works in CakePHP.
I have the following route set up which shows all posts in a category:
Router::connect('/:parent/:category',
array('controller' => 'posts', 'action' => 'viewCategory'),
array('parent' => '[a-z0-9-]+', 'category' => '[a-z0-9-]+'));
The pages work fine, however the pagination helper is outputting the wrong links for pagination.
I'm using $this->Paginator->numbers().
It's outputting links in this format: mysite.com/posts/viewCategory?page=2
rather than like this: mysite.com/parent-category/sub-category?page=2.
I've tried adding the following route after the first one and it still doesn't work:
Router::connect('/:parent/:category/:page',
array('controller' => 'posts', 'action' => 'viewCategory'),
array('parent' => '[a-z0-9-]+',
'category' => '[a-z0-9-]+',
'page' => '[0-9]+'));
For reference, my pagination options set in my view are as so:
<?php $this->Paginator->options(
array('url' =>
array('controller' => 'posts', 'action' => 'viewCategory')
)); ?>
What am I doing wrong here?
You are setting the url yourself
This is your paginator options call:
<?php
$this->Paginator->options(array(
'url' => array(
'controller' => 'posts',
'action' => 'viewCategory'
)
));
?>
Where you are overriding the current url - and explicitly requesting that the paginator uses the the '/posts/viewCategory' url (with no arguments) as it's base url.
Just don't define the url
Simply don't call options and the helper will use the current url - that should mean that if the current url is:
/parent-category/sub-category
Then page 2 will be (assuming you are using the paramType option to use GET arguments rather than named parameters):
/parent-category/sub-category?page=2
If that's not the case there's information missing from the question; it's important to distinguish between "vanity routes not being used" and "the url is not equivalent (the current situation).
Just had a battle fixing something similar and came across this post. Though old, but I think my answer might save someone the time I had to spend fixing it.
Basically, what you need to do is call the Paginator->options() before Paginator->numbers(), thus:
$this->Paginator->options(
array(
'controller' => 'parent-category',
'action' => 'sub-category'
)
);
Though the controller and action do not exist, it just tricks CakePHP to use them "AS IS", since the reverse routing isn't working!
And for those (like me), who want have set up a route similar to
Router::connect(
'/go/page:id',
array(
'controller' => 'blog',
'action' => 'paginated'
)
);
There might be difficulty setting up the Paginator options. This, however, worked for me:
$this->Paginator->options(
array(
'controller' => 'go',
'action' => '/'
)
);
I guess you know why it worked ;)
I have built a simple portfolio using CakePHP and it has urls like: domain.com/portfolio/82/This_is_an_item
What I want to do is remove the ID from the url. How would I do this?
Here is my controller code for the view:
function view ( $id, $slug )
{
$post = $this->Portfolio->read(null, $id));
$this->set(compact('post'));
}
and here is the link generator:
<?php echo $this->Html->link($post['Portfolio']['title'],
array('admin' => false, 'controller' => 'portfolio', 'action' => 'view', $post['Portfolio']['id'], Inflector::slug($post['Portfolio']['title'])),
array('title' => $post['Portfolio']['title'])); ?>
I'm guessing I need to change the controller method to do some sort of find on the title?
Any help would be much appreciated. Thanks
I would save the slug in the database, along with the title. That way you only have to create it once. Also you may or may not be able to get a unique link from slug to title, so it's best not to try.
For easy processing you can use the Sluggable behaviour, see https://gist.github.com/338096 (or just Google).
Update
If you use the sluggable behaviour from https://gist.github.com/338096 (save as sluggable.php in app/model/behaviors), you would only need to do a few steps:
In your Profile model class add var $actsAs = array('Sluggable'); or
var $actsAs = array(
'Sluggable' => array(
'fields' => 'title',
'scope' => false,
'conditions' => false,
'slugfield' => 'slug',
'separator' => '-',
'overwrite' => false,
'length' => 256,
'lower' => true
)
);
if you want to override settings
In the database, add a column slug in the profiles table.
When you save a profile, it will automagically add fill in the slug field, you do not need to take any special actions.
You can eliminate the id completely, but you'll have to make sure the slugs are unique (specify inUnique validation rule is a option). When saving the post, use Inflector::slug() on the 'title' field (you might want to save it to a 'slug' field, if you want to keep the title intact:
$this->data['Portfolio']['slug'] = Inflector::slug($this->data['Portfolio']['title'])
function view ($slug ){
$post = $this->Portfolio->find('first', array('conditions'=>array('Portfolio.slug'=>$slug))));
$this->set(compact('post'));
}
and for the link:
<?php echo $this->Html->link($post['Portfolio']['title'],
array('admin' => false, 'controller' => 'portfolio', 'action' => 'view', $post['Portfolio']['slug']),
array('title' => $post['Portfolio']['title']));