Chapter 10.2: Curry_Twig_QueryWrapper and Curry_OnDemand
It is not a good idea to pass a PropelObjectCollection to the Twig template. The reason I write this is because there are some dangerous methods defined on a Propel object. Let's say you "innocently" pass a PropelObjectCollection
to the Twig template attached with a module and carelessly do something like this:
{% for obj in my_collection %}
{{ obj.delete }}
{% endfor %}
When you refresh your web page, all model instances present in my_collection
will be deleted. Now, this is not a pleasant thing. So likewise, there may be some operations that you have defined on the model class which should not be accessible from the template. Curry provides a solution for this scenario. You must define a toTwig
method on the model class as follows:
public function toTwig()
{
// merge columns defined in the schema and virtual columns added with withColumn().
$ret = array_merge($this->toArray(), $this->getVirtualColumns());
// TODO: Add properties that should be accessible in the template
$ret['PrimaryKey'] = $this->getPrimaryKey();
return $ret;
}
In your page module, you should not add a termination method to the query but instead pass the ModelCriteria
object to the template. Termination query methods are find
, findOne
, findOneByAAA
and paginate
. In the template, you work with the model instance as if it were an array. In this way you have "sandboxed" the model instance. In the above example, the delete
method is not defined on the array and will do nothing if you try to access it from the template.
Let me demonstrate this with an example. Let's create a page to list companies. Create a new demo page as seen in the picture:
Now create a new CompanyList page module and attach it to this page. The code is as below:
class Project_Module_CompanyList extends Curry_Module
{
public function toTwig()
{
$q = CompanyQuery::create()
->orderByName();
return array(
'query' => $q,
);
}
}
Notice that we do not call the termination method (i.e. find()
). Calling find
will hydrate the Company
objects and defeat our "sandbox" purpose. We simply pass the ModelCriteria
object to the template.
Now let's create a module template to render companies. In the path cms/templates/Modules/
create a new template named CompanyList.twig. We generally give the module template the same name as the module to make it easy to identify the module. Type the following template code:
<h2>List of companies</h2>
<ul>
{% for company in query %}
<li>{{ company.Name }}, Contact: {{ company.ContactPerson }} ( {{ company.Email }} )</li>
{% endfor %}
</ul>
Finally, refresh the web page and voila! Companies get listed.
Behind the scenes, when you pass a ModelCriteria
object (i.e. the query), Curry wraps it in a Curry_Twig_QueryWrapper
object. This class implements the Traversable
interface. Hence, when you call the for
loop in the template, the Curry_Twig_QueryWrapper
class appends the find()
termination method to the ModelCriteria
object and hydrates the PropelObjectCollection
. If the class were to allow you to iterate on this collection, it would defeat the "sandbox" purpose. Why? because "dangerous" methods like delete
will become accessible on the model instance. So, the QueryWrapper class converts each instance into an array by calling a toTwig
method and gives this "array" to the template.
Let's modify our query in the Project_Module_CompanyList
page module:
$q = CompanyQuery::create('c')
->orderByName()
->leftJoinQuoteCompany('a')
// virtual column added
->withColumn('COUNT(a.CompanyId)', 'NbrAgreements')
->groupBy('c.CompanyId');
Also, modify the attached template code to:
// Code removed to focus on relevant sections
<li>{{ company.Name }}, agreements: {{ company.NbrAgreements }}</li>
Now refresh the page and voila! We have even virtual columns showing.
The QueryWrapper class checks to see whether your model class, in this case Company
, has defined a toTwig
method. If defined, that method is called and its value is returned to the template. If one is not defined, Curry calls a default toTwig
method that returns an array.
Let's override the default toTwig
method for the Company
class. In the file cms/propel/build/classes/project/Company.php
type the following code:
public function toTwig()
{
// merge columns defined in the schema + virtual columns
$ret = array_merge($this->toArray(), $this->getVirtualColumns());
// Additional properties that can be accessed in the template.
$ret['PrimaryKey'] = $this->getPrimaryKey();
return $ret;
}
Notice that in the toTwig
method we have defined a custom PrimaryKey
property. Likewise, you can add custom properties to the array.
Now, let us see the purpose of Curry_OnDemand
function. For each company, let us list the quotes for which it made agreements. Add a new item to the array of the toTwig
method:
public function toTwig()
{
// Code removed to focus on relevant portions
$ret['AgreementQuotes'] = new Curry_OnDemand(array($this, 'getAgreementQuotes'));
...
}
Also, add the getAgreementQuotes
method to the same file:
public function getAgreementQuotes()
{
return QuoteQuery::create('q')
->useQuoteCompanyQuery()
->filterByCompany($this)
->endUse()
->find();
}
Modify the module template as follows:
// Code removed to focus on relevant portions
<li>{{ company.Name }}, agreements: {{ company.NbrAgreements }}
{% if company.AgreementQuotes %}
<ul>
{% for q in company.AgreementQuotes %}
<li>Quote: {{ q.Heading }}</li>
{% endfor %}
</ul>
{% endif %}
</li>
Now, refresh the web page. If any company has made agreements or purchased quotes, those quotes will show in the list. Hence, the getAgreementQuotes
method will execute only if it is called in the template. Had we not used Curry_OnDemand
, then the getAgreementQuotes
method would have executed irrespective of it being used in the template.