dolibarr 24.0.0-beta
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 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 $this->contextarray = array_unique(array_merge($arraycontext, $this->contextarray)); // All contexts are concatenated but kept unique
128
129 $foundcontextmodule = false;
130
131 // 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
132 foreach ($conf->modules_parts['hooks'] as $module => $hooks) {
133 if (!isModEnabled($module)) {
134 continue;
135 }
136
137 //dol_syslog(get_class($this).'::initHooks module='.$module.' arraycontext='.join(',',$arraycontext));
138 foreach ($arraycontext as $context) {
139 if (is_array($hooks)) {
140 $arrayhooks = $hooks; // New system = array of hook contexts claimed by the module $module
141 } else {
142 $arrayhooks = explode(':', $hooks); // Old system (for backward compatibility)
143 }
144
145 if (!in_array($context, $arrayhooks) && !in_array('all', $arrayhooks)) {
146 // We instantiate action class only if initialized hook is handled by the module
147 // Hook was already initialized for this context and module
148 continue;
149 }
150
151 // Include actions class overwriting hooks
152 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.
153 $path = '/'.$module.'/class/';
154 $actionfile = 'actions_'.$module.'.class.php';
155
156 $resaction = dol_include_once($path.$actionfile);
157 if ($resaction) {
158 $controlclassname = 'Actions'.ucfirst($module);
159
160 $actionInstance = new $controlclassname($this->db);
161 '@phan-var-force CommonHookActions $actionInstance';
162
163 // @phan-suppress-next-line PhanUndeclaredProperty
164 $priority = (!property_exists($actionInstance, 'priority') || empty($actionInstance->priority)) ? 50 : $actionInstance->priority;
165
166 $this->hooks[$context][$module] = $actionInstance;
167 $this->hooksSorted[$context][$priority.':'.$module] = $actionInstance;
168
169 $foundcontextmodule = true;
170
171 // Hook has been initialized with another couple $context/$module
172 $stringtolog = 'context='.$context.'-path='.$path.$actionfile.'-priority='.$priority;
173 dol_syslog(get_class($this)."::initHooks Loading hooks: ".$stringtolog, LOG_DEBUG);
174 } else {
175 dol_syslog(get_class($this)."::initHooks Failed to load hook in ".$path.$actionfile, LOG_WARNING);
176 }
177 }
178 }
179 }
180
181 // Log the init of hook
182 // dol_syslog(get_class($this)."::initHooks Loading hooks: ".implode(', ', $arraytolog), LOG_DEBUG);
183
184 if ($foundcontextmodule) {
185 foreach ($arraycontext as $context) {
186 if (!empty($this->hooksSorted[$context])) {
187 ksort($this->hooksSorted[$context], SORT_NATURAL);
188 }
189 }
190 }
191
192 return 1;
193 }
194
211 public function executeHooks($method, $parameters = array(), &$object = null, &$action = '')
212 {
213 if (isModEnabled('debugbar') && function_exists('debug_backtrace')) {
214 $trace = debug_backtrace();
215 if (isset($trace[0])) {
216 $hookInformations = [
217 'name' => $method,
218 'contexts' => $this->contextarray,
219 'file' => $trace[0]['file'],
220 'line' => $trace[0]['line'],
221 'count' => 0,
222 ];
223 $hash = md5(json_encode($hookInformations));
224 if (!empty($this->hooksHistory[$hash])) {
225 $this->hooksHistory[$hash]['count']++;
226 } else {
227 $hookInformations['count'] = 1;
228 $this->hooksHistory[$hash] = $hookInformations;
229 }
230 }
231 }
232
233 if (!is_array($this->hooks) || empty($this->hooks)) {
234 return 0; // No hook available, do nothing.
235 }
236 if (!is_array($parameters)) {
237 dol_syslog('executeHooks was called with a non array $parameters. Surely a bug.', LOG_WARNING);
238 $parameters = array();
239 }
240
241 $parameters['context'] = implode(':', $this->contextarray);
242 //dol_syslog(get_class($this).'::executeHooks method='.$method." action=".$action." context=".$parameters['context']);
243
244 // Define type of hook ('output' or 'addreplace').
245 $hooktype = 'addreplace';
246 // TODO Remove hooks with type 'output' (example createFrom). All these hooks must be converted into 'addreplace' hooks.
247 if (in_array($method, array(
248 'createFrom',
249 'dashboardAccountancy',
250 'dashboardActivities',
251 'dashboardCommercials',
252 'dashboardContracts',
253 'dashboardDonation',
254 'dashboardEmailings',
255 'dashboardExpenseReport',
256 'dashboardHRM',
257 'dashboardInterventions',
258 'dashboardMRP',
259 'dashboardMembers',
260 'dashboardOpensurvey',
261 'dashboardOrders',
262 'dashboardOrdersSuppliers',
263 'dashboardProductServices',
264 'dashboardProjects',
265 'dashboardPropals',
266 'dashboardSpecialBills',
267 'dashboardSupplierProposal',
268 'dashboardThirdparties',
269 'dashboardTickets',
270 'dashboardUsersGroups',
271 'dashboardWarehouse',
272 'dashboardWarehouseReceptions',
273 'dashboardWarehouseSendings',
274 'insertExtraHeader',
275 'insertExtraFooter',
276 'printLeftBlock',
277 'formAddObjectLine',
278 'formBuilddocOptions',
279 'showSocinfoOnPrint'
280 ))) {
281 $hooktype = 'output';
282 }
283
284 // Init return properties
285 $localResPrint = '';
286 $localResArray = array();
287
288 $this->resNbOfHooks = 0;
289
290 // Here, the value for $method and $hooktype are given.
291 // Loop on each hook to qualify modules that have declared context
292 $modulealreadyexecuted = array();
293 $resaction = 0;
294 $error = 0;
295 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
296 if (!empty($modules)) {
297 '@phan-var-force array<string,CommonHookActions> $modules';
298 // Loop on each active hooks of module for this context
299 foreach ($modules as $module => $actionclassinstance) {
300 $module = preg_replace('/^\d+:/', '', $module); // $module string is 'priority:module'
301 //print "Before hook ".get_class($actionclassinstance)." method=".$method." module=".$module." hooktype=".$hooktype." results=".count($actionclassinstance->results)." resprints=".count($actionclassinstance->resprints)." resaction=".$resaction."<br>\n";
302
303 // test to avoid running twice a hook, when a module implements several active contexts
304 if (in_array($module, $modulealreadyexecuted)) {
305 continue;
306 }
307
308 // jump to next module/class if method does not exist
309 if (!method_exists($actionclassinstance, $method)) {
310 continue;
311 }
312
313 $this->resNbOfHooks++;
314
315 $modulealreadyexecuted[$module] = $module;
316
317 // Clean class (an error may have been set from a previous call of another method for same module/hook)
318 $actionclassinstance->error = '';
319 $actionclassinstance->errors = array();
320 $actionclassinstance->warnings = array();
321
322 if (getDolGlobalInt('MAIN_HOOK_DEBUG')) {
323 // This is too verbose, enabled if const enabled only // False positive about id & element: @phan-suppress-next-line PhanUndeclaredProperty
324 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);
325 }
326
327 // Add current context to avoid method execution in bad context, you can add this test in your method : eg if($currentcontext != 'formfile') return;
328 // Note: The hook can use the $currentcontext in its code to avoid to be ran twice or be ran for one given context only
329 $parameters['currentcontext'] = $context;
330 // Hooks that must return int (hooks with type 'addreplace')
331 if ($hooktype == 'addreplace') {
332 // @phan-suppress-next-line PhanUndeclaredMethod The method's existence is tested above.
333 $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)
334 $resaction += $resactiontmp;
335
336 if ($resactiontmp < 0 || !empty($actionclassinstance->error) || (!empty($actionclassinstance->errors) && count($actionclassinstance->errors) > 0)) {
337 $error++;
338 $this->error = $actionclassinstance->error;
339 $this->errors = array_merge($this->errors, (array) $actionclassinstance->errors);
340 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);
341 }
342
343 if (!empty($actionclassinstance->warnings) && count($actionclassinstance->warnings) > 0) {
344 $this->warnings = array_merge($this->warnings, (array) $actionclassinstance->warnings);
345 dol_syslog("Warning on hook module=".$module.", method ".$method.", class ".get_class($actionclassinstance).", hooktype=".$hooktype.(empty($this->warnings) ? '' : " ".implode(",", $this->warnings)), LOG_DEBUG);
346 }
347
348 if (isset($actionclassinstance->results) && is_array($actionclassinstance->results)) {
349 if ($resactiontmp > 0) {
350 $localResArray = $actionclassinstance->results;
351 } else {
352 $localResArray = array_merge_recursive($localResArray, $actionclassinstance->results);
353 }
354 }
355
356 if (!empty($actionclassinstance->resprints)) {
357 if ($resactiontmp > 0) {
358 $localResPrint = (string) $actionclassinstance->resprints;
359 } else {
360 $localResPrint .= (string) $actionclassinstance->resprints;
361 }
362 }
363 } else {
364 // Generic old hooks that return a string or array (printLeftBlock, formAddObjectLine, formBuilddocOptions, ...)
365
366 // TODO. this test should be done in the hook method by returning nothing @phan-suppress-next-line PhanTypeInvalidDimOffset,PhanUndeclaredProperty
367 if (is_array($parameters) && !empty($parameters['special_code']) && $parameters['special_code'] > 3 && (property_exists($actionclassinstance, 'module_number') && ($parameters['special_code'] != $actionclassinstance->module_number))) {
368 continue;
369 }
370
371 if (getDolGlobalInt('MAIN_HOOK_DEBUG')) {
372 dol_syslog("Call method ".$method." of class ".get_class($actionclassinstance).", module=".$module.", hooktype=".$hooktype, LOG_DEBUG);
373 }
374
375 // @phan-suppress-next-line PhanUndeclaredMethod The method's existence is tested above.
376 $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)
377 $resaction += $resactiontmp;
378
379 if (!empty($actionclassinstance->results) && is_array($actionclassinstance->results)) {
380 $localResArray = array_merge_recursive($localResArray, $actionclassinstance->results);
381 }
382 if (!empty($actionclassinstance->resprints)) {
383 $localResPrint .= (string) $actionclassinstance->resprints;
384 }
385 if (is_numeric($resactiontmp) && $resactiontmp < 0) {
386 $error++;
387 $this->error = $actionclassinstance->error;
388 $this->errors = array_merge($this->errors, (array) $actionclassinstance->errors);
389 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);
390 }
391
392 // 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
393 if (!is_array($resactiontmp) && !is_numeric($resactiontmp)) {
394 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);
395 if (empty($actionclassinstance->resprints)) {
396 $localResPrint .= $resactiontmp;
397 }
398 }
399 }
400
401 //print "After hook context=".$context." ".get_class($actionclassinstance)." method=".$method." hooktype=".$hooktype." results=".count($actionclassinstance->results)." resprints=".count($actionclassinstance->resprints)." resaction=".$resaction."<br>\n";
402
403 $actionclassinstance->results = array();
404 $actionclassinstance->resprints = null;
405 }
406 }
407 }
408
409 $this->resPrint = $localResPrint;
410 $this->resArray = $localResArray;
411
412 return ($error ? -1 : $resaction);
413 }
414}
if(! $sortfield) if(! $sortorder) $object
Definition account.php:100
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
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.
print $langs trans("Show") . '< td style="' . $timeColor . '" align="center"> s</td > badge status0 badge status4 badge status3 Error badge status8< td align="center">< span class="badge ' . $badge . '"></span ></td >< td align="center">< a href="#" class="button button-small" onclick="openLogModal(this)" data-req="' . dol_escape_htmltag($reqSafe) . '" data-res="' . dol_escape_htmltag($resSafe) . '" data-err="' . dol_escape_htmltag($errSafe) . '">< span class="fa fa-search-plus"></span ></a ></td ></tr >< tr >< td colspan="' . $colspan . '" class="opacitymedium"></td ></tr ></table ></div ></form > logModal none logModal none s a JSON string
buildzip.php
$context
@method int call_trigger(string $triggerName, ?User $user)
Definition logout.php:42