This forum has moved to a new location and is in read-only mode. Please visit talk.octobercms.com to access the new location.

nullpointer
nullpointer

I am currently testing October for a company that is looking for framework allowing to combine a CMS and a custom business application running in the background. It seemed to me that the architecture of October offers really good possibilities for developing applications running on the backend, but I have encountered problems that I am not able to solve easily and I feel that I am doing something wrong.

Currently the biggest problem is programming the application business logic. As an example I will give the classic order-orderline form. Creating such a form on the backend is really very simple and fast thanks to the RelationController behaviour. Problems arise as soon as various dependencies and computed fields are defined. For example, for oder item there are the following requirements

  1. Unit price depends on product, customer category and quantity ordered
  2. Total item price depends on quantity and unit price.
  3. It must be possible to overwrite the unit price manually and then the calculation from point 1. is not valid.
  4. If the order item is taken from a quotation where all prices have already been negotiated, the calculations from points 1. and 2. are not performed at all.

October allows me to define dependencies in fields.yaml, but it is not clear to me where these dependencies will be programmed? The dependsOn property activates the call to filterFields() on the model (I never understood why the form field level calculations are performed on the model and not in the controller), which I find completely abnormal because

  1. There is only one single method in which the entire business logic of the form is programmed. This method is soon full of various if ... then ...else conditions in which the programmer gets completely lost. There is never clear, which field change fired the calculation, so everything is calculated again and again.
  2. This method is not called consistently. For example, if someone changes the quantity in a form and clicks Update button without moving focus out of the quantity field, filterFields() is not called at all! This means I have to program the exact same logic again and place it somewhere. But where? On the Update button? The customer wants to see the result of the calculation in the form, before the user does Save/Commit into the database.
  3. If I place the logic on 'beforeSave', 'beforeCreate' or 'beforeUpdate', which is another form of database triggers, it doesn't work in case the oder item is created from a quotation in which all model attribute values have already been negotiated and no calculation is performed.

Colleagues testing other systems (not all of them are PHP, there are also Java and Python frameworks in the competition) have callback methods available for each field at the controller level for programming. All the logic is consequently defined at the controller level and not at the model level, as in October.

What am I doing wrong? Or do I have extreme requirements for OctoberCMS, which the system cannot meet?

Robert

daftspunky
daftspunky

Hi Robert,

Based on your requirements at a high level, this should be entirely possible.

The dependsOn property activates the call to filterFields() on the model (I never understood why the form field level calculations are performed on the model and not in the controller), which I find completely abnormal because

Fields calculations can be performed on the Model (global logic) or the Controller (local logic). It's quite common to see both depending on the situation. For example, filterFields in the model is similar to formExtendFields from the controller.

The documentation covers a range of different ways to extend the form controller behavior. These include relevant overrides such as formExtendFields and formExtendModel. The relation behavior can also be extended via the controller.

For example, if someone changes the quantity in a form and clicks Update button without moving focus out of the quantity field, filterFields() is not called at all!

The filterFields filters field states and values based on the current state of the model, it doesn't set values on the model for saving. To set these values, use the model events such as beforeSave(), beforeUpdate(), afterCreate(), etc. The controller equivalents are formBeforeSave, formBeforeUpdate, etc. It is not uncommon to share logic between these events.

I hope this helps.

nullpointer
nullpointer

I guess it was not clear from my post that the results of the business logic calculation should be visible in the form before the user performs Save/Update. Pricing calculation is in this case quite complex. This implies the necessity of calculation in the controller (local logic, as you write) with immediate presentation update. I would also like to point out that the problem I have is mainly with relation behavior, specifically with the relationManage form, in which the price calculation takes place.

The relation behavior can also be extended via the controller.

Yes, I have tried that. For me the only possible method I found was relationExtendManageWidget($widget, $field, $model). But that doesn't work for me. The $field is the name (string) of the relation (order-item) field. Unusable because I need currently edited order-item. The $model is the order model (master) and all I have left here is $widget. I expected I could take advantage of the attributes $widget.model or $widget.data. However, if I make a change in the relationManage form, neither the model nor the data changes the content.

I just can't find a reasonable place in the whole workflow where I would react to changes in the form and update other values in the form accordingly. The only thing that works for me is filterFields where a comparison with getOriginal() detects a field value change, and I can update other model attributes. Other frameworks have clearly defined controller callback methods such as onFieldValueChange, onItemValueChange and onFormChange, i.e callbacks on field, tuple and form level, in which the controller can be updated and the changes are immediately propagated to the presentation. I am missing an equivalent for such callback methods in OctoberCMS.

daftspunky
daftspunky

I had the same thought later, you shouldn't need to repeat/share the logic since the form values themselves should be updated... filterFields is indeed special because it populates the model and the form field values at the same time, but it uses a cloned model object to keep the concerns separate (the population of the model should come from the form values, not the filterFields logic).

It might be better to express ideas as code, the outcome seems to hinge on the execution. Here is a low-level code example where changing the value of a dropdown refreshes the entire form. This is effectively how the dependsOn mechanism works.

// JS

$(function() {

    // When the `entry_type` field on the `EntryRecord` model changes
    $('#Form-field-EntryRecord-entry_type').on('change', function() {

        // Call the onChangeEntryType AJAX handler
        $(this).request('onChangeEntryType');
    });

});

// PHP / Controller

public function onChangeEntryType()
{
    // Run the page action to initialize the form widget
    $this->pageAction();
    $formWidget = $this->formGetWidget();

    // Reset set the form values based on the new postback data
    $formWidget->setFormValues();

    // Refresh the whole form
    return $formWidget->onRefresh();
}

public function formExtendModel($model)
{
    // A new entry type has been supplied
    if ($entryType = post('EntryRecord[entry_type]')) {

        // Manipulate the model accordingly
        $model->title = "Selected entry type: $entryType";
    }
}

Last updated

nullpointer
nullpointer

Thanks for the hint, although I find it quite complicated, I think these things should be solved by the framework and the developer should focus only on the application logic. However, I tried it and the result did not satisfy me.

For testing I defined the simplest example. The Order model has a Customer and Currency relationship.

fields:
    customer:
        label: Customer
        type: relation
        relation: customer
        select:  name
    currency:
        label: Currency
        type: relation
        relation: currency
        select: iso
        dependsOn: customer

My goal is to set the order currency to the customer's default currency whenever the customer changes. This can happen on create or update form. Of course, in this case I could certainly use filterFields(), I think it was designed for these simple cases. But I don't want to have the whole business logic in one method, so I tried your idea.

What about JavaScript, I had to use the selector there, for some reason on('change', function() ... didn't work. But otherwise ok and AJAX callback was called.

// When the `customer` field on the `Order` model changes
$(document).on('change', '#Form-field-Order-customer', function () {
         // Call the onChangeCustomer AJAX handler
         $(this).request('onChangeCustomer');
});

But in the controller I couldn't do such a simple thing as setting the new currency value in the form. return $formWidget-onRefresh() didn't work at all, I tried to return a field partial render result, in that case the new currency value appeared briefly in the selector and was immediately overwritten by the old value. I made test with update of a simple text field - it works fine. Obviously there is a problem to set a new value for a relation field. Here is the example of my code


public function onChangeCustomer($recordId) {

    $this->pageAction();
    $formWidget = $this->formGetWidget();

    // get form data 
    $data = $formWidget->getSaveData();

    // select default currency
    $curr =  Customer::select('currency_id')->where('id', $data['customer'])->first()->currency_id;

    // set form value
    $formWidget->setFormValues([
        'currency' => $curr
    ]);

    // render partial 
    return [
        '#Form-field-Order-currency-group' => $this->formRenderField('currency', ['useContainer' => false]),
    ];

    // Refresh the whole form
    // return $formWidget->onRefresh();
}

Looks like I'm done here. I few days ago when we compared the results of implementation in different frameworks, OctoberCMS won some points only in the category of backend and frontend collaboration. For example, creating a page in the frontend where the customer could see all his orders and invoices. Other frameworks had to solve it with some RESTful API to get data from secondary database. However, the elegance and speed with which the business application was programmed was incomparable to OctoberCMS. I do not assume that October would be deployed for this intranet project. Maybe some other time for something simpler.

nullpointer
nullpointer

I've been doing a little more testing to see where the problem might be. If I deleted the dependsOn definition in fields.yaml, the fields in the form were updated correctly. It seems that the dependsOn property does not work well with other AJAX callback methods. Unfortunately, this still did not work for fields of type relation that have a scope in the relation definition. Finally, I ended with a working version of my controller, where I loaded the new values through the model, without dependsOn definitions in field.yaml

        $model = $formWidget->model;
        $model->currency = isset($curr) ? $curr : null;
        $this->initForm($model);
        $formWidget->onRefresh();

        return [
            '#Form-field-Order-currency-group'  => $this->formRenderField('currency',   ['useContainer' => false]),
        ];

The call of $formWidget onRefresh() was necessary to load new values into the relation selectors with a defined scope.

I don't know if this is the right solution, but even if it works it's the end of my efforts, I don't want to invest time in such basic things like a live update of form values, that work out of the box in other frameworks.

1-6 of 6

You cannot edit posts or make replies: the forum has moved to talk.octobercms.com.