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:

List companies page

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.

results matching ""

    No results matching ""