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