Passed
Push — main ( d07ee1...38048d )
by Rafael
50:23
created

HookManager::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
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
 *
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
21
namespace DoliCore\Lib;
22
23
/**
24
 *  \file       htdocs/core/class/hookmanager.class.php
25
 *  \ingroup    core
26
 *  \brief      File of class to manage hooks
27
 */
28
29
30
use DoliDB;
31
32
/**
33
 *  Class to manage hooks
34
 */
35
class HookManager
36
{
37
    /**
38
     * @var DoliDB Database handler.
39
     */
40
    public $db;
41
42
    /**
43
     * @var string Error code (or message)
44
     */
45
    public $error = '';
46
47
    /**
48
     * @var string[] Error codes (or messages)
49
     */
50
    public $errors = array();
51
52
    // Context hookmanager was created for ('thirdpartycard', 'thirdpartydao', ...)
53
    public $contextarray = array();
54
55
    // Array with instantiated classes
56
    public $hooks = array();
57
58
    // Array result
59
    public $resArray = array();
60
    // Printable result
61
    public $resPrint = '';
62
    // Nb of qualified hook ran
63
    public $resNbOfHooks = 0;
64
65
    /**
66
     * Constructor
67
     *
68
     * @param DoliDB $db Database handler
69
     */
70
    public function __construct($db)
71
    {
72
        $this->db = $db;
73
    }
74
75
76
    /**
77
     *  Init array $this->hooks with instantiated action controllers.
78
     *  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.
79
     *  This makes $conf->hooks_modules loaded with an entry ('modulename'=>array(nameofcontext1,nameofcontext2,...))
80
     *  When initHooks function is called, with initHooks(list_of_contexts), an array $this->hooks is defined with instance of controller
81
     *  class found into file /mymodule/class/actions_mymodule.class.php (if module has declared the context as a managed context).
82
     *  Then when a hook executeHooks('aMethod'...) is called, the method aMethod found into class will be executed.
83
     *
84
     * @param string[] $arraycontext Array list of searched hooks tab/features. For example: 'thirdpartycard' (for hook methods into page card thirdparty), 'thirdpartydao' (for hook methods into Societe), ...
85
     * @return int                             0 or 1
86
     */
87
    public function initHooks($arraycontext)
88
    {
89
        global $conf;
90
91
        if (!isset($conf->modules_parts)) {
92
            return 0;
93
        }
94
95
        // Test if there is hooks to manage
96
        if (!is_array($conf->modules_parts['hooks']) || empty($conf->modules_parts['hooks'])) {
97
            return 0;
98
        }
99
100
        // For backward compatibility
101
        if (!is_array($arraycontext)) {
102
            $arraycontext = array($arraycontext);
103
        }
104
105
        $this->contextarray = array_unique(array_merge($arraycontext, $this->contextarray)); // All contexts are concatenated
106
107
        $arraytolog = array();
108
        foreach ($conf->modules_parts['hooks'] as $module => $hooks) {  // Loop on each module that brings hooks
109
            if (empty($conf->$module->enabled)) {
110
                continue;
111
            }
112
113
            //dol_syslog(get_class($this).'::initHooks module='.$module.' arraycontext='.join(',',$arraycontext));
114
            foreach ($arraycontext as $context) {
115
                if (is_array($hooks)) {
116
                    $arrayhooks = $hooks; // New system
117
                } else {
118
                    $arrayhooks = explode(':', $hooks); // Old system (for backward compatibility)
119
                }
120
121
                if (in_array($context, $arrayhooks) || in_array('all', $arrayhooks)) {    // We instantiate action class only if initialized hook is handled by module
122
                    // Include actions class overwriting hooks
123
                    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.
124
                        $path = '/' . $module . '/class/';
125
                        $actionfile = 'actions_' . $module . '.class.php';
126
127
                        $arraytolog[] = 'context=' . $context . '-path=' . $path . $actionfile;
128
                        $resaction = dol_include_once($path . $actionfile);
129
                        if ($resaction) {
130
                            $controlclassname = 'Actions' . ucfirst($module);
131
                            $actionInstance = new $controlclassname($this->db);
132
                            $priority = empty($actionInstance->priority) ? 50 : $actionInstance->priority;
133
                            $this->hooks[$context][$priority . ':' . $module] = $actionInstance;
134
                        }
135
                    }
136
                }
137
            }
138
        }
139
        // Log the init of hook but only for hooks there are declared to be managed
140
        if (count($arraytolog) > 0) {
141
            dol_syslog(get_class($this) . "::initHooks Loading hooks: " . implode(', ', $arraytolog), LOG_DEBUG);
142
        }
143
144
        foreach ($arraycontext as $context) {
145
            if (!empty($this->hooks[$context])) {
146
                ksort($this->hooks[$context], SORT_NATURAL);
147
            }
148
        }
149
150
        return 1;
151
    }
152
153
    /**
154
     *  Execute hooks (if they were initialized) for the given method
155
     *
156
     * @param string $method Name of method hooked ('doActions', 'printSearchForm', 'showInputField', ...)
157
     * @param array $parameters Array of parameters
158
     * @param Object $object Object to use hooks on
159
     * @param string $action Action code on calling page ('create', 'edit', 'view', 'add', 'update', 'delete'...)
160
     * @return     int                     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.
161
     *                                      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.
162
     *                                      All types can also return some values into an array ->results that will be merged into this->resArray for caller.
163
     *                                      $this->error or this->errors are also defined by class called by this function if error.
164
     */
165
    public function executeHooks($method, $parameters = array(), &$object = null, &$action = '')
166
    {
167
        if (!is_array($this->hooks) || empty($this->hooks)) {
168
            return 0; // No hook available, do nothing.
169
        }
170
        if (!is_array($parameters)) {
171
            dol_syslog('executeHooks was called with a non array $parameters. Surely a bug.', LOG_WARNING);
172
            $parameters = array();
173
        }
174
175
        $parameters['context'] = implode(':', $this->contextarray);
176
        //dol_syslog(get_class($this).'::executeHooks method='.$method." action=".$action." context=".$parameters['context']);
177
178
        // Define type of hook ('output' or 'addreplace').
179
        $hooktype = 'addreplace';
180
        // TODO Remove hooks with type 'output' (example createFrom). All hooks must be converted into 'addreplace' hooks.
181
        if (
182
            in_array($method, array(
183
                'createFrom',
184
                'dashboardAccountancy',
185
                'dashboardActivities',
186
                'dashboardCommercials',
187
                'dashboardContracts',
188
                'dashboardDonation',
189
                'dashboardEmailings',
190
                'dashboardExpenseReport',
191
                'dashboardHRM',
192
                'dashboardInterventions',
193
                'dashboardMRP',
194
                'dashboardMembers',
195
                'dashboardOpensurvey',
196
                'dashboardOrders',
197
                'dashboardOrdersSuppliers',
198
                'dashboardProductServices',
199
                'dashboardProjects',
200
                'dashboardPropals',
201
                'dashboardSpecialBills',
202
                'dashboardSupplierProposal',
203
                'dashboardThirdparties',
204
                'dashboardTickets',
205
                'dashboardUsersGroups',
206
                'dashboardWarehouse',
207
                'dashboardWarehouseReceptions',
208
                'dashboardWarehouseSendings',
209
                'insertExtraHeader',
210
                'insertExtraFooter',
211
                'printLeftBlock',
212
                'formAddObjectLine',
213
                'formBuilddocOptions',
214
                'showSocinfoOnPrint'
215
            ))
216
        ) {
217
            $hooktype = 'output';
218
        }
219
220
        // Init return properties
221
        $localResPrint = '';
222
        $localResArray = array();
223
224
        $this->resNbOfHooks = 0;
225
226
        // Here, the value for $method and $hooktype are given.
227
        // Loop on each hook to qualify modules that have declared context
228
        $modulealreadyexecuted = array();
229
        $resaction = 0;
230
        $error = 0;
231
        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
232
            if (!empty($modules)) {
233
                // Loop on each active hooks of module for this context
234
                foreach ($modules as $module => $actionclassinstance) {
235
                    $module = preg_replace('/^\d+:/', '', $module);
236
                    //print "Before hook ".get_class($actionclassinstance)." method=".$method." module=".$module." hooktype=".$hooktype." results=".count($actionclassinstance->results)." resprints=".count($actionclassinstance->resprints)." resaction=".$resaction."<br>\n";
237
238
                    // test to avoid running twice a hook, when a module implements several active contexts
239
                    if (in_array($module, $modulealreadyexecuted)) {
240
                        continue;
241
                    }
242
243
                    // jump to next module/class if method does not exist
244
                    if (!method_exists($actionclassinstance, $method)) {
245
                        continue;
246
                    }
247
248
                    $this->resNbOfHooks++;
249
250
                    $modulealreadyexecuted[$module] = $module;
251
252
                    // Clean class (an error may have been set from a previous call of another method for same module/hook)
253
                    $actionclassinstance->error = '';
254
                    $actionclassinstance->errors = array();
255
256
                    if (getDolGlobalInt('MAIN_HOOK_DEBUG')) {
257
                        // This his too much verbose, enabled if const enabled only
258
                        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);
259
                    }
260
261
                    // Add current context to avoid method execution in bad context, you can add this test in your method : eg if($currentcontext != 'formfile') return;
262
                    // Note: The hook can use the $currentcontext in its code to avoid to be ran twice or be ran for one given context only
263
                    $parameters['currentcontext'] = $context;
264
                    // Hooks that must return int (hooks with type 'addreplace')
265
                    if ($hooktype == 'addreplace') {
266
                        $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)
267
                        $resaction += $resactiontmp;
268
269
                        if ($resactiontmp < 0 || !empty($actionclassinstance->error) || (!empty($actionclassinstance->errors) && count($actionclassinstance->errors) > 0)) {
270
                            $error++;
271
                            $this->error = $actionclassinstance->error;
272
                            $this->errors = array_merge($this->errors, (array)$actionclassinstance->errors);
273
                            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);
274
                        }
275
276
                        if (isset($actionclassinstance->results) && is_array($actionclassinstance->results)) {
277
                            if ($resactiontmp > 0) {
278
                                $localResArray = $actionclassinstance->results;
279
                            } else {
280
                                $localResArray = array_merge_recursive($localResArray, $actionclassinstance->results);
281
                            }
282
                        }
283
284
                        if (!empty($actionclassinstance->resprints)) {
285
                            if ($resactiontmp > 0) {
286
                                $localResPrint = $actionclassinstance->resprints;
287
                            } else {
288
                                $localResPrint .= $actionclassinstance->resprints;
289
                            }
290
                        }
291
                    } else {
292
                        // Generic hooks that return a string or array (printLeftBlock, formAddObjectLine, formBuilddocOptions, ...)
293
294
                        // TODO. this test should be done into the method of hook by returning nothing
295
                        if (is_array($parameters) && !empty($parameters['special_code']) && $parameters['special_code'] > 3 && $parameters['special_code'] != $actionclassinstance->module_number) {
296
                            continue;
297
                        }
298
299
                        if (getDolGlobalInt('MAIN_HOOK_DEBUG')) {
300
                            dol_syslog("Call method " . $method . " of class " . get_class($actionclassinstance) . ", module=" . $module . ", hooktype=" . $hooktype, LOG_DEBUG);
301
                        }
302
303
                        $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)
304
                        $resaction += $resactiontmp;
305
306
                        if (!empty($actionclassinstance->results) && is_array($actionclassinstance->results)) {
307
                            $localResArray = array_merge_recursive($localResArray, $actionclassinstance->results);
308
                        }
309
                        if (!empty($actionclassinstance->resprints)) {
310
                            $localResPrint .= $actionclassinstance->resprints;
311
                        }
312
                        if (is_numeric($resactiontmp) && $resactiontmp < 0) {
313
                            $error++;
314
                            $this->error = $actionclassinstance->error;
315
                            $this->errors = array_merge($this->errors, (array)$actionclassinstance->errors);
316
                            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);
317
                        }
318
319
                        // 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
320
                        if (!is_array($resactiontmp) && !is_numeric($resactiontmp)) {
321
                            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);
322
                            if (empty($actionclassinstance->resprints)) {
323
                                $localResPrint .= $resactiontmp;
324
                            }
325
                        }
326
                    }
327
328
                    //print "After hook context=".$context." ".get_class($actionclassinstance)." method=".$method." hooktype=".$hooktype." results=".count($actionclassinstance->results)." resprints=".count($actionclassinstance->resprints)." resaction=".$resaction."<br>\n";
329
330
                    unset($actionclassinstance->results);
331
                    unset($actionclassinstance->resprints);
332
                }
333
            }
334
        }
335
336
        $this->resPrint = $localResPrint;
337
        $this->resArray = $localResArray;
338
339
        return ($error ? -1 : $resaction);
340
    }
341
}
342