Chapter 12: Model Routes
According to Wikipedia, "routing" simply means "forwarding". To better understand this, let's consider the example of company links that we discussed in the last chapter. Let's say we want to create a detail page for each company. They will probably have the same content, viz. the name, organization number, address and the recycling types they deal with. If we are sure that there will be one or two companies then we could create two pages for each of these companies. But here lies the problem. We don't know how many companies there will be. We want to show company information as they are added to the database and we don't want to create a page for each of them. So the solution would be to create one template page (aka. placeholder page) for company details and detect (from the URL) which company is the current context. Hence, when a company URL is followed, Curry must "forward" control to the placeholder page and also pass along the company_id
in the same request. Curry does this with the Model Route
feature.
Let us use Propel's sluggable behavior to generate slugs for company page links. Modify the company
schema as follows:
<table name="company">
<column name="company_id" type="INTEGER" required="true" autoIncrement="true" primaryKey="true" />
<column name="name" type="VARCHAR" size="255" required="true" primaryString="true" />
<!-- Code removed to focus on relevant portions -->
<behavior name="sluggable" />
</table>
We added the behavior to the company table. The behavior will create a new column named slug
and populate it will a string generated from the name
column. We added the primaryString
attribute to the name
column.
I will not "rebuild" the database from now onward but instead "migrate". Here are the steps to migrate from your terminal or console.
./propel-gen diff
./propel-gen migrate
./propel-gen om
Sweet! No need to backup your database.
Sometimes you may need to create a new table with migrations. In that case you will need to do a ./propel-gen main
or simply ./propel-gen
in addition to the steps mentioned above.
If you check the Company
table in the backend (admin.php?module=Curry_Backend_Database&view=Table&table=Company
) you will see the slug
fields empty. Go to the Companies
backend module (admin.php?module=Project_Backend_Companies
), edit and save the companies. Don't enter anything into the Slug field. You should edit the module form code to remove the slug
field from showing in the form.
Now let's create a "placeholder" page to show company details.
Next, modify the getUrl
method in the Company
model class:
public function getUrl()
{
return 'demo/company/'.$this->getSlug().'/';
}
Maybe you should rebuild the index now and see what the company page links look like. When you click them you get a "Page not found" error. Let's fix this.
Go to the Properties tab in the Company placeholder page. In the Advanced section, type Company
in the Model route
field. Company
is the model class name. By this, I am informing Curry that the last part of the pathname is a "slug" and Curry should check the Company
model for a matching value. If a match is found, remove the slug part from the URL and whatever remains is the actual URL which should match one of the pages where "Model route" matches "Company". If more than one match is found (should not happen unless you have a badly structured page tree) then pick one and "forward" to that page. Also, pass the object's primary key as a query parameter whose key has the same name as the "primaryKey" field in the schema.
Now you create a module and attach it with a template to render the current company's detail. Just to give you a hint, I'll write a small snippet:
public function toTwig()
{
$r = $this->getRequest();
if (!$r->hasParam('company_id')) {
throw new Exception('Company not found');
}
$q = CompanyQuery::create()
->filterByCompanyId($r->getParam('company_id'));
// Do you know why I am not passing a model instance but instead
// passing a ModelCriteria object? I explained this in a previous chapter.
return array(
'q' => $q,
);
}
In the template, you can access the model instance from the Curry_Twig_QueryWrapper
object as follows:
{# If the query was a collection, you should iterate over it with the for loop.
But we want to acquire just one model instance. This is how we do it.
#}
{% set iterator = q.Iterator %}
{% set company = iterator.current %}
I will leave this to you as an exercise.
Now another question might arise in your mind. Can we do an "OR" operation here - for e.g. can we check whether the slug is a company or a quote? My answer is "NO". No you cannot do such an operation with "Model route". If you want something complex like that you need to write code to handle a custom route.
Custom routes
Let's handle the case when the slug is either a city slug or a company slug. If the slug is a city slug then we list companies from that city. If it's a company slug then we show detail information for that company.
Clearly, from the specification above you cannot handle this case with Model route. Model route can handle a simple case. This case has an "OR" condition. Hence we are going to create a custom router.
Add the sluggable
behavior to the city
table and do the migration. Next, add another "placeholder" page:
When you create a custom route you create a new "router" class which implements the Curry_IRoute
interface. You have to define only one method viz. route()
. Please see the documentation in the source code for Curry_IRoute::route
. The return values are well explained.
This "router" class must be executed just after the CMS initializes. Why? because it needs to handle the URL and do the necessary routing or "forwarding". It's only after the "routing" is handled that the CMS knows which page to render or whether to throw a 404 Not found error. So, if I look at the source code in www/index.php
I see a line that reads Curry_Application::getInstance()->run();
. This Application
class is a singleton
. Now, I can't go about "hacking" the core just to execute my router class there. Curry provides you with a Project_Application
class. Let's put our initialization code there instead. I will modify the Project_Application
class as follows:
<?php
class Project_Application extends Curry_Application
{
public function __construct()
{
parent::__construct();
$this->setupRoutes();
}
/**
* TODO: Setup your custom routes here.
*/
protected function setupRoutes()
{
$this->addRoute(new Project_CityOrCompanyRoute());
}
}
Now create a new CityOrCompanyRoute
class and type the following code:
<?php
class Project_CityOrCompanyRoute implements Curry_IRoute
{
public function route(Curry_Request $request)
{
$path = $request->getUrl()->getPath();
// URL of the placeholder page.
$base = 'demo/customroute/';
$m = array();
if (preg_match('@^'.preg_quote($base, '@').'([^/]+)/$@', $path, $m)) {
$slug = $m[1];
try {
// Check whether the slug exists in either the "city" or "company" table.
// FIXME: this is something like a "Null coalescing" expression (php7 has ??)
// FIXME: I will be glad if somebody tells me what this "ternary" expression is called.
$o = CityQuery::create()->findOneBySlug($slug) ?: CompanyQuery::create()->findOneBySlug($slug);
if ($o) {
// Add custom query params so that our module can identify them.
$request->setParam('get', 'object_id', $o->getPrimaryKey());
$request->setParam('get', 'object_class', get_class($o));
// we will "forward" to the same page as defined in $base
$next = $base;
$request->setUri($next);
// forward/route to the page.
return true;
}
} catch (Exception $e) {
// TODO: Log exception
}
}
// do not re-route
return false;
}
}
Finally, create another module to do the processing as we did in the last exercise. I'll leave this to you as an exercise too. I have written the modules in the demo project but try not to cheat. You should be able to write code by now.
Test now:
demo/customroute/bombayworks-recycling/
should list detail for Bombayworks Recyclingdemo/customroute/stockholm/
should list detail of companies in Stockholmdemo/customroute/solna/
should list nothing.