dolibarr  20.0.0-beta
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 $hooksHistory = [];
62 
66  public $resArray = array();
67 
71  public $resPrint = '';
72 
76  public $resNbOfHooks = 0;
77 
84  public function __construct($db)
85  {
86  $this->db = $db;
87  }
88 
89 
101  public function initHooks($arraycontext)
102  {
103  global $conf;
104 
105  // Test if there is hooks to manage
106  if (!is_array($conf->modules_parts['hooks']) || empty($conf->modules_parts['hooks'])) {
107  return 0;
108  }
109 
110  // For backward compatibility
111  if (!is_array($arraycontext)) {
112  $arraycontext = array($arraycontext);
113  }
114 
115  $this->contextarray = array_unique(array_merge($arraycontext, $this->contextarray)); // All contexts are concatenated
116 
117  $arraytolog = array();
118  foreach ($conf->modules_parts['hooks'] as $module => $hooks) { // Loop on each module that brings hooks
119  if (empty($conf->$module->enabled)) {
120  continue;
121  }
122 
123  //dol_syslog(get_class($this).'::initHooks module='.$module.' arraycontext='.join(',',$arraycontext));
124  foreach ($arraycontext as $context) {
125  if (is_array($hooks)) {
126  $arrayhooks = $hooks; // New system
127  } else {
128  $arrayhooks = explode(':', $hooks); // Old system (for backward compatibility)
129  }
130 
131  if (in_array($context, $arrayhooks) || in_array('all', $arrayhooks)) { // We instantiate action class only if initialized hook is handled by module
132  // Include actions class overwriting hooks
133  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.
134  $path = '/'.$module.'/class/';
135  $actionfile = 'actions_'.$module.'.class.php';
136 
137  $arraytolog[] = 'context='.$context.'-path='.$path.$actionfile;
138  $resaction = dol_include_once($path.$actionfile);
139  if ($resaction) {
140  $controlclassname = 'Actions'.ucfirst($module);
141  $actionInstance = new $controlclassname($this->db);
142  '@phan-var-force CommonHookActions $actionInstance';
143  $priority = empty($actionInstance->priority) ? 50 : $actionInstance->priority;
144  $this->hooks[$context][$priority.':'.$module] = $actionInstance;
145  }
146  }
147  }
148  }
149  }
150  // Log the init of hook but only for hooks there are declared to be managed
151  if (count($arraytolog) > 0) {
152  dol_syslog(get_class($this)."::initHooks Loading hooks: ".implode(', ', $arraytolog), LOG_DEBUG);
153  }
154 
155  foreach ($arraycontext as $context) {
156  if (!empty($this->hooks[$context])) {
157  ksort($this->hooks[$context], SORT_NATURAL);
158  }
159  }
160 
161  return 1;
162  }
163 
176  public function executeHooks($method, $parameters = array(), &$object = null, &$action = '')
177  {
178  //global $debugbar;
179  //if (is_object($debugbar) && get_class($debugbar) === 'DolibarrDebugBar') {
180  if (isModEnabled('debugbar') && function_exists('debug_backtrace')) {
181  $trace = debug_backtrace();
182  if (isset($trace[0])) {
183  $hookInformations = [
184  'name' => $method,
185  'contexts' => $this->contextarray,
186  'file' => $trace[0]['file'],
187  'line' => $trace[0]['line'],
188  'count' => 0,
189  ];
190  $hash = md5(json_encode($hookInformations));
191  if (!empty($this->hooksHistory[$hash])) {
192  $this->hooksHistory[$hash]['count']++;
193  } else {
194  $hookInformations['count'] = 1;
195  $this->hooksHistory[$hash] = $hookInformations;
196  }
197  }
198  }
199 
200  if (!is_array($this->hooks) || empty($this->hooks)) {
201  return 0; // No hook available, do nothing.
202  }
203  if (!is_array($parameters)) {
204  dol_syslog('executeHooks was called with a non array $parameters. Surely a bug.', LOG_WARNING);
205  $parameters = array();
206  }
207 
208  $parameters['context'] = implode(':', $this->contextarray);
209  //dol_syslog(get_class($this).'::executeHooks method='.$method." action=".$action." context=".$parameters['context']);
210 
211  // Define type of hook ('output' or 'addreplace').
212  $hooktype = 'addreplace';
213  // TODO Remove hooks with type 'output' (example createFrom). All hooks must be converted into 'addreplace' hooks.
214  if (in_array($method, array(
215  'createFrom',
216  'dashboardAccountancy',
217  'dashboardActivities',
218  'dashboardCommercials',
219  'dashboardContracts',
220  'dashboardDonation',
221  'dashboardEmailings',
222  'dashboardExpenseReport',
223  'dashboardHRM',
224  'dashboardInterventions',
225  'dashboardMRP',
226  'dashboardMembers',
227  'dashboardOpensurvey',
228  'dashboardOrders',
229  'dashboardOrdersSuppliers',
230  'dashboardProductServices',
231  'dashboardProjects',
232  'dashboardPropals',
233  'dashboardSpecialBills',
234  'dashboardSupplierProposal',
235  'dashboardThirdparties',
236  'dashboardTickets',
237  'dashboardUsersGroups',
238  'dashboardWarehouse',
239  'dashboardWarehouseReceptions',
240  'dashboardWarehouseSendings',
241  'insertExtraHeader',
242  'insertExtraFooter',
243  'printLeftBlock',
244  'formAddObjectLine',
245  'formBuilddocOptions',
246  'showSocinfoOnPrint'
247  ))) {
248  $hooktype = 'output';
249  }
250 
251  // Init return properties
252  $localResPrint = '';
253  $localResArray = array();
254 
255  $this->resNbOfHooks = 0;
256 
257  // Here, the value for $method and $hooktype are given.
258  // Loop on each hook to qualify modules that have declared context
259  $modulealreadyexecuted = array();
260  $resaction = 0;
261  $error = 0;
262  foreach ($this->hooks as $context => $modules) { // $this->hooks is an array with context as key and value is an array of modules that handle this context
263  if (!empty($modules)) {
264  '@phan-var-force array<string,CommonHookActions> $modules';
265  // Loop on each active hooks of module for this context
266  foreach ($modules as $module => $actionclassinstance) {
267  $module = preg_replace('/^\d+:/', '', $module);
268  //print "Before hook ".get_class($actionclassinstance)." method=".$method." module=".$module." hooktype=".$hooktype." results=".count($actionclassinstance->results)." resprints=".count($actionclassinstance->resprints)." resaction=".$resaction."<br>\n";
269 
270  // test to avoid running twice a hook, when a module implements several active contexts
271  if (in_array($module, $modulealreadyexecuted)) {
272  continue;
273  }
274 
275  // jump to next module/class if method does not exist
276  if (!method_exists($actionclassinstance, $method)) {
277  continue;
278  }
279 
280  $this->resNbOfHooks++;
281 
282  $modulealreadyexecuted[$module] = $module;
283 
284  // Clean class (an error may have been set from a previous call of another method for same module/hook)
285  $actionclassinstance->error = '';
286  $actionclassinstance->errors = array();
287 
288  if (getDolGlobalInt('MAIN_HOOK_DEBUG')) {
289  // This his too much verbose, enabled if const enabled only
290  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);
291  }
292 
293  // Add current context to avoid method execution in bad context, you can add this test in your method : eg if($currentcontext != 'formfile') return;
294  // Note: The hook can use the $currentcontext in its code to avoid to be ran twice or be ran for one given context only
295  $parameters['currentcontext'] = $context;
296  // Hooks that must return int (hooks with type 'addreplace')
297  if ($hooktype == 'addreplace') {
298  // @phan-suppress-next-line PhanUndeclaredMethod The method's existence is tested above.
299  $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)
300  $resaction += $resactiontmp;
301 
302  if ($resactiontmp < 0 || !empty($actionclassinstance->error) || (!empty($actionclassinstance->errors) && count($actionclassinstance->errors) > 0)) {
303  $error++;
304  $this->error = $actionclassinstance->error;
305  $this->errors = array_merge($this->errors, (array) $actionclassinstance->errors);
306  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);
307  }
308 
309  if (isset($actionclassinstance->results) && is_array($actionclassinstance->results)) {
310  if ($resactiontmp > 0) {
311  $localResArray = $actionclassinstance->results;
312  } else {
313  $localResArray = array_merge_recursive($localResArray, $actionclassinstance->results);
314  }
315  }
316 
317  if (!empty($actionclassinstance->resprints)) {
318  if ($resactiontmp > 0) {
319  $localResPrint = (string) $actionclassinstance->resprints;
320  } else {
321  $localResPrint .= (string) $actionclassinstance->resprints;
322  }
323  }
324  } else {
325  // Generic hooks that return a string or array (printLeftBlock, formAddObjectLine, formBuilddocOptions, ...)
326 
327  // TODO. this test should be done into the method of hook by returning nothing
328  if (is_array($parameters) && !empty($parameters['special_code']) && $parameters['special_code'] > 3 && $parameters['special_code'] != $actionclassinstance->module_number) {
329  continue;
330  }
331 
332  if (getDolGlobalInt('MAIN_HOOK_DEBUG')) {
333  dol_syslog("Call method ".$method." of class ".get_class($actionclassinstance).", module=".$module.", hooktype=".$hooktype, LOG_DEBUG);
334  }
335 
336  // @phan-suppress-next-line PhanUndeclaredMethod The method's existence is tested above.
337  $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)
338  $resaction += $resactiontmp;
339 
340  if (!empty($actionclassinstance->results) && is_array($actionclassinstance->results)) {
341  $localResArray = array_merge_recursive($localResArray, $actionclassinstance->results);
342  }
343  if (!empty($actionclassinstance->resprints)) {
344  $localResPrint .= (string) $actionclassinstance->resprints;
345  }
346  if (is_numeric($resactiontmp) && $resactiontmp < 0) {
347  $error++;
348  $this->error = $actionclassinstance->error;
349  $this->errors = array_merge($this->errors, (array) $actionclassinstance->errors);
350  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);
351  }
352 
353  // 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
354  if (!is_array($resactiontmp) && !is_numeric($resactiontmp)) {
355  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);
356  if (empty($actionclassinstance->resprints)) {
357  $localResPrint .= $resactiontmp;
358  }
359  }
360  }
361 
362  //print "After hook context=".$context." ".get_class($actionclassinstance)." method=".$method." hooktype=".$hooktype." results=".count($actionclassinstance->results)." resprints=".count($actionclassinstance->resprints)." resaction=".$resaction."<br>\n";
363 
364  $actionclassinstance->results = array();
365  $actionclassinstance->resprints = null;
366  }
367  }
368  }
369 
370  $this->resPrint = $localResPrint;
371  $this->resArray = $localResArray;
372 
373  return ($error ? -1 : $resaction);
374  }
375 }
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.
__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(!function_exists('dol_getprefix')) dol_include_once($relpath, $classname='')
Make an include_once using default root and alternate root if it fails.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
isModEnabled($module)
Is Dolibarr module enabled.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.