Commit | Line | Data |
---|---|---|
762dc04d TO |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
41498ac5 | 4 | | Copyright CiviCRM LLC. All rights reserved. | |
762dc04d | 5 | | | |
41498ac5 TO |
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 | | |
762dc04d TO |
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 | * | |
0d681395 TO |
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): | |
762dc04d | 28 | * |
0b882a86 | 29 | * ``` |
0d681395 | 30 | * // Creating an event object. |
762dc04d | 31 | * $event = GenericHookEvent::create(array( |
0d681395 TO |
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. | |
4162ab6f | 43 | * Civi::dispatcher()->dispatch('hook_civicrm_foo', $event); |
0b882a86 | 44 | * ``` |
0d681395 TO |
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 | * | |
0b882a86 | 59 | * ``` |
0d681395 TO |
60 | * $event->addReturnValues(array(...)); |
61 | * foreach ($event->getReturnValues() as $retVal) { ... } | |
0b882a86 | 62 | * ``` |
762dc04d TO |
63 | */ |
64 | class GenericHookEvent extends \Symfony\Component\EventDispatcher\Event { | |
65 | ||
66 | /** | |
67 | * @var array | |
0d681395 | 68 | * Ex: array(0 => &$contactID, 1 => &$contentPlacement). |
762dc04d TO |
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 | */ | |
c64f69d9 | 95 | private $returnValues = []; |
762dc04d | 96 | |
0fcad357 TO |
97 | /** |
98 | * List of field names that are prohibited due to conflicts | |
99 | * in the class-hierarchy. | |
100 | * | |
101 | * @var array | |
102 | */ | |
c64f69d9 | 103 | private static $BLACKLIST = [ |
0fcad357 TO |
104 | 'name', |
105 | 'dispatcher', | |
106 | 'propagationStopped', | |
107 | 'hookBlacklist', | |
108 | 'hookValues', | |
109 | 'hookFields', | |
110 | 'hookFieldsFlip', | |
c64f69d9 | 111 | ]; |
0fcad357 | 112 | |
762dc04d TO |
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); | |
0fcad357 | 125 | self::assertValidHookFields($e->hookFields); |
762dc04d TO |
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); | |
0fcad357 | 146 | self::assertValidHookFields($e->hookFields); |
762dc04d TO |
147 | return $e; |
148 | } | |
149 | ||
0fcad357 TO |
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 | ||
762dc04d TO |
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 | } |