4

I want to give others the chance to do something when certain events happen in my extension.

So to be clear: I'm not wanting to use existing hooks in my extension, I'm wanting to create a new hook that my extension will invoke and other code may choose to react to.

I couldn't find anything in the developers' guide about this. Looking at the source makes me think that maybe I should be doing something with CRM_Utils_Hook or perhaps \Civi::dispatcher()->dispatch('hook_my_new_hook', $some_event_object)?

Is there a standardised way to do this? Are there any good examples?

4 Answers 4

7

EDIT

This answer is now deprecated, you should use @bgm's answer instead, or for an alternative and possibly cleaner approach, try my other answer

All thanks to the code example link in Eric's answer here's a simplified example of how to offer up a hook (my_new_hook) which takes one argument, $the_argument:

CRM_Utils_Hook::singleton()->invoke(
  1, $the_argument,
  $dummy, $dummy, $dummy, $dummy, $dummy,
  'my_new_hook');                                                                      

Then other extensions can include functions like

function myextension_my_new_hook($arg) {
  do_something_with($arg);
}

The $dummy is just because we must pass a variable (since it's passed by reference), but this hook does not need it. If you had a hook that took two arguments, you'd use 2 in place of 1 and replace the first $dummy with your actual second argument etc. up to 6.

Nb. this code assumes CiviCRM 4.5+, but obviously you're not using anything older than that these days, right?!

6
  • 1
    Instead of $dummy you could use CRM_Core_DAO::$_nullObject Commented May 24, 2018 at 14:19
  • Yep, but not sure what the advantage of that is? Commented May 25, 2018 at 13:41
  • There is no advantage except that is somehow a standard way of doing such things in CiviCRM. Commented May 28, 2018 at 7:22
  • 1
    Standards are important, I agree. However in this case it is a little odd as the reason parameters have to be passed in this way is because they are passed by reference so that the function receiving them can change them. Passing a static variable could result in that static var being changed, so I think the way I've done it is safer as well as clearer and more succinct. Commented Jun 4, 2018 at 8:06
  • 1
    Note the invoke() syntax with the first parameter being an integer is now deprecated. See the named parameter example from @bgm
    – Aidan
    Commented Oct 8, 2020 at 18:40
5

Here is an example of a hook we created for CiviRules:

https://lab.civicrm.org/extensions/civirules/blob/master/CRM/Civirules/Utils/HookInvoker.php

Hope this helps :-)

2
5

Here is an example using the named parameter syntax:

CRM_Utils_Hook::singleton()->invoke(
  ['pdf', 'pdf_variables', 'receipt'],
  $pdf, $pdf_variables, $receipt,
  CRM_Utils_Hook::$_nullObject, CRM_Utils_Hook::$_nullObject, CRM_Utils_Hook::$_nullObject,
  'cdntaxreceipts_writeReceipt');

One advantage of this syntax, is that extension can then implement the hook with the Symfony syntax. When managed by Symfony, it's easier to change the priority or add/remove hooks from other extensions.

Example:

/**
 * Implements hook_civicrm_container().
 *
 * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_container/
 */
function myext_civicrm_container($container) {
  // https://docs.civicrm.org/dev/en/latest/hooks/usage/symfony/
  $container->findDefinition('dispatcher')
    ->addMethodCall('addListener', ['hook_cdntaxreceipts_writeReceipt', 'myext_cdntaxreceipts_symfony_writeReceipt']);
}

/**
 * Implements hook_cdntaxreceipts_writeReceipt() via Symfony.
 *
 * @see myext_civicrm_container()
 */
function myext_cdntaxreceipts_symfony_writeReceipt($event) {
  // Access the parameters with:
  // $event->pdf;
  // $event->pdf_variables;
  // $event->receipt;
}

Note that the implementing function myext_cdntaxreceipts_symfony_writeReceipt has symfony in the name only to avoid being called by the traditional hook system. We can name the function however we want.

1

I find the 'normal' way pretty ugly.

  • listing the args by name in an array, then
  • supplying the args, then
  • needing to pass by ref a NULL/dummy variable for each of the remaining 6 or was it 7? arguments - this is ugly and error prone and passing CRM_Core_DAO::$_nullObject is risky as it allows the hook to overwrite that so it's no longer NULL.

I think the following can be a nicer approach, here's an example:

Fire an event

// Create an event object with all the data you wan to pass in.
$event = Civi\Core\Event\GenericHookEvent::create(
  ['fname' => $fname, 'votes' => &$votes]);
Civi::dispatcher()->dispatch('civi.something.you.make.up', $event);

Example listener

function your_listener_callable($event) {
  print "running " . __FUNCTION__ . "\n";
  // We can overwrite data
  $event->fname = 'Wilma Flintstone';
  // We can alter anything passed by reference.
  $event->votes++;
  // To return a value you need to pass it in an array like so:
  $event->addReturnValues(['Successfully recorded votes!']);
  // We can't write any properties that the event did
  // not originally have defined, so this won't work:
  $event->friend = 'Barney';
}

Use data resulting from the event

This code would come directly after the 'Fire an event' code.

print "$fname ($event->fname) now has $votes votes.\n";
$return = $event->getReturnValues();
print "The hook returned: $return[0]\n";
// $event->friend === NULL

Link your listener to the event.

You'd normally do this in a hook_civicrm_container() function to ensure it is only added once.

/**
 * Implements hook_civicrm_container().
 *
 * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_container/
 */
function myext_civicrm_container($container) {
  // https://docs.civicrm.org/dev/en/latest/hooks/usage/symfony/
  $container->findDefinition('dispatcher')
  ->addMethodCall('addListener', [
    'civi.something.you.made.up',
    'your_listener_callable']);
}

Full working command line example

You can run this with cv scr hookexample.php

<?php
use \Civi\Core\Event\GenericHookEvent;

if (php_sapi_name() !== 'cli') {
  http_response_code(404);
  exit;
}

function your_listener_callable($event) {
  print "running " . __FUNCTION__ . "\n";
  // We can overwrite data passed by value, but that's probably confusing.
  $event->fname = 'Wilma Flintstone';
  // We can alter anything passed by reference.
  $event->votes++;
  // To return a value you need to pass it in an array like so:
  $event->addReturnValues(['Successfully recorded votes!']);
  // We can't write any properties that the event did not originally 
  // have defined, so this won't work:
  $event->friend = 'Barney';
}

// As this is a single-pass script we can just do this:
Civi::dispatcher()->addListener('civi.something.you.make.up', 'your_listener_callable');

$votes = 0;
$fname = 'Wilma';
print "$fname has $votes votes.\n";

$event = GenericHookEvent::create(['fname' => 'Wilma', 'votes' => &$votes]);
Civi::dispatcher()->dispatch('civi.something.you.make.up', $event);

$return = $event->getReturnValues();

print "$fname ($event->fname) now has $votes votes.\n";
print "The hook returned: $return[0]\n";
// $event->friend is NULL.

Outputs:

Wilma has 0 votes.
running your_listener_callable
Wilma (Wilma Flintstone) now has 1 votes.
The hook returned: Successfully recorded votes!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.