Chapter 5.1.1: The Companies backend module

The Cities module was a very simple module. As an exercise I would request you to build a module for recycling types. We will require data from both the Cities and the RecyclingTypes module when we build the Companies module.

This is the plan for the Companies module. The main view will show a listing of companies from the Company table. We want to be able to add new companies and edit existing companies. We also want to show a nested view to manage (i.e. Add, Edit or Delete) recycling types for the company.

The first part of the specification is straightforward. Below is the code.

<?php

class Project_Backend_Companies extends Curry_Backend
{
    /**
     * Create a group icon in the Admin module panel
     * and make this module available in the specified group.
     * @return string
     */
    public static function getGroup()
    {
        return 'Demo ModelView';
    }

    /**
     * The method called by default by the cms.
     * Something similar to C program - or rather the first function called.
     */
    public function showMain()
    {
        $list = new Curry_ModelView_List('Company');
        $list->show($this);
    }

}

We now have to chain this "main" view to another view that shows recycling types for the company. We do this by creating an action. As mentioned earlier, an action can be a link or a button that when executed can either render another view or do some operation such as delete a record. We will create a "single" action. A "single" action is an action that affects the list item it is associated with. You create a custom action by adding an action array to the actions option of the list. Let's create a new action named action_recycling_types. Modify the code in the showMain method as follows:

public function showMain()
{
    $list = new Curry_ModelView_List('Company', array(
        'actions' => array(
            'action_recycling_types' => array(
                'label' => 'Recycling types',
                'action' => $this->getRecyclingTypesList(),
                'single' => true,
                'class' => 'inline',
            ),
        ),
    ));
    $list->show($this);
}

Notice the action function $this->getRecyclingTypesList(). We are chaining to another view that returns a Curry_ModelView_List. How this list is rendered is based on the class parameter. In our case class => inline specifies that the view should be inline or nested. So when the list item is expanded the view will show as a nested view. The next code snippet will return the nested view.

public function getRecyclingTypesList()
{
    return new Curry_ModelView_List('CoRt');
}

Go to the Companies backend module and test your code. Can you create a new company record? Perfect! Do you know why the company's name is a link? It's an action connected to the Edit action. But why is the action attached to the company's name and not some other column? That's because, the company's name has a primaryString="true" attribute in the schema definition. Open cms/propel/project.schema.xml and see the schema definition for the company table. Now, click the company's name. The list item expands to reveal the Curry_Form_ModelForm view. Do you know why it's nested? By default the Edit action has class => 'inline'. You can see the code in core Curry_ModelView_List::addDefaultActions at line 264. Let's show the "edit" form in a dialog instead. In the showMain() method of the Companies module, override the Edit action options in the Curry_ModelView_List options array:

'actions' => array(
    // override the default 'edit' action options
    'edit' => array(
        // show view in popup
        'class' => 'dialog',
    ),
    // create a custom action
    'action_recycling_types' => array(
        'label' => 'Recycling types',
        'action' => $this->getRecyclingTypesList(),
        'single' => true,
        // show nested view
        'class' => 'inline',
    ),
),

Go back to your module and refresh the browser. Now click the company's name. Is the form showing in a nested view or in a popup now?

Now, let's see the execution of our custom action action_recycling_types. Hover your mouse pointer to the right of the list item and click Recycling types. The view should open in a nested view. Do you know why "nested"? Good! Click the Create new button. What do you see? An empty form? What!!

Why is the form empty? Any guesses? When you created a new company record did you see the field CompanyId showing? No! That's because ModelForm (i.e. short name for Curry_Form_ModelForm) does not include primary keys when constructing fields for the Zend form. In your schema definition for the intermediate table co_rt you have composite primary keys. ModelForm removed these fields when constructing the Zend form elements. Had there been more non-key fields, they would have been shown in the form. But there are none. Hence, the empty form. Therefore, we have to tell ModelForm that we need to show the recycling_type_id field. You can do this by constructing a new ModelForm and specifying the withRelations option. Let's see the code to understand better.

public function getRecyclingTypesList()
{
    $mf = new Curry_Form_ModelForm('CoRt', array(
        'withRelations' => array('RecyclingType'),
    ));

    return new Curry_ModelView_List('CoRt', array(
        'modelForm' => $mf,
    ));
}

As you can see in the code snippet above, we have constructed a new ModelForm and told the form to show all foreign key fields defined by the relationships specified in the withRelations option parameter. How do we figure out the relationships? From your schema.

<table name="co_rt" idMethod="native" isCrossRef="true">
  <!--
    Other field definitions removed for clarity.
  -->

  <column name="recycling_type_id" type="INTEGER" required="true" autoIncrement="false" primaryKey="true" />
  <foreign-key foreignTable="recycling_type" onDelete="cascade" onUpdate="cascade">
      <reference local="recycling_type_id" foreign="recycling_type_id" />
  </foreign-key>
</table>

The "camelCase" version of the value in the foreignTable attribute is generally the name of the relationship. "Generally"! Yes, "generally". You can override the relationship name by specifying the phpName attribute. Refer documentation on the Propel website here http://propelorm.org/Propel/reference/schema.html#foreign-key-element.

But wait a minute. I have told ModelForm to show the field corresponding to the RecyclingType relationship. What if I had more non-key fields in the table. Will those fields also show? Yes they will show because when you created the ModelForm, you specified the model class name as the first parameter.

Now, go back to your browser, refresh and test your code. You should see a dropdown with the recycling types. It would be a nice idea to show the text [ Select ] if no item was selected in the dropdown. Modify your form code as follows:

$mf = new Curry_Form_ModelForm('CoRt', array(
    'withRelations' => array('RecyclingType'),
    'columnElements' => array(
        'relation__recyclingtype' => array('select', array(
            'multiOptions' => array(null => '[ Select ]') + RecyclingTypeQuery::create()
                ->orderByName()
                ->find()
                ->toKeyValue('PrimaryKey', 'Name'),
        )),
    ),
));

Notice the columnElements option. You can use that to override field definitions in the form. ModelForm will merge the columnElements array with the default one it constructed. Your form field names will have the same "snakecased" field names specified in the schema. The relation__ is a special type of field. That's the field that corresponds to the relation specified in the withRelations array. The relation name is lowercased and appended to `relation_` to get the column name.

Try creating a recycling type for the company. Heck! The entry is created. I can tell because I can edit it. The list however, looks empty... or is something wrong with my eyes? Nah! nothing wrong with my eyes. I see correctly. The same explanation of primary keys being ignored holds true in the list too. Let's add the recycling type name and the recycling type id to the list. You do this by adding another list option named columns. Below is the code snippet for the list.

'columns' => array(
    // add custom field.
    'recycling_type_id' => array(
        'label' => 'RT ID',
        'callback' => function($o)
        {
            return $o->getRecyclingTypeId();
        },
    ),
    // add custom field.
    'recycling_type_name' => array(
        'label' => 'Recycling type name',
        'callback' => function($o)
        {
            return $o->getRecyclingType()->getName();
        },
    ),
),

Notice, that we use a callback option. When you add a custom field, the list will pass the row object to each custom field of the list item. The row object is a Propel model instance. In our case, an instance of CoRt filtered by CompanyId. Would this lead to a performance problem. Yes, the N+1 problem holds true here. To alleviate this problem you can construct a custom query using JoinWith and pass the query object to the list. Let's optimize this query.

$q = CoRtQuery::create()
    ->joinWithRecyclingType();
return new Curry_ModelView_List($q, array(
    'modelForm' => $mf,
    'columns' => array(
        // add custom field.
        'recycling_type_id' => array(
            'label' => 'RT ID',
            'callback' => function($o)
            {
                return $o->getRecyclingTypeId();
            },
        ),
        // add custom field.
        'recycling_type_name' => array(
            'label' => 'Recycling type name',
            'callback' => function($o)
            {
                return $o->getRecyclingType()->getName();
            },
        ),
    ),
));

We don't specify the filterByCompanyId in the query. The list will do that automagically for you.

Assign an action to a list item column.

So far, we have seen that the list automagically selects a column and assigns an action to it. This is true for the Edit action where a column having the primaryString="true" attribute is chosen. But if you have created a custom action, the list does not know which column to pick. Hence, it will not assign the action to a column. You will have to do this manually.

Let's add a "recycling types count" field to the company's list so that we know how many recycling types a company deals with. Modify the list in the showMain method as follows:

public function showMain()
{
    $q = CompanyQuery::create('c')
        ->leftJoinCoRt('cr')
        ->withColumn('COUNT(cr.RecyclingTypeId)', 'NbrRt')
        ->groupBy('cr.CompanyId');
    $list = new Curry_ModelView_List($q, array(
        'columns' => array(
            'NbrRt' => array(
                // assign action to this column item.
                'action' => 'action_recycling_types',
            ),
        ),
        'actions' => array(
            // override the default 'edit' action options
            'edit' => array(
                // show view in popup
                'class' => 'dialog',
            ),
            // create a custom action
            'action_recycling_types' => array(
                'label' => 'Recycling types',
                'action' => $this->getRecyclingTypesList(),
                'single' => true,
                // show nested view
                'class' => 'inline',
            ),
        ),
    ));
    $list->show($this);
}

Notice that when we add a virtual column with the withColumn query method, the column is automagically added to the list. Also notice the new columns option added to the list. The columns parameter is used to override properties of columns shown in the list. Let's say we wanted to hide the pincode column. We can just set pincode => false. Now, to attach an action with a column we specify the action parameter.

We still have to do some more exciting features for the Quotes and Companies module. Maybe, we would want to see which quotes a company has viewed. But before we can do that we need to have some quotes in our database. Let's first create a page module to capture quotes from the frontend and then we will come back and write the Quotes backend module. How's the plan?

results matching ""

    No results matching ""