dolibarr 25.0.0-alpha
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-2026 MDW <mdeweerd@users.noreply.github.com>
7 * Copyright (C) 2025-2026 Frédéric France <frederic.france@free.fr>
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 3 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 */
22
34{
38 public $db;
39
43 public $error = '';
44
48 public $errors = array();
49
53 public $warnings = array();
54
58 public $contextarray = array();
59
63 public $hooks = array();
64
68 public $hooksSorted = array();
69
73 public $hooksHistory = [];
74
78 public $resArray = array();
79
83 public $resPrint = '';
84
88 public $resNbOfHooks = 0;
89
96 public function __construct($db)
97 {
98 $this->db = $db;
99 }
100
101
113 public function initHooks($arraycontext)
114 {
115 global $conf;
116
117 // Test if there is at least one hook to manage
118 if (!is_array($conf->modules_parts['hooks']) || empty($conf->modules_parts['hooks'])) {
119 return 0;
120 }
121
122 // For backward compatibility
123 if (!is_array($arraycontext)) {
124 $arraycontext = array($arraycontext);
125 }
126
127 // Contexts already initialized on this instance: their actions classes were loaded at that time (the loop below would find
128 // them all "already loaded" and do nothing). This method is called on every getNomUrl(), showOutputField()... so on a
129 // list page the same contexts come back hundreds of times.
130 if (!array_diff($arraycontext, $this->contextarray)) {
131 return 1;
132 }
133
134 $this->contextarray = array_unique(array_merge($arraycontext, $this->contextarray)); // All contexts are concatenated but kept unique
135
136 $foundcontextmodule = false;
137
138 // 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
139 foreach ($conf->modules_parts['hooks'] as $module => $hooks) {
140 if (!isModEnabled($module)) {
141 continue;
142 }
143
144 //dol_syslog(get_class($this).'::initHooks module='.$module.' arraycontext='.join(',',$arraycontext));
145 foreach ($arraycontext as $context) {
146 if (is_array($hooks)) {
147 $arrayhooks = $hooks; // New system = array of hook contexts claimed by the module $module
148 } else {
149 $arrayhooks = explode(':', $hooks); // Old system (for backward compatibility)
150 }
151
152 if (!in_array($context, $arrayhooks) && !in_array('all', $arrayhooks)) {
153 // We instantiate action class only if initialized hook is handled by the module
154 // Hook was already initialized for this context and module
155 continue;
156 }
157
158 // Include actions class overwriting hooks
159 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.
160 $path = '/'.$module.'/class/';
161 $actionfile = 'actions_'.$module.'.class.php';
162
163 $resaction = dol_include_once($path.$actionfile);
164 if ($resaction) {
165 $controlclassname = 'Actions'.ucfirst($module);
166
167 $actionInstance = new $controlclassname($this->db);
168 '@phan-var-force CommonHookActions $actionInstance';
169
170 // @phan-suppress-next-line PhanUndeclaredProperty
171 $priority = (!property_exists($actionInstance, 'priority') || empty($actionInstance->priority)) ? 50 : $actionInstance->priority;
172
173 $this->hooks[$context][$module] = $actionInstance;
174 $this->hooksSorted[$context][$priority.':'.$module] = $actionInstance;
175
176 $foundcontextmodule = true;
177
178 // Hook has been initialized with another couple $context/$module
179 $stringtolog = 'context='.$context.'-path='.$path.$actionfile.'-priority='.$priority;
180 dol_syslog(get_class($this)."::initHooks Loading hooks: ".$stringtolog, LOG_DEBUG);
181 } else {
182 dol_syslog(get_class($this)."::initHooks Failed to load hook in ".$path.$actionfile, LOG_WARNING);
183 }
184 }
185 }
186 }
187
188 // Log the init of hook
189 // dol_syslog(get_class($this)."::initHooks Loading hooks: ".implode(', ', $arraytolog), LOG_DEBUG);
190
191 if ($foundcontextmodule) {
192 foreach ($arraycontext as $context) {
193 if (!empty($this->hooksSorted[$context])) {
194 ksort($this->hooksSorted[$context], SORT_NATURAL);
195 }
196 }
197 }
198
199 return 1;
200 }
201
218 public function executeHooks($method, $parameters = array(), &$object = null, &$action = '')
219 {
220 if (isModEnabled('debugbar') && function_exists('debug_backtrace')) {
221 $trace = debug_backtrace();
222 if (isset($trace[0])) {
223 $hookInformations = [
224 'name' => $method,
225 'contexts' => $this->contextarray,
226 'file' => $trace[0]['file'],
227 'line' => $trace[0]['line'],
228 'count' => 0,
229 ];
230 $hash = md5(json_encode($hookInformations));
231 if (!empty($this->hooksHistory[$hash])) {
232 $this->hooksHistory[$hash]['count']++;
233 } else {
234 $hookInformations['count'] = 1;
235 $this->hooksHistory[$hash] = $hookInformations;
236 }
237 }
238 }
239
240 if (!is_array($this->hooks) || empty($this->hooks)) {
241 return 0; // No hook available, do nothing.
242 }
243 if (!is_array($parameters)) {
244 dol_syslog('executeHooks was called with a non array $parameters. Surely a bug.', LOG_WARNING);
245 $parameters = array();
246 }
247
248 $parameters['context'] = implode(':', $this->contextarray);
249 //dol_syslog(get_class($this).'::executeHooks method='.$method." action=".$action." context=".$parameters['context']);
250
251 // Define type of hook ('output' or 'addreplace').
252 $hooktype = 'addreplace';
253 // TODO Remove hooks with type 'output' (example createFrom). All these hooks must be converted into 'addreplace' hooks.
254 if (in_array($method, array(
255 'createFrom',
256 'dashboardAccountancy',
257 'dashboardActivities',
258 'dashboardCommercials',
259 'dashboardContracts',
260 'dashboardDonation',
261 'dashboardEmailings',
262 'dashboardExpenseReport',
263 'dashboardHRM',
264 'dashboardInterventions',
265 'dashboardMRP',
266 'dashboardMembers',
267 'dashboardOpensurvey',
268 'dashboardOrders',
269 'dashboardOrdersSuppliers',
270 'dashboardProductServices',
271 'dashboardProjects',
272 'dashboardPropals',
273 'dashboardSpecialBills',
274 'dashboardSupplierProposal',
275 'dashboardThirdparties',
276 'dashboardTickets',
277 'dashboardUsersGroups',
278 'dashboardWarehouse',
279 'dashboardWarehouseReceptions',
280 'dashboardWarehouseSendings',
281 'insertExtraHeader',
282 'insertExtraFooter',
283 'printLeftBlock',
284 'formAddObjectLine',
285 'formBuilddocOptions',
286 'showSocinfoOnPrint'
287 ))) {
288 $hooktype = 'output';
289 }
290
291 // Init return properties
292 $localResPrint = '';
293 $localResArray = array();
294
295 $this->resNbOfHooks = 0;
296
297 // Here, the value for $method and $hooktype are given.
298 // Loop on each hook to qualify modules that have declared context
299 $modulealreadyexecuted = array();
300 $resaction = 0;
301 $error = 0;
302 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
303 if (!empty($modules)) {
304 '@phan-var-force array<string,CommonHookActions> $modules';
305 // Loop on each active hooks of module for this context
306 foreach ($modules as $module => $actionclassinstance) {
307 $module = preg_replace('/^\d+:/', '', $module); // $module string is 'priority:module'
308 //print "Before hook ".get_class($actionclassinstance)." method=".$method." module=".$module." hooktype=".$hooktype." results=".count($actionclassinstance->results)." resprints=".count($actionclassinstance->resprints)." resaction=".$resaction."<br>\n";
309
310 // test to avoid running twice a hook, when a module implements several active contexts
311 if (in_array($module, $modulealreadyexecuted)) {
312 continue;
313 }
314
315 // jump to next module/class if method does not exist
316 if (!method_exists($actionclassinstance, $method)) {
317 continue;
318 }
319
320 $this->resNbOfHooks++;
321
322 $modulealreadyexecuted[$module] = $module;
323
324 // Clean class (an error may have been set from a previous call of another method for same module/hook)
325 $actionclassinstance->error = '';
326 $actionclassinstance->errors = array();
327 $actionclassinstance->warnings = array();
328
329 if (getDolGlobalInt('MAIN_HOOK_DEBUG')) {
330 // This is too verbose, enabled if const enabled only // False positive about id & element: @phan-suppress-next-line PhanUndeclaredProperty
331 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);
332 }
333
334 // Add current context to avoid method execution in bad context, you can add this test in your method : eg if($currentcontext != 'formfile') return;
335 // Note: The hook can use the $currentcontext in its code to avoid to be ran twice or be ran for one given context only
336 $parameters['currentcontext'] = $context;
337 // Hooks that must return int (hooks with type 'addreplace')
338 if ($hooktype == 'addreplace') {
339 // @phan-suppress-next-line PhanUndeclaredMethod The method's existence is tested above.
340 $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)
341 $resaction += $resactiontmp;
342
343 if ($resactiontmp < 0 || !empty($actionclassinstance->error) || (!empty($actionclassinstance->errors) && count($actionclassinstance->errors) > 0)) {
344 $error++;
345 $this->error = $actionclassinstance->error;
346 $this->errors = array_merge($this->errors, (array) $actionclassinstance->errors);
347 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);
348 }
349
350 if (!empty($actionclassinstance->warnings) && count($actionclassinstance->warnings) > 0) {
351 $this->warnings = array_merge($this->warnings, (array) $actionclassinstance->warnings);
352 dol_syslog("Warning on hook module=".$module.", method ".$method.", class ".get_class($actionclassinstance).", hooktype=".$hooktype.(empty($this->warnings) ? '' : " ".implode(",", $this->warnings)), LOG_DEBUG);
353 }
354
355 if (isset($actionclassinstance->results) && is_array($actionclassinstance->results)) {
356 if ($resactiontmp > 0) {
357 $localResArray = $actionclassinstance->results;
358 } else {
359 $localResArray = array_merge_recursive($localResArray, $actionclassinstance->results);
360 }
361 }
362
363 if (!empty($actionclassinstance->resprints)) {
364 if ($resactiontmp > 0) {
365 $localResPrint = (string) $actionclassinstance->resprints;
366 } else {
367 $localResPrint .= (string) $actionclassinstance->resprints;
368 }
369 }
370 } else {
371 // Generic old hooks that return a string or array (printLeftBlock, formAddObjectLine, formBuilddocOptions, ...)
372
373 // TODO. this test should be done in the hook method by returning nothing @phan-suppress-next-line PhanTypeInvalidDimOffset,PhanUndeclaredProperty
374 if (is_array($parameters) && !empty($parameters['special_code']) && $parameters['special_code'] > 3 && (property_exists($actionclassinstance, 'module_number') && ($parameters['special_code'] != $actionclassinstance->module_number))) {
375 continue;
376 }
377
378 if (getDolGlobalInt('MAIN_HOOK_DEBUG')) {
379 dol_syslog("Call method ".$method." of class ".get_class($actionclassinstance).", module=".$module.", hooktype=".$hooktype, LOG_DEBUG);
380 }
381
382 // @phan-suppress-next-line PhanUndeclaredMethod The method's existence is tested above.
383 $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)
384 $resaction += $resactiontmp;
385
386 if (!empty($actionclassinstance->results) && is_array($actionclassinstance->results)) {
387 $localResArray = array_merge_recursive($localResArray, $actionclassinstance->results);
388 }
389 if (!empty($actionclassinstance->resprints)) {
390 $localResPrint .= (string) $actionclassinstance->resprints;
391 }
392 if (is_numeric($resactiontmp) && $resactiontmp < 0) {
393 $error++;
394 $this->error = $actionclassinstance->error;
395 $this->errors = array_merge($this->errors, (array) $actionclassinstance->errors);
396 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);
397 }
398
399 // Test old code (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
400 if (!is_array($resactiontmp) && !is_numeric($resactiontmp)) {
401 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);
402 if (empty($actionclassinstance->resprints)) {
403 $localResPrint .= $resactiontmp;
404 }
405 }
406 }
407
408 //print "After hook context=".$context." ".get_class($actionclassinstance)." method=".$method." hooktype=".$hooktype." results=".count($actionclassinstance->results)." resprints=".count($actionclassinstance->resprints)." resaction=".$resaction."<br>\n";
409
410 $actionclassinstance->results = array();
411 $actionclassinstance->resprints = null;
412 }
413 }
414 }
415
416 $this->resPrint = $localResPrint;
417 $this->resArray = $localResArray;
418
419 return ($error ? -1 : $resaction);
420 }
421}
if(! $sortfield) if(! $sortorder) $object
Definition account.php:100
Class to manage hooks.
initHooks($arraycontext)
Init array $this->hooks with instantiated action controllers.
__construct($db)
Constructor.
executeHooks($method, $parameters=array(), &$object=null, &$action='')
Execute hooks (if they were initialized) for the given method.
if(! $sortfield) if(! $sortorder) $module
Definition list.php:193
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $conf
The main.inc.php has been included so the following variable are now defined:
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.
isModEnabled($module)
Is Dolibarr module enabled.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
$context
@method int call_trigger(string $triggerName, ?User $user)
Definition logout.php:42