4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\HttpKernel\Profiler;
15 * Storage for profiler using files.
17 * @author Alexandre Salomé <alexandre.salome@gmail.com>
19 class FileProfilerStorage implements ProfilerStorageInterface
22 * Folder where profiler data are stored.
29 * Constructs the file storage using a "dsn-like" path.
31 * Example : "file:/path/to/the/storage/folder"
33 * @param string $dsn The DSN
35 * @throws \RuntimeException
37 public function __construct($dsn)
39 if (0 !== strpos($dsn, 'file:')) {
40 throw new \RuntimeException(sprintf('Please check your configuration. You are trying to use FileStorage with an invalid dsn "%s". The expected format is "file:/path/to/the/storage/folder".', $dsn));
42 $this->folder = substr($dsn, 5);
44 if (!is_dir($this->folder) && false === @mkdir($this->folder, 0777, true) && !is_dir($this->folder)) {
45 throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $this->folder));
52 public function find($ip, $url, $limit, $method, $start = null, $end = null, $statusCode = null)
54 $file = $this->getIndexFilename();
56 if (!file_exists($file)) {
60 $file = fopen($file, 'r');
61 fseek($file, 0, SEEK_END);
64 while (\count($result) < $limit && $line = $this->readLineFromFile($file)) {
65 $values = str_getcsv($line);
66 list($csvToken, $csvIp, $csvMethod, $csvUrl, $csvTime, $csvParent, $csvStatusCode) = $values;
67 $csvTime = (int) $csvTime;
69 if ($ip && false === strpos($csvIp, $ip) || $url && false === strpos($csvUrl, $url) || $method && false === strpos($csvMethod, $method) || $statusCode && false === strpos($csvStatusCode, $statusCode)) {
73 if (!empty($start) && $csvTime < $start) {
77 if (!empty($end) && $csvTime > $end) {
81 $result[$csvToken] = array(
84 'method' => $csvMethod,
87 'parent' => $csvParent,
88 'status_code' => $csvStatusCode,
94 return array_values($result);
100 public function purge()
102 $flags = \FilesystemIterator::SKIP_DOTS;
103 $iterator = new \RecursiveDirectoryIterator($this->folder, $flags);
104 $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST);
106 foreach ($iterator as $file) {
107 if (is_file($file)) {
118 public function read($token)
120 if (!$token || !file_exists($file = $this->getFilename($token))) {
124 return $this->createProfileFromData($token, unserialize(file_get_contents($file)));
130 * @throws \RuntimeException
132 public function write(Profile $profile)
134 $file = $this->getFilename($profile->getToken());
136 $profileIndexed = is_file($file);
137 if (!$profileIndexed) {
139 $dir = \dirname($file);
140 if (!is_dir($dir) && false === @mkdir($dir, 0777, true) && !is_dir($dir)) {
141 throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $dir));
145 $profileToken = $profile->getToken();
146 // when there are errors in sub-requests, the parent and/or children tokens
147 // may equal the profile token, resulting in infinite loops
148 $parentToken = $profile->getParentToken() !== $profileToken ? $profile->getParentToken() : null;
149 $childrenToken = array_filter(array_map(function ($p) use ($profileToken) {
150 return $profileToken !== $p->getToken() ? $p->getToken() : null;
151 }, $profile->getChildren()));
155 'token' => $profileToken,
156 'parent' => $parentToken,
157 'children' => $childrenToken,
158 'data' => $profile->getCollectors(),
159 'ip' => $profile->getIp(),
160 'method' => $profile->getMethod(),
161 'url' => $profile->getUrl(),
162 'time' => $profile->getTime(),
163 'status_code' => $profile->getStatusCode(),
166 if (false === file_put_contents($file, serialize($data))) {
170 if (!$profileIndexed) {
172 if (false === $file = fopen($this->getIndexFilename(), 'a')) {
176 fputcsv($file, array(
177 $profile->getToken(),
179 $profile->getMethod(),
182 $profile->getParentToken(),
183 $profile->getStatusCode(),
192 * Gets filename to store data, associated to the token.
194 * @param string $token
196 * @return string The profile filename
198 protected function getFilename($token)
200 // Uses 4 last characters, because first are mostly the same.
201 $folderA = substr($token, -2, 2);
202 $folderB = substr($token, -4, 2);
204 return $this->folder.'/'.$folderA.'/'.$folderB.'/'.$token;
208 * Gets the index filename.
210 * @return string The index filename
212 protected function getIndexFilename()
214 return $this->folder.'/index.csv';
218 * Reads a line in the file, backward.
220 * This function automatically skips the empty lines and do not include the line return in result value.
222 * @param resource $file The file resource, with the pointer placed at the end of the line to read
224 * @return mixed A string representing the line or null if beginning of file is reached
226 protected function readLineFromFile($file)
229 $position = ftell($file);
231 if (0 === $position) {
236 $chunkSize = min($position, 1024);
237 $position -= $chunkSize;
238 fseek($file, $position);
240 if (0 === $chunkSize) {
245 $buffer = fread($file, $chunkSize);
247 if (false === ($upTo = strrpos($buffer, "\n"))) {
248 $line = $buffer.$line;
253 $line = substr($buffer, $upTo + 1).$line;
254 fseek($file, max(0, $position), SEEK_SET);
261 return '' === $line ? null : $line;
264 protected function createProfileFromData($token, $data, $parent = null)
266 $profile = new Profile($token);
267 $profile->setIp($data['ip']);
268 $profile->setMethod($data['method']);
269 $profile->setUrl($data['url']);
270 $profile->setTime($data['time']);
271 $profile->setStatusCode($data['status_code']);
272 $profile->setCollectors($data['data']);
274 if (!$parent && $data['parent']) {
275 $parent = $this->read($data['parent']);
279 $profile->setParent($parent);
282 foreach ($data['children'] as $token) {
283 if (!$token || !file_exists($file = $this->getFilename($token))) {
287 $profile->addChild($this->createProfileFromData($token, unserialize(file_get_contents($file)), $profile));