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    private $storage;
29
30    /**
31     * @var DataCollectorInterface[]
32     */
33    private $collectors = array();
34
35    private $logger;
36    private $initiallyEnabled = true;
37    private $enabled = true;
38
39    /**
40     * @param bool $enable  The initial enabled state
41     */
42    public function __construct(ProfilerStorageInterface $storage, LoggerInterface $logger = null, $enable = true)
43    {
44        $this->storage = $storage;
45        $this->logger = $logger;
46        $this->initiallyEnabled = $this->enabled = (bool) $enable;
47    }
48
49    /**
50     * Disables the profiler.
51     */
52    public function disable()
53    {
54        $this->enabled = false;
55    }
56
57    /**
58     * Enables the profiler.
59     */
60    public function enable()
61    {
62        $this->enabled = true;
63    }
64
65    /**
66     * Loads the Profile for the given Response.
67     *
68     * @return Profile|false A Profile instance
69     */
70    public function loadProfileFromResponse(Response $response)
71    {
72        if (!$token = $response->headers->get('X-Debug-Token')) {
73            return false;
74        }
75
76        return $this->loadProfile($token);
77    }
78
79    /**
80     * Loads the Profile for the given token.
81     *
82     * @param string $token A token
83     *
84     * @return Profile A Profile instance
85     */
86    public function loadProfile($token)
87    {
88        return $this->storage->read($token);
89    }
90
91    /**
92     * Saves a Profile.
93     *
94     * @return bool
95     */
96    public function saveProfile(Profile $profile)
97    {
98        // late collect
99        foreach ($profile->getCollectors() as $collector) {
100            if ($collector instanceof LateDataCollectorInterface) {
101                $collector->lateCollect();
102            }
103        }
104
105        if (!($ret = $this->storage->write($profile)) && null !== $this->logger) {
106            $this->logger->warning('Unable to store the profiler information.', array('configured_storage' => get_class($this->storage)));
107        }
108
109        return $ret;
110    }
111
112    /**
113     * Purges all data from the storage.
114     */
115    public function purge()
116    {
117        $this->storage->purge();
118    }
119
120    /**
121     * Finds profiler tokens for the given criteria.
122     *
123     * @param string $ip         The IP
124     * @param string $url        The URL
125     * @param string $limit      The maximum number of tokens to return
126     * @param string $method     The request method
127     * @param string $start      The start date to search from
128     * @param string $end        The end date to search to
129     * @param string $statusCode The request status code
130     *
131     * @return array An array of tokens
132     *
133     * @see http://php.net/manual/en/datetime.formats.php for the supported date/time formats
134     */
135    public function find($ip, $url, $limit, $method, $start, $end, $statusCode = null)
136    {
137        return $this->storage->find($ip, $url, $limit, $method, $this->getTimestamp($start), $this->getTimestamp($end), $statusCode);
138    }
139
140    /**
141     * Collects data for the given Response.
142     *
143     * @return Profile|null A Profile instance or null if the profiler is disabled
144     */
145    public function collect(Request $request, Response $response, \Exception $exception = null)
146    {
147        if (false === $this->enabled) {
148            return;
149        }
150
151        $profile = new Profile(substr(hash('sha256', uniqid(mt_rand(), true)), 0, 6));
152        $profile->setTime(time());
153        $profile->setUrl($request->getUri());
154        $profile->setMethod($request->getMethod());
155        $profile->setStatusCode($response->getStatusCode());
156        try {
157            $profile->setIp($request->getClientIp());
158        } catch (ConflictingHeadersException $e) {
159            $profile->setIp('Unknown');
160        }
161
162        $response->headers->set('X-Debug-Token', $profile->getToken());
163
164        foreach ($this->collectors as $collector) {
165            $collector->collect($request, $response, $exception);
166
167            // we need to clone for sub-requests
168            $profile->addCollector(clone $collector);
169        }
170
171        return $profile;
172    }
173
174    public function reset()
175    {
176        foreach ($this->collectors as $collector) {
177            if (!method_exists($collector, 'reset')) {
178                continue;
179            }
180
181            $collector->reset();
182        }
183        $this->enabled = $this->initiallyEnabled;
184    }
185
186    /**
187     * Gets the Collectors associated with this profiler.
188     *
189     * @return array An array of collectors
190     */
191    public function all()
192    {
193        return $this->collectors;
194    }
195
196    /**
197     * Sets the Collectors associated with this profiler.
198     *
199     * @param DataCollectorInterface[] $collectors An array of collectors
200     */
201    public function set(array $collectors = array())
202    {
203        $this->collectors = array();
204        foreach ($collectors as $collector) {
205            $this->add($collector);
206        }
207    }
208
209    /**
210     * Adds a Collector.
211     */
212    public function add(DataCollectorInterface $collector)
213    {
214        if (!method_exists($collector, 'reset')) {
215            @trigger_error(sprintf('Implementing "%s" without the "reset()" method is deprecated since Symfony 3.4 and will be unsupported in 4.0 for class "%s".', DataCollectorInterface::class, \get_class($collector)), E_USER_DEPRECATED);
216        }
217
218        $this->collectors[$collector->getName()] = $collector;
219    }
220
221    /**
222     * Returns true if a Collector for the given name exists.
223     *
224     * @param string $name A collector name
225     *
226     * @return bool
227     */
228    public function has($name)
229    {
230        return isset($this->collectors[$name]);
231    }
232
233    /**
234     * Gets a Collector by name.
235     *
236     * @param string $name A collector name
237     *
238     * @return DataCollectorInterface A DataCollectorInterface instance
239     *
240     * @throws \InvalidArgumentException if the collector does not exist
241     */
242    public function get($name)
243    {
244        if (!isset($this->collectors[$name])) {
245            throw new \InvalidArgumentException(sprintf('Collector "%s" does not exist.', $name));
246        }
247
248        return $this->collectors[$name];
249    }
250
251    private function getTimestamp($value)
252    {
253        if (null === $value || '' == $value) {
254            return;
255        }
256
257        try {
258            $value = new \DateTime(is_numeric($value) ? '@'.$value : $value);
259        } catch (\Exception $e) {
260            return;
261        }
262
263        return $value->getTimestamp();
264    }
265}
266