Passed
Push — EXTRACT_CLASSES ( 0382f2...c25e41 )
by Rafael
52:18
created

HookManager::executeHooks()   F

Complexity

Conditions 47
Paths 4452

Size

Total Lines 200
Code Lines 122

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 47
eloc 122
nc 4452
nop 4
dl 0
loc 200
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/* Copyright (C) 2010-2016  Laurent Destailleur         <[email protected]>
4
 * Copyright (C) 2010-2014  Regis Houssin               <[email protected]>
5
 * Copyright (C) 2010-2011  Juanjo Menent               <[email protected]>
6
 * Copyright (C) 2024		MDW							<[email protected]>
7
 * Copyright (C) 2024       Rafael San José             <[email protected]>
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
23
namespace Dolibarr\Code\Core\Classes;
24
25
use DoliDB;
26
27
/**
28
 *  \file       htdocs/core/class/hookmanager.class.php
29
 *  \ingroup    core
30
 *  \brief      File of class to manage hooks
31
 */
32
33
34
/**
35
 *  Class to manage hooks
36
 */
37
class HookManager
38
{
39
    /**
40
     * @var DoliDB Database handler.
41
     */
42
    public $db;
43
44
    /**
45
     * @var string Error code (or message)
46
     */
47
    public $error = '';
48
49
    /**
50
     * @var string[] Error codes (or messages)
51
     */
52
    public $errors = array();
53
54
    /**
55
     * @var string[] Context hookmanager was created for ('thirdpartycard', 'thirdpartydao', ...)
56
     */
57
    public $contextarray = array();
58
59
    /**
60
     * array<string,array<string,null|string|CommonHookActions>>    Array with instantiated classes
61
     */
62
    public $hooks = array();
63
64
    /**
65
     * array<string,array<string,null|string|CommonHookActions>>    Array with instantiated classes sorted by hook priority
66
     */
67
    public $hooksSorted = array();
68
69
    /**
70
     * @var array<string,array{name:string,contexts:string[],file:string,line:string,count:int}>    List of hooks called during this request (key = hash)
71
     */
72
    public $hooksHistory = [];
73
74
    /**
75
     * @var mixed[] Result
76
     */
77
    public $resArray = array();
78
79
    /**
80
     * @var string Printable result
81
     */
82
    public $resPrint = '';
83
84
    /**
85
     * @var int Nb of qualified hook ran
86
     */
87
    public $resNbOfHooks = 0;
88
89
    /**
90
     * Constructor
91
     *
92
     * @param   DoliDB      $db     Database handler
93
     * @return void
94
     */
95
    public function __construct($db)
96
    {
97
        $this->db = $db;
98
    }
99
100
101
    /**
102
     *  Init array $this->hooks with instantiated action controllers.
103
     *  First, a hook is declared by a module by adding a constant MAIN_MODULE_MYMODULENAME_HOOKS with value 'nameofcontext1:nameofcontext2:...' into $this->const of module descriptor file.
104
     *  This makes $conf->hooks_modules loaded with an entry ('modulename'=>array(nameofcontext1,nameofcontext2,...))
105
     *  When initHooks function is called, with initHooks(list_of_contexts), an array $this->hooks is defined with instance of controller
106
     *  class found into file /mymodule/class/actions_mymodule.class.php (if module has declared the context as a managed context).
107
     *  Then when a hook executeHooks('aMethod'...) is called, the method aMethod found into class will be executed.
108
     *
109
     *  @param  string[]    $arraycontext       Array list of context hooks to activate. For example: 'thirdpartycard' (for hook methods into page card thirdparty), 'thirdpartydao' (for hook methods into Societe), ...
110
     *  @return int<0,1>                        0 or 1
111
     */
112
    public function initHooks($arraycontext)
113
    {
114
        global $conf;
115
116
        // Test if there is at least one hook to manage
117
        if (!is_array($conf->modules_parts['hooks']) || empty($conf->modules_parts['hooks'])) {
118
            return 0;
119
        }
120
121
        // For backward compatibility
122
        if (!is_array($arraycontext)) {
123
            $arraycontext = array($arraycontext);
124
        }
125
126
        $this->contextarray = array_unique(array_merge($arraycontext, $this->contextarray)); // All contexts are concatenated but kept unique
127
128
        $foundcontextmodule = false;
129
130
        // 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
131
        foreach ($conf->modules_parts['hooks'] as $module => $hooks) {
132
            if (!isModEnabled($module)) {
133
                continue;
134
            }
135
136
            //dol_syslog(get_class($this).'::initHooks module='.$module.' arraycontext='.join(',',$arraycontext));
137
            foreach ($arraycontext as $context) {
138
                if (is_array($hooks)) {
139
                    $arrayhooks = $hooks; // New system = array of hook contexts claimed by the module $module
140
                } else {
141
                    $arrayhooks = explode(':', $hooks); // Old system (for backward compatibility)
142
                }
143
144
                if (in_array($context, $arrayhooks) || in_array('all', $arrayhooks)) {    // We instantiate action class only if initialized hook is handled by the module
145
                    // Include actions class overwriting hooks
146
                    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.
147
                        $path = '/' . $module . '/class/';
148
                        $actionfile = 'actions_' . $module . '.class.php';
149
150
                        $resaction = dol_include_once($path . $actionfile);
151
                        if ($resaction) {
152
                            $controlclassname = 'Actions' . ucfirst($module);
153
154
                            $actionInstance = new $controlclassname($this->db);
155
                            '@phan-var-force CommonHookActions $actionInstance';
156
157
158
                            $priority = empty($actionInstance->priority) ? 50 : $actionInstance->priority;
159
160
                            $this->hooks[$context][$module] = $actionInstance;
161
                            $this->hooksSorted[$context][$priority . ':' . $module] = $actionInstance;
162
163
                            $foundcontextmodule = true;
164
165
                            // Hook has been initialized with another couple $context/$module
166
                            $stringtolog = 'context=' . $context . '-path=' . $path . $actionfile . '-priority=' . $priority;
167
                            dol_syslog(get_class($this) . "::initHooks Loading hooks: " . $stringtolog, LOG_DEBUG);
168
                        } else {
169
                            dol_syslog(get_class($this) . "::initHooks Failed to load hook in " . $path . $actionfile, LOG_WARNING);
170
                        }
171
                    } else {
172
                        // Hook was already initialized for this context and module
173
                    }
174
                }
175
            }
176
        }
177
178
        // Log the init of hook
179
        // dol_syslog(get_class($this)."::initHooks Loading hooks: ".implode(', ', $arraytolog), LOG_DEBUG);
180
181
        if ($foundcontextmodule) {
182
            foreach ($arraycontext as $context) {
183
                if (!empty($this->hooksSorted[$context])) {
184
                    ksort($this->hooksSorted[$context], SORT_NATURAL);
185
                }
186
            }
187
        }
188
189
        return 1;
190
    }
191
192
    /**
193
     *  Execute hooks (if they were initialized) for the given method
194
     *
195
     *  @param      string  $method         Name of method hooked ('doActions', 'printSearchForm', 'showInputField', ...)
196
     *  @param      array<string,mixed> $parameters     Array of parameters
197
     *  @param      object  $object         Object to use hooks on
198
     *  @param      string  $action         Action code on calling page ('create', 'edit', 'view', 'add', 'update', 'delete'...)
199
     *  @return     int<-1,1>               For 'addreplace' hooks (doActions, formConfirm, formObjectOptions, pdf_xxx,...):    Return 0 if we want to keep standard actions, >0 if we want to stop/replace standard actions, <0 if KO. Things to print are returned into ->resprints and set into ->resPrint. Things to return are returned into ->results by hook and set into ->resArray for caller.
200
     *                                      For 'output' hooks (printLeftBlock, formAddObjectLine, formBuilddocOptions, ...):   Return 0 if we want to keep standard actions, >0 uf we want to stop/replace standard actions (at least one > 0 and replacement will be done), <0 if KO. Things to print are returned into ->resprints and set into ->resPrint. Things to return are returned into ->results by hook and set into ->resArray for caller.
201
     *                                      All types can also return some values into an array ->results that will be merged into this->resArray for caller.
202
     *                                      $this->error or this->errors are also defined by class called by this function if error.
203
     */
204
    public function executeHooks($method, $parameters = array(), &$object = null, &$action = '')
205
    {
206
        //global $debugbar;
207
        //if (is_object($debugbar) && get_class($debugbar) === 'DolibarrDebugBar') {
208
        if (isModEnabled('debugbar') && function_exists('debug_backtrace')) {
209
            $trace = debug_backtrace();
210
            if (isset($trace[0])) {
211
                $hookInformations = [
212
                    'name' => $method,
213
                    'contexts' => $this->contextarray,
214
                    'file' => $trace[0]['file'],
215
                    'line' => $trace[0]['line'],
216
                    'count' => 0,
217
                ];
218
                $hash = md5(json_encode($hookInformations));
219
                if (!empty($this->hooksHistory[$hash])) {
220
                    $this->hooksHistory[$hash]['count']++;
221
                } else {
222
                    $hookInformations['count'] = 1;
223
                    $this->hooksHistory[$hash] = $hookInformations;
224
                }
225
            }
226
        }
227
228
        if (!is_array($this->hooks) || empty($this->hooks)) {
229
            return 0; // No hook available, do nothing.
230
        }
231
        if (!is_array($parameters)) {
232
            dol_syslog('executeHooks was called with a non array $parameters. Surely a bug.', LOG_WARNING);
233
            $parameters = array();
234
        }
235
236
        $parameters['context'] = implode(':', $this->contextarray);
237
        //dol_syslog(get_class($this).'::executeHooks method='.$method." action=".$action." context=".$parameters['context']);
238
239
        // Define type of hook ('output' or 'addreplace').
240
        $hooktype = 'addreplace';
241
        // TODO Remove hooks with type 'output' (example createFrom). All hooks must be converted into 'addreplace' hooks.
242
        if (
243
            in_array($method, array(
244
            'createFrom',
245
            'dashboardAccountancy',
246
            'dashboardActivities',
247
            'dashboardCommercials',
248
            'dashboardContracts',
249
            'dashboardDonation',
250
            'dashboardEmailings',
251
            'dashboardExpenseReport',
252
            'dashboardHRM',
253
            'dashboardInterventions',
254
            'dashboardMRP',
255
            'dashboardMembers',
256
            'dashboardOpensurvey',
257
            'dashboardOrders',
258
            'dashboardOrdersSuppliers',
259
            'dashboardProductServices',
260
            'dashboardProjects',
261
            'dashboardPropals',
262
            'dashboardSpecialBills',
263
            'dashboardSupplierProposal',
264
            'dashboardThirdparties',
265
            'dashboardTickets',
266
            'dashboardUsersGroups',
267
            'dashboardWarehouse',
268
            'dashboardWarehouseReceptions',
269
            'dashboardWarehouseSendings',
270
            'insertExtraHeader',
271
            'insertExtraFooter',
272
            'printLeftBlock',
273
            'formAddObjectLine',
274
            'formBuilddocOptions',
275
            'showSocinfoOnPrint'
276
            ))
277
        ) {
278
            $hooktype = 'output';
279
        }
280
281
        // Init return properties
282
        $localResPrint = '';
283
        $localResArray = array();
284
285
        $this->resNbOfHooks = 0;
286
287
        // Here, the value for $method and $hooktype are given.
288
        // Loop on each hook to qualify modules that have declared context
289
        $modulealreadyexecuted = array();
290
        $resaction = 0;
291
        $error = 0;
292
        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
293
            if (!empty($modules)) {
294
                '@phan-var-force array<string,CommonHookActions> $modules';
295
                // Loop on each active hooks of module for this context
296
                foreach ($modules as $module => $actionclassinstance) {
297
                    $module = preg_replace('/^\d+:/', '', $module);     // $module string is 'priority:module'
298
                    //print "Before hook ".get_class($actionclassinstance)." method=".$method." module=".$module." hooktype=".$hooktype." results=".count($actionclassinstance->results)." resprints=".count($actionclassinstance->resprints)." resaction=".$resaction."<br>\n";
299
300
                    // test to avoid running twice a hook, when a module implements several active contexts
301
                    if (in_array($module, $modulealreadyexecuted)) {
302
                        continue;
303
                    }
304
305
                    // jump to next module/class if method does not exist
306
                    if (!method_exists($actionclassinstance, $method)) {
307
                        continue;
308
                    }
309
310
                    $this->resNbOfHooks++;
311
312
                    $modulealreadyexecuted[$module] = $module;
313
314
                    // Clean class (an error may have been set from a previous call of another method for same module/hook)
315
                    $actionclassinstance->error = '';
316
                    $actionclassinstance->errors = array();
317
318
                    if (getDolGlobalInt('MAIN_HOOK_DEBUG')) {
319
                        // This his too much verbose, enabled if const enabled only
320
                        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);
321
                    }
322
323
                    // Add current context to avoid method execution in bad context, you can add this test in your method : eg if($currentcontext != 'formfile') return;
324
                    // Note: The hook can use the $currentcontext in its code to avoid to be ran twice or be ran for one given context only
325
                    $parameters['currentcontext'] = $context;
326
                    // Hooks that must return int (hooks with type 'addreplace')
327
                    if ($hooktype == 'addreplace') {
328
                        // @phan-suppress-next-line PhanUndeclaredMethod  The method's existence is tested above.
329
                        $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)
330
                        $resaction += $resactiontmp;
331
332
                        if ($resactiontmp < 0 || !empty($actionclassinstance->error) || (!empty($actionclassinstance->errors) && count($actionclassinstance->errors) > 0)) {
333
                            $error++;
334
                            $this->error = $actionclassinstance->error;
335
                            $this->errors = array_merge($this->errors, (array) $actionclassinstance->errors);
336
                            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);
337
                        }
338
339
                        if (isset($actionclassinstance->results) && is_array($actionclassinstance->results)) {
340
                            if ($resactiontmp > 0) {
341
                                $localResArray = $actionclassinstance->results;
342
                            } else {
343
                                $localResArray = array_merge_recursive($localResArray, $actionclassinstance->results);
344
                            }
345
                        }
346
347
                        if (!empty($actionclassinstance->resprints)) {
348
                            if ($resactiontmp > 0) {
349
                                $localResPrint = (string) $actionclassinstance->resprints;
350
                            } else {
351
                                $localResPrint .= (string) $actionclassinstance->resprints;
352
                            }
353
                        }
354
                    } else {
355
                        // Generic hooks that return a string or array (printLeftBlock, formAddObjectLine, formBuilddocOptions, ...)
356
357
                        // TODO. this test should be done into the method of hook by returning nothing
358
                        if (is_array($parameters) && !empty($parameters['special_code']) && $parameters['special_code'] > 3 && $parameters['special_code'] != $actionclassinstance->module_number) {
359
                            continue;
360
                        }
361
362
                        if (getDolGlobalInt('MAIN_HOOK_DEBUG')) {
363
                            dol_syslog("Call method " . $method . " of class " . get_class($actionclassinstance) . ", module=" . $module . ", hooktype=" . $hooktype, LOG_DEBUG);
364
                        }
365
366
                        // @phan-suppress-next-line PhanUndeclaredMethod  The method's existence is tested above.
367
                        $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)
368
                        $resaction += $resactiontmp;
369
370
                        if (!empty($actionclassinstance->results) && is_array($actionclassinstance->results)) {
371
                            $localResArray = array_merge_recursive($localResArray, $actionclassinstance->results);
372
                        }
373
                        if (!empty($actionclassinstance->resprints)) {
374
                            $localResPrint .= (string) $actionclassinstance->resprints;
375
                        }
376
                        if (is_numeric($resactiontmp) && $resactiontmp < 0) {
377
                            $error++;
378
                            $this->error = $actionclassinstance->error;
379
                            $this->errors = array_merge($this->errors, (array) $actionclassinstance->errors);
380
                            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);
381
                        }
382
383
                        // 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
384
                        if (!is_array($resactiontmp) && !is_numeric($resactiontmp)) {
385
                            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);
386
                            if (empty($actionclassinstance->resprints)) {
387
                                $localResPrint .= $resactiontmp;
388
                            }
389
                        }
390
                    }
391
392
                    //print "After hook context=".$context." ".get_class($actionclassinstance)." method=".$method." hooktype=".$hooktype." results=".count($actionclassinstance->results)." resprints=".count($actionclassinstance->resprints)." resaction=".$resaction."<br>\n";
393
394
                    $actionclassinstance->results = array();
395
                    $actionclassinstance->resprints = null;
396
                }
397
            }
398
        }
399
400
        $this->resPrint = $localResPrint;
401
        $this->resArray = $localResArray;
402
403
        return ($error ? -1 : $resaction);
404
    }
405
}
406