ErrorControlChain.php
4.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
<?php
/**
* Class ErrorControlChain
*
* Runs a set of steps, optionally suppressing uncaught errors or exceptions which would otherwise be fatal that
* occur in each step. If an error does occur, subsequent steps are normally skipped, but can optionally be run anyway.
*
* Usage:
*
* $chain = new ErrorControlChain();
* $chain->then($callback1)->then($callback2)->thenIfErrored($callback3)->execute();
*
* WARNING: This class is experimental and designed specifically for use pre-startup in main.php
* It will likely be heavily refactored before the release of 3.2
*/
class ErrorControlChain {
public static $fatal_errors = null; // Initialised after class definition
protected $error = false;
protected $steps = array();
protected $suppression = true;
/** We can't unregister_shutdown_function, so this acts as a flag to enable handling */
protected $handleFatalErrors = false;
/** We overload display_errors to hide errors during execution, so we need to remember the original to restore to */
protected $originalDisplayErrors = null;
public function hasErrored() {
return $this->error;
}
public function setErrored($error) {
$this->error = (bool)$error;
}
public function setSuppression($suppression) {
$this->suppression = (bool)$suppression;
if ($this->handleFatalErrors) ini_set('display_errors', !$suppression);
}
/**
* Add this callback to the chain of callbacks to call along with the state
* that $error must be in this point in the chain for the callback to be called
*
* @param $callback - The callback to call
* @param $onErrorState - false if only call if no errors yet, true if only call if already errors, null for either
* @return $this
*/
public function then($callback, $onErrorState = false) {
$this->steps[] = array(
'callback' => $callback,
'onErrorState' => $onErrorState
);
return $this;
}
public function thenWhileGood($callback) {
return $this->then($callback, false);
}
public function thenIfErrored($callback) {
return $this->then($callback, true);
}
public function thenAlways($callback) {
return $this->then($callback, null);
}
protected function lastErrorWasFatal() {
$error = error_get_last();
return $error && ($error['type'] & self::$fatal_errors) != 0;
}
protected function lastErrorWasMemoryExhaustion() {
$error = error_get_last();
$message = $error ? $error['message'] : '';
return stripos($message, 'memory') !== false && stripos($message, 'exhausted') !== false;
}
static $transtable = array(
'k' => 1024,
'm' => 1048576,
'g' => 1073741824
);
protected function translateMemstring($memString) {
$char = strtolower(substr($memString, -1));
$fact = isset(self::$transtable[$char]) ? self::$transtable[$char] : 1;
return ((int)$memString) * $fact;
}
public function handleFatalError() {
if ($this->handleFatalErrors && $this->suppression) {
if ($this->lastErrorWasFatal()) {
if ($this->lastErrorWasMemoryExhaustion()) {
// Bump up memory limit by an arbitrary 10% / 10MB (whichever is bigger) since we've run out
$cur = $this->translateMemstring(ini_get('memory_limit'));
if ($cur != -1) ini_set('memory_limit', $cur + max(round($cur*0.1), 10000000));
}
$this->error = true;
$this->step();
}
}
}
public function execute() {
register_shutdown_function(array($this, 'handleFatalError'));
$this->handleFatalErrors = true;
$this->originalDisplayErrors = ini_get('display_errors');
ini_set('display_errors', !$this->suppression);
$this->step();
}
protected function step() {
if ($this->steps) {
$step = array_shift($this->steps);
if ($step['onErrorState'] === null || $step['onErrorState'] === $this->error) {
call_user_func($step['callback'], $this);
}
$this->step();
}
else {
// Now clean up
$this->handleFatalErrors = false;
ini_set('display_errors', $this->originalDisplayErrors);
}
}
}
ErrorControlChain::$fatal_errors = E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR;
if (defined('E_RECOVERABLE_ERROR')) ErrorControlChain::$fatal_errors |= E_RECOVERABLE_ERROR;