Skip to content

Instantly share code, notes, and snippets.

@konadave
Last active July 17, 2022 19:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save konadave/0f9a6db5f536a84e626abe09873ff86f to your computer and use it in GitHub Desktop.
Save konadave/0f9a6db5f536a84e626abe09873ff86f to your computer and use it in GitHub Desktop.
How to subclass a core class in CiviCRM

If you are an extension developer, you may have come across a scenario where you need to change core behavior but there isn't an available hook or other means to go about achieving it. It's tempting to override the core file by copying it into your extension and then making your changes. Avoid it if you can.

An extension that overrides a core file has two giant strikes against it. First off, any changes to the file in core, whether security fixes or new features or whatever, will not be seen until you incorporate those same changes into your override. Secondly, an extension containing a core override is automatically incompatible with every other extension that overrides the same file.

Before committing to a core override, please do your due diligence. Make sure none of the existing hooks can be used. Ask in chat. Maybe post an issue in GitLab suggesting a new hook be added to core, etc. You might spend a little more time upfront, but avoiding the hassle of maintaining your override will be worth the effort.

Once you're certain the override is required, you can avoid the first strike mentioned above by subclassing the core class. Depending on your needs, this may or may not be possible, e.g. if you need to change behavior in the middle of an existing method then subclassing wouldn't really work. This method doesn't get past the second strike because it is still technically an override.

I recently created an extension, https://lab.civicrm.org/extensions/btrmsgtpl, where I wanted to capture the data being sent to a message template. There is a hook I could use for this, hook_civicrm_alterMailParams(), but it needed to be the last extension called. Before I realized I could use EventDispatcher to make sure that happened, I tried the technique shown in MessageTemplate.php and it worked without issues.

The gist of it:

  • override the file in the extension, but don't start with a copy from core.
  • read in the original core class.
  • do some string replacing.
  • evalutate it.
  • subclass it.

As far as "do some string replacing", you might be able to just blanket search/replace on the class name and on self:: and be fine, I didn't try that. I wanted to be very specific in what I replaced. Depending on the class you want to override, you may have to do some adjusting, i.e. YMMV.

<?php
global $civicrm_root;
// whether civicrm-core enforces an immediate opening php tag and no closing php tag?
define('CIVI_STYLE_ENFORCED', FALSE);
// load core class
$core = file_get_contents("{$civicrm_root}CRM/Core/BAO/MessageTemplate.php");
if (CIVI_STYLE_ENFORCED) {
// remove opening php tag
$core = substr($core, 5);
}
else {
// remove opening php tag
$core = preg_replace('/^\s*<\?php/', '', $core);
// remove closing php tag
$core = preg_replace('/\?>\s*$/', '', $core);
}
// change core class name
$core = str_replace([
// original class name
'class CRM_Core_BAO_MessageTemplate ',
// update singleton instantiation - not needed in this particular example
'new CRM_Core_BAO_MessageTemplate(',
// if class calls static method you override, update for late static binding
'self::renderTemplateRaw'
], [
'class CRM_Core_BAO_MessageTemplate_BaseClass ',
'new CRM_Core_BAO_MessageTemplate_BaseClass(',
'static::renderTemplateRaw'
], $core);
// evaulate it
eval($core);
// now we can subclass it!
class CRM_Core_BAO_MessageTemplate extends CRM_Core_BAO_MessageTemplate_BaseClass {
protected static function renderTemplateRaw($params) {
$result = parent::renderTemplateRaw($params);
if (!empty($result[1]['workflow'])) {
$vars = [];
$tplParams = $result[1]['tplParams'];
foreach($tplParams as $key => $val) {
if (is_array($val)) {
$type = 'structured';
$val = var_export($val, TRUE);
}
elseif (is_bool($val)) {
$type = 'bool';
}
else {
$type = 'simple';
}
if (!is_null($val)) {
$vars[] = [
'name' => "\$$key",
'type' => $type,
'value' => $val
];
}
}
if (!empty($params['tokenContext'])) {
$vars[] = array_merge(['name' => 'tokenContext'], $params['tokenContext']);
}
try {
if (!empty($result[1]['messageTemplateID'])) {
$tplID = $result[1]['messageTemplateID'];
}
else {
$tplID = civicrm_api3('MessageTemplate', 'getvalue', [
'workflow_name' => $result[1]['workflow'],
'is_default' => 1,
'return' => 'id'
]);
}
if (!empty($result[1]['contactId'])) {
$name = civicrm_api3('Contact', 'getvalue', [
'id' => $result[1]['contactId'],
'return' => 'display_name'
]);
}
else {
$name = 'Anonymous';
}
$now = date('Ymd.His');
civicrm_api3('MessageTemplateData', 'create', [
'msg_template_id' => $tplID,
'msg_variables' => json_encode($vars),
'sample_name' => "$name $now"
]);
}
catch (Exception $e) {
\Civi::log()->error($e->getMessage());
}
}
return $result;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment