dolibarr 21.0.0-alpha
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 //global $debugbar;
201 //if (is_object($debugbar) && get_class($debugbar) === 'DolibarrDebugBar') {
202 if (isModEnabled('debugbar') && function_exists('debug_backtrace')) {
203 $trace = debug_backtrace();
204 if (isset($trace[0])) {
205 $hookInformations = [
206 'name' => $method,
207 'contexts' => $this->contextarray,
208 'file' => $trace[0]['file'],
209 'line' => $trace[0]['line'],
210 'count' => 0,
211 ];
212 $hash = md5(json_encode($hookInformations));
213 if (!empty($this->hooksHistory[$hash])) {
214 $this->hooksHistory[$hash]['count']++;
215 } else {
216 $hookInformations['count'] = 1;
217 $this->hooksHistory[$hash] = $hookInformations;
218 }
219 }
220 }
221
222 if (!is_array($this->hooks) || empty($this->hooks)) {
223 return 0; // No hook available, do nothing.
224 }
225 if (!is_array($parameters)) {
226 dol_syslog('executeHooks was called with a non array $parameters. Surely a bug.', LOG_WARNING);
227 $parameters = array();
228 }
229
230 $parameters['context'] = implode(':', $this->contextarray);
231 //dol_syslog(get_class($this).'::executeHooks method='.$method." action=".$action." context=".$parameters['context']);
232
233 // Define type of hook ('output' or 'addreplace').
234 $hooktype = 'addreplace';
235 // TODO Remove hooks with type 'output' (example createFrom). All hooks must be converted into 'addreplace' hooks.
236 if (in_array($method, array(
237 'createFrom',
238 'dashboardAccountancy',
239 'dashboardActivities',
240 'dashboardCommercials',
241 'dashboardContracts',
242 'dashboardDonation',
243 'dashboardEmailings',
244 'dashboardExpenseReport',
245 'dashboardHRM',
246 'dashboardInterventions',
247 'dashboardMRP',
248 'dashboardMembers',
249 'dashboardOpensurvey',
250 'dashboardOrders',
251 'dashboardOrdersSuppliers',
252 'dashboardProductServices',
253 'dashboardProjects',
254 'dashboardPropals',
255 'dashboardSpecialBills',
256 'dashboardSupplierProposal',
257 'dashboardThirdparties',
258 'dashboardTickets',
259 'dashboardUsersGroups',
260 'dashboardWarehouse',
261 'dashboardWarehouseReceptions',
262 'dashboardWarehouseSendings',
263 'insertExtraHeader',
264 'insertExtraFooter',
265 'printLeftBlock',
266 'formAddObjectLine',
267 'formBuilddocOptions',
268 'showSocinfoOnPrint'
269 ))) {
270 $hooktype = 'output';
271 }
272
273 // Init return properties
274 $localResPrint = '';
275 $localResArray = array();
276
277 $this->resNbOfHooks = 0;
278
279 // Here, the value for $method and $hooktype are given.
280 // Loop on each hook to qualify modules that have declared context
281 $modulealreadyexecuted = array();
282 $resaction = 0;
283 $error = 0;
284 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
285 if (!empty($modules)) {
286 '@phan-var-force array<string,CommonHookActions> $modules';
287 // Loop on each active hooks of module for this context
288 foreach ($modules as $module => $actionclassinstance) {
289 $module = preg_replace('/^\d+:/', '', $module); // $module string is 'priority:module'
290 //print "Before hook ".get_class($actionclassinstance)." method=".$method." module=".$module." hooktype=".$hooktype." results=".count($actionclassinstance->results)." resprints=".count($actionclassinstance->resprints)." resaction=".$resaction."<br>\n";
291
292 // test to avoid running twice a hook, when a module implements several active contexts
293 if (in_array($module, $modulealreadyexecuted)) {
294 continue;
295 }
296
297 // jump to next module/class if method does not exist
298 if (!method_exists($actionclassinstance, $method)) {
299 continue;
300 }
301
302 $this->resNbOfHooks++;
303
304 $modulealreadyexecuted[$module] = $module;
305
306 // Clean class (an error may have been set from a previous call of another method for same module/hook)
307 $actionclassinstance->error = '';
308 $actionclassinstance->errors = array();
309
310 if (getDolGlobalInt('MAIN_HOOK_DEBUG')) {
311 // This his too much verbose, enabled if const enabled only
312 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);
313 }
314
315 // Add current context to avoid method execution in bad context, you can add this test in your method : eg if($currentcontext != 'formfile') return;
316 // Note: The hook can use the $currentcontext in its code to avoid to be ran twice or be ran for one given context only
317 $parameters['currentcontext'] = $context;
318 // Hooks that must return int (hooks with type 'addreplace')
319 if ($hooktype == 'addreplace') {
320 // @phan-suppress-next-line PhanUndeclaredMethod The method's existence is tested above.
321 $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)
322 $resaction += $resactiontmp;
323
324 if ($resactiontmp < 0 || !empty($actionclassinstance->error) || (!empty($actionclassinstance->errors) && count($actionclassinstance->errors) > 0)) {
325 $error++;
326 $this->error = $actionclassinstance->error;
327 $this->errors = array_merge($this->errors, (array) $actionclassinstance->errors);
328 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);
329 }
330
331 if (isset($actionclassinstance->results) && is_array($actionclassinstance->results)) {
332 if ($resactiontmp > 0) {
333 $localResArray = $actionclassinstance->results;
334 } else {
335 $localResArray = array_merge_recursive($localResArray, $actionclassinstance->results);
336 }
337 }
338
339 if (!empty($actionclassinstance->resprints)) {
340 if ($resactiontmp > 0) {
341 $localResPrint = (string) $actionclassinstance->resprints;
342 } else {
343 $localResPrint .= (string) $actionclassinstance->resprints;
344 }
345 }
346 } else {
347 // Generic hooks that return a string or array (printLeftBlock, formAddObjectLine, formBuilddocOptions, ...)
348
349 // TODO. this test should be done into the method of hook by returning nothing @phan-suppress-next-line PhanTypeInvalidDimOffset
350 if (is_array($parameters) && !empty($parameters['special_code']) && $parameters['special_code'] > 3 && $parameters['special_code'] != $actionclassinstance->module_number) {
351 continue;
352 }
353
354 if (getDolGlobalInt('MAIN_HOOK_DEBUG')) {
355 dol_syslog("Call method ".$method." of class ".get_class($actionclassinstance).", module=".$module.", hooktype=".$hooktype, LOG_DEBUG);
356 }
357
358 // @phan-suppress-next-line PhanUndeclaredMethod The method's existence is tested above.
359 $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)
360 $resaction += $resactiontmp;
361
362 if (!empty($actionclassinstance->results) && is_array($actionclassinstance->results)) {
363 $localResArray = array_merge_recursive($localResArray, $actionclassinstance->results);
364 }
365 if (!empty($actionclassinstance->resprints)) {
366 $localResPrint .= (string) $actionclassinstance->resprints;
367 }
368 if (is_numeric($resactiontmp) && $resactiontmp < 0) {
369 $error++;
370 $this->error = $actionclassinstance->error;
371 $this->errors = array_merge($this->errors, (array) $actionclassinstance->errors);
372 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);
373 }
374
375 // 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
376 if (!is_array($resactiontmp) && !is_numeric($resactiontmp)) {
377 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);
378 if (empty($actionclassinstance->resprints)) {
379 $localResPrint .= $resactiontmp;
380 }
381 }
382 }
383
384 //print "After hook context=".$context." ".get_class($actionclassinstance)." method=".$method." hooktype=".$hooktype." results=".count($actionclassinstance->results)." resprints=".count($actionclassinstance->resprints)." resaction=".$resaction."<br>\n";
385
386 $actionclassinstance->results = array();
387 $actionclassinstance->resprints = null;
388 }
389 }
390 }
391
392 $this->resPrint = $localResPrint;
393 $this->resArray = $localResArray;
394
395 return ($error ? -1 : $resaction);
396 }
397}
if( $user->socid > 0) if(! $user->hasRight('accounting', 'chartofaccount')) $object
Definition card.php:58
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.
$context
@method int call_trigger(string $triggerName, User $user)
Definition logout.php:42