Skip to content

Step by Step Guide: Create a Cached Config Container

Introduction

The cached configuration container in your extension could be used to store any sort of configuration in code to have it easily and quickly accessible.

Configuration could be anything, such as custom groups, custom fields, activity types, case types etc...

The cached configuration container is an on the fly generated PHP file and stored in the templates_c folder of your CiviCRM installation. We don't need any extra database calls to retrieve the configuration it is all in code and therefore extremely fast to load.

In this guide we create a basic container for relationship types and for a custom group and a custom field.

Building the cached config container

When no cached config container exists one is created and then the following happens:

  1. The function ConfigContainer::build is called with a containerBuilder object as a parameter. In the function build we define how the ConfigContainer should be build when it is requested.
  2. The result from the containerBuilder object is stored as PHP code in the templates_c folder

Creating a config container instance

An instance of the config container is created by checking if the PHP file exists in the templates_c folder. If it exists then it is included and the config container object is created.

Requirements

We need two files:

  • ConfigContainer class, this one holds the actual configuration
  • ConfigContainerBuilder class, this one holds the functionality to generate a ConfigContainer class.

Create the config container skeleton

First we need to create the skeleton of the config container. The config container extends the symfony dependency injection Container class, this makes it easier to store it in a PHP file.

namespace \Civi\MyExtension;

use \Symfony\Component\DependencyInjection\Container;
use \Symfony\Component\DependencyInjection\ContainerBuilder;

class ConfigContainer extends Container {

  /**
   * Build the container with the custom field and custom groups.
   *
   * @param ContainerBuilder $containerBuilder
   */
  public static function build(ContainerBuilder $containerBuilder) {
    // This is where the actual retrieval of configuration happens.
  }

}

The first three lines consist of the namespace declaration and the use statements. We both need the Container and ContainerBuilder class. Those classes are from the symfony dependency injection system.

Then we define the name of our class: ConfigContainer extending the symfony Container class.

Retrieve and set the configuration

In this step we will define the contents of the config container. We will lookup a custom group (with the name MyCustomGroup), a custom field (with the name MyCustomField) and a relationship type (with the name Is Child).

We do this in the build function of the ConfigContainer class.

class ConfigContainer extends Container {

  /**
   * Build the container with the custom field and custom groups.
   *
   * @param ContainerBuilder $containerBuilder
   */
  public static function build(ContainerBuilder $containerBuilder) {
    $customGroup = civicrm_api3('CustomGroup', 'getsingle', ['name' => 'MyCustomGroup']);
    $customField = civicrm_api3('CustomField', 'getsingle', ['name' => 'MyCustomField']);
    $childRelationshipType = civicrm_api3('RelationshipType', 'getsingle', ['name_a_b' => 'Is Child']);

    // Now store the data in the container builder.
    $containerBuilder->setParameter('customGroup', $customGroup);
    $containerBuilder->setParameter('customField', $customField);
    $containerBuilder->setParameter('childRelationshipType', $childRelationshipType);
  }
}

Creating the Getter functions

Now it is time to add the getter methods to the class. We can access the data passed to the builder with the getParameter function. For maintainability, it is better to use our own defined getter methods so that we can make sure the return value is what we expected.

We will define the following getter methods:

  • GetMyCustomGroupId: to return the ID of the custom group
  • GetMyCustomFieldId: to return the ID of the custom field
  • GetChildRelationshipTypeId: to return the ID of the Is Child relationship type
class ConfigContainer extends Container {

  /**
   * @return int
   */
  public function getMyCustomGroupId() {
    return $this->getParameter('CustomGroup')['id'];
  }

  /**
   * @return int
   */
  public function getMyCustomFieldId() {
    return $this->getParameter('CustomField')['id'];
  }

  /**
   * @return int
   */
  public function getChildRelationshipTypeId() {
    return $this->getParameter('childRelationshipType')['id'];
  }

  /**
   * Build the container with the custom field and custom groups.
   *
   * @param ContainerBuilder $containerBuilder
   */
  public static function build(ContainerBuilder $containerBuilder) {
    // ... see previous step 
  }
}

Create the ConfigContainerBuilder class

We need to do one more thing and that is to create the config container builder class. This class takes care of creating and caching the ConfigContainer.

The ConfigContainerBuilder class follows the singleton pattern. An instance is returned by the function getInstance, if a cached class already exists it will include the cached class, if it does not exist it will create one.

The cached container is stored in the templates_c directory of CiviCRM. So whenvever the CiviCRM cache is cleared our cached config container class is also removed.

First we create the class by putting it in the same namespace as the ConfigContainer class.

Next we define a getInstance function where all the magic happens:

  1. First we check whether we already have an existing instance of configContainer if so we return that one.
  2. In the next bit we define the filename in $file. The filename contains an ID of the environment.
  3. If the file $file exists we will include it and create and return an instance of MyCachedConfigContainer, which is a sub class of ConfigContainer.
  4. In the next bit we will build and create the file $file. We do this by creating a new ContainerBuilder class and pass this to our earlier defined build function of ConfigContainer class.
  5. Lastly we will write the ContainerBuilder to the PHP file ($file) with the PhpDumper class. We state that we want to call the class MyCachedConfigContainer and that it is a subclass of ConfigContainer.
namespace \Civi\MyExtension;

use \Symfony\Component\DependencyInjection\Container;
use \Symfony\Component\DependencyInjection\ContainerBuilder;
use \Symfony\Component\DependencyInjection\Dumper\PhpDumper;

class ConfigContainerBuilder {

  /**
   * @var \Symfony\Component\DependencyInjection\Container
   */
  private static $configContainer;

  /**
   * @return \Civi\MyExtension\ConfigContainer
   */
  public static function getInstance() {
    if (!self::$configContainer) {
      $envId = \CRM_Core_Config_Runtime::getId();
      $file = CIVICRM_TEMPLATE_COMPILEDIR."/MyCachedConfigContainer.{$envId}.php";
      if (!file_exists($file)) {
        // Create the builder for the config container
        $containerBuilder = new ContainerBuilder();
        ConfigContainer::build($containerBuilder);
        $containerBuilder->compile();
        // Store the builder as a php file.
        $dumper = new PhpDumper($containerBuilder);
        file_put_contents($file, $dumper->dump([
          'class' => 'MyCachedConfigContainer',
          'base_class' => '\Civi\MyExtension\ConfigContainer',
        ]));
      }
      // Include the cached config container and instanciate a new instance
      require_once $file;
      self::$configContainer = new \MyCachedConfigContainer();
    }
    return self::$configContainer;
  }
}

How to use the ConfigContainer

Below an example of how to use the ConfigContainer class in your code.

We will first retrieve an instance of the ConfigContainer class from the ConfigContainerBuilder. We can then use this class to retrieve the configuration.

$configContainer = \Civi\MyExtension\ConfigContainerBuilder::getInstance();

$customGroupId = $configContainer->getCustomGroupId();
$isChildRelationshipTypeId = $configContainer->getChildRelationshipTypeId();

An example of the CachedConfigContainer

You can lookup the cached config container in the templates_c directory.

It has the filename of MyCachedConfigContainer.1234cdefgh.php (whereby 1234cdefgh is a random string).

Opening the file gives something like this below. The function getDefaultParameters contains the build configuration.

/**
 * This class has been auto-generated
 * by the Symfony Dependency Injection Component.
 *
 * @final since Symfony 3.3
 */
class MyCachedConfigContainer extends \Civi\MyExtension\ConfigContainer
{
  // ...
  /**
   * Gets the default parameters.
   *
   * @return array An array of the default parameters
   */
  protected function getDefaultParameters()
  {
    return [
      'customGroup' => [
        'id' => '1',
        'name' => 'MyCustomGroup',
        'title' => 'My Custom group',
        'extends' => 'Participant',
        'extends_entity_column_id' => '2',
        'extends_entity_column_value' => [0 => '1'],
        'style' => 'Inline',
        'collapse_display' => '0',
        'weight' => '1',
        'is_active' => '1',
        'table_name' => 'civicrm_value_my_custom_group_1',
        'is_multiple' => '0',
        'collapse_adv_display' => '0',
        'is_reserved' => '0',
        'is_public' => '1',
      ],
      'customField' => [
        'id' => '2',
        'custom_group_id' => '1',
        'name' => 'MyCustomField',
        'label' => 'My Custom Field',
        'data_type' => 'String',
        'html_type' => 'Text',
        'is_required' => '0',
        'is_searchable' => '1',
        'is_search_range' => '0',
        'weight' => '10',
        'is_active' => '1',
        'is_view' => '0',
        'text_length' => '255',
        'column_name' => 'my_custom_field_2',
        'serialize' => '0',
        'in_selector' => '0',
      ],
      // ...
    ];
  }

  // ...
}

Test your knowledge

See also