1<?php
2
3/**
4 * Hoa
5 *
6 *
7 * @license
8 *
9 * New BSD License
10 *
11 * Copyright © 2007-2017, Hoa community. All rights reserved.
12 *
13 * Redistribution and use in source and binary forms, with or without
14 * modification, are permitted provided that the following conditions are met:
15 *     * Redistributions of source code must retain the above copyright
16 *       notice, this list of conditions and the following disclaimer.
17 *     * Redistributions in binary form must reproduce the above copyright
18 *       notice, this list of conditions and the following disclaimer in the
19 *       documentation and/or other materials provided with the distribution.
20 *     * Neither the name of the Hoa nor the names of its contributors may be
21 *       used to endorse or promote products derived from this software without
22 *       specific prior written permission.
23 *
24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
27 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE
28 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
29 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
30 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
31 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
32 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
33 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
34 * POSSIBILITY OF SUCH DAMAGE.
35 */
36
37namespace Hoa\Exception;
38
39/**
40 * Class \Hoa\Exception\Idle.
41 *
42 * `\Hoa\Exception\Idle` is the mother exception class of libraries. The only
43 * difference between `\Hoa\Exception\Idle` and its directly child
44 * `\Hoa\Exception` is that the latter fires events after beeing constructed.
45 *
46 * @copyright  Copyright © 2007-2017 Hoa community
47 * @license    New BSD License
48 */
49class Idle extends \Exception
50{
51    /**
52     * Delay processing on arguments.
53     *
54     * @var array
55     */
56    protected $_tmpArguments = null;
57
58    /**
59     * Arguments to format message.
60     *
61     * @var array
62     */
63    protected $_arguments    = null;
64
65    /**
66     * Backtrace.
67     *
68     * @var array
69     */
70    protected $_trace        = null;
71
72    /**
73     * Previous.
74     *
75     * @var \Exception
76     */
77    protected $_previous     = null;
78
79    /**
80     * Original message.
81     *
82     * @var string
83     */
84    protected $_rawMessage   = null;
85
86
87
88    /**
89     * Create an exception.
90     * An exception is built with a formatted message, a code (an ID) and an
91     * array that contains the list of formatted strings for the message. If
92     * chaining, we can add a previous exception.
93     *
94     * @param   string      $message      Formatted message.
95     * @param   int         $code         Code (the ID).
96     * @param   array       $arguments    Arguments to format message.
97     * @param   \Exception  $previous     Previous exception in chaining.
98     */
99    public function __construct(
100        $message,
101        $code = 0,
102        $arguments = [],
103        \Exception $previous = null
104    ) {
105        $this->_tmpArguments = $arguments;
106        parent::__construct($message, $code, $previous);
107        $this->_rawMessage   = $message;
108        $this->message       = @vsprintf($message, $this->getArguments());
109
110        return;
111    }
112
113    /**
114     * Get the backtrace.
115     * Do not use \Exception::getTrace() any more.
116     *
117     * @return  array
118     */
119    public function getBacktrace()
120    {
121        if (null === $this->_trace) {
122            $this->_trace = $this->getTrace();
123        }
124
125        return $this->_trace;
126    }
127
128    /**
129     * Get previous.
130     * Do not use \Exception::getPrevious() any more.
131     *
132     * @return  \Exception
133     */
134    public function getPreviousThrow()
135    {
136        if (null === $this->_previous) {
137            $this->_previous = $this->getPrevious();
138        }
139
140        return $this->_previous;
141    }
142
143    /**
144     * Get arguments for the message.
145     *
146     * @return  array
147     */
148    public function getArguments()
149    {
150        if (null === $this->_arguments) {
151            $arguments = $this->_tmpArguments;
152
153            if (!is_array($arguments)) {
154                $arguments = [$arguments];
155            }
156
157            foreach ($arguments as &$value) {
158                if (null === $value) {
159                    $value = '(null)';
160                }
161            }
162
163            $this->_arguments = $arguments;
164            unset($this->_tmpArguments);
165        }
166
167        return $this->_arguments;
168    }
169
170    /**
171     * Get the raw message.
172     *
173     * @return  string
174     */
175    public function getRawMessage()
176    {
177        return $this->_rawMessage;
178    }
179
180    /**
181     * Get the message already formatted.
182     *
183     * @return  string
184     */
185    public function getFormattedMessage()
186    {
187        return $this->getMessage();
188    }
189
190    /**
191     * Get the source of the exception (class, method, function, main etc.).
192     *
193     * @return  string
194     */
195    public function getFrom()
196    {
197        $trace = $this->getBacktrace();
198        $from  = '{main}';
199
200        if (!empty($trace)) {
201            $t    = $trace[0];
202            $from = '';
203
204            if (isset($t['class'])) {
205                $from .= $t['class'] . '::';
206            }
207
208            if (isset($t['function'])) {
209                $from .= $t['function'] . '()';
210            }
211        }
212
213        return $from;
214    }
215
216    /**
217     * Raise an exception as a string.
218     *
219     * @param   bool    $previous    Whether raise previous exception if exists.
220     * @return  string
221     */
222    public function raise($previous = false)
223    {
224        $message = $this->getFormattedMessage();
225        $trace   = $this->getBacktrace();
226        $file    = '/dev/null';
227        $line    = -1;
228        $pre     = $this->getFrom();
229
230        if (!empty($trace)) {
231            $file = isset($trace['file']) ? $trace['file'] : null;
232            $line = isset($trace['line']) ? $trace['line'] : null;
233        }
234
235        $pre .= ': ';
236
237        try {
238            $out =
239                $pre . '(' . $this->getCode() . ') ' . $message . "\n" .
240                'in ' . $this->getFile() . ' at line ' .
241                $this->getLine() . '.';
242        } catch (\Exception $e) {
243            $out =
244                $pre . '(' . $this->getCode() . ') ' . $message . "\n" .
245                'in ' . $file . ' around line ' . $line . '.';
246        }
247
248        if (true === $previous &&
249            null !== $previous = $this->getPreviousThrow()) {
250            $out .=
251                "\n\n" . '    ⬇' . "\n\n" .
252                'Nested exception (' . get_class($previous) . '):' . "\n" .
253                ($previous instanceof self
254                    ? $previous->raise(true)
255                    : $previous->getMessage());
256        }
257
258        return $out;
259    }
260
261    /**
262     * Catch uncaught exception (only \Hoa\Exception\Idle and children).
263     *
264     * @param   \Throwable  $exception    The exception.
265     * @return  void
266     * @throws  \Throwable
267     */
268    public static function uncaught($exception)
269    {
270        if (!($exception instanceof self)) {
271            throw $exception;
272        }
273
274        while (0 < ob_get_level()) {
275            ob_end_flush();
276        }
277
278        echo
279            'Uncaught exception (' . get_class($exception) . '):' . "\n" .
280            $exception->raise(true);
281
282        return;
283    }
284
285    /**
286     * String representation of object.
287     *
288     * @return  string
289     */
290    public function __toString()
291    {
292        return $this->raise();
293    }
294
295    /**
296     * Enable uncaught exception handler.
297     * This is restricted to Hoa's exceptions only.
298     *
299     * @param   bool  $enable    Enable.
300     * @return  mixed
301     */
302    public static function enableUncaughtHandler($enable = true)
303    {
304        if (false === $enable) {
305            return restore_exception_handler();
306        }
307
308        return set_exception_handler(function ($exception) {
309            return self::uncaught($exception);
310        });
311    }
312}
313