Appendix B. The Active Form Primer

When making the Yii-flavored user interface in Chapter 11, The Grid, we centered only around the GridView widget, as it was the topic of the chapter. But there is another important part of any web application, and it's an HTML form.

The Yii 2 framework provides a very robust and convenient-to-use widget called ActiveForm, which can semi-automatically construct an HTML form for us given the ActiveRecord instance describing the model being manipulated. We'll just continue from where we stopped, at the very end of Chapter 11, The Grid.

Making the Edit form for customer

Our Customer models are already being stored in two separate tables in the database, and two different active records are used to create a single customer. With the addition of the Address and Email models, we are meeting the problem of the need for a user interface to create and update Customer models.

We don't have either the book volume or the intention necessary to implement some JavaScript-heavy rich UI, even if it would undoubtedly be more responsive and visually pleasing. So, let's go old school and make a web interface traditional to the world of static HTML pages. Here is the sketch:

Making the Edit form for customer

So, phones, e-mails, and addresses will be presented as tables, with the buttons Add, Edit, and Delete. These tables will behave in exactly the same manner as the user interface used at /user/index and /services/index, which we have made using the Gii automatic CRUD generator. The Add and Edit buttons will transfer us to the add/edit pages corresponding to the submodel we want to add/edit, and when we click on Save on those pages, we'll be transferred back to this Edit Customer Form page.

Active query

What is the active query concept? It is a domain-specific language (DSL) for various tasks used to query the database for active records, which are possibly related to one other. On the one hand, it hides the intricacies of constructing the appropriate SQL and on the other, constructs the ActiveRecord instances from raw datasets. This concept is implemented as a yiidbActiveQuery class, which is an extension of yiidbQuery.

While a simple query returns data as associative arrays from the database, an active query is tailored to return iterable collections of active records, which is useful when you are on a level higher than raw data from the database. Of course, constructing the full ActiveRecord instance for each record returned by a query is a costly operation, so with convenience comes a performance penalty. However, generally it's wiser to think about your architecture first and tweak performance later when needed, because if you don't use ActiveRecords or Repository pattern over ActiveRecords, you will use the API for direct database access, and it's a lot harder to maintain it in the long term.

Note

The ActiveQuery is a very large class ultimately. It inherits from the Query class, which uses the trait yiidbQueryTrait, which itself uses two traits, yiidbActiveQueryTrait and yiidbActiveRelationTrait. There can be a whole book written only for the sake of explaining the intricacies of using the ActiveQuery class of Yii 2. You are encouraged to look at the documentation and source code for this class and explore it yourself.

We don't need all the functionality of ActiveQuery. To show a simple, expressive example, here is how you can get the customers who should be congratulated with their birthday in this week, but only those who were registered by the manager currently being logged in:

        $week_ago = (new DateTime)->sub(new DateInterval('P1W'))->format( 'Y-m-d' );
        $current_user = Yii::$app->user->id;
        $customers = CustomerRecord::find()
            ->where(
                ['and', 'created_by=:current_user', 'birth_date>=:week_ago'],
                compact('current_user', 'week_ago')
            )->all();

What does this code do? Here's what it does:

  1. The call to ActiveRecord.find() returns us an ActiveQuery instance configured for the CustomerRecord active record.
  2. Its where() method configures this ActiveQuery instance to filter the records according to two of our conditions.
  3. Finally, the all() method returns the array of ActiveRecord instances to us.

We already mentioned long ago the built-in PHP function named compact().

This is usually the way to use ActiveQuery: we enter the querying mode by calling find() on the ActiveRecord class, and when we finish chaining all the methods required, we pull out the ActiveRecord instances back, returning from the ActiveQuery DSL.

Another way to use ActiveQuery is to manually construct and pass them to the methods that expect it. One such place is the DataProvider instance, which was described back in Chapter 2, Making a Custom Application with Yii 2.

Customizing the autogenerated form

Let's look at what the Create Customer Record form looks like by opening the /customer-records page and clicking on the big green Create Customer Record button above the table:

Customizing the autogenerated form

How is it implemented? Tracing the /customer-records/create route, we end in the views/customer-records/create.php view file, which calls _form.php in the same folder. This view file is what governs the rendering of this form. You can note that update.php and create.php are almost the same and use the same form inside the _form.php script. This is intentional, as in Yii 2 ideology, the creating and updating of an ActiveRecord are very similar actions.

If you open the view file, you will see quite a simple structure, ignoring the raw HTML code:

  1. We initialize the ActiveForm by calling $form = ActiveForm::begin().
  2. Then we output input fields by calling $form->field($model, $attr)->textInput($setup).
  3. Then, we do some magic to render either the Create or Update submit button with proper CSS classes.
  4. Then, we end the ActiveForm by calling ActiveForm::end() (note that we call the static method and not the method of $form instance).

First, let's change this form layout to horizontal according to our sketch. As we already have the Yii 2-bootstrap extension attached to our application, this is extremely simple. We just have to declare our form to be not of the class yiiwidgetsActiveForm, but of the class yiiootstrapActiveForm. This can be done in the block of use clauses at the top of the view file. Find the following declaration:

use yiiwidgetsActiveForm;

Replace it with the following line of code:

use yiiootstrapActiveForm;

With this declaration substitution, we don't even need to change the code in the view file itself.

After that, we need to modify the ActiveForm::begin() call by adding a layout setting to the widget config:

    <?php $form = ActiveForm::begin(['layout' => 'horizontal']);?>

That's all, our form now looks like the one shown in the following screenshot:

Customizing the autogenerated form

We certainly don't need an editable ID of the customer, so we need to remove the following line:

    <?= $form->field($model, 'id')->textInput() ?>

We have to introduce an important guard case here. To create the new customer record, we need to remove the subtables for phones, addresses, and e-mails, because we need an ID that will be assigned to the customer record only after it is saved to the database.

Put the following if-endif brackets into the _form.php file:

    <?= $form->field($model, 'notes')->textarea(['rows' => 6]) ?>

    <?php if (!$model->isNewRecord):?>
    <!-- subtables will be here… -->
    <?php endif?>

    <div class="form-group">

This will prevent Yii from rendering the tables we're going to describe next just in case the customer record is a new one. All the following code examples are assumed inside the if-endif brackets!

Next, let's turn to the topic of adding a grid view for phone records associated with the customer being updated. We will start simple:

    <h2>Phones</h2>
    <?= yiigridGridView::widget([
        'dataProvider' => new yiidataActiveDataProvider([
            'query' => $model->getPhones(),
            'pagination' => false
        ]),
        'columns' => ['number']
    ]);?>

This will give us the following output when phones are to be attached to this new customer record:

Customizing the autogenerated form

We will use the relation method created in Chapter 11, The Grid, on the CustomerRecord class to return an ActiveQuery instance to find the PhoneRecord instances.

We don't need pagination in this grid view, as the number of records most probably will be small anyway. While GridView has a special pager setting, it is to customize the whole yiiwidgetsLinkPager widget, which is responsible for rendering all of those numbers and arrows inside squares. We, on the other hand, want to disable the whole notion of pagination for the Phones list. Thus, we need to use the pagination setting on ActiveDataProvider one abstraction deeper.

We don't need anything apart from the phone number, so only one column is explicitly declared. Without the declaration of columns, this grid view will break inside the Create Customer Record form, because there will no PhoneRecord instances to extract the column schema from.

Now, we'll do the real magic and wire this table to the CRUD functionality we have made for PhoneRecord in the previous section. For this, we need to add a special column that defines actions available to be performed on the corresponding active phone records as follows:

    <?= yiigridGridView::widget([
        // ...
        'columns' => [
            'number',
            [
                'class' => yiigridActionColumn::className(),
            ]
        ]
    ]);?>

You can guess now that the column definition can be the configuration array to be consumed by Yii::createObject(). Here is how this column will look:

Customizing the autogenerated form

If you look at the URLs behind the icons, you can see that it's the view, update, and delete actions we need, but they're tied to the current controller; we want phones instead of customer-records. This is solved quite simply by telling the ActionColumn class which controller it should relate itself to:

    <?= yiigridGridView::widget([
        // ...
        'columns' => [
            'number',
            [
                'class' => yiigridActionColumn::className(),
                'controller' => 'phones'
            ]
        ]
    ]);?>

Last, but not least, we need the Add Phone button. Let's do this now and put the button link right inside the column header as we sketched it previously:

    <?= yiigridGridView::widget([
        // ...
        'columns' => [
            'number',
            [
                'class' => yiigridActionColumn::className(),
                'controller' => 'phones',
                'header' => Html::a('Add New', ['phones/create']),
            ]
        ]
    ]);?>

To make it better, let's stuff the icon for the plus sign beside the Add New label as follows:

'header' => Html::a(
    '<i class="glyphicon glyphicon-plus"></i>&nbsp;Add New',
    ['phones/create']
),

Also, we don't need the view icon (the one with the eye), as the number is shown to us anyway, and we can see the details in the Update Phone form in the same way. Have a look at the following code:

    <?= yiigridGridView::widget([
        // ...
        'columns' => [
            'number',
            [
                'class' => yiigridActionColumn::className(),
                // ...
                'template' => '{update}{delete}',
            ]
        ]
    ]);?>

Now it's perfect. The preceding code will give us the following output:

Customizing the autogenerated form

The column width can be corrected by applying CSS, which we don't care about now.

Then we add the Addresses grid view as follows:

    <h2>Addresses</h2>
    <?= yiigridGridView::widget([
        'dataProvider' => new yiidataActiveDataProvider(
            ['query' => $model->getAddresses(), 'pagination' => false]
        ),
        'columns' => [
            'purpose',
            'country',
            'city',
            'receiver_name',
            'postal_code',
            [
                'class' => yiigridActionColumn::className(),
                'controller' => 'addresses',
                'template' => '{update}{delete}',
                'header' => Html::a(
                    '<i class="glyphicon glyphicon-plus"></i>&nbsp;Add New',
                    ['addresses/create']
                ),
            ],
        ],
    ]);?>

The pieces different from the Phones grid view are highlighted. This should result in the following table:

Customizing the autogenerated form

Similarly, you can craft the code yourself for the Emails grid view given this empty reference table:

Customizing the autogenerated form

Passing the customer ID to submodels

We have a small problem in our UI. Let's click on this new Add New button that we have created just now for the Phones grid view:

Passing the customer ID to submodels

We don't pass the customer ID to the phone record being created! And we don't need this field at all, actually, especially if we are passing the ID automatically. We need a way to pass the customer ID to the Create Phone Record submodel (and other submodels too).

The simplest way is to modify SubmodelController.actionCreate() in such a way that it accepts an additional parameter. Let's name it in a generic way as follows:

    public function actionCreate($relation_id)

Then, in the configuration for the header of the last action column, we can add the relation_id parameter to the URL being created:

'header' => Html::a(
    '<i class="glyphicon glyphicon-plus"></i>&nbsp;Add New',
    ['phones/create', 'relation_id' => $model->id]
),

It should be clear now after explanations in Chapter 12, Route Management, that arguments to the action* methods on the controllers become mandatory GET or POST parameters for the associated route with the same name.

Then, we can correctly place the given relation_id in the record being created:

    public function actionCreate($relation_id)
    {
        /** @var ActiveRecord $model */
        $model = new $this->recordClass;
        $model->customer_id = $relation_id;
// … other code ...

However, as we already named our input argument in a generic way, let's abstract from the customer concept here and make the target field generic too as follows:

        $model->{$this->relationAttribute} = $relation_id;

This is another example of the capabilities of PHP in metaprogramming, as we have just used the string stored inside a variable as an attribute name of an object (whose class itself was deduced from a string stored inside a variable). Of course, we have to declare this property now. Have a look at the following code:

    /** @var string Name of the attribute which will store the given relation ID */
    public $relationAttribute;

Also, correct the definition of AddressesController, EmailsController, and PhonesController by adding the following declaration inside them:

    public $relationAttribute = 'customer_id';

Tip

This is an example of the harm caused by over-generalizing in your code. What began with seemingly small and nice generalization now cost us triple code duplication. And we cannot default the value to customer_id, because we have SubmodelController completely decoupled from our domain now, and by doing so, we will break the abstraction, which looks even worse than blunt code duplication.

We can now remove the fields for the customer_id property from views/addresses/_form.php, views/emails/_form.php, and views/phones/_form.php in the same way we removed the id field from the Update Customer form.

Returning to the Update Customer form after updating the submodel

That's not all, though. When we create or update the phone, address, or e-mail, after clicking on the submit button, we are redirected to the /phone/view, /address/view, or /email/view, respectively, which is not what we want. It's better if we would be redirected to the page we were on before, that is, the Update Customer Record form page. This is quite easy to achieve using the tools Yii 2 provides us with.

First, we need to record the Update Record page URL by doing this in the appcontrollersCustomerRecordsController::actionUpdate() method as follows:

$this->storeReturnUrl();

The storeReturnUrl() is a well-named function, which we define ourselves as follows:

    private function storeReturnUrl()
    {
        Yii::$app->user->returnUrl = Yii::$app->request->url;
    }

We are storing the URL from yii webRequest.getUrl() with a yiiwebUser.setReturnUrl() call using a nice syntax provided to us by the __get() and __set() magic methods from Yii 2.

Then we go to the SubmodelController class again to the method actionCreate(), and find the following redirection:

            return $this->redirect(['view', 'id' => $model->id]);

We need to change it to the following code:

            return $this->goBack();

This is simpler and does what we need. The yiiwebController.goBack() method does redirect to the URL stored in yiiwebUser.getReturnUrl(), and it's exactly what we set before. Documentation for this property of the User component states that it's the URL we should redirect the user to after a successful login, but in reality, you can call goBack() in any place, thus this returnUrl is general-purpose.

You should perform the same replacement in the actionUpdate() method. Additionally, inside the actionDelete() method, you should replace the following redirect:

        return $this->redirect(['index']);

The preceding code should be replaced with the following code:

        return $this->goBack();

Or else, after you click on the Delete button beside any phone, address, or e-mail and confirm deletion, the system will try to redirect you to the nonexistent actionIndex() for the corresponding model.

Custom column value for the addresses table

We have only one discrepancy left between our sketch and the current state of the Update Customer form: the look of the addresses subtable. We need only a single column with the whole address written in one line.

Yii 2 provides two options for us here, which are as follows:

  1. The first option is to subclass yiigridDataColumn, which is the default class for the GridView column, and write our own code to make the cell contents.
  2. The second option is to use just the yiigridDataColumn.value property to display the value we need. This is perfectly suitable for our problem as we don't need to do much apart from gluing together pieces of information from AddressRecord.

So, let's make the following straightforward implementation of the value for the Address column in the Addresses subtable:

        'columns' => [
            [
                'label' => 'Address',
                'value' => function ($model) {
                    return implode(', ',
                        array_filter(
                            $model->getAttributes(
                                ['country', 'state', 'city', 'street', 'building', 'apartment'])));
                }
            ],
            'purpose',
            [
                // … ActionColumn here which we don't care about...
            ],
        ],

You can see that we can pass arbitrary callables to the value property. Also, the label property controls the text in the header of the column. In the end, here is what we get with this column definition:

Custom column value for the addresses table

In the end, we get the result pretty close to what we sketched at the beginning of this section:

Custom column value for the addresses table

We have not covered in detail the important topic of the active form handling: the validators. Do not miss the opportunity to learn about form validation in official documentation, as it's a really useful feature of the framework.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.141.27