1<?php
2/**
3 * Wrapper around backtraces providing utility methods.
4 *
5 * Copyright 1999-2017 Horde LLC (http://www.horde.org/)
6 *
7 * @category   Horde
8 * @package    Support
9 * @license    http://www.horde.org/licenses/bsd
10 */
11class Horde_Support_Backtrace
12{
13    /**
14     * Backtrace.
15     *
16     * @var array
17     */
18    public $backtrace;
19
20    /**
21     * Constructor.
22     *
23     * @param Exception|array $backtrace  The backtrace source. Either a
24     *                                    trowable, an exception or an existing
25     *                                    backtrace.
26     *                                    Defaults to the current stack.
27     */
28    public function __construct($backtrace = null)
29    {
30        if ($backtrace instanceof Throwable) {
31            $this->createFromThrowable($backtrace);
32        } elseif ($backtrace instanceof Exception) {
33            $this->createFromException($backtrace);
34        } elseif ($backtrace) {
35            $this->createFromDebugBacktrace($backtrace);
36        } else {
37            $this->createFromDebugBacktrace(debug_backtrace(), 1);
38        }
39    }
40
41    /**
42     * Wraps the result of debug_backtrace().
43     *
44     * By specifying a non-zero $nestingLevel, levels of the backtrace can be
45     * ignored. For instance, when Horde_Support_Backtrace creates a backtrace
46     * for you, it ignores the Horde_Backtrace constructor in the wrapped
47     * trace.
48     *
49     * @param array $backtrace       The debug_backtrace() result.
50     * @param integer $nestingLevel  The number of levels of the backtrace to
51     *                               ignore.
52     */
53    public function createFromDebugBacktrace($backtrace, $nestingLevel = 0)
54    {
55        while ($nestingLevel > 0) {
56            array_shift($backtrace);
57            --$nestingLevel;
58        }
59
60        $this->backtrace = $backtrace;
61    }
62
63    /**
64     * Wraps an error object's backtrace.
65     *
66     * @since Horde_Support 2.2.0
67     *
68     * @param Throwable $e  The error to wrap.
69     */
70    public function createFromThrowable(Throwable $e)
71    {
72        $this->_createFromThrowable($e);
73    }
74
75    /**
76     * Wraps an error object's backtrace.
77     *
78     * @todo Merge with createFromThrowable with PHP 7.
79     *
80     * @param Throwable $e  The error to wrap.
81     */
82    protected function _createFromThrowable($e)
83    {
84        $this->backtrace = $e->getTrace();
85        if ($previous = $e->getPrevious()) {
86            $backtrace = new self($previous);
87            $this->backtrace = array_merge($backtrace->backtrace,
88                                           $this->backtrace);
89        }
90    }
91
92    /**
93     * Wraps an Exception object's backtrace.
94     *
95     * @todo Remove with PHP 7.
96     *
97     * @param Exception $e  The exception to wrap.
98     */
99    public function createFromException(Exception $e)
100    {
101        $this->_createFromThrowable($e);
102    }
103
104    /**
105     * Returns the nesting level (number of calls deep) of the current context.
106     *
107     * @return integer  Nesting level.
108     */
109    public function getNestingLevel()
110    {
111        return count($this->backtrace);
112    }
113
114    /**
115     * Returns the context at a specific nesting level.
116     *
117     * @param integer $nestingLevel  0 == current level, 1 == caller, and so on
118     *
119     * @return array  The requested context.
120     */
121    public function getContext($nestingLevel)
122    {
123        if (!isset($this->backtrace[$nestingLevel])) {
124            throw new Horde_Exception('Unknown nesting level');
125        }
126        return $this->backtrace[$nestingLevel];
127    }
128
129    /**
130     * Returns details about the routine where the exception occurred.
131     *
132     * @return array $caller
133     */
134    public function getCurrentContext()
135    {
136        return $this->getContext(0);
137    }
138
139    /**
140     * Returns details about the caller of the routine where the exception
141     * occurred.
142     *
143     * @return array $caller
144     */
145    public function getCallingContext()
146    {
147        return $this->getContext(1);
148    }
149
150    /**
151     * Returns a simple, human-readable list of the complete backtrace.
152     *
153     * @return string  The backtrace map.
154     */
155    public function __toString()
156    {
157        $count = count($this->backtrace);
158        $pad = strlen($count);
159        $map = '';
160        for ($i = $count - 1; $i >= 0; $i--) {
161            $map .= str_pad($count - $i, $pad, ' ', STR_PAD_LEFT) . '. ';
162            if (isset($this->backtrace[$i]['class'])) {
163                $map .= $this->backtrace[$i]['class']
164                    . $this->backtrace[$i]['type'];
165            }
166            $map .= $this->backtrace[$i]['function'] . '()';
167            if (isset($this->backtrace[$i]['file'])) {
168                $map .= ' ' . $this->backtrace[$i]['file']
169                    . ':' . $this->backtrace[$i]['line'];
170            }
171            $map .= "\n";
172        }
173        return $map;
174    }
175}
176