Skip to content
Tim Otten edited this page Sep 17, 2024 · 16 revisions

(This documentation is a draft/WIP.)

Background

In CiviCRM, an entity is a persistent data-type. Entities are defined by their schema; then ordinarily stored in MySQL; and finally accessed with various tools (like "DAO", "BAO", "APIv3", "APIv4", "Search Kit", and "Form Builder").

As of May-June 2024 (CiviCRM v5.74-5.75; dev/core#4999), major elements of the entity framework have changed -- and we now have two distinctive designs. We can call these Entity Framework v1 (EFv1) and Entity Framework v2 (EFv2).

The two versions have a high degree of runtime interoperability, enabling you to mix-and-match versions. They differ mostly in the file-layout and implementation.

In civix v24.09+, all templates and commands will generate code for EFv2. It will no longer generate EFv1. It can convert existing entities from EFv1 to EFv2, although this is discretionary.

This page aims to describe key differences and any major trade-offs.

Comparison

The essential difference is the number of files and the heaviness of the code-generation.

Entity Framework v1 (EFv1) Entity Framework v2 (EFv2)
Design Summary More files.
More duplicate information.
More generated code.
Fewer files.
Less duplicate information.
Less generated code.
Adoption CiviCRM <= 5.73
Civix <= 23.12
CiviCRM >= 5.74
Civix >= 24.09
Compatibility CiviCRM 5.x (roughly; varies somewhat) CiviCRM >= 5.45

EFv1 and EFv2 use many of the same concepts -- which simplifies migration and enables interoperability. However, the changes are deep - and we should examine them from multiple angles.

File layout

Browsing the files gives a quick overview of what's changed:

Entity Framework v1 (EFv1) Entity Framework v2 (EFv2) Comment
info.xml info.xml Changed <upgrader> and <mixin>
my_extension.civix.php my_extension.civix.php Changed class-loading
xml/schema/MyEntity.xml
xml/schema/MyEntity.entityType.php
schema/MyEntity.entityType.php
 
Changed file format
 
CRM/MyExtension/Upgrader.php CRM/MyExtension/Upgrader.php Unchanged (mostly)
CRM/MyExtension/BAO/MyEntity.php CRM/MyExtension/BAO/MyEntity.php Unchanged
CRM/MyExtension/DAO/MyEntity.php CRM/MyExtension/DAO/MyEntity.php Complete rewrite (heavy vs light)
Civi/Api4/MyEntity.php Civi/Api4/MyEntity.php Unchanged
api/v3/MyEntity.php api/v3/MyEntity.php Unchanged
sql/auto_install.sql
sql/auto_uninstall.sql
(none)
 
Complete removal
 
mixin/entity-type-php@1.0.0.php
 
mixin/entity-type-php@2.0.0.php
mixin/lib/civimix-schema@5.XX.php
New helpers
 

Workflow

  • To make a new entity in EFv1, run 3 commands:

    civix generate:upgrader
    civix generate:entity MyEntity
    civix generate:entity-boilerplate
  • To make a new entity in EFv2, run 1 commands:

    civix generate:entity MyEntity
  • N.B. EFv2 removes the command civix generate:entity-boilerplate.

Install SQL

  • For installation, EFv2 uses dynamic SQL. Statements like CREATE TABLE are generated on-the-fly by the schema-helper (E::schema() a.k.a. mixin/lib/civimix-schema@XXX.phar/src/SchemaHelper.php).

  • For installation, EFv1 uses static SQL files. When you run generate:entity-boilerplate, it reads from xml/schema/*.xml and creates sql/auto_install.sql (with statements like CREATE TABLE).

  • N.B. Dynamic SQL is more attuned. CiviCRM databases may have different collations. EFv2 will always match the local collation. EFv1 will not.

  • N.B. Static SQL is more hackable. EFv1 allows you to hand-edit the SQL files. EFv2 does not.

Upgraders

  • EFv2 registers an upgrader via info.xml:

    <!-- EFv2 registration via info.xml -->
    
    <extension key="my_extension">
      <upgrader>CiviMix\Schema\MyExtension\AutomaticUpgrader</upgrader>
    </extension>

    The AutomaticUpgrader delegates responsibility to other files, i.e.

    • It calls E::schema()->install(), which reads schema/*.entityType.php and automatically creates the SQL tables.
    • It calls your custom upgrade rules (CRM_MyExtension_Upgrader). Any functions like upgrade_XXX() will work the same as before.
  • EFv1 directly registers your upgrader. It may use either *.civix.php or info.xml:

    // EFv1 registration via *.civix.php
    
    function _my_extension_civix_civicrm_install() {
      _my_extension_civix_civicrm_config();
      if ($upgrader = _my_extension_civix_upgrader()) {
        $upgrader->onInstall();
      }
    }
    
    function _my_extension_civix_upgrader() {
      if (!file_exists(__DIR__ . '/CRM/MyExtension/Upgrader.php')) {
        return NULL;
      }
      else {
        return CRM_MyExtension_Upgrader_Base::instance();
      }
    }

    or

    <!-- EFv1 registration via info.xml -->
    
    <extension key="my_extension">
      <upgrader>CRM_MyExtension_Upgrader</upgrader>
    </extension>
  • N.B. EFv2's AutomaticUpgrader still calls your custom CRM_MyExtension_Upgrader. It should be compatible with ~99% of implementations, and it allows us to swap the SQL generator without patching downstream code.

    There can be exceptions. How do you recognize compatible and incompatible upgraders?

    • CRM_*_Upgrader defines supported methods for overrides/customization. These are fine: install(), enable(), disable(), uninstall(), and upgrade_XXX().
    • CRM_*_Upgrader also inherits some utility and adapter methods. Overriding these can be problematic.
      • Some overrides are strictly unsupported. Overriding these functions will lead to exceptions: appendTask(), onUpgrade(), getRevisions(), getCurrentRevision(), setCurrentRevision(), enqueuePendingRevisions(), hasPendingRevisions()
      • Some overrides are merely suspicious. They may or may not behave as intended: notify(), onInstall(), onUninstall()

    If you have an upgrader that cannot be made to work with these restrictions, then you don't have to use AutomaticUpgrader. You may alternatively patch your own upgrader to explicitly call E::schema()->install() and E::schema()->uninstall().

DAOs

  • EFv2 uses light-weight DAO class-files. The files appear almost empty:

    class CRM_MyExtension_DAO_MyEntity extends CRM_MyExtension_DAO_Base {
    }

    All major functions are provided by the base-class. Information is loaded directly from schema/MyEntity.entityType.php.

    The base-class CRM_MyExtension_DAO_Base is generally the same as CRM_Core_DAO_Base (from CiviCRM 5.74+). The exact details depend on your runtime environment. Either:

    • (Deployed on CiviCRM >=5.74): CRM_MyExtension_DAO_Base is literally an alias for CRM_Core_DAO_Base.
    • (Deployed on CiviCRM <=5.73): CRM_MyExtension_DAO_Base is a polyfill based on CiviMix\Schema\MyExtension\DAO (a.k.a. mixin/lib/civimix-schema@XXX.phar/src/DAO.php).
  • EFv1 uses heavy-weight DAO class-files. When you run generate:entity-boilerplate, it copies information from xml/schema/*.xml to the DAO file. The DAO file resembles this skeleton:

    class CRM_MyExtension_DAO_MyEntity extends CRM_Core_DAO {
      public static function &fields() {
        return ...[ 
          field1... data type... html formatting... comments...,
          field2... data type... html formatting... comments...,
          ...
        ]...; 
      }
    
      public static function &fieldKeys() {
        return ...;
      }
    
      public static function &indices() {
        return ...;
      }
    
      // Ad nauseam...
    }
  • With EFv2's lightweight DAO, there are more ways to seamlessly evolve the contract (updating CRM_Core_DAO_Base and/or CiviMix\Schema\MyExtension\DAO) without needing to re-generate specific DAO files.

  • With EFv2's lightweight DAO, we lose IDE support for auto-completing DAO fields.

    • It's expected that this will be less problematic as DAO fields have become a smaller part of overall developer-experience. (APIv3, APIv4, SearchKit, and Afform are more common ways to interact with fields -- and they're not affected.)
    • However, if you want IDE auto-complete for fields, then you can add docblocks. This will have no effect on functionality or upgradeability.
    /**
     * @property int $id
     * @property int $contact_id
     * @property string $title
     */
    class CRM_MyExtension_DAO_MyEntity extends CRM_MyExtension_DAO_Base {

Mixins

  • EFv2 uses mixin/lib/civimix-schema@5.XXX.phar to load helpers like E::schema(), CiviMix\Schema\*\AutomaticUpgrader, and CiviMix\Schema\*\DAO.
  • EFv2 uses mixin/entity-type-php@2.XXX.php to register entities.
  • EFv1 uses mixin/entity-type-php@1.XXX.php to register entities.
Clone this wiki locally