1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\HttpKernel\Profiler;
13
14use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException;
15use Symfony\Component\HttpFoundation\Request;
16use Symfony\Component\HttpFoundation\Response;
17use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
18use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
19use Psr\Log\LoggerInterface;
20
21/**
22 * Profiler.
23 *
24 * @author Fabien Potencier <fabien@symfony.com>
25 */
26class Profiler
27{
28    /**
29     * @var ProfilerStorageInterface
30     */
31    private $storage;
32
33    /**
34     * @var DataCollectorInterface[]
35     */
36    private $collectors = array();
37
38    /**
39     * @var LoggerInterface
40     */
41    private $logger;
42
43    /**
44     * @var bool
45     */
46    private $enabled = true;
47
48    /**
49     * Constructor.
50     *
51     * @param ProfilerStorageInterface $storage A ProfilerStorageInterface instance
52     * @param LoggerInterface          $logger  A LoggerInterface instance
53     */
54    public function __construct(ProfilerStorageInterface $storage, LoggerInterface $logger = null)
55    {
56        $this->storage = $storage;
57        $this->logger = $logger;
58    }
59
60    /**
61     * Disables the profiler.
62     */
63    public function disable()
64    {
65        $this->enabled = false;
66    }
67
68    /**
69     * Enables the profiler.
70     */
71    public function enable()
72    {
73        $this->enabled = true;
74    }
75
76    /**
77     * Loads the Profile for the given Response.
78     *
79     * @param Response $response A Response instance
80     *
81     * @return Profile|false A Profile instance
82     */
83    public function loadProfileFromResponse(Response $response)
84    {
85        if (!$token = $response->headers->get('X-Debug-Token')) {
86            return false;
87        }
88
89        return $this->loadProfile($token);
90    }
91
92    /**
93     * Loads the Profile for the given token.
94     *
95     * @param string $token A token
96     *
97     * @return Profile A Profile instance
98     */
99    public function loadProfile($token)
100    {
101        return $this->storage->read($token);
102    }
103
104    /**
105     * Saves a Profile.
106     *
107     * @param Profile $profile A Profile instance
108     *
109     * @return bool
110     */
111    public function saveProfile(Profile $profile)
112    {
113        // late collect
114        foreach ($profile->getCollectors() as $collector) {
115            if ($collector instanceof LateDataCollectorInterface) {
116                $collector->lateCollect();
117            }
118        }
119
120        if (!($ret = $this->storage->write($profile)) && null !== $this->logger) {
121            $this->logger->warning('Unable to store the profiler information.', array('configured_storage' => get_class($this->storage)));
122        }
123
124        return $ret;
125    }
126
127    /**
128     * Purges all data from the storage.
129     */
130    public function purge()
131    {
132        $this->storage->purge();
133    }
134
135    /**
136     * Exports the current profiler data.
137     *
138     * @param Profile $profile A Profile instance
139     *
140     * @return string The exported data
141     *
142     * @deprecated since Symfony 2.8, to be removed in 3.0.
143     */
144    public function export(Profile $profile)
145    {
146        @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED);
147
148        return base64_encode(serialize($profile));
149    }
150
151    /**
152     * Imports data into the profiler storage.
153     *
154     * @param string $data A data string as exported by the export() method
155     *
156     * @return Profile|false A Profile instance
157     *
158     * @deprecated since Symfony 2.8, to be removed in 3.0.
159     */
160    public function import($data)
161    {
162        @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED);
163
164        $profile = unserialize(base64_decode($data));
165
166        if ($this->storage->read($profile->getToken())) {
167            return false;
168        }
169
170        $this->saveProfile($profile);
171
172        return $profile;
173    }
174
175    /**
176     * Finds profiler tokens for the given criteria.
177     *
178     * @param string $ip     The IP
179     * @param string $url    The URL
180     * @param string $limit  The maximum number of tokens to return
181     * @param string $method The request method
182     * @param string $start  The start date to search from
183     * @param string $end    The end date to search to
184     *
185     * @return array An array of tokens
186     *
187     * @see http://php.net/manual/en/datetime.formats.php for the supported date/time formats
188     */
189    public function find($ip, $url, $limit, $method, $start, $end)
190    {
191        return $this->storage->find($ip, $url, $limit, $method, $this->getTimestamp($start), $this->getTimestamp($end));
192    }
193
194    /**
195     * Collects data for the given Response.
196     *
197     * @param Request    $request   A Request instance
198     * @param Response   $response  A Response instance
199     * @param \Exception $exception An exception instance if the request threw one
200     *
201     * @return Profile|null A Profile instance or null if the profiler is disabled
202     */
203    public function collect(Request $request, Response $response, \Exception $exception = null)
204    {
205        if (false === $this->enabled) {
206            return;
207        }
208
209        $profile = new Profile(substr(hash('sha256', uniqid(mt_rand(), true)), 0, 6));
210        $profile->setTime(time());
211        $profile->setUrl($request->getUri());
212        $profile->setMethod($request->getMethod());
213        $profile->setStatusCode($response->getStatusCode());
214        try {
215            $profile->setIp($request->getClientIp());
216        } catch (ConflictingHeadersException $e) {
217            $profile->setIp('Unknown');
218        }
219
220        $response->headers->set('X-Debug-Token', $profile->getToken());
221
222        foreach ($this->collectors as $collector) {
223            $collector->collect($request, $response, $exception);
224
225            // we need to clone for sub-requests
226            $profile->addCollector(clone $collector);
227        }
228
229        return $profile;
230    }
231
232    /**
233     * Gets the Collectors associated with this profiler.
234     *
235     * @return array An array of collectors
236     */
237    public function all()
238    {
239        return $this->collectors;
240    }
241
242    /**
243     * Sets the Collectors associated with this profiler.
244     *
245     * @param DataCollectorInterface[] $collectors An array of collectors
246     */
247    public function set(array $collectors = array())
248    {
249        $this->collectors = array();
250        foreach ($collectors as $collector) {
251            $this->add($collector);
252        }
253    }
254
255    /**
256     * Adds a Collector.
257     *
258     * @param DataCollectorInterface $collector A DataCollectorInterface instance
259     */
260    public function add(DataCollectorInterface $collector)
261    {
262        $this->collectors[$collector->getName()] = $collector;
263    }
264
265    /**
266     * Returns true if a Collector for the given name exists.
267     *
268     * @param string $name A collector name
269     *
270     * @return bool
271     */
272    public function has($name)
273    {
274        return isset($this->collectors[$name]);
275    }
276
277    /**
278     * Gets a Collector by name.
279     *
280     * @param string $name A collector name
281     *
282     * @return DataCollectorInterface A DataCollectorInterface instance
283     *
284     * @throws \InvalidArgumentException if the collector does not exist
285     */
286    public function get($name)
287    {
288        if (!isset($this->collectors[$name])) {
289            throw new \InvalidArgumentException(sprintf('Collector "%s" does not exist.', $name));
290        }
291
292        return $this->collectors[$name];
293    }
294
295    private function getTimestamp($value)
296    {
297        if (null === $value || '' == $value) {
298            return;
299        }
300
301        try {
302            $value = new \DateTime(is_numeric($value) ? '@'.$value : $value);
303        } catch (\Exception $e) {
304            return;
305        }
306
307        return $value->getTimestamp();
308    }
309}
310