Configuration Objects

Introduction

Configuration objects provide a common way to access and encapsulate business logic rules to a service based on the user input data. The configuration object is specific to each revision since that data is also different for each revision.

For simple rules, you probably do not need this as you can just access a model directly but sometimes conditions might grow large if there is a tendency to repeat multiple choices.

When to Use

The difference between the phase model data and the configuration object is roughly that the model data is the "raw" data that the configuration wraps to process or provide default values. They are not in way necessary but provide a way to simplify phase conditions or to encapsulate business logic around the user input data.

The phase models are loaded when necessary (ie. when entering the page or calling the QuotationFacade::getPhaseModel() method for the first time). This means that whatever defaults the editor provides will not be available before the user enters the page. Therefore, it is good to provide some default values even though the user didn't visit that specific page.

For example, a condition like this:

condition: 'models["taxes"].SalesTaxChoice == "service" and models["taxes"].SalesTaxValue > 0'

Could be moved to the configuration object and rewritten as:

condition: 'configuration.taxes.has("SalesTax")'

When the condition is evaluated it will call the method in the configuration which can return if the condition is true or not based on the form selections.

Use cases:

  • Transform selection values for display, eg. "firm" from the model data to "Firm Quotation"
  • Provide a default value if nothing selected
  • Provide options for selections.
  • Wrap multiple nested phases into coherent whole, eg. the configuration for a motor might wrap the phase data from drive type and gear selection pages to provide access to the whole motor configuration.
  • Provide an interface for other dependent software to access the configuration.

Usage

Configuration Manager

The configurations are fetched through the configuration manager service which can be fetched with the service id klaro_quotation.configuration_manager. Use the getConfigurationMethod() to load a configuration:

<?php
$configurationManager = $this->get('klaro_quotation.configuration_manager');

$myConfig = $configurationManager->getConfiguration($revision, 'my_configuration');

For every configuration you must provide the revision in whose context the data is in. If the configuration is already loaded for the given revision, then a cached copy is return instead.

NOTE: The configuration manager is always initialized in the Quotation Bundle controller methods with the current quotation and revision. If the configuration manager is used outside the controller contexts, the you should call the initialize() methods explicitly. Othewise the methods to get the current revision or models will not work!

The configuration manager provides methods to get the current revision and to load phase models.

Method Description
getCurrentQuotationRevision() Get the revision that the manager was initialized with.
getModelProvider() Get access to the model provider.
getPhaseModel() Get a phase model
getPhaseModelForCurrent() Get a phase model for the current revision
getPhaseData() Load a phase model using phase id and the models configuration object (see "Phase Models" below)

Adding the Configuration Service

Configurations can be added by tagging a service with the name klaro_quotation.configuration. These can then be accessed in expressions through the configuration variable. The tag alias defines the name that you can get the service from the configuration object.

All configuration objects must implement the interface Klaro\QuotationBundle\Api\ConfigurationInterface. In some cases the configuration needs to be a list (for example if a phase is repeatable and one configuration matches to one phase), you might want to implement Klaro\QuotationBundle\Api\ConfigurationListInterface.

To access a configuration list, use array access:

condition: 'configuration.lines[2].has("Valve")'

In practice, it is better to extend the abstract configuration class in Klaro\QuotationBundle\Configuration\BaseConfiguration and Klaro\QuotationBundle\Configuration\BaseConfigurationList which provide all the necessary implementations for object notation and array access. All you have to do then is the add the approriate getters and check functions to your sub class.

Below is the class diagram for configuration objects (includes also the existing ones, see "Available Configuration Objects" below).

image

After loading the configuration object, the configuration manager will call the object's setContext() method to provide the revision and access to the configuration manager itself.

Example of a service configuration:

acme_test_configuration:
    class: Acme\SomeBundle\Configuration\TestConfiguration
    tags:
        - { name: 'klaro_quotation.configuration', alias: 'acme_config' }

This can now be used in expressions with configuration.acme_config.has("Property"). This will result in a call to TestConfiguration::has("Property") method - or if it extends the BaseConfiguration class, in a call to TestConfiguration::hasProperty().

Simple attribute access will trigger a get() call:

condition: 'configuration.acme_config.SomeField == "OK"'

Is the same as:

condition: 'configuration.acme_config.get("SomeField") == "OK"'

This will call:

<?php
class TestConfiguration extends BaseConfiguration {
    public function getSomeField() {
        return $this->model->get('SomeField');
    }
}

Configuration lists can be also looped:

<?php

// Assume "lines" is a configuration object that inherits BaseConfigurationList.
$lines = $configurationManager->getConfiguration($revision, 'lines');

$total = count($lines);

foreach($lines as $line) {
    if($line->hasSomeFieldEnabled()) {
        $value = $line->getSomeField();
    }
}

Extending from the BaseConfiguration also gives you helpers to easily access the context and load phase models:

<?php

class MyConfiguration extends BaseConfiguration {
    public function getSomeField() {
       $model = $this->getPhaseData('phaseId');

       return $model->get('SomeField');
    }
}

NOTE: Do not load the phase models in the constructor because the revision context has to be known before that. Load models either in the setContext() method after getting the revision or lazy load them:

<?php

class MyConfiguration extends BaseConfiguration {
     /** @var FormModelInterface */  
    protected $model;

    public function __construct() {
        parent::__construct();

        $this->model = null;
    }

    public function getModel() {
        if(!$this->model) {
            $this->model = $this->getPhaseData('phaseId');
        }

        return $this->model;
    }

    public function getSomeField() {
       return $this->getModel()->get('SomeField');
    }
}

Available Configuration Objects

The Klaro\QuotationBundle\Configuration\BaseConfigs class provides constants to the configuration aliases below.

Current User

The current user configuration object exists to provide a way to access the currently logged in user in condition. This is mainly useful for testing if the user a has a permission or not. The configuration alias is current_user and is available also in conditions as user.

condition: 'configuration.current_user.has("Role", "ALLOW_DISCOUNT")'

Is the same as:

condition: 'user.has("Role", "ALLOW_DISCOUNT")'

In PHP:

<?php
$currentUser = $configurationManager->getConfiguration($revision, BaseConfigs::CURRENT_USER);

$currentUser->has('Role', 'ALLOW_DISCOUNT');

Phase Models

Accessing phase models is possible with models configuration object. This is also available in phase conditions as in shorter form, so configuration.models is the same as just models.

To access the phase data, use the phase id with object or array notation:

models.general.SomeField
models["general"].SomeField

In PHP:

<?php
$models = $configurationManager->getConfiguration($revision, BaseConfigs::MODELS);

$value = $models["general"]->get('SomeField');
// or
$value = $models->general->get('SomeField');

For nested phases, use must use the array notation:

models["line/valves"].SomeField
$value = $models["line/valves"]->get('SomeField');

If a phase is repeatable, then you need also the ordinal:

models["repeatablePhase"][0].SomeField
<?php
$models = $configurationManager->getConfiguration($revision, BaseConfigs::MODELS);

$value = $models["repeatablePhase"][0]->get('SomeField');

// Get number of models:
$repeatablePhase = $models["repeatablePhase"];
$count = count($repeatablePhase);

// Loop
for($i = 0; $i < $count; ++$i) {
    $model = $repeatablePhase[$i];

    $value = $model->get('SomeField');
}

NOTE: The difference between using this configuration object and just calling QuotationFacade::getPhaseModel() directly is that the phase id is used to load the actual phase definition. The model name is gotten from the phase definition which is then used to load the model with getPhaseModel(). It also does the necessary wrapping to provide repeated models with array access.