Completed
Push — namespace-template ( 7967f2...367a36 )
by Sam
10:48
created

ErrorControlChain   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 211
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 1
Bugs 1 Features 0
Metric Value
dl 0
loc 211
rs 9.8
c 1
b 1
f 0
wmc 31
lcom 1
cbo 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A hasErrored() 0 3 1
A setErrored() 0 3 1
A setSuppression() 0 12 3
A setDisplayErrors() 0 3 1
A getDisplayErrors() 0 3 1
A then() 0 7 1
A thenWhileGood() 0 3 1
A thenIfErrored() 0 3 1
A thenAlways() 0 3 1
A lastErrorWasFatal() 0 5 3
A lastErrorWasMemoryExhaustion() 0 5 3
A translateMemstring() 0 5 2
B handleFatalError() 0 14 6
A execute() 0 9 1
B step() 0 21 5
1
<?php
2
3
/**
4
 * Class ErrorControlChain
5
 *
6
 * Runs a set of steps, optionally suppressing uncaught errors or exceptions which would otherwise be fatal that
7
 * occur in each step. If an error does occur, subsequent steps are normally skipped, but can optionally be run anyway.
8
 *
9
 * Usage:
10
 *
11
 * $chain = new ErrorControlChain();
12
 * $chain->then($callback1)->then($callback2)->thenIfErrored($callback3)->execute();
13
 *
14
 * WARNING: This class is experimental and designed specifically for use pre-startup in main.php
15
 * It will likely be heavily refactored before the release of 3.2
16
 *
17
 * @package framework
18
 * @subpackage misc
19
 */
20
class ErrorControlChain {
21
	public static $fatal_errors = null; // Initialised after class definition
22
23
	/**
24
	 * Is there an error?
25
	 *
26
	 * @var bool
27
	 */
28
	protected $error = false;
29
30
	/**
31
	 * List of steps
32
	 *
33
	 * @var array
34
	 */
35
	protected $steps = array();
36
37
	/**
38
	 * True if errors should be hidden
39
	 *
40
	 * @var bool
41
	 */
42
	protected $suppression = true;
43
44
	/** We can't unregister_shutdown_function, so this acts as a flag to enable handling */
45
	protected $handleFatalErrors = false;
46
47
	/** We overload display_errors to hide errors during execution, so we need to remember the original to restore to */
48
	protected $originalDisplayErrors = null;
49
50
	/**
51
	 * Any exceptions passed through the chain
52
	 *
53
	 * @var Exception
54
	 */
55
	protected $lastException = null;
56
57
	/**
58
	 * Determine if an error has been found
59
	 *
60
	 * @return bool
61
	 */
62
	public function hasErrored() {
63
		return $this->error;
64
	}
65
66
	public function setErrored($error) {
67
		$this->error = (bool)$error;
68
	}
69
70
	/**
71
	 * Sets whether errors are suppressed or not
72
	 * Notes:
73
	 * - Errors cannot be suppressed if not handling errors.
74
	 * - Errors cannot be un-suppressed if original mode dis-allowed visible errors
75
	 *
76
	 * @param bool $suppression
77
	 */
78
	public function setSuppression($suppression) {
79
		$this->suppression = (bool)$suppression;
80
		// If handling fatal errors, conditionally disable, or restore error display
81
		// Note: original value of display_errors could also evaluate to "off"
82
		if ($this->handleFatalErrors) {
83
			if($suppression) {
84
				$this->setDisplayErrors(0);
85
			} else {
86
				$this->setDisplayErrors($this->originalDisplayErrors);
87
			}
88
		}
89
	}
90
91
	/**
92
	 * Set display_errors
93
	 *
94
	 * @param mixed $errors
95
	 */
96
	protected function setDisplayErrors($errors) {
97
		ini_set('display_errors', $errors);
98
	}
99
100
	/**
101
	 * Get value of display_errors ini value
102
	 *
103
	 * @return mixed
104
	 */
105
	protected function getDisplayErrors() {
106
		return ini_get('display_errors');
107
	}
108
109
	/**
110
	 * Add this callback to the chain of callbacks to call along with the state
111
	 * that $error must be in this point in the chain for the callback to be called
112
	 *
113
	 * @param $callback - The callback to call
114
	 * @param $onErrorState - false if only call if no errors yet, true if only call if already errors, null for either
115
	 * @return $this
116
	 */
117
	public function then($callback, $onErrorState = false) {
118
		$this->steps[] = array(
119
			'callback' => $callback,
120
			'onErrorState' => $onErrorState
121
		);
122
		return $this;
123
	}
124
125
	/**
126
	 * Request that the callback is invoked if not errored
127
	 *
128
	 * @param callable $callback
129
	 * @return $this
130
	 */
131
	public function thenWhileGood($callback) {
132
		return $this->then($callback, false);
133
	}
134
135
	/**
136
	 * Request that the callback is invoked on error
137
	 *
138
	 * @param callable $callback
139
	 * @return $this
140
	 */
141
	public function thenIfErrored($callback) {
142
		return $this->then($callback, true);
143
	}
144
145
	/**
146
	 * Request that the callback is invoked always
147
	 *
148
	 * @param callable $callback
149
	 * @return $this
150
	 */
151
	public function thenAlways($callback) {
152
		return $this->then($callback, null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
153
	}
154
155
	/**
156
	 * Return true if the last error was fatal
157
	 *
158
	 * @return boolean
159
	 */
160
	protected function lastErrorWasFatal() {
161
		if($this->lastException) return true;
162
		$error = error_get_last();
163
		return $error && ($error['type'] & self::$fatal_errors) != 0;
0 ignored issues
show
Bug Best Practice introduced by
The expression $error of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
164
	}
165
166
	protected function lastErrorWasMemoryExhaustion() {
167
		$error = error_get_last();
168
		$message = $error ? $error['message'] : '';
169
		return stripos($message, 'memory') !== false && stripos($message, 'exhausted') !== false;
170
	}
171
172
	static $transtable = array(
173
		'k' => 1024,
174
		'm' => 1048576,
175
		'g' => 1073741824
176
	);
177
178
	protected function translateMemstring($memString) {
179
		$char = strtolower(substr($memString, -1));
180
		$fact = isset(self::$transtable[$char]) ? self::$transtable[$char] : 1;
181
		return ((int)$memString) * $fact;
182
	}
183
184
	public function handleFatalError() {
185
		if ($this->handleFatalErrors && $this->suppression) {
186
			if ($this->lastErrorWasFatal()) {
187
				if ($this->lastErrorWasMemoryExhaustion()) {
188
					// Bump up memory limit by an arbitrary 10% / 10MB (whichever is bigger) since we've run out
189
					$cur = $this->translateMemstring(ini_get('memory_limit'));
190
					if ($cur != -1) ini_set('memory_limit', $cur + max(round($cur*0.1), 10000000));
191
				}
192
193
				$this->error = true;
194
				$this->step();
195
			}
196
		}
197
	}
198
199
	public function execute() {
200
		register_shutdown_function(array($this, 'handleFatalError'));
201
		$this->handleFatalErrors = true;
202
203
		$this->originalDisplayErrors = $this->getDisplayErrors();
204
		$this->setSuppression($this->suppression);
205
206
		$this->step();
207
	}
208
209
	protected function step() {
210
		if ($this->steps) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->steps of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
211
			$step = array_shift($this->steps);
212
213
			if ($step['onErrorState'] === null || $step['onErrorState'] === $this->error) {
214
				try {
215
					call_user_func($step['callback'], $this);
216
				} catch (Exception $ex) {
217
					$this->lastException = $ex;
218
					throw $ex;
219
				}
220
			}
221
222
			$this->step();
223
		}
224
		else {
225
			// Now clean up
226
			$this->handleFatalErrors = false;
227
			$this->setDisplayErrors($this->originalDisplayErrors);
228
		}
229
	}
230
}
231
232
ErrorControlChain::$fatal_errors = E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR;
233
if (defined('E_RECOVERABLE_ERROR')) ErrorControlChain::$fatal_errors |= E_RECOVERABLE_ERROR;
234