dolibarr 22.0.5
hookmanager.class.php
Go to the documentation of this file.
1<?php
2
3/* Copyright (C) 2010-2016 Laurent Destailleur <eldy@users.sourceforge.net>
4 * Copyright (C) 2010-2014 Regis Houssin <regis.houssin@inodbox.com>
5 * Copyright (C) 2010-2011 Juanjo Menent <jmenent@2byte.es>
6 * Copyright (C) 2024-2025 MDW <mdeweerd@users.noreply.github.com>
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see <https://www.gnu.org/licenses/>.
20 */
21
33{
37 public $db;
38
42 public $error = '';
43
47 public $errors = array();
48
52 public $contextarray = array();
53
57 public $hooks = array();
58
62 public $hooksSorted = array();
63
67 public $hooksHistory = [];
68
72 public $resArray = array();
73
77 public $resPrint = '';
78
82 public $resNbOfHooks = 0;
83
90 public function __construct($db)
91 {
92 $this->db = $db;
93 }
94
95
107 public function initHooks($arraycontext)
108 {
109 global $conf;
110
111 // Test if there is at least one hook to manage
112 if (!is_array($conf->modules_parts['hooks']) || empty($conf->modules_parts['hooks'])) {
113 return 0;
114 }
115
116 // For backward compatibility
117 if (!is_array($arraycontext)) {
118 $arraycontext = array($arraycontext);
119 }
120
121 $this->contextarray = array_unique(array_merge($arraycontext, $this->contextarray)); // All contexts are concatenated but kept unique
122
123 $foundcontextmodule = false;
124
125 // Loop on each module that bring hooks. Add an entry into $arraytolog if we found a module that ask to act in the context $arraycontext
126 foreach ($conf->modules_parts['hooks'] as $module => $hooks) {
127 if (!isModEnabled($module)) {
128 continue;
129 }
130
131 //dol_syslog(get_class($this).'::initHooks module='.$module.' arraycontext='.join(',',$arraycontext));
132 foreach ($arraycontext as $context) {
133 if (is_array($hooks)) {
134 $arrayhooks = $hooks; // New system = array of hook contexts claimed by the module $module
135 } else {
136 $arrayhooks = explode(':', $hooks); // Old system (for backward compatibility)
137 }
138
139 if (!in_array($context, $arrayhooks) && !in_array('all', $arrayhooks)) {
140 // We instantiate action class only if initialized hook is handled by the module
141 // Hook was already initialized for this context and module
142 continue;
143 }
144
145 // Include actions class overwriting hooks
146 if (empty($this->hooks[$context][$module]) || !is_object($this->hooks[$context][$module])) { // If set to an object value, class was already loaded so we do nothing.
147 $path = '/'.$module.'/class/';
148 $actionfile = 'actions_'.$module.'.class.php';
149
150 $resaction = dol_include_once($path.$actionfile);
151 if ($resaction) {
152 $controlclassname = 'Actions'.ucfirst($module);
153
154 $actionInstance = new $controlclassname($this->db);
155 '@phan-var-force CommonHookActions $actionInstance';
156
157 // @phan-suppress-next-line PhanUndeclaredProperty
158 $priority = (!property_exists($actionInstance, 'priority') || empty($actionInstance->priority)) ? 50 : $actionInstance->priority;
159
160 $this->hooks[$context][$module] = $actionInstance;
161 $this->hooksSorted[$context][$priority.':'.$module] = $actionInstance;
162
163 $foundcontextmodule = true;
164
165 // Hook has been initialized with another couple $context/$module
166 $stringtolog = 'context='.$context.'-path='.$path.$actionfile.'-priority='.$priority;
167 dol_syslog(get_class($this)."::initHooks Loading hooks: ".$stringtolog, LOG_DEBUG);
168 } else {
169 dol_syslog(get_class($this)."::initHooks Failed to load hook in ".$path.$actionfile, LOG_WARNING);
170 }
171 }
172 }
173 }
174
175 // Log the init of hook
176 // dol_syslog(get_class($this)."::initHooks Loading hooks: ".implode(', ', $arraytolog), LOG_DEBUG);
177
178 if ($foundcontextmodule) {
179 foreach ($arraycontext as $context) {
180 if (!empty($this->hooksSorted[$context])) {
181 ksort($this->hooksSorted[$context], SORT_NATURAL);
182 }
183 }
184 }
185
186 return 1;
187 }
188
201 public function executeHooks($method, $parameters = array(), &$object = null, &$action = '')
202 {
203 if (isModEnabled('debugbar') && function_exists('debug_backtrace')) {
204 $trace = debug_backtrace();
205 if (isset($trace[0])) {
206 $hookInformations = [
207 'name' => $method,
208 'contexts' => $this->contextarray,
209 'file' => $trace[0]['file'],
210 'line' => $trace[0]['line'],
211 'count' => 0,
212 ];
213 $hash = md5(json_encode($hookInformations));
214 if (!empty($this->hooksHistory[$hash])) {
215 $this->hooksHistory[$hash]['count']++;
216 } else {
217 $hookInformations['count'] = 1;
218 $this->hooksHistory[$hash] = $hookInformations;
219 }
220 }
221 }
222
223 if (!is_array($this->hooks) || empty($this->hooks)) {
224 return 0; // No hook available, do nothing.
225 }
226 if (!is_array($parameters)) {
227 dol_syslog('executeHooks was called with a non array $parameters. Surely a bug.', LOG_WARNING);
228 $parameters = array();
229 }
230
231 $parameters['context'] = implode(':', $this->contextarray);
232 //dol_syslog(get_class($this).'::executeHooks method='.$method." action=".$action." context=".$parameters['context']);
233
234 // Define type of hook ('output' or 'addreplace').
235 $hooktype = 'addreplace';
236 // TODO Remove hooks with type 'output' (example createFrom). All these hooks must be converted into 'addreplace' hooks.
237 if (in_array($method, array(
238 'createFrom',
239 'dashboardAccountancy',
240 'dashboardActivities',
241 'dashboardCommercials',
242 'dashboardContracts',
243 'dashboardDonation',
244 'dashboardEmailings',
245 'dashboardExpenseReport',
246 'dashboardHRM',
247 'dashboardInterventions',
248 'dashboardMRP',
249 'dashboardMembers',
250 'dashboardOpensurvey',
251 'dashboardOrders',
252 'dashboardOrdersSuppliers',
253 'dashboardProductServices',
254 'dashboardProjects',
255 'dashboardPropals',
256 'dashboardSpecialBills',
257 'dashboardSupplierProposal',
258 'dashboardThirdparties',
259 'dashboardTickets',
260 'dashboardUsersGroups',
261 'dashboardWarehouse',
262 'dashboardWarehouseReceptions',
263 'dashboardWarehouseSendings',
264 'insertExtraHeader',
265 'insertExtraFooter',
266 'printLeftBlock',
267 'formAddObjectLine',
268 'formBuilddocOptions',
269 'showSocinfoOnPrint'
270 ))) {
271 $hooktype = 'output';
272 }
273
274 // Init return properties
275 $localResPrint = '';
276 $localResArray = array();
277
278 $this->resNbOfHooks = 0;
279
280 // Here, the value for $method and $hooktype are given.
281 // Loop on each hook to qualify modules that have declared context
282 $modulealreadyexecuted = array();
283 $resaction = 0;
284 $error = 0;
285 foreach ($this->hooksSorted as $context => $modules) { // $this->hooks is an array with the context as key and the value is an array of modules that handle this context
286 if (!empty($modules)) {
287 '@phan-var-force array<string,CommonHookActions> $modules';
288 // Loop on each active hooks of module for this context
289 foreach ($modules as $module => $actionclassinstance) {
290 $module = preg_replace('/^\d+:/', '', $module); // $module string is 'priority:module'
291 //print "Before hook ".get_class($actionclassinstance)." method=".$method." module=".$module." hooktype=".$hooktype." results=".count($actionclassinstance->results)." resprints=".count($actionclassinstance->resprints)." resaction=".$resaction."<br>\n";
292
293 // test to avoid running twice a hook, when a module implements several active contexts
294 if (in_array($module, $modulealreadyexecuted)) {
295 continue;
296 }
297
298 // jump to next module/class if method does not exist
299 if (!method_exists($actionclassinstance, $method)) {
300 continue;
301 }
302
303 $this->resNbOfHooks++;
304
305 $modulealreadyexecuted[$module] = $module;
306
307 // Clean class (an error may have been set from a previous call of another method for same module/hook)
308 $actionclassinstance->error = '';
309 $actionclassinstance->errors = array();
310
311 if (getDolGlobalInt('MAIN_HOOK_DEBUG')) {
312 // This is too verbose, enabled if const enabled only // False positive about id & element: @phan-suppress-next-line PhanUndeclaredProperty
313 dol_syslog(get_class($this)."::executeHooks Qualified hook found (hooktype=".$hooktype."). We call method ".get_class($actionclassinstance).'->'.$method.", context=".$context.", module=".$module.", action=".$action.((is_object($object) && property_exists($object, 'id')) ? ', object id='.$object->id : '').((is_object($object) && property_exists($object, 'element')) ? ', object element='.$object->element : ''), LOG_DEBUG);
314 }
315
316 // Add current context to avoid method execution in bad context, you can add this test in your method : eg if($currentcontext != 'formfile') return;
317 // Note: The hook can use the $currentcontext in its code to avoid to be ran twice or be ran for one given context only
318 $parameters['currentcontext'] = $context;
319 // Hooks that must return int (hooks with type 'addreplace')
320 if ($hooktype == 'addreplace') {
321 // @phan-suppress-next-line PhanUndeclaredMethod The method's existence is tested above.
322 $resactiontmp = (int) $actionclassinstance->$method($parameters, $object, $action, $this); // $object and $action can be changed by method ($object->id during creation for example or $action to go back to other action for example)
323 $resaction += $resactiontmp;
324
325 if ($resactiontmp < 0 || !empty($actionclassinstance->error) || (!empty($actionclassinstance->errors) && count($actionclassinstance->errors) > 0)) {
326 $error++;
327 $this->error = $actionclassinstance->error;
328 $this->errors = array_merge($this->errors, (array) $actionclassinstance->errors);
329 dol_syslog("Error on hook module=".$module.", method ".$method.", class ".get_class($actionclassinstance).", hooktype=".$hooktype.(empty($this->error) ? '' : " ".$this->error).(empty($this->errors) ? '' : " ".implode(",", $this->errors)), LOG_ERR);
330 }
331
332 if (isset($actionclassinstance->results) && is_array($actionclassinstance->results)) {
333 if ($resactiontmp > 0) {
334 $localResArray = $actionclassinstance->results;
335 } else {
336 $localResArray = array_merge_recursive($localResArray, $actionclassinstance->results);
337 }
338 }
339
340 if (!empty($actionclassinstance->resprints)) {
341 if ($resactiontmp > 0) {
342 $localResPrint = (string) $actionclassinstance->resprints;
343 } else {
344 $localResPrint .= (string) $actionclassinstance->resprints;
345 }
346 }
347 } else {
348 // Generic hooks that return a string or array (printLeftBlock, formAddObjectLine, formBuilddocOptions, ...)
349
350 // TODO. this test should be done in the hook method by returning nothing @phan-suppress-next-line PhanTypeInvalidDimOffset,PhanUndeclaredProperty
351 if (is_array($parameters) && !empty($parameters['special_code']) && $parameters['special_code'] > 3 && (property_exists($actionclassinstance, 'module_number') && ($parameters['special_code'] != $actionclassinstance->module_number))) {
352 continue;
353 }
354
355 if (getDolGlobalInt('MAIN_HOOK_DEBUG')) {
356 dol_syslog("Call method ".$method." of class ".get_class($actionclassinstance).", module=".$module.", hooktype=".$hooktype, LOG_DEBUG);
357 }
358
359 // @phan-suppress-next-line PhanUndeclaredMethod The method's existence is tested above.
360 $resactiontmp = $actionclassinstance->$method($parameters, $object, $action, $this); // $object and $action can be changed by method ($object->id during creation for example or $action to go back to other action for example)
361 $resaction += $resactiontmp;
362
363 if (!empty($actionclassinstance->results) && is_array($actionclassinstance->results)) {
364 $localResArray = array_merge_recursive($localResArray, $actionclassinstance->results);
365 }
366 if (!empty($actionclassinstance->resprints)) {
367 $localResPrint .= (string) $actionclassinstance->resprints;
368 }
369 if (is_numeric($resactiontmp) && $resactiontmp < 0) {
370 $error++;
371 $this->error = $actionclassinstance->error;
372 $this->errors = array_merge($this->errors, (array) $actionclassinstance->errors);
373 dol_syslog("Error on hook module=".$module.", method ".$method.", class ".get_class($actionclassinstance).", hooktype=".$hooktype.(empty($this->error) ? '' : " ".$this->error).(empty($this->errors) ? '' : " ".implode(",", $this->errors)), LOG_ERR);
374 }
375
376 // TODO dead code to remove (do not disable this, but fix your hook instead): result must not be a string but an int. you must use $actionclassinstance->resprints to return a string
377 if (!is_array($resactiontmp) && !is_numeric($resactiontmp)) {
378 dol_syslog('Error: Bug into hook '.$method.' of module class '.get_class($actionclassinstance).'. Method must not return a string but an int (0=OK, 1=Replace, -1=KO) and set string into ->resprints', LOG_ERR);
379 if (empty($actionclassinstance->resprints)) {
380 $localResPrint .= $resactiontmp;
381 }
382 }
383 }
384
385 //print "After hook context=".$context." ".get_class($actionclassinstance)." method=".$method." hooktype=".$hooktype." results=".count($actionclassinstance->results)." resprints=".count($actionclassinstance->resprints)." resaction=".$resaction."<br>\n";
386
387 $actionclassinstance->results = array();
388 $actionclassinstance->resprints = null;
389 }
390 }
391 }
392
393 $this->resPrint = $localResPrint;
394 $this->resArray = $localResArray;
395
396 return ($error ? -1 : $resaction);
397 }
398}
if( $user->socid > 0) if(! $user->hasRight('accounting', 'chartofaccount')) $object
Definition card.php:67
Class to manage hooks.
initHooks($arraycontext)
Init array $this->hooks with instantiated action controllers.
$hooksSorted
array<string,array<string,null|string|CommonHookActions>> Array with instantiated classes sorted by h...
__construct($db)
Constructor.
executeHooks($method, $parameters=array(), &$object=null, &$action='')
Execute hooks (if they were initialized) for the given method.
$hooks
array<string,array<string,null|string|CommonHookActions>> Array with instantiated classes
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
if(!function_exists( 'dol_getprefix')) dol_include_once($relpath, $classname='')
Make an include_once using default root and alternate root if it fails.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
global $conf
The following vars must be defined: $type2label $form $conf, $lang, The following vars may also be de...
Definition member.php:79
$context
@method int call_trigger(string $triggerName, User $user)
Definition logout.php:42