Merge pull request #17244 from totten/master-preboot
[civicrm-core.git] / Civi / Core / Event / GenericHookEvent.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 namespace Civi\Core\Event;
13
14 /**
15 * Class GenericHookEvent
16 * @package Civi\API\Event
17 *
18 * The GenericHookEvent is used to expose all traditional hooks to the
19 * Symfony EventDispatcher.
20 *
21 * The traditional notation for a hook is based on a function signature:
22 *
23 * function hook_civicrm_foo($bar, &$whiz, &$bang);
24 *
25 * The notation for Symfony Events is based on a class with properties
26 * and methods. This requires some kind of mapping. `GenericHookEvent`
27 * maps each parameter to a field (using magic methods):
28 *
29 * ```
30 * // Creating an event object.
31 * $event = GenericHookEvent::create(array(
32 * 'bar' => 'abc',
33 * 'whiz' => &$whiz,
34 * 'bang' => &$bang,
35 * );
36 *
37 * // Accessing event properties.
38 * echo $event->bar;
39 * $event->whiz['array_field'] = 123;
40 * $event->bang->objProperty = 'abcd';
41 *
42 * // Dispatching an event.
43 * Civi::dispatcher()->dispatch('hook_civicrm_foo', $event);
44 * ```
45 *
46 * Design Discussion:
47 *
48 * 1. Implementing new event classes for every hook would produce a
49 * large amount of boilerplate. Symfony Events have an interesting solution to
50 * that problem: use `GenericEvent` instead of custom event classes.
51 * `GenericHookEvent` is conceptually similar to `GenericEvent`, but it adds
52 * support for (a) altering properties and (b) mapping properties to hook notation
53 * (an ordered parameter list).
54 *
55 * 2. A handful of hooks define a return-value. The return-value is treated
56 * as an array, and all the returned values are merged into one big array.
57 * You can add and retrieve return-values using these methods:
58 *
59 * ```
60 * $event->addReturnValues(array(...));
61 * foreach ($event->getReturnValues() as $retVal) { ... }
62 * ```
63 */
64 class GenericHookEvent extends \Symfony\Component\EventDispatcher\Event {
65
66 /**
67 * @var array
68 * Ex: array(0 => &$contactID, 1 => &$contentPlacement).
69 */
70 protected $hookValues;
71
72 /**
73 * @var array
74 * Ex: array(0 => 'contactID', 1 => 'contentPlacement').
75 */
76 protected $hookFields;
77
78 /**
79 * @var array
80 * Ex: array('contactID' => 0, 'contentPlacement' => 1).
81 */
82 protected $hookFieldsFlip;
83
84 /**
85 * Some legacy hooks expect listener-functions to return a value.
86 * OOP listeners may set the $returnValue.
87 *
88 * This field is not recommended for use in new hooks. The return-value
89 * convention is not portable across different implementations of the hook
90 * system. Instead, it's more portable to provide an alterable, named field.
91 *
92 * @var mixed
93 * @deprecated
94 */
95 private $returnValues = [];
96
97 /**
98 * List of field names that are prohibited due to conflicts
99 * in the class-hierarchy.
100 *
101 * @var array
102 */
103 private static $BLACKLIST = [
104 'name',
105 'dispatcher',
106 'propagationStopped',
107 'hookBlacklist',
108 'hookValues',
109 'hookFields',
110 'hookFieldsFlip',
111 ];
112
113 /**
114 * Create a GenericHookEvent using key-value pairs.
115 *
116 * @param array $params
117 * Ex: array('contactID' => &$contactID, 'contentPlacement' => &$contentPlacement).
118 * @return \Civi\Core\Event\GenericHookEvent
119 */
120 public static function create($params) {
121 $e = new static();
122 $e->hookValues = array_values($params);
123 $e->hookFields = array_keys($params);
124 $e->hookFieldsFlip = array_flip($e->hookFields);
125 self::assertValidHookFields($e->hookFields);
126 return $e;
127 }
128
129 /**
130 * Create a GenericHookEvent using ordered parameters.
131 *
132 * @param array $hookFields
133 * Ex: array(0 => 'contactID', 1 => 'contentPlacement').
134 * @param array $hookValues
135 * Ex: array(0 => &$contactID, 1 => &$contentPlacement).
136 * @return \Civi\Core\Event\GenericHookEvent
137 */
138 public static function createOrdered($hookFields, $hookValues) {
139 $e = new static();
140 if (count($hookValues) > count($hookFields)) {
141 $hookValues = array_slice($hookValues, 0, count($hookFields));
142 }
143 $e->hookValues = $hookValues;
144 $e->hookFields = $hookFields;
145 $e->hookFieldsFlip = array_flip($e->hookFields);
146 self::assertValidHookFields($e->hookFields);
147 return $e;
148 }
149
150 /**
151 * @param array $fields
152 * List of field names.
153 */
154 private static function assertValidHookFields($fields) {
155 $bad = array_intersect($fields, self::$BLACKLIST);
156 if ($bad) {
157 throw new \RuntimeException("Hook relies on conflicted field names: "
158 . implode(', ', $bad));
159 }
160 }
161
162 /**
163 * @return array
164 * Ex: array(0 => &$contactID, 1 => &$contentPlacement).
165 */
166 public function getHookValues() {
167 return $this->hookValues;
168 }
169
170 /**
171 * @return mixed
172 * @deprecated
173 */
174 public function getReturnValues() {
175 return empty($this->returnValues) ? TRUE : $this->returnValues;
176 }
177
178 /**
179 * @param mixed $fResult
180 * @return GenericHookEvent
181 * @deprecated
182 */
183 public function addReturnValues($fResult) {
184 if (!empty($fResult) && is_array($fResult)) {
185 $this->returnValues = array_merge($this->returnValues, $fResult);
186 }
187 return $this;
188 }
189
190 /**
191 * @inheritDoc
192 */
193 public function &__get($name) {
194 if (isset($this->hookFieldsFlip[$name])) {
195 return $this->hookValues[$this->hookFieldsFlip[$name]];
196 }
197 }
198
199 /**
200 * @inheritDoc
201 */
202 public function __set($name, $value) {
203 if (isset($this->hookFieldsFlip[$name])) {
204 $this->hookValues[$this->hookFieldsFlip[$name]] = $value;
205 }
206 }
207
208 /**
209 * @inheritDoc
210 */
211 public function __isset($name) {
212 return isset($this->hookFieldsFlip[$name])
213 && isset($this->hookValues[$this->hookFieldsFlip[$name]]);
214 }
215
216 /**
217 * @inheritDoc
218 */
219 public function __unset($name) {
220 if (isset($this->hookFieldsFlip[$name])) {
221 // Unset while preserving order.
222 $this->hookValues[$this->hookFieldsFlip[$name]] = NULL;
223 }
224 }
225
226 /**
227 * Determine whether the hook supports the given field.
228 *
229 * The field may or may not be empty. Use isset() or empty() to
230 * check that.
231 *
232 * @param string $name
233 * @return bool
234 */
235 public function hasField($name) {
236 return isset($this->hookFieldsFlip[$name]);
237 }
238
239 }