Express Form Rendering Flow

Ok, so we've implemented our form rendering code:

$controller = $express->getEntityController($entity);
$context = new FrontendFormContext();
$renderer = new Renderer($context, $form);

And we've rendered it in the page:

print $renderer->render();

And its printed out the form:

form

How exactly does it work? Let's walk through it.

Form\Express\Renderer::render()

Concrete\Core\Form\Express\Renderer::render() retrieves a Control View object for the form itself, by calling Concrete\Core\Entity\Express\Form::getControlView, which returns the Concrete\Core\Express\Form\Control\View\FormView object. Since this object implements the Concrete\Core\Form\Control\ViewInterface interface, we know it must contain the getControlRenderer() method. So this method is called.

getControlRenderer() must return an object of the Concrete\Core\Form\Control\RendererInterface (which specifies that the class must contain a render() method. In this particular case, the FormView class returns Concrete\Core\Form\Control\Renderer from its getControlRenderer() call, and passes the following parameters to the class:

  • Concrete\Core\Form\Control\View\FormView as the View object.
  • FrontendFormContext as the context.

You'll see this happen a lot – the final class Concrete\Core\Form\Control\Renderer renders a singular control. That control might be the outer form control, or it might be a single specific control in the Express Form (like an association control or an attribute key control.) In all those cases, the specific view object (which contains data about the object that's being rendered) and the context are passed to this generic renderer object.

Form\Control\Renderer::render()

Once this generic control renderer gets to work, it first retrieves a Concrete\Core\Filesystem\TemplateLocator object from the Concrete\Core\Form\Control\ViewInterface object that's passed in (in this case it was FormView). This method is part of the ViewInterface class and is used to tell Concrete CMS where this particular view should load its templates from. In this case, the ViewInterface in question is Concrete\Core\Form\Control\View\FormView. Let's check out its createTemplateLocator method:

public function createTemplateLocator()
{
    $locator = new TemplateLocator('form');
    return $locator;
}

This doesn't make a ton of sense. What is this object? What's it doing?

TemplateLocator Object

Let's back up for a second. What's this TemplateLocator class all about? Simply put, this is an object oriented way to search for a particular Concrete file in a number of locations. In Concrete 5.7 you might have spied a ton of code that looks more or less like this:

if (file_exists(DIR_APPLICATION.'/'.$segment)) {
    $file = DIR_APPLICATION.'/'.$segment;
    $override = true;
    $url = REL_DIR_APPLICATION.'/'.$segment;
} elseif ($pkgHandle) {
    $dirp1 = DIR_PACKAGES.'/'.$pkgHandle.'/'.$segment;
    $dirp2 = DIR_PACKAGES_CORE.'/'.$pkgHandle.'/'.$segment;
    if (file_exists($dirp2)) {
        $file = $dirp2;
        $url = ASSETS_URL.'/'.DIRNAME_PACKAGES.'/'.$pkgHandle.'/'.$segment;
    } elseif (file_exists($dirp1)) {
        $file = $dirp1;
        $url = DIR_REL.'/'.DIRNAME_PACKAGES.'/'.$pkgHandle.'/'.$segment;
    }
} else {
    $file = DIR_BASE_CORE.'/'.$segment;
    $url = ASSETS_URL.'/'.$segment;
}

The same code can be written using a template locator like this:

$app = Facade::getFacadeApplication();
$locator = $app->make(FileLocator::class);
if ($pkgHandle) {
    $locator->addLocation(new FileLocator\PackageLocation($pkgHandle));
}
$record = $locator->getRecord($segment);
$file = $record->getFile();
$url = $record->getURL();

Furthermore, you can add more locations to search, including the active theme and all package directories.

The TemplateLocator isn't just useful because it cleans up code; it's also an integral part of this view/context system, when paired with a particular template to return.

So, in this particular instance, we've created a TemplateLocator object and passed in a template handle, named "form". This tells Concrete that we will be looking for a file named form.php in order to render this particular control. But where? That's what happens in the next section of the Renderer::render() method. Next, we run setLocation() with the passed $locator object on the passed Concrete\Core\Form\Context\ContextInterface object (in this case, FrontendFormContext). This method is a required part of any context objects, and returns a modified version of the Concrete\Core\Filesystem\TemplateLocator object that was passed to it. In this instance, let's see what happens when setLocation is run on the FrontendFormContext object. Here's the setLocation($locator) method as found in the FormContext class, which FrontendFormContext extends:

public function setLocation(TemplateLocator $locator)
{
    $locator = parent::setLocation($locator);
    $locator->prependLocation(DIRNAME_ELEMENTS .
        DIRECTORY_SEPARATOR .
        DIRNAME_EXPRESS .
        DIRECTORY_SEPARATOR .
        DIRNAME_EXPRESS_FORM_CONTROLS .
        DIRECTORY_SEPARATOR .
        DIRNAME_EXPRESS_FORM_CONTROLS // not a typo
    );
    return $locator;
}

In this case we're adding an additional spot to the template locator that was passed, and telling the template locator where to prefer to look for control templates for this control. In this example, the path we're passing is 'elements/express/form/form'. Since we created our template locator and told it the template we were interested was "form.php" that means we're going to look for 'elements/express/form/form/form.php' in order to render this control. By default, the TemplateLocator will look in application/ first, then concrete/. It takes care of overrides automatically (and also caches for performance sake.)

Next, we extract the current view object into the $view variable, the current context object into $context, and include the passed template. That means those variables will be available in that templates local scope. So let's check out `concrete/elements/express/form/form/form.php':

concrete/elements/express/form/form/form.php

Here's what that default template looks like:

<input type="hidden" name="express_form_id" value="<?=$form->getID()?>">
<?=$token->output('express_form')?>

<div class="ccm-dashboard-express-form">
    <?php
    foreach ($form->getFieldSets() as $fieldSet) { ?>

        <fieldset>
            <?php if ($fieldSet->getTitle()) { ?>
                <legend><?= $fieldSet->getTitle() ?></legend>
            <?php } ?>

            <?php

            foreach($fieldSet->getControls() as $setControl) {
                $controlView = $setControl->getControlView($context);

                if (is_object($controlView)) {
                    $renderer = $controlView->getControlRenderer();
                    print $renderer->render();
                }
            }

            ?>
        </fieldset>
    <?php } ?>
</div>

You should be able to notice the most important part about this template:

foreach($fieldSet->getControls() as $setControl) {
    $controlView = $setControl->getControlView($context);

    if (is_object($controlView)) {
        $renderer = $controlView->getControlRenderer();
        print $renderer->render();
    }
}

We're looping through all the field sets found in the form, and getting each field set's set control. Each Express Form Control implements the Concrete\Core\Form\Control\ControlInterface, which means it has to be specify a getControlView method that takes the current form context as its parameter. That control view then is responsible for delivering the control renderer, which works for each control exactly how it worked for the form as a whole.

Lets look at two particular controls in here: the "Name" and the "Binder" controls. The first of these is a custom express attribute for this entity. The second is actually an association control, which we include to let you specify which binder (which is a second Express entity) this document you're adding belongs to.

Rendering the Name Control

When running getControlView on an attribute key control, we first check to see the context that we're passing in, so we know which view class to deliver. We deliver a different view class when we're viewing express entities than when we're creating or updating them. In this instance, we retrieve an instance of the Concrete\Core\Express\Form\Control\View\AttributeKeyFormView object, which holds our control and our context.

Things get a little tricky at this point; this class is our Express Form Control View – but attributes themselves also have a control view (because attributes can be rendered outside of Express forms.). So, first we need to retrieve the appropriate attribute context (used by attributes) from our express form context (used by express form controls.) This method is part of the Concrete\Core\Express\Form\Context\ContextInterface – so it must be part of any express form contexts that are used. In our example, the FrontendFormContext::getAttributeContext method delivers the following object:

public function getAttributeContext()
{
    return new BasicFormContext();
}

BasicFormContext is an attribute context, found at Concrete\Core\Attribute\Context\BasicFormContext. This attribute context is then stored inside our express attribute key control view – and remember, it's separate from the express form context. These things are somewhat related by they are separate objects and operate independently.

Next, we need to get our attribute's control view – which again, is separate from the attribute key control's form view. We do this from within AttributeKeyFormView.

$this->view = $this->key->getController()->getControlView($this->context);

$this->context in this code is the attribute key context that we just retrieved. In this example, the text attribute returns the basic attribute control view Concrete\Core\Attribute\Form\Control\View\View. This attribute-specific form view extends a basic form view used by all form controls (express or otherwise) that contains generic useful methods like isRequired() and getLabel(). These values are then set within the AttributeKeyFormView class. Since the view object will later be injected into the template, we'll have access to them.

Running render()

At the end of all of this, this code runs:

$renderer = $controlView->getControlRenderer();
print $renderer->render();

It's exactly like the earlier code that was responsible for rendering the outer express form; it creates a basic Concrete\Core\Form\Control\Renderer object, providing it the current view object (in this case AttributeKeyFormView) and the current FrontendFormContext method.

So what happens when we run render() in this case? Again, first we create the template locator using AttributeKeyFormView::createTemplateLocator; in this case, it simply delegates this responsibility to the attribute that it's wrapping:

public function createTemplateLocator()
{
    return $this->view->createTemplateLocator();
}

The attribute view itself then takes care of this.

public function createTemplateLocator()
{
    $locator = new TemplateLocator();
    $locator->addLocation(DIRNAME_ELEMENTS . DIRECTORY_SEPARATOR . DIRNAME_FORM_CONTROL_WRAPPER_TEMPLATES);
    return $locator;
}

This tells Concrete it will eventually need to look within elements/form. This is a general directory containing generic form wrapper templates. Next, we run setLocation($locator) from the passed context. Since we've swapped out contexts to use Concrete\Core\Attribute\Context\BasicFormContext at this point, our method looks like this:

public function setLocation(TemplateLocator $locator)
{
    $locator->setTemplate('bootstrap3');
    return $locator;
}

That means that we will be loading wrapper templates for this control from concrete/elements/form/bootstrap3.php (or application/elements/form/bootstrap3.php or elsewhere, by default.) What's this template look like?

<div class="form-group">
    <?php if ($view->supportsLabel()) { ?>
        <label class="control-label"><?=$view->getLabel()?></label>
    <?php } ?>

    <?php if ($view->isRequired()) { ?>
        <span class="text-muted small"><?=t('Required')?></span>
    <?php } ?>

    <?php $view->renderControl()?>
</div>

Pretty simple, right? Wherever $view->renderControl() exists within this wrapper template is where the attribute key will be rendered. Again, its rendering depends on the attribute context in question.

We'll go over more about how customizable this is in the next chapter – but for now lets check out a different express form control type.

Rendering the Binder (Association) Control

When running getControlView on an association control, we do something similar to when rendering an attribute key control: we check the context we're passing in, so we know which view class to deliver. In this instance, we retrieve an instance of the Concrete\Core\Express\Form\Control\View\AssociationFormView class.

Running render()

Next, we run render() on this control view. First this method creates the template locator, with some association-specific logic:

public function createTemplateLocator()
{
    // Is this an owning entity with display order? If so, we render a separate reordering control
    $element = $this->getFormFieldElement($this->control);
    $association = $this->association;
    if ($association->isOwningAssociation()) {
        if ($association->getTargetEntity()->supportsCustomDisplayOrder()) {
            $element = 'select_multiple_reorder';
        } else {
            return false;
        }
    }
    $locator = new TemplateLocator('association/' . $element);
    return $locator;
}

This looks confusing but it's pretty simple: if this the type of association that supports display order, we create a template locator with the template association/select_multiple_reoder; otherwise, if it's a one-to-many association we use association/select_multiple, or for single associations association/select. Next, our render() method calls setLocation($locator) on our locator, which tells us where to load these templates from, within the form context object. Combined, that means that our many-to-one document object will be loading its association control from elements/express/form/form/association/select.php.

Customization

Let's recap:

  • Every Express Control type and specific control gets a control view object
  • The control view object, when combined with a form context, tells Concrete what template to load
  • The control view object is injected into the template, so that control-specific logic can be used.

Now that we know a bit more about how these forms are rendered, let's figure out how to customize them for our particular site.