Ask your Symfony questions! Pay money and get answers fast! (more info)

What's the best way to deal with subcategories in url? Symfony


The problem with the category urls is the fact that I am using slugs that relates an item to a category. So each category item has a slug field for the category name.

Lets say I have these categories:


For example, to list items in the animal category, I click on the the animal category from a category list, I then get the route from the object and the URL will look like this:
/category/animal/ Items in the 'Animal' category are listed on the screen.

My routing rule looks like this: url: /category/:slug

If I drill down on Dog and click 'Golden Retriever' from the list, my url looks like this /category/dog/golden-retriever

But 'golden retriever' is a child of its parent category, animal. I would like for my URL to look like this: ''/category/animal/dog/golden retriever' when I drill down on 'golden retriever'.

So, the question is what is the best way to store the subcategory information when you only have one slug for each category_id? Is the way to do this is to have the dog category record have two slugs, one for the main category 'animal' and one its name 'dog' Should I create another field to store subcategories for each category record?

Answers (6)


Nate Flink answers:

It's a little general, but here are a couple ideas.

Anything after "/:slug" in the routing rule "/category/:slug" is considered a parameter.

In other words /category/dog is a direct reference to the dog action of the category module.

It's possible to handle the whole thing from /category/index by mapping /category/:whatever to always forward to category/index within a filter. (Need to watch out for too many forward loops by doing something like checking the action stack for the index action already existing.

Now a routing rule like /category/index/:slug1/:slug2/:slug3 becomes usable and makes it useful to mangle those values, just about anywhere in the application you would like, as parameters. Filters are a good place to do things like this. One can force urls by adding parameter requirements as well. Typically, one can grab the parameters in the action.

This question concerns the best way of "storing" subcategory information. I'm not really sure what is meant by storing it, but I don't take it as a question about how to persist the various urls which could possibly be sent, but rather what is the best means of mapping an arbitrary url/filepath with multiple levels to a slug type construct.

Depending on how many levels deep it is needed to go something like:

url: /category/:species
param: { module: category, action: levelOne }
requirements: {species: (?:dog|cat|chicken|lemur|fruitbat|pencil|frisbee)}

url: /category/:species/:family
param: { module: category, action: levelTwo }
requirements: {species: (?:dog|cat|chicken|lemur|fruitbat|pencil|frisbee), family: (?:bloodhound|bulldog|dalmation)}

etc ..

If you've got tons of different possibilities than, it might not be possible to enforce requirements, and some other checking could occur in the action or filter.


Ludovic Fleury answers:

With this kind of context I would go for Doctrine NestedSet behavior (or propel nestedset) and I would use a custom routing.

Using a generic routing like "/:categories"
And adds "/" as separator, or using another separator ( undersocre ? ), then deal manually with the categories passed to the url (explode separator, retrieve parameters as node in the nested tree).
( I would use a custom route, extending the object route, wich will implememt this logic (separator+nestedset)).

Thats how I see dynamics categories with unknown nested level.
But as Nat Flink say, you've got tons of possibilities?


Loban Rahman answers:

If I understand you right, what you REALLY want is the ability to have an unlimited nesting of categories, and then show it in the url. For example, if you have

Animal -> Dog -> Golden Retreiver

you want it to be


And if you have Animal -> Dog -> Terrier -> Miniature Bull Terrier

you want it to be


So, your Category should be using the NestedSet behavior (assuming you are using Doctrine), while your Item has one foreign key to Category.

Finally, your route for any Item's VIEW should be as follows:


Your action will obviously be loading the Item based on the ":slug" variable alone. The Item model should have a method getCategoryBreadCrumb() which will be used when generating the url for the item.

UNFORTUNATELY, I'm not 100% sure if Symfony's routing system will barf if your breadcrumb has "/" in it. Please check on that.

Finally, how to handle url's like /category/animal/dog, which is supposed to list all dogs? Well, you could make a route


but Symfony might not be able to differentiate between that and the previous route. What you could do in this case is just keep the first route. In your VIEW action above, first try and find an item with ":slug". If not, call the LIST action, giving it a category ":slug". For example,


Your view action first tries to load an item with slug "dog". When it fails, it then passes to the list action which loads all items with category "dog" (or any child categories of "dog").

Obviously, your Category model should also have a getCategoryBreadCrumb() method to use when generating the url for a category (it will include everything before the category).

Hope this makes sense.


Jimish Gamit answers:

Add one more filed in your table, let say 'parent_id'. So, now your table has 3 fields (id,slug & parent id). Now, any category who has no parent set 'parent_id' to 0 and for who has parent, set appropriate 'id' of parent category.

So suppose category 'Animal' has 2 child category 'Animal_1' and 'Animal_2'
again category 'Animal_1' has child category 'Dog' & 'Cat'. For category 'Animal_2' only child category is 'Snake'.

1 | Animal | 0
2 | Fruits | 0
3 | Animal_1 | 1
4 | Animal_2 | 1
5 | Dog | 3
6 | Cat | 3
7 | Apple | 2
8 | Snake | 4
and so on...

url: /category/:slug_1
param: { module: category, action: l_one }

url: /category/:slug_1/:slug_2
param: { module: category, action: l_two }

url: /category/:slug_1/:slug_2/:slug_3
param: { module: category, action: l_three }

Now, fetch all in single query “Select * from TABLE” in let say $cat_result; This way you don't need to make lots of query

/****Code Start****/
$cat_slug = $parent_id = array();
for($cat_result as $cat){
$cat_slug[$cat['id']] = $cat['slug'];
$parent_id[$cat['id']] = $cat['parent_id'];

for($cat_result as $cat){
$last_leaf_slug = $cat['slug'];
// use @category_level_three:
link_to($last_leaf_slug,[email protected]_level_three?slug_1=”.$cat_slug[parent_id[$cat['parent_id']]].”&slug_2=”.$cat_slug[$cat['parent_id']].”&slug_3=”.$last_leaf_slug);
// use @category_level_two:
link_to($last_leaf_slug,[email protected]_level_two?slug_1=”.$cat_slug[$cat['parent_id']].”&slug_2=”.$last_leaf_slug);
} else {
// use @category_level_one:
link_to($cat['slug'],[email protected]_level_one?slug_1=”.$cat['slug'])

/****Code End****/


Sergey Kushniruk answers:

Generate routes dynamically based on your categories:


Joshua Estes answers:

You will have in your routing yml file two different urls (or as many as you want)


and the original


you can give the different module/action or what not, might be best to use the same module/action and and check to make sure those are set.

That should work. If you don't have a sub-category-slug either update your schema.yml or use the sub-cat-id PK