We have moved our forum to GitHub Discussions. For questions about Phalcon v3/v4/v5 you can visit here and for Phalcon v6 here.

Routing with main category, undefined amount of sub categories and article name

Good morning,

I'm struggling with building the router for a shop system. The following should work:

  • / <- displays landing page
  • /Food <- displays all articles in Food
  • /Food/Beverages <- displays all articles in Food/Beverages
  • /Food/Beverages/Tea <- displays all articles in Food/Beverages/Tea
  • /Food/Beverages/Tea/seo-friendly-product-name <- displays the actual article
  • /Food/Sweets/seo-friendly-product-name <- displays the actual article
  • /Outdoor <- display all articles in Outdoor ...

The main categories (food, outdoor, ..) are hardcoded and can easily be matched with something like

$router->add('(Food|Outdoor|...)') [ .. 
...

The articles all have one main category, and (theoretically) an infinite amount of sub categories (Beverages, Tea, Sweets, ...). And their SEO-friendly name of course.

So, in theory, I need a route that would match on something like "get the main category", then "get an undefined amount of subcategories" and then allow one more part as the SEO name of the article, and if the latter is not provided, route to category controller.

The sub categories themselves are defined in relation to the article, so I can't hardcode them. Is it possible to maybe retrieve the categories from the database and build the routes from that?

How can I make the router to do this?

Thank you.

The problem is there's nothing different between /Food/Beverages/Tea and /Food/Sweets/seo-friendly-product-name. Without there being a rhyme or reason, there's really no way to write a regex that differentiates between those two. As far as I can figure, you have 3 options:

  1. Change the way your URLs are structured. If each product name is unique, you could have /Food/, /Food/Beverages, etc, and /seo-friendly-product-name. Of course, each product name would need to be unique.
  2. Have all the category listings go to one controller, and the rest go to a second controller. That second controller does the work of pulling apart the URL and searching each component for an SEO friendly name - then doing some logic to find the actual product to display. It could then forward (not redirect) to a different controller. This would also need to be done if viewing a subcategory
  3. Make a unique id part of the URL, so something like /Food/Beverages/Tea/uniqueid--seo-friendly-product-name. Your controller can then look for a component in the format (\d*)--.*, pull out the ID, then display the relevant article. #2 would then have to be done if someone is viewing all the articles in a subcategory.


4.0k

I solved it by generating the routes on the fly:

for($i = 0, $count = sizeof($routes); $i < $count; $i++)
{
    $parts = explode('/', $routes[$i]);
    $maincategory = array_shift($parts);

    $router->add(
        '/'.$routes[$i].'/{article}',
        [
            'controller' => 'article',
            'action' => 'show'
        ]
    );

    $router->add(
        '/'.$routes[$i],
        [
            'controller' => 'category',
            'action' => 'show',
            'subcategories' => implode('/', $parts),
            'maincategory' => $maincategory
        ]
    );
}
$router->add('(Food|Outdoor|...)') [ .. 
...

$routes comes from the database or from the Redis cache, which is updated if a new article gets published. It contains all possible "category chains", which contain at least one article. I think that is a mix of your points #2 and #3, I am not quite sure about the performance.

By the way, is there a possibility to disable the default routes?

#^/([\w0-9\_\-]+)[/]{0,1}$#u
#^/([\w0-9\_\-]+)/([\w0-9\.\_]+)(/.*)*$#u
edited Aug '20

I'm pretty sure performance will be bad because you're making a route for each article. Eventually you'll have hundreds and hundreds of routes that each need to be generated then evaluated for each request. Eventually you're going to get to a point where this becomes a bottleneck - but you'll have all those article URLs published that you can't go back.

Maybe an approach would be to have simplified routes, but in your controller, you just assume the last "part" of the URL is an article name. If the database doesn't have an article by that name, the fallback could be to assume that it's then a subcategory.

It may be too late for this, but I really think your best solution would be to standardize the formats of your URLs.

To answer your followup question: You can disable default routes by passing FALSE to the Router constructor.



4.0k
edited Aug '20

I do not generate a route for each article, but for each valid category. I'm testing with about 200 articles, of which the most sit in Food/Beverages/Coffee. $routes contains only unique full routes, not the full route for each article. The data inside $routes doesn't get build up on every request, since it's saved inside the cache. It is true that building the data inside $routes is a memory hungry task, but it only happens on request (like I said, if an article gets added in the backend). For those 200 articles, I end up with about 10 routes generated by this for loop (or actually 20 in total, 10 to match ROUTE/article and 10 to match the route to the subcategory page).

It isn't too late for changes, nothing is published yet, everything is still on my local machine ;) Therefore, I'll try to simplify the routes inside the controller, with the fallback you've mentioned. That whole loop-thing I built felt like a workaround all the time, nothing I would want to carry for eternity with me.