diff --git a/src/Symfony/Component/Console/Profiler/FileProfilerStorage.php b/src/Symfony/Component/Console/Profiler/FileProfilerStorage.php new file mode 100644 index 0000000000000..bd8761f5dd8a8 --- /dev/null +++ b/src/Symfony/Component/Console/Profiler/FileProfilerStorage.php @@ -0,0 +1,284 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Profiler; + +/** + * Storage for profiler using files. + * + * @author Alexandre Salomé + */ +class FileProfilerStorage implements ProfilerStorageInterface +{ + /** + * Folder where profiler data are stored. + * + * @var string + */ + private $folder; + + /** + * Constructs the file storage using a "dsn-like" path. + * + * Example : "file:/path/to/the/storage/folder" + * + * @param string $dsn The DSN + * + * @throws \RuntimeException + */ + public function __construct($dsn) + { + if (0 !== strpos($dsn, 'file:')) { + 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)); + } + $this->folder = substr($dsn, 5); + + if (!is_dir($this->folder) && false === @mkdir($this->folder, 0777, true) && !is_dir($this->folder)) { + throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $this->folder)); + } + } + + /** + * {@inheritdoc} + */ + public function find($ip, $url, $limit, $method, $start = null, $end = null, $statusCode = null) + { + $file = $this->getIndexFilename(); + + if (!file_exists($file)) { + return array(); + } + + $file = fopen($file, 'r'); + fseek($file, 0, SEEK_END); + + $result = array(); + while (count($result) < $limit && $line = $this->readLineFromFile($file)) { + $values = str_getcsv($line); + list($csvToken, $csvIp, $csvMethod, $csvUrl, $csvTime, $csvParent, $csvStatusCode) = $values; + $csvTime = (int) $csvTime; + + if ($ip && false === strpos($csvIp, $ip) || $url && false === strpos($csvUrl, $url) || $method && false === strpos($csvMethod, $method) || $statusCode && false === strpos($csvStatusCode, $statusCode)) { + continue; + } + + if (!empty($start) && $csvTime < $start) { + continue; + } + + if (!empty($end) && $csvTime > $end) { + continue; + } + + $result[$csvToken] = array( + 'token' => $csvToken, + 'ip' => $csvIp, + 'method' => $csvMethod, + 'url' => $csvUrl, + 'time' => $csvTime, + 'parent' => $csvParent, + 'status_code' => $csvStatusCode, + ); + } + + fclose($file); + + return array_values($result); + } + + /** + * {@inheritdoc} + */ + public function purge() + { + $flags = \FilesystemIterator::SKIP_DOTS; + $iterator = new \RecursiveDirectoryIterator($this->folder, $flags); + $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST); + + foreach ($iterator as $file) { + if (is_file($file)) { + unlink($file); + } else { + rmdir($file); + } + } + } + + /** + * {@inheritdoc} + */ + public function read($token) + { + if (!$token || !file_exists($file = $this->getFilename($token))) { + return; + } + + return $this->createProfileFromData($token, unserialize(file_get_contents($file))); + } + + /** + * {@inheritdoc} + * + * @throws \RuntimeException + */ + public function write(Profile $profile) + { + $file = $this->getFilename($profile->getToken()); + + $profileIndexed = is_file($file); + if (!$profileIndexed) { + // Create directory + $dir = dirname($file); + if (!is_dir($dir) && false === @mkdir($dir, 0777, true) && !is_dir($dir)) { + throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $dir)); + } + } + + // Store profile + $data = array( + 'token' => $profile->getToken(), + 'parent' => $profile->getParentToken(), + 'children' => array_map(function ($p) { return $p->getToken(); }, $profile->getChildren()), + 'data' => $profile->getCollectors(), + 'ip' => $profile->getIp(), + 'method' => $profile->getMethod(), + 'url' => $profile->getUrl(), + 'time' => $profile->getTime(), + 'status_code' => $profile->getStatusCode(), + ); + + if (false === file_put_contents($file, serialize($data))) { + return false; + } + + if (!$profileIndexed) { + // Add to index + if (false === $file = fopen($this->getIndexFilename(), 'a')) { + return false; + } + + fputcsv($file, array( + $profile->getToken(), + $profile->getIp(), + $profile->getMethod(), + $profile->getUrl(), + $profile->getTime(), + $profile->getParentToken(), + $profile->getStatusCode(), + )); + fclose($file); + } + + return true; + } + + /** + * Gets filename to store data, associated to the token. + * + * @param string $token + * + * @return string The profile filename + */ + protected function getFilename($token) + { + // Uses 4 last characters, because first are mostly the same. + $folderA = substr($token, -2, 2); + $folderB = substr($token, -4, 2); + + return $this->folder.'/'.$folderA.'/'.$folderB.'/'.$token; + } + + /** + * Gets the index filename. + * + * @return string The index filename + */ + protected function getIndexFilename() + { + return $this->folder.'/index.csv'; + } + + /** + * Reads a line in the file, backward. + * + * This function automatically skips the empty lines and do not include the line return in result value. + * + * @param resource $file The file resource, with the pointer placed at the end of the line to read + * + * @return mixed A string representing the line or null if beginning of file is reached + */ + protected function readLineFromFile($file) + { + $line = ''; + $position = ftell($file); + + if (0 === $position) { + return; + } + + while (true) { + $chunkSize = min($position, 1024); + $position -= $chunkSize; + fseek($file, $position); + + if (0 === $chunkSize) { + // bof reached + break; + } + + $buffer = fread($file, $chunkSize); + + if (false === ($upTo = strrpos($buffer, "\n"))) { + $line = $buffer.$line; + continue; + } + + $position += $upTo; + $line = substr($buffer, $upTo + 1).$line; + fseek($file, max(0, $position), SEEK_SET); + + if ('' !== $line) { + break; + } + } + + return '' === $line ? null : $line; + } + + protected function createProfileFromData($token, $data, $parent = null) + { + $profile = new Profile($token); + $profile->setIp($data['ip']); + $profile->setMethod($data['method']); + $profile->setUrl($data['url']); + $profile->setTime($data['time']); + $profile->setStatusCode($data['status_code']); + $profile->setCollectors($data['data']); + + if (!$parent && $data['parent']) { + $parent = $this->read($data['parent']); + } + + if ($parent) { + $profile->setParent($parent); + } + + foreach ($data['children'] as $token) { + if (!$token || !file_exists($file = $this->getFilename($token))) { + continue; + } + + $profile->addChild($this->createProfileFromData($token, unserialize(file_get_contents($file)), $profile)); + } + + return $profile; + } +} diff --git a/src/Symfony/Component/Console/Profiler/Profile.php b/src/Symfony/Component/Console/Profiler/Profile.php new file mode 100644 index 0000000000000..1ea045a46f251 --- /dev/null +++ b/src/Symfony/Component/Console/Profiler/Profile.php @@ -0,0 +1,292 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Profiler; + +use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; + +/** + * Profile. + * + * @author Fabien Potencier + */ +class Profile +{ + private $token; + + /** + * @var DataCollectorInterface[] + */ + private $collectors = array(); + + private $ip; + private $method; + private $url; + private $time; + private $statusCode; + + /** + * @var Profile + */ + private $parent; + + /** + * @var Profile[] + */ + private $children = array(); + + /** + * Constructor. + * + * @param string $token The token + */ + public function __construct($token) + { + $this->token = $token; + } + + /** + * Sets the token. + * + * @param string $token The token + */ + public function setToken($token) + { + $this->token = $token; + } + + /** + * Gets the token. + * + * @return string The token + */ + public function getToken() + { + return $this->token; + } + + /** + * Sets the parent token. + * + * @param Profile $parent The parent Profile + */ + public function setParent(Profile $parent) + { + $this->parent = $parent; + } + + /** + * Returns the parent profile. + * + * @return Profile The parent profile + */ + public function getParent() + { + return $this->parent; + } + + /** + * Returns the parent token. + * + * @return null|string The parent token + */ + public function getParentToken() + { + return $this->parent ? $this->parent->getToken() : null; + } + + /** + * Returns the IP. + * + * @return string The IP + */ + public function getIp() + { + return $this->ip; + } + + /** + * Sets the IP. + * + * @param string $ip + */ + public function setIp($ip) + { + $this->ip = $ip; + } + + /** + * Returns the request method. + * + * @return string The request method + */ + public function getMethod() + { + return $this->method; + } + + public function setMethod($method) + { + $this->method = $method; + } + + /** + * Returns the URL. + * + * @return string The URL + */ + public function getUrl() + { + return $this->url; + } + + public function setUrl($url) + { + $this->url = $url; + } + + /** + * Returns the time. + * + * @return string The time + */ + public function getTime() + { + if (null === $this->time) { + return 0; + } + + return $this->time; + } + + public function setTime($time) + { + $this->time = $time; + } + + /** + * @param int $statusCode + */ + public function setStatusCode($statusCode) + { + $this->statusCode = $statusCode; + } + + /** + * @return int + */ + public function getStatusCode() + { + return $this->statusCode; + } + + /** + * Finds children profilers. + * + * @return Profile[] An array of Profile + */ + public function getChildren() + { + return $this->children; + } + + /** + * Sets children profiler. + * + * @param Profile[] $children An array of Profile + */ + public function setChildren(array $children) + { + $this->children = array(); + foreach ($children as $child) { + $this->addChild($child); + } + } + + /** + * Adds the child token. + * + * @param Profile $child The child Profile + */ + public function addChild(Profile $child) + { + $this->children[] = $child; + $child->setParent($this); + } + + /** + * Gets a Collector by name. + * + * @param string $name A collector name + * + * @return DataCollectorInterface A DataCollectorInterface instance + * + * @throws \InvalidArgumentException if the collector does not exist + */ + public function getCollector($name) + { + if (!isset($this->collectors[$name])) { + throw new \InvalidArgumentException(sprintf('Collector "%s" does not exist.', $name)); + } + + return $this->collectors[$name]; + } + + /** + * Gets the Collectors associated with this profile. + * + * @return DataCollectorInterface[] + */ + public function getCollectors() + { + return $this->collectors; + } + + /** + * Sets the Collectors associated with this profile. + * + * @param DataCollectorInterface[] $collectors + */ + public function setCollectors(array $collectors) + { + $this->collectors = array(); + foreach ($collectors as $collector) { + $this->addCollector($collector); + } + } + + /** + * Adds a Collector. + * + * @param DataCollectorInterface $collector A DataCollectorInterface instance + */ + public function addCollector(DataCollectorInterface $collector) + { + $this->collectors[$collector->getName()] = $collector; + } + + /** + * Returns true if a Collector for the given name exists. + * + * @param string $name A collector name + * + * @return bool + */ + public function hasCollector($name) + { + return isset($this->collectors[$name]); + } + + public function __sleep() + { + return array('token', 'parent', 'children', 'collectors', 'ip', 'method', 'url', 'time', 'statusCode'); + } +} diff --git a/src/Symfony/Component/Console/Profiler/Profiler.php b/src/Symfony/Component/Console/Profiler/Profiler.php new file mode 100644 index 0000000000000..f31e243770cbf --- /dev/null +++ b/src/Symfony/Component/Console/Profiler/Profiler.php @@ -0,0 +1,270 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Profiler; + +use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; +use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; +use Psr\Log\LoggerInterface; + +/** + * Profiler. + * + * @author Fabien Potencier + */ +class Profiler +{ + /** + * @var ProfilerStorageInterface + */ + private $storage; + + /** + * @var DataCollectorInterface[] + */ + private $collectors = array(); + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var bool + */ + private $enabled = true; + + /** + * Constructor. + * + * @param ProfilerStorageInterface $storage A ProfilerStorageInterface instance + * @param LoggerInterface $logger A LoggerInterface instance + */ + public function __construct(ProfilerStorageInterface $storage, LoggerInterface $logger = null) + { + $this->storage = $storage; + $this->logger = $logger; + } + + /** + * Disables the profiler. + */ + public function disable() + { + $this->enabled = false; + } + + /** + * Enables the profiler. + */ + public function enable() + { + $this->enabled = true; + } + + /** + * Loads the Profile for the given Response. + * + * @param Response $response A Response instance + * + * @return Profile|false A Profile instance + */ + public function loadProfileFromResponse(Response $response) + { + if (!$token = $response->headers->get('X-Debug-Token')) { + return false; + } + + return $this->loadProfile($token); + } + + /** + * Loads the Profile for the given token. + * + * @param string $token A token + * + * @return Profile A Profile instance + */ + public function loadProfile($token) + { + return $this->storage->read($token); + } + + /** + * Saves a Profile. + * + * @param Profile $profile A Profile instance + * + * @return bool + */ + public function saveProfile(Profile $profile) + { + // late collect + foreach ($profile->getCollectors() as $collector) { + if ($collector instanceof LateDataCollectorInterface) { + $collector->lateCollect(); + } + } + + if (!($ret = $this->storage->write($profile)) && null !== $this->logger) { + $this->logger->warning('Unable to store the profiler information.', array('configured_storage' => get_class($this->storage))); + } + + return $ret; + } + + /** + * Purges all data from the storage. + */ + public function purge() + { + $this->storage->purge(); + } + + /** + * Finds profiler tokens for the given criteria. + * + * @param string $ip The IP + * @param string $url The URL + * @param string $limit The maximum number of tokens to return + * @param string $method The request method + * @param string $start The start date to search from + * @param string $end The end date to search to + * @param string $statusCode The request status code + * + * @return array An array of tokens + * + * @see http://php.net/manual/en/datetime.formats.php for the supported date/time formats + */ + public function find($ip, $url, $limit, $method, $start, $end, $statusCode = null) + { + return $this->storage->find($ip, $url, $limit, $method, $this->getTimestamp($start), $this->getTimestamp($end), $statusCode); + } + + /** + * Collects data for the given Response. + * + * @param Request $request A Request instance + * @param Response $response A Response instance + * @param \Exception $exception An exception instance if the request threw one + * + * @return Profile|null A Profile instance or null if the profiler is disabled + */ + public function collect(Request $request, Response $response, \Exception $exception = null) + { + if (false === $this->enabled) { + return; + } + + $profile = new Profile(substr(hash('sha256', uniqid(mt_rand(), true)), 0, 6)); + $profile->setTime(time()); + $profile->setUrl($request->getUri()); + $profile->setMethod($request->getMethod()); + $profile->setStatusCode($response->getStatusCode()); + try { + $profile->setIp($request->getClientIp()); + } catch (ConflictingHeadersException $e) { + $profile->setIp('Unknown'); + } + + $response->headers->set('X-Debug-Token', $profile->getToken()); + + foreach ($this->collectors as $collector) { + $collector->collect($request, $response, $exception); + + // we need to clone for sub-requests + $profile->addCollector(clone $collector); + } + + return $profile; + } + + /** + * Gets the Collectors associated with this profiler. + * + * @return array An array of collectors + */ + public function all() + { + return $this->collectors; + } + + /** + * Sets the Collectors associated with this profiler. + * + * @param DataCollectorInterface[] $collectors An array of collectors + */ + public function set(array $collectors = array()) + { + $this->collectors = array(); + foreach ($collectors as $collector) { + $this->add($collector); + } + } + + /** + * Adds a Collector. + * + * @param DataCollectorInterface $collector A DataCollectorInterface instance + */ + public function add(DataCollectorInterface $collector) + { + $this->collectors[$collector->getName()] = $collector; + } + + /** + * Returns true if a Collector for the given name exists. + * + * @param string $name A collector name + * + * @return bool + */ + public function has($name) + { + return isset($this->collectors[$name]); + } + + /** + * Gets a Collector by name. + * + * @param string $name A collector name + * + * @return DataCollectorInterface A DataCollectorInterface instance + * + * @throws \InvalidArgumentException if the collector does not exist + */ + public function get($name) + { + if (!isset($this->collectors[$name])) { + throw new \InvalidArgumentException(sprintf('Collector "%s" does not exist.', $name)); + } + + return $this->collectors[$name]; + } + + private function getTimestamp($value) + { + if (null === $value || '' == $value) { + return; + } + + try { + $value = new \DateTime(is_numeric($value) ? '@'.$value : $value); + } catch (\Exception $e) { + return; + } + + return $value->getTimestamp(); + } +} diff --git a/src/Symfony/Component/Console/Profiler/ProfilerStorageInterface.php b/src/Symfony/Component/Console/Profiler/ProfilerStorageInterface.php new file mode 100644 index 0000000000000..ea72af2314f6f --- /dev/null +++ b/src/Symfony/Component/Console/Profiler/ProfilerStorageInterface.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Profiler; + +/** + * ProfilerStorageInterface. + * + * @author Fabien Potencier + */ +interface ProfilerStorageInterface +{ + /** + * Finds profiler tokens for the given criteria. + * + * @param string $ip The IP + * @param string $url The URL + * @param string $limit The maximum number of tokens to return + * @param string $method The request method + * @param int|null $start The start date to search from + * @param int|null $end The end date to search to + * + * @return array An array of tokens + */ + public function find($ip, $url, $limit, $method, $start = null, $end = null); + + /** + * Reads data associated with the given token. + * + * The method returns false if the token does not exist in the storage. + * + * @param string $token A token + * + * @return Profile The profile associated with token + */ + public function read($token); + + /** + * Saves a Profile. + * + * @param Profile $profile A Profile instance + * + * @return bool Write operation successful + */ + public function write(Profile $profile); + + /** + * Purges all data from the database. + */ + public function purge(); +} diff --git a/src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php b/src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php index 2b630d8a72276..195e45822ef5a 100644 --- a/src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php +++ b/src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php @@ -181,7 +181,7 @@ public function removeCookie($name, $path = '/', $domain = null) * * @param string $format * - * @return array + * @return array|Cookie[]|Cookie[][][] * * @throws \InvalidArgumentException When the $format is invalid */ diff --git a/src/Symfony/Component/Profiler/Context/ConsoleCommandContext.php b/src/Symfony/Component/Profiler/Context/ConsoleCommandContext.php new file mode 100644 index 0000000000000..b76863170183d --- /dev/null +++ b/src/Symfony/Component/Profiler/Context/ConsoleCommandContext.php @@ -0,0 +1,58 @@ +exception = $exception; + $this->exitCode = $exitCode; + $this->command = $command; + } + + public function getException() + { + return $this->exception; + } + + public function getName() + { + return $this->command->getName(); + } + + public function getStatusCode() + { + return $this->exitCode; + } + + /** + * @return string + */ + public function getType() + { + return Types::COMMAND; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Profiler/Context/ContextInterface.php b/src/Symfony/Component/Profiler/Context/ContextInterface.php new file mode 100644 index 0000000000000..4517c993cf11b --- /dev/null +++ b/src/Symfony/Component/Profiler/Context/ContextInterface.php @@ -0,0 +1,32 @@ +exception = $exception; + $this->request = $request; + $this->response = $response; + } + + public function getException() + { + return $this->exception; + } + + public function getName() + { + return $this->request->getUri(); + } + + public function getStatusCode() + { + return $this->response->getStatusCode(); + } + + /** + * @return Request + */ + public function getRequest() + { + return $this->request; + } + + /** + * @return Response + */ + public function getResponse() + { + return $this->response; + } + + /** + * @return string + */ + public function getType() + { + return Types::REQUEST; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Profiler/Context/Types.php b/src/Symfony/Component/Profiler/Context/Types.php new file mode 100644 index 0000000000000..540097c66553f --- /dev/null +++ b/src/Symfony/Component/Profiler/Context/Types.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\DataCollector; + +use Symfony\Component\Profiler\Context\ContextInterface; +use Symfony\Component\Profiler\Context\RequestContext; +use Symfony\Component\Profiler\Profile; + +/** + * AjaxDataCollector. + * + * @author Bart van den Burg + */ +class AjaxDataCollector extends DataCollector +{ + public function collectData(ContextInterface $context, Profile $profile) + { + return $context instanceof RequestContext; + } + + public function getName() + { + return 'ajax'; + } +} diff --git a/src/Symfony/Component/Profiler/DataCollector/ConfigDataCollector.php b/src/Symfony/Component/Profiler/DataCollector/ConfigDataCollector.php new file mode 100644 index 0000000000000..077ae2d8d607e --- /dev/null +++ b/src/Symfony/Component/Profiler/DataCollector/ConfigDataCollector.php @@ -0,0 +1,293 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\DataCollector; + +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\Profiler\Context\ContextInterface; +use Symfony\Component\Profiler\Profile; + +/** + * ConfigDataCollector. + * + * @author Fabien Potencier + */ +class ConfigDataCollector extends DataCollector +{ + /** + * @var KernelInterface + */ + private $kernel; + private $name; + private $version; + private $cacheVersionInfo = true; + + /** + * Constructor. + * + * @param string $name The name of the application using the web profiler + * @param string $version The version of the application using the web profiler + */ + public function __construct($name = null, $version = null) + { + $this->name = $name; + $this->version = $version; + } + + /** + * Sets the Kernel associated with this Request. + * + * @param KernelInterface $kernel A KernelInterface instance + */ + public function setKernel(KernelInterface $kernel = null) + { + $this->kernel = $kernel; + } + + /** + * {@inheritdoc} + */ + public function collectData(ContextInterface $context, Profile $profile) + { + $this->data = array( + 'app_name' => $this->name, + 'app_version' => $this->version, + 'token' => $profile->getToken(), + 'symfony_version' => Kernel::VERSION, + 'symfony_state' => 'unknown', + 'name' => isset($this->kernel) ? $this->kernel->getName() : 'n/a', + 'env' => isset($this->kernel) ? $this->kernel->getEnvironment() : 'n/a', + 'debug' => isset($this->kernel) ? $this->kernel->isDebug() : 'n/a', + 'php_version' => PHP_VERSION, + 'xdebug_enabled' => extension_loaded('xdebug'), + 'eaccel_enabled' => extension_loaded('eaccelerator') && ini_get('eaccelerator.enable'), + 'apc_enabled' => extension_loaded('apc') && ini_get('apc.enabled'), + 'xcache_enabled' => extension_loaded('xcache') && ini_get('xcache.cacher'), + 'wincache_enabled' => extension_loaded('wincache') && ini_get('wincache.ocenabled'), + 'zend_opcache_enabled' => extension_loaded('Zend OPcache') && ini_get('opcache.enable'), + 'bundles' => array(), + 'sapi_name' => PHP_SAPI, + ); + + if (isset($this->kernel)) { + foreach ($this->kernel->getBundles() as $name => $bundle) { + $this->data['bundles'][$name] = $bundle->getPath(); + } + + $this->data['symfony_state'] = $this->determineSymfonyState(); + } + + return true; + } + + public function getApplicationName() + { + return $this->data['app_name']; + } + + public function getApplicationVersion() + { + return $this->data['app_version']; + } + + /** + * Gets the token. + * + * @return string The token + */ + public function getToken() + { + return $this->data['token']; + } + + /** + * Gets the Symfony version. + * + * @return string The Symfony version + */ + public function getSymfonyVersion() + { + return $this->data['symfony_version']; + } + + /** + * Returns the state of the current Symfony release. + * + * @return string One of: unknown, dev, stable, eom, eol + */ + public function getSymfonyState() + { + return $this->data['symfony_state']; + } + + public function setCacheVersionInfo($cacheVersionInfo) + { + $this->cacheVersionInfo = $cacheVersionInfo; + } + + /** + * Gets the PHP version. + * + * @return string The PHP version + */ + public function getPhpVersion() + { + return $this->data['php_version']; + } + + /** + * Gets the application name. + * + * @return string The application name + */ + public function getAppName() + { + return $this->data['name']; + } + + /** + * Gets the environment. + * + * @return string The environment + */ + public function getEnv() + { + return $this->data['env']; + } + + /** + * Returns true if the debug is enabled. + * + * @return bool true if debug is enabled, false otherwise + */ + public function isDebug() + { + return $this->data['debug']; + } + + /** + * Returns true if the XDebug is enabled. + * + * @return bool true if XDebug is enabled, false otherwise + */ + public function hasXDebug() + { + return $this->data['xdebug_enabled']; + } + + /** + * Returns true if EAccelerator is enabled. + * + * @return bool true if EAccelerator is enabled, false otherwise + */ + public function hasEAccelerator() + { + return $this->data['eaccel_enabled']; + } + + /** + * Returns true if APC is enabled. + * + * @return bool true if APC is enabled, false otherwise + */ + public function hasApc() + { + return $this->data['apc_enabled']; + } + + /** + * Returns true if Zend OPcache is enabled. + * + * @return bool true if Zend OPcache is enabled, false otherwise + */ + public function hasZendOpcache() + { + return $this->data['zend_opcache_enabled']; + } + + /** + * Returns true if XCache is enabled. + * + * @return bool true if XCache is enabled, false otherwise + */ + public function hasXCache() + { + return $this->data['xcache_enabled']; + } + + /** + * Returns true if WinCache is enabled. + * + * @return bool true if WinCache is enabled, false otherwise + */ + public function hasWinCache() + { + return $this->data['wincache_enabled']; + } + + /** + * Returns true if any accelerator is enabled. + * + * @return bool true if any accelerator is enabled, false otherwise + */ + public function hasAccelerator() + { + return $this->hasApc() || $this->hasZendOpcache() || $this->hasEAccelerator() || $this->hasXCache() || $this->hasWinCache(); + } + + public function getBundles() + { + return $this->data['bundles']; + } + + /** + * Gets the PHP SAPI name. + * + * @return string The environment + */ + public function getSapiName() + { + return $this->data['sapi_name']; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'config'; + } + + /** + * Tries to retrieve information about the current Symfony version. + * + * @return string One of: dev, stable, eom, eol + */ + private function determineSymfonyState() + { + $now = new \DateTime(); + $eom = \DateTime::createFromFormat('m/Y', Kernel::END_OF_MAINTENANCE)->modify('last day of this month'); + $eol = \DateTime::createFromFormat('m/Y', Kernel::END_OF_LIFE)->modify('last day of this month'); + + if ($now > $eol) { + $versionState = 'eol'; + } elseif ($now > $eom) { + $versionState = 'eom'; + } elseif ('' !== Kernel::EXTRA_VERSION) { + $versionState = 'dev'; + } else { + $versionState = 'stable'; + } + + return $versionState; + } +} diff --git a/src/Symfony/Component/Profiler/DataCollector/DataCollector.php b/src/Symfony/Component/Profiler/DataCollector/DataCollector.php new file mode 100644 index 0000000000000..47c8c6e8342b9 --- /dev/null +++ b/src/Symfony/Component/Profiler/DataCollector/DataCollector.php @@ -0,0 +1,102 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\DataCollector; + +use Symfony\Component\VarDumper\Caster\ClassStub; +use Symfony\Component\VarDumper\Caster\LinkStub; +use Symfony\Component\VarDumper\Caster\StubCaster; +use Symfony\Component\VarDumper\Cloner\ClonerInterface; +use Symfony\Component\VarDumper\Cloner\Data; +use Symfony\Component\VarDumper\Cloner\Stub; +use Symfony\Component\VarDumper\Cloner\VarCloner; + +/** + * DataCollector. + * + * Children of this class must store the collected data in the data property. + * + * @author Fabien Potencier + * @author Bernhard Schussek + */ +abstract class DataCollector implements DataCollectorInterface, \Serializable +{ + protected $data = array(); + + /** + * @var ClonerInterface + */ + private $cloner; + + public function serialize() + { + return serialize($this->data); + } + + public function unserialize($data) + { + $this->data = unserialize($data); + } + + /** + * Converts the variable into a serializable Data instance. + * + * This array can be displayed in the template using + * the VarDumper component. + * + * @param mixed $var + * + * @return Data + */ + protected function cloneVar($var) + { + if (null === $this->cloner) { + $this->cloner = new VarCloner(); + $this->cloner->setMaxItems(250); + $this->cloner->addCasters(array( + Stub::class => function (Stub $v, array $a, Stub $s, $isNested) { + return $isNested ? $a : StubCaster::castStub($v, $a, $s, true); + }, + )); + } + + return $this->cloner->cloneVar($this->decorateVar($var)); + } + + private function decorateVar($var) + { + if (is_array($var)) { + if (isset($var[0], $var[1]) && is_callable($var)) { + return ClassStub::wrapCallable($var); + } + foreach ($var as $k => $v) { + if ($v !== $d = $this->decorateVar($v)) { + $var[$k] = $d; + } + } + + return $var; + } + if (is_string($var)) { + if (false !== strpos($var, '\\')) { + $c = (false !== $i = strpos($var, '::')) ? substr($var, 0, $i) : $var; + if (class_exists($c, false) || interface_exists($c, false) || trait_exists($c, false)) { + return new ClassStub($var); + } + } + if (false !== strpos($var, DIRECTORY_SEPARATOR) && file_exists($var)) { + return new LinkStub($var); + } + } + + return $var; + } +} diff --git a/src/Symfony/Component/Profiler/DataCollector/DataCollectorInterface.php b/src/Symfony/Component/Profiler/DataCollector/DataCollectorInterface.php new file mode 100644 index 0000000000000..e317edd7ae90a --- /dev/null +++ b/src/Symfony/Component/Profiler/DataCollector/DataCollectorInterface.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\DataCollector; + +use Symfony\Component\Profiler\Context\ContextInterface; +use Symfony\Component\Profiler\Profile; + +/** + * DataCollectorInterface. + * + * @author Fabien Potencier + */ +interface DataCollectorInterface +{ + /** + * Collects data for the given Request and Response. + * + * @param ContextInterface $context + * @param Profile|Profile $profile + * @return + */ + public function collectData(ContextInterface $context, Profile $profile); + + /** + * Returns the name of the collector. + * + * @return string The collector name + */ + public function getName(); +} diff --git a/src/Symfony/Component/Profiler/DataCollector/DumpDataCollector.php b/src/Symfony/Component/Profiler/DataCollector/DumpDataCollector.php new file mode 100644 index 0000000000000..b9d6b553780bf --- /dev/null +++ b/src/Symfony/Component/Profiler/DataCollector/DumpDataCollector.php @@ -0,0 +1,325 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\DataCollector; + +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Profiler\Context\ContextInterface; +use Symfony\Component\Profiler\Context\RequestContext; +use Symfony\Component\Profiler\Profile; +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Component\VarDumper\Cloner\Data; +use Symfony\Component\VarDumper\Cloner\VarCloner; +use Symfony\Component\VarDumper\Dumper\CliDumper; +use Symfony\Component\VarDumper\Dumper\HtmlDumper; +use Symfony\Component\VarDumper\Dumper\DataDumperInterface; + +/** + * @author Nicolas Grekas + */ +class DumpDataCollector extends DataCollector implements DataDumperInterface +{ + private $stopwatch; + private $fileLinkFormat; + private $dataCount = 0; + private $isCollected = true; + private $clonesCount = 0; + private $clonesIndex = 0; + private $rootRefs; + private $charset; + private $requestStack; + private $dumper; + private $dumperIsInjected; + + public function __construct(Stopwatch $stopwatch = null, $fileLinkFormat = null, $charset = null, RequestStack $requestStack = null, DataDumperInterface $dumper = null) + { + $fileLinkFormat = $fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); + if ($fileLinkFormat && !is_array($fileLinkFormat)) { + $i = max(strpos($fileLinkFormat, '%f'), strpos($fileLinkFormat, '%l')); + $i = strpos($fileLinkFormat, '#"', $i) ?: strlen($fileLinkFormat); + $fileLinkFormat = array(substr($fileLinkFormat, 0, $i), substr($fileLinkFormat, $i + 1)); + $fileLinkFormat[1] = @json_decode('{'.$fileLinkFormat[1].'}', true) ?: array(); + } + $this->stopwatch = $stopwatch; + $this->fileLinkFormat = $fileLinkFormat; + $this->charset = $charset ?: ini_get('php.output_encoding') ?: ini_get('default_charset') ?: 'UTF-8'; + $this->requestStack = $requestStack; + $this->dumper = $dumper; + $this->dumperIsInjected = null !== $dumper; + + // All clones share these properties by reference: + $this->rootRefs = array( + &$this->data, + &$this->dataCount, + &$this->isCollected, + &$this->clonesCount, + ); + } + + public function __clone() + { + $this->clonesIndex = ++$this->clonesCount; + } + + public function dump(Data $data) + { + if ($this->stopwatch) { + $this->stopwatch->start('dump'); + } + if ($this->isCollected) { + $this->isCollected = false; + } + + $trace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, 7); + + $file = $trace[0]['file']; + $line = $trace[0]['line']; + $name = false; + $fileExcerpt = false; + + for ($i = 1; $i < 7; ++$i) { + if (isset($trace[$i]['class'], $trace[$i]['function']) + && 'dump' === $trace[$i]['function'] + && 'Symfony\Component\VarDumper\VarDumper' === $trace[$i]['class'] + ) { + $file = $trace[$i]['file']; + $line = $trace[$i]['line']; + + while (++$i < 7) { + if (isset($trace[$i]['function'], $trace[$i]['file']) && empty($trace[$i]['class']) && 0 !== strpos($trace[$i]['function'], 'call_user_func')) { + $file = $trace[$i]['file']; + $line = $trace[$i]['line']; + + break; + } elseif (isset($trace[$i]['object']) && $trace[$i]['object'] instanceof \Twig_Template) { + $info = $trace[$i]['object']; + $name = $info->getTemplateName(); + $src = method_exists($info, 'getSource') ? $info->getSource() : $info->getEnvironment()->getLoader()->getSource($name); + $info = $info->getDebugInfo(); + if (null !== $src && isset($info[$trace[$i - 1]['line']])) { + $file = false; + $line = $info[$trace[$i - 1]['line']]; + $src = explode("\n", $src); + $fileExcerpt = array(); + + for ($i = max($line - 3, 1), $max = min($line + 3, count($src)); $i <= $max; ++$i) { + $fileExcerpt[] = ''.$this->htmlEncode($src[$i - 1]).''; + } + + $fileExcerpt = '
    '.implode("\n", $fileExcerpt).'
'; + } + break; + } + } + break; + } + } + + if (false === $name) { + $name = str_replace('\\', '/', $file); + $name = substr($name, strrpos($name, '/') + 1); + } + + if ($this->dumper) { + $this->doDump($data, $name, $file, $line); + } + + $this->data[] = compact('data', 'name', 'file', 'line', 'fileExcerpt'); + ++$this->dataCount; + + if ($this->stopwatch) { + $this->stopwatch->stop('dump'); + } + } + + public function collectData(ContextInterface $context, Profile $profile) + { + // Sub-requests and programmatic calls stay in the collected profile. + if ($this->dumper) { + return true; + } + + if ($context instanceof RequestContext) { + $request = $context->getRequest(); + + if (($this->requestStack && $this->requestStack->getMasterRequest() !== $request) || $request->isXmlHttpRequest() || $request->headers->has('Origin')) { + return true; + } + } + + // In all other conditions that remove the web debug toolbar, dumps are written on the output. + if (!$context instanceof RequestContext + || !$this->requestStack + || !$context->getResponse()->headers->has('X-Debug-Token') + || $context->getResponse()->isRedirection() + || ($context->getResponse()->headers->has('Content-Type') && false === strpos($context->getResponse()->headers->get('Content-Type'), 'html')) + || 'html' !== $context->getRequest()->getRequestFormat() + || false === strripos($context->getResponse()->getContent(), '') + ) { + if ($context instanceof RequestContext && $context->getResponse()->headers->has('Content-Type') && false !== strpos($context->getResponse()->headers->get('Content-Type'), 'html')) { + $this->dumper = new HtmlDumper('php://output', $this->charset); + $this->dumper->setDisplayOptions(array('fileLinkFormat' => $this->fileLinkFormat)); + } else { + $this->dumper = new CliDumper('php://output', $this->charset); + } + + foreach ($this->data as $dump) { + $this->doDump($dump['data'], $dump['name'], $dump['file'], $dump['line']); + } + } + + return true; + } + + public function serialize() + { + if ($this->clonesCount !== $this->clonesIndex) { + return 'a:0:{}'; + } + + $this->data[] = $this->fileLinkFormat; + $this->data[] = $this->charset; + $ser = serialize($this->data); + $this->data = array(); + $this->dataCount = 0; + $this->isCollected = true; + if (!$this->dumperIsInjected) { + $this->dumper = null; + } + + return $ser; + } + + public function unserialize($data) + { + parent::unserialize($data); + $charset = array_pop($this->data); + $fileLinkFormat = array_pop($this->data); + $this->dataCount = count($this->data); + self::__construct($this->stopwatch, $fileLinkFormat, $charset); + } + + public function getDumpsCount() + { + return $this->dataCount; + } + + public function getDumps($format, $maxDepthLimit = -1, $maxItemsPerDepth = -1) + { + $data = fopen('php://memory', 'r+b'); + + if ('html' === $format) { + $dumper = new HtmlDumper($data, $this->charset); + $dumper->setDisplayOptions(array('fileLinkFormat' => $this->fileLinkFormat)); + } else { + throw new \InvalidArgumentException(sprintf('Invalid dump format: %s', $format)); + } + $dumps = array(); + + foreach ($this->data as $dump) { + $dumper->dump($dump['data']->withMaxDepth($maxDepthLimit)->withMaxItemsPerDepth($maxItemsPerDepth)); + $dump['data'] = stream_get_contents($data, -1, 0); + ftruncate($data, 0); + rewind($data); + $dumps[] = $dump; + } + + return $dumps; + } + + public function getName() + { + return 'dump'; + } + + public function __destruct() + { + if (0 === $this->clonesCount-- && !$this->isCollected && $this->data) { + $this->clonesCount = 0; + $this->isCollected = true; + + $h = headers_list(); + $i = count($h); + array_unshift($h, 'Content-Type: '.ini_get('default_mimetype')); + while (0 !== stripos($h[$i], 'Content-Type:')) { + --$i; + } + + if ('cli' !== PHP_SAPI && stripos($h[$i], 'html')) { + $this->dumper = new HtmlDumper('php://output', $this->charset); + $this->dumper->setDisplayOptions(array('fileLinkFormat' => $this->fileLinkFormat)); + } else { + $this->dumper = new CliDumper('php://output', $this->charset); + } + + foreach ($this->data as $i => $dump) { + $this->data[$i] = null; + $this->doDump($dump['data'], $dump['name'], $dump['file'], $dump['line']); + } + + $this->data = array(); + $this->dataCount = 0; + } + } + + private function doDump($data, $name, $file, $line) + { + if ($this->dumper instanceof CliDumper) { + $contextDumper = function ($name, $file, $line, $fileLinkFormat) { + if ($this instanceof HtmlDumper) { + if ('' !== $file) { + $s = $this->style('meta', '%s'); + $name = strip_tags($this->style('', $name)); + $file = strip_tags($this->style('', $file)); + if ($fileLinkFormat) { + foreach ($fileLinkFormat[1] as $k => $v) { + if (0 === strpos($file, $k)) { + $file = substr_replace($file, $v, 0, strlen($k)); + break; + } + } + $link = strtr(strip_tags($this->style('', $fileLinkFormat[0])), array('%f' => $file, '%l' => (int) $line)); + $name = sprintf(''.$s.'', $link, $file, $name); + } else { + $name = sprintf(''.$s.'', $file, $name); + } + } else { + $name = $this->style('meta', $name); + } + $this->line = $name.' on line '.$this->style('meta', $line).':'; + } else { + $this->line = $this->style('meta', $name).' on line '.$this->style('meta', $line).':'; + } + $this->dumpLine(0); + }; + $contextDumper = $contextDumper->bindTo($this->dumper, $this->dumper); + $contextDumper($name, $file, $line, $this->fileLinkFormat); + } else { + $cloner = new VarCloner(); + $this->dumper->dump($cloner->cloneVar($name.' on line '.$line.':')); + } + $this->dumper->dump($data); + } + + private function htmlEncode($s) + { + $html = ''; + + $dumper = new HtmlDumper(function ($line) use (&$html) {$html .= $line;}, $this->charset); + $dumper->setDumpHeader(''); + $dumper->setDumpBoundaries('', ''); + + $cloner = new VarCloner(); + $dumper->dump($cloner->cloneVar($s)); + + return substr(strip_tags($html), 1, -1); + } +} diff --git a/src/Symfony/Component/Profiler/DataCollector/EventDataCollector.php b/src/Symfony/Component/Profiler/DataCollector/EventDataCollector.php new file mode 100644 index 0000000000000..49a71aea5023b --- /dev/null +++ b/src/Symfony/Component/Profiler/DataCollector/EventDataCollector.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\DataCollector; + +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcherInterface; +use Symfony\Component\Profiler\Context\ContextInterface; +use Symfony\Component\Profiler\Profile; + +/** + * EventDataCollector. + * + * @author Fabien Potencier + */ +class EventDataCollector extends DataCollector implements LateDataCollectorInterface +{ + protected $dispatcher; + + public function __construct(EventDispatcherInterface $dispatcher = null) + { + $this->dispatcher = $dispatcher; + } + + /** + * {@inheritdoc} + */ + public function collectData(ContextInterface $context, Profile $profile) + { + $this->data = array( + 'called_listeners' => array(), + 'not_called_listeners' => array(), + ); + return true; + } + + public function lateCollect(Profile $profile) + { + if ($this->dispatcher instanceof TraceableEventDispatcherInterface) { + $this->setCalledListeners($this->dispatcher->getCalledListeners()); + $this->setNotCalledListeners($this->dispatcher->getNotCalledListeners()); + } + } + + /** + * Sets the called listeners. + * + * @param array $listeners An array of called listeners + * + * @see TraceableEventDispatcherInterface + */ + public function setCalledListeners(array $listeners) + { + $this->data['called_listeners'] = $listeners; + } + + /** + * Gets the called listeners. + * + * @return array An array of called listeners + * + * @see TraceableEventDispatcherInterface + */ + public function getCalledListeners() + { + return $this->data['called_listeners']; + } + + /** + * Sets the not called listeners. + * + * @param array $listeners An array of not called listeners + * + * @see TraceableEventDispatcherInterface + */ + public function setNotCalledListeners(array $listeners) + { + $this->data['not_called_listeners'] = $listeners; + } + + /** + * Gets the not called listeners. + * + * @return array An array of not called listeners + * + * @see TraceableEventDispatcherInterface + */ + public function getNotCalledListeners() + { + return $this->data['not_called_listeners']; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'events'; + } +} diff --git a/src/Symfony/Component/Profiler/DataCollector/ExceptionDataCollector.php b/src/Symfony/Component/Profiler/DataCollector/ExceptionDataCollector.php new file mode 100644 index 0000000000000..921513a13b134 --- /dev/null +++ b/src/Symfony/Component/Profiler/DataCollector/ExceptionDataCollector.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\DataCollector; + +use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\Profiler\Context\ContextInterface; +use Symfony\Component\Profiler\Profile; + +/** + * ExceptionDataCollector. + * + * @author Fabien Potencier + */ +class ExceptionDataCollector extends DataCollector +{ + /** + * {@inheritdoc} + */ + public function collectData(ContextInterface $context, Profile $profile) + { + if (null !== $context->getException()) { + $this->data = array( + 'exception' => FlattenException::create($context->getException()), + ); + } + + return true; + } + + /** + * Checks if the exception is not null. + * + * @return bool true if the exception is not null, false otherwise + */ + public function hasException() + { + return isset($this->data['exception']); + } + + /** + * Gets the exception. + * + * @return \Exception The exception + */ + public function getException() + { + return $this->data['exception']; + } + + /** + * Gets the exception message. + * + * @return string The exception message + */ + public function getMessage() + { + return $this->data['exception']->getMessage(); + } + + /** + * Gets the exception code. + * + * @return int The exception code + */ + public function getCode() + { + return $this->data['exception']->getCode(); + } + + /** + * Gets the status code. + * + * @return int The status code + */ + public function getStatusCode() + { + return $this->data['exception']->getStatusCode(); + } + + /** + * Gets the exception trace. + * + * @return array The exception trace + */ + public function getTrace() + { + return $this->data['exception']->getTrace(); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'exception'; + } +} diff --git a/src/Symfony/Component/Profiler/DataCollector/LateDataCollectorInterface.php b/src/Symfony/Component/Profiler/DataCollector/LateDataCollectorInterface.php new file mode 100644 index 0000000000000..3ce443e613c3a --- /dev/null +++ b/src/Symfony/Component/Profiler/DataCollector/LateDataCollectorInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\DataCollector; + +use Symfony\Component\Profiler\Profile; + +/** + * LateDataCollectorInterface. + * + * @author Fabien Potencier + */ +interface LateDataCollectorInterface +{ + /** + * Collects data as late as possible. + * @param Profile $profile + * @return + */ + public function lateCollect(Profile $profile); +} diff --git a/src/Symfony/Component/Profiler/DataCollector/LoggerDataCollector.php b/src/Symfony/Component/Profiler/DataCollector/LoggerDataCollector.php new file mode 100644 index 0000000000000..62ad66c7ece59 --- /dev/null +++ b/src/Symfony/Component/Profiler/DataCollector/LoggerDataCollector.php @@ -0,0 +1,184 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\DataCollector; + +use Symfony\Component\Debug\Exception\SilencedErrorContext; +use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; +use Symfony\Component\Profiler\Context\ContextInterface; +use Symfony\Component\Profiler\Profile; + +/** + * LogDataCollector. + * + * @author Fabien Potencier + */ +class LoggerDataCollector extends DataCollector implements LateDataCollectorInterface +{ + private $logger; + + public function __construct($logger = null) + { + if (null !== $logger && $logger instanceof DebugLoggerInterface) { + $this->logger = $logger; + } + } + + /** + * {@inheritdoc} + */ + public function collectData(ContextInterface $context, Profile $profile) + { + return true; + } + + /** + * {@inheritdoc} + */ + public function lateCollect(Profile $profile) + { + if (null !== $this->logger) { + $this->data = $this->computeErrorsCount(); + $this->data['logs'] = $this->sanitizeLogs($this->logger->getLogs()); + } + } + + /** + * Gets the logs. + * + * @return array An array of logs + */ + public function getLogs() + { + return isset($this->data['logs']) ? $this->data['logs'] : array(); + } + + public function getPriorities() + { + return isset($this->data['priorities']) ? $this->data['priorities'] : array(); + } + + public function countErrors() + { + return isset($this->data['error_count']) ? $this->data['error_count'] : 0; + } + + public function countDeprecations() + { + return isset($this->data['deprecation_count']) ? $this->data['deprecation_count'] : 0; + } + + public function countWarnings() + { + return isset($this->data['warning_count']) ? $this->data['warning_count'] : 0; + } + + public function countScreams() + { + return isset($this->data['scream_count']) ? $this->data['scream_count'] : 0; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'logger'; + } + + private function sanitizeLogs($logs) + { + $sanitizedLogs = array(); + + foreach ($logs as $log) { + if (!$this->isSilencedOrDeprecationErrorLog($log)) { + $log['context'] = $log['context'] ? $this->cloneVar($log['context']) : $log['context']; + $sanitizedLogs[] = $log; + + continue; + } + + $exception = $log['context']['exception']; + $errorId = md5("{$exception->getSeverity()}/{$exception->getLine()}/{$exception->getFile()}".($exception instanceof \Exception ? "\0".$exception->getMessage() : ''), true); + + if (isset($sanitizedLogs[$errorId])) { + ++$sanitizedLogs[$errorId]['errorCount']; + } else { + $log['context'] = $log['context'] ? $this->cloneVar($log['context']) : $log['context']; + + $log += array( + 'errorCount' => 1, + 'scream' => $exception instanceof SilencedErrorContext, + ); + + $sanitizedLogs[$errorId] = $log; + } + } + + return array_values($sanitizedLogs); + } + + private function isSilencedOrDeprecationErrorLog(array $log) + { + if (!isset($log['context']['exception'])) { + return false; + } + + $exception = $log['context']['exception']; + + if ($exception instanceof SilencedErrorContext) { + return true; + } + + if ($exception instanceof \ErrorException && in_array($exception->getSeverity(), array(E_DEPRECATED, E_USER_DEPRECATED), true)) { + return true; + } + + return false; + } + + private function computeErrorsCount() + { + $count = array( + 'error_count' => $this->logger->countErrors(), + 'deprecation_count' => 0, + 'warning_count' => 0, + 'scream_count' => 0, + 'priorities' => array(), + ); + + foreach ($this->logger->getLogs() as $log) { + if (isset($count['priorities'][$log['priority']])) { + ++$count['priorities'][$log['priority']]['count']; + } else { + $count['priorities'][$log['priority']] = array( + 'count' => 1, + 'name' => $log['priorityName'], + ); + } + if ('WARNING' === $log['priorityName']) { + ++$count['warning_count']; + } + + if ($this->isSilencedOrDeprecationErrorLog($log)) { + if ($log['context']['exception'] instanceof SilencedErrorContext) { + ++$count['scream_count']; + } else { + ++$count['deprecation_count']; + } + } + } + + ksort($count['priorities']); + + return $count; + } +} diff --git a/src/Symfony/Component/Profiler/DataCollector/MemoryDataCollector.php b/src/Symfony/Component/Profiler/DataCollector/MemoryDataCollector.php new file mode 100644 index 0000000000000..1fefbaf27f62a --- /dev/null +++ b/src/Symfony/Component/Profiler/DataCollector/MemoryDataCollector.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\DataCollector; + +use Symfony\Component\Profiler\Context\ContextInterface; +use Symfony\Component\Profiler\Profile; + +/** + * MemoryDataCollector. + * + * @author Fabien Potencier + */ +class MemoryDataCollector extends DataCollector implements LateDataCollectorInterface +{ + public function __construct() + { + $this->data = array( + 'memory' => 0, + 'memory_limit' => $this->convertToBytes(ini_get('memory_limit')), + ); + } + + /** + * {@inheritdoc} + */ + public function collectData(ContextInterface $context, Profile $profile) + { + $this->updateMemoryUsage(); + return true; + } + + /** + * {@inheritdoc} + */ + public function lateCollect(Profile $profile) + { + $this->updateMemoryUsage(); + } + + /** + * Gets the memory. + * + * @return int The memory + */ + public function getMemory() + { + return $this->data['memory']; + } + + /** + * Gets the PHP memory limit. + * + * @return int The memory limit + */ + public function getMemoryLimit() + { + return $this->data['memory_limit']; + } + + /** + * Updates the memory usage data. + */ + public function updateMemoryUsage() + { + $this->data['memory'] = memory_get_peak_usage(true); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'memory'; + } + + private function convertToBytes($memoryLimit) + { + if ('-1' === $memoryLimit) { + return -1; + } + + $memoryLimit = strtolower($memoryLimit); + $max = strtolower(ltrim($memoryLimit, '+')); + if (0 === strpos($max, '0x')) { + $max = intval($max, 16); + } elseif (0 === strpos($max, '0')) { + $max = intval($max, 8); + } else { + $max = (int) $max; + } + + switch (substr($memoryLimit, -1)) { + case 't': $max *= 1024; + case 'g': $max *= 1024; + case 'm': $max *= 1024; + case 'k': $max *= 1024; + } + + return $max; + } +} diff --git a/src/Symfony/Component/Profiler/DataCollector/RequestDataCollector.php b/src/Symfony/Component/Profiler/DataCollector/RequestDataCollector.php new file mode 100644 index 0000000000000..6d5f7f43eae42 --- /dev/null +++ b/src/Symfony/Component/Profiler/DataCollector/RequestDataCollector.php @@ -0,0 +1,454 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\DataCollector; + +use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException; +use Symfony\Component\HttpFoundation\ParameterBag; +use Symfony\Component\HttpFoundation\HeaderBag; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\Event\FilterControllerEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Profiler\Context\ContextInterface; +use Symfony\Component\Profiler\Profile; +use Symfony\Component\Profiler\Context\RequestContext; +use Symfony\Component\Routing\Route; + +/** + * RequestDataCollector. + * + * @author Fabien Potencier + */ +class RequestDataCollector extends DataCollector implements SummaryCollectorInterface, EventSubscriberInterface +{ + /** @var \SplObjectStorage */ + protected $controllers; + + public function __construct() + { + $this->controllers = new \SplObjectStorage(); + } + + /** + * {@inheritdoc} + */ + public function collectData(ContextInterface $context, Profile $profile) + { + if (!$context instanceof RequestContext) { + return false; + } + + $response = $context->getResponse(); + $request = $context->getRequest(); + $responseHeaders = $response->headers->all(); + $cookies = array(); + foreach ($response->headers->getCookies() as $cookie) { + $cookies[] = $this->getCookieHeader( + $cookie->getName(), + $cookie->getValue(), + $cookie->getExpiresTime(), + $cookie->getPath(), + $cookie->getDomain(), + $cookie->isSecure(), + $cookie->isHttpOnly() + ); + } + if (count($cookies) > 0) { + $responseHeaders['Set-Cookie'] = $cookies; + } + + // attributes are serialized and as they can be anything, they need to be converted to strings. + $attributes = array(); + $route = ''; + foreach ($request->attributes->all() as $key => $value) { + if ('_route' === $key && $value instanceof Route) { + $attributes[$key] = $this->cloneVar($value->getPath()); + } else { + $attributes[$key] = $this->cloneVar($value); + } + + if ('_route' === $key) { + $route = $value instanceof Route ? $value->getPath() : $value; + } + } + + $content = null; + try { + $content = $request->getContent(); + } catch (\LogicException $e) { + // the user already got the request content as a resource + $content = false; + } + + $sessionMetadata = array(); + $sessionAttributes = array(); + $session = null; + $flashes = array(); + if ($request->hasSession()) { + $session = $request->getSession(); + if ($session->isStarted()) { + $sessionMetadata['Created'] = date(DATE_RFC822, $session->getMetadataBag()->getCreated()); + $sessionMetadata['Last used'] = date(DATE_RFC822, $session->getMetadataBag()->getLastUsed()); + $sessionMetadata['Lifetime'] = $session->getMetadataBag()->getLifetime(); + $sessionAttributes = $session->all(); + $flashes = $session->getFlashBag()->peekAll(); + } + } + + $statusCode = $response->getStatusCode(); + + $this->data = array( + 'method' => $request->getMethod(), + 'format' => $request->getRequestFormat(), + 'content' => $content, + 'content_type' => $response->headers->get('Content-Type', 'text/html'), + 'status_text' => isset(Response::$statusTexts[$statusCode]) ? Response::$statusTexts[$statusCode] : '', + 'status_code' => $statusCode, + 'request_query' => array_map(array($this, 'cloneVar'), $request->query->all()), + 'request_request' => array_map(array($this, 'cloneVar'), $request->request->all()), + 'request_headers' => $request->headers->all(), + 'request_server' => $request->server->all(), + 'request_cookies' => $request->cookies->all(), + 'request_attributes' => $attributes, + 'route' => $route, + 'response_headers' => $responseHeaders, + 'session_metadata' => $sessionMetadata, + 'session_attributes' => $sessionAttributes, + 'flashes' => $flashes, + 'path_info' => $request->getPathInfo(), + 'controller' => 'n/a', + 'locale' => $request->getLocale(), + ); + + try { + $this->data['id'] = $request->getClientIp(); + } catch (ConflictingHeadersException $e) { + $this->data['id'] = 'Unknown'; + } + if (isset($this->data['request_headers']['php-auth-pw'])) { + $this->data['request_headers']['php-auth-pw'] = '******'; + } + + if (isset($this->data['request_server']['PHP_AUTH_PW'])) { + $this->data['request_server']['PHP_AUTH_PW'] = '******'; + } + + if (isset($this->data['request_request']['_password'])) { + $this->data['request_request']['_password'] = '******'; + } + + if (isset($this->controllers[$request])) { + $this->data['controller'] = $this->parseController($this->controllers[$request]); + unset($this->controllers[$request]); + } + + if (null !== $session && $session->isStarted()) { + if ($request->attributes->has('_redirected')) { + $this->data['redirect'] = $session->remove('sf_redirect'); + } + + if ($response->isRedirect()) { + $session->set('sf_redirect', array( + 'token' => $response->headers->get('x-debug-token'), + 'route' => $request->attributes->get('_route', 'n/a'), + 'method' => $request->getMethod(), + 'controller' => $this->parseController($request->attributes->get('_controller')), + 'status_code' => $statusCode, + 'status_text' => Response::$statusTexts[(int) $statusCode], + )); + } + } + + return true; + } + + public function getMethod() + { + return $this->data['method']; + } + + public function getPathInfo() + { + return $this->data['path_info']; + } + + public function getRequestRequest() + { + return new ParameterBag($this->data['request_request']); + } + + public function getRequestQuery() + { + return new ParameterBag($this->data['request_query']); + } + + public function getRequestHeaders() + { + return new HeaderBag($this->data['request_headers']); + } + + public function getRequestServer() + { + return new ParameterBag($this->data['request_server']); + } + + public function getRequestCookies() + { + return new ParameterBag($this->data['request_cookies']); + } + + public function getRequestAttributes() + { + return new ParameterBag($this->data['request_attributes']); + } + + public function getResponseHeaders() + { + return new ResponseHeaderBag($this->data['response_headers']); + } + + public function getSessionMetadata() + { + return $this->data['session_metadata']; + } + + public function getSessionAttributes() + { + return $this->data['session_attributes']; + } + + public function getFlashes() + { + return $this->data['flashes']; + } + + public function getContent() + { + return $this->data['content']; + } + + public function getContentType() + { + return $this->data['content_type']; + } + + public function getStatusText() + { + return $this->data['status_text']; + } + + public function getStatusCode() + { + return $this->data['status_code']; + } + + public function getFormat() + { + return $this->data['format']; + } + + public function getLocale() + { + return $this->data['locale']; + } + + /** + * Gets the route name. + * + * The _route request attributes is automatically set by the Router Matcher. + * + * @return string The route + */ + public function getRoute() + { + return $this->data['route']; + } + + public function getIdentifier() + { + return $this->data['route'] ?: (is_array($this->data['controller']) ? $this->data['controller']['class'].'::'.$this->data['controller']['method'].'()' : $this->data['controller']); + } + + /** + * Gets the route parameters. + * + * The _route_params request attributes is automatically set by the RouterListener. + * + * @return array The parameters + */ + public function getRouteParams() + { + return isset($this->data['request_attributes']['_route_params']) ? $this->data['request_attributes']['_route_params'] : $this->cloneVar(array()); + } + + /** + * Gets the parsed controller. + * + * @return array|string The controller as a string or array of data + * with keys 'class', 'method', 'file' and 'line' + */ + public function getController() + { + return $this->data['controller']; + } + + /** + * Gets the previous request attributes. + * + * @return array|bool A legacy array of data from the previous redirection response + * or false otherwise + */ + public function getRedirect() + { + return isset($this->data['redirect']) ? $this->data['redirect'] : false; + } + + public function onKernelController(FilterControllerEvent $event) + { + $this->controllers[$event->getRequest()] = $event->getController(); + } + + public function onKernelResponse(FilterResponseEvent $event) + { + if (!$event->isMasterRequest() || !$event->getRequest()->hasSession() || !$event->getRequest()->getSession()->isStarted()) { + return; + } + + if ($event->getRequest()->getSession()->has('sf_redirect')) { + $event->getRequest()->attributes->set('_redirected', true); + } + } + + public static function getSubscribedEvents() + { + return array( + KernelEvents::CONTROLLER => 'onKernelController', + KernelEvents::RESPONSE => 'onKernelResponse', + ); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'request'; + } + + /** + * Parse a controller. + * + * @param mixed $controller The controller to parse + * + * @return array|string An array of controller data or a simple string + */ + protected function parseController($controller) + { + if (is_string($controller) && false !== strpos($controller, '::')) { + $controller = explode('::', $controller); + } + + if (is_array($controller)) { + try { + $r = new \ReflectionMethod($controller[0], $controller[1]); + + return array( + 'class' => is_object($controller[0]) ? get_class($controller[0]) : $controller[0], + 'method' => $controller[1], + 'file' => $r->getFileName(), + 'line' => $r->getStartLine(), + ); + } catch (\ReflectionException $e) { + if (is_callable($controller)) { + // using __call or __callStatic + return array( + 'class' => is_object($controller[0]) ? get_class($controller[0]) : $controller[0], + 'method' => $controller[1], + 'file' => 'n/a', + 'line' => 'n/a', + ); + } + } + } + + if ($controller instanceof \Closure) { + $r = new \ReflectionFunction($controller); + + return array( + 'class' => $r->getName(), + 'method' => null, + 'file' => $r->getFileName(), + 'line' => $r->getStartLine(), + ); + } + + if (is_object($controller)) { + $r = new \ReflectionClass($controller); + + return array( + 'class' => $r->getName(), + 'method' => null, + 'file' => $r->getFileName(), + 'line' => $r->getStartLine(), + ); + } + + return (string) $controller ?: 'n/a'; + } + + private function getCookieHeader($name, $value, $expires, $path, $domain, $secure, $httponly) + { + $cookie = sprintf('%s=%s', $name, urlencode($value)); + + if (0 !== $expires) { + if (is_numeric($expires)) { + $expires = (int) $expires; + } elseif ($expires instanceof \DateTime) { + $expires = $expires->getTimestamp(); + } else { + $tmp = strtotime($expires); + if (false === $tmp || -1 == $tmp) { + throw new \InvalidArgumentException(sprintf('The "expires" cookie parameter is not valid (%s).', $expires)); + } + $expires = $tmp; + } + + $cookie .= '; expires='.str_replace('+0000', '', \DateTime::createFromFormat('U', $expires, new \DateTimeZone('GMT'))->format('D, d-M-Y H:i:s T')); + } + + if ($domain) { + $cookie .= '; domain='.$domain; + } + + $cookie .= '; path='.$path; + + if ($secure) { + $cookie .= '; secure'; + } + + if ($httponly) { + $cookie .= '; httponly'; + } + + return $cookie; + } + + public function getSummary(Profile $profile) + { + return array( + 'ip' => $this->data['ip'], + 'method' => $this->data['method'] + ); + } +} diff --git a/src/Symfony/Component/Profiler/DataCollector/RouterDataCollector.php b/src/Symfony/Component/Profiler/DataCollector/RouterDataCollector.php new file mode 100644 index 0000000000000..0260bed1fb9b2 --- /dev/null +++ b/src/Symfony/Component/Profiler/DataCollector/RouterDataCollector.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\DataCollector; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpKernel\Event\FilterControllerEvent; +use Symfony\Component\Profiler\Context\ContextInterface; +use Symfony\Component\Profiler\Context\RequestContext; +use Symfony\Component\Profiler\Profile; + +/** + * RouterDataCollector. + * + * @author Fabien Potencier + */ +class RouterDataCollector extends DataCollector +{ + protected $controllers; + + public function __construct() + { + $this->controllers = new \SplObjectStorage(); + + $this->data = array( + 'redirect' => false, + 'url' => null, + 'route' => null, + ); + } + + /** + * {@inheritdoc} + */ + public function collectData(ContextInterface $context, Profile $profile) + { + if (!$context instanceof RequestContext) { + return false; + } + + $request = $context->getRequest(); + $response = $context->getResponse(); + + if ($response instanceof RedirectResponse) { + $this->data['redirect'] = true; + $this->data['url'] = $response->getTargetUrl(); + + if ($this->controllers->contains($request)) { + $this->data['route'] = $this->guessRoute($request, $this->controllers[$request]); + } + } + + unset($this->controllers[$request]); + } + + protected function guessRoute(Request $request, $controller) + { + return 'n/a'; + } + + /** + * Remembers the controller associated to each request. + * + * @param FilterControllerEvent $event The filter controller event + */ + public function onKernelController(FilterControllerEvent $event) + { + $this->controllers[$event->getRequest()] = $event->getController(); + } + + /** + * @return bool Whether this request will result in a redirect + */ + public function getRedirect() + { + return $this->data['redirect']; + } + + /** + * @return string|null The target URL + */ + public function getTargetUrl() + { + return $this->data['url']; + } + + /** + * @return string|null The target route + */ + public function getTargetRoute() + { + return $this->data['route']; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'router'; + } +} diff --git a/src/Symfony/Component/Profiler/DataCollector/TimeDataCollector.php b/src/Symfony/Component/Profiler/DataCollector/TimeDataCollector.php new file mode 100644 index 0000000000000..ed802c45b02ab --- /dev/null +++ b/src/Symfony/Component/Profiler/DataCollector/TimeDataCollector.php @@ -0,0 +1,146 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\DataCollector; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Component\Profiler\Context\ContextInterface; +use Symfony\Component\Profiler\Context\RequestContext; +use Symfony\Component\Profiler\Profile; +use Symfony\Component\Stopwatch\Stopwatch; + +/** + * TimeDataCollector. + * + * @author Fabien Potencier + */ +class TimeDataCollector extends DataCollector implements LateDataCollectorInterface +{ + protected $kernel; + protected $stopwatch; + + public function __construct(KernelInterface $kernel = null, Stopwatch $stopwatch = null) + { + $this->kernel = $kernel; + $this->stopwatch = $stopwatch; + } + + /** + * {@inheritdoc} + */ + public function collectData(ContextInterface $context, Profile $profile) + { + if (null !== $this->kernel) { + $startTime = $this->kernel->getStartTime(); + } else { + if ($context instanceof RequestContext) { + $request = $context->getRequest(); + } else { + $request = Request::createFromGlobals(); + } + + $startTime = $request->server->get('REQUEST_TIME_FLOAT', $request->server->get('REQUEST_TIME')); + } + + $this->data = array( + 'token' => $profile->getToken(), + 'start_time' => $startTime * 1000, + 'events' => array(), + ); + return true; + } + + /** + * {@inheritdoc} + */ + public function lateCollect(Profile $profile) + { + if (null !== $this->stopwatch && isset($this->data['token'])) { + $this->setEvents($this->stopwatch->getSectionEvents($this->data['token'])); + } + unset($this->data['token']); + } + + /** + * Sets the request events. + * + * @param array $events The request events + */ + public function setEvents(array $events) + { + foreach ($events as $event) { + $event->ensureStopped(); + } + + $this->data['events'] = $events; + } + + /** + * Gets the request events. + * + * @return array The request events + */ + public function getEvents() + { + return $this->data['events']; + } + + /** + * Gets the request elapsed time. + * + * @return float The elapsed time + */ + public function getDuration() + { + if (!isset($this->data['events']['__section__'])) { + return 0; + } + + $lastEvent = $this->data['events']['__section__']; + + return $lastEvent->getOrigin() + $lastEvent->getDuration() - $this->getStartTime(); + } + + /** + * Gets the initialization time. + * + * This is the time spent until the beginning of the request handling. + * + * @return float The elapsed time + */ + public function getInitTime() + { + if (!isset($this->data['events']['__section__'])) { + return 0; + } + + return $this->data['events']['__section__']->getOrigin() - $this->getStartTime(); + } + + /** + * Gets the request time. + * + * @return int The time + */ + public function getStartTime() + { + return $this->data['start_time']; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'time'; + } +} diff --git a/src/Symfony/Component/Profiler/Profile.php b/src/Symfony/Component/Profiler/Profile.php new file mode 100644 index 0000000000000..1ccbb8623ce7e --- /dev/null +++ b/src/Symfony/Component/Profiler/Profile.php @@ -0,0 +1,309 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler; + +use Symfony\Component\Profiler\DataCollector\DataCollectorInterface; + +/** + * Profile. + * + * @author Fabien Potencier + */ +class Profile +{ + private $token; + + /** + * @var DataCollectorInterface[] + */ + private $collectors = array(); + + private $ip; + private $method; + private $name; + private $time; + private $statusCode; + private $type; + + /** + * @var Profile + */ + private $parent; + + /** + * @var Profile[] + */ + private $children = array(); + + /** + * Constructor. + * + * @param string $token The token + */ + public function __construct($token) + { + $this->token = $token; + } + + /** + * Sets the token. + * + * @param string $token The token + */ + public function setToken($token) + { + $this->token = $token; + } + + /** + * Gets the token. + * + * @return string The token + */ + public function getToken() + { + return $this->token; + } + + /** + * Sets the parent token. + * + * @param Profile $parent The parent Profile + */ + public function setParent(Profile $parent) + { + $this->parent = $parent; + } + + /** + * Returns the parent profile. + * + * @return Profile The parent profile + */ + public function getParent() + { + return $this->parent; + } + + /** + * Returns the parent token. + * + * @return null|string The parent token + */ + public function getParentToken() + { + return $this->parent ? $this->parent->getToken() : null; + } + + /** + * Returns the IP. + * + * @return string The IP + */ + public function getIp() + { + return $this->ip; + } + + /** + * Sets the IP. + * + * @param string $ip + */ + public function setIp($ip) + { + $this->ip = $ip; + } + + /** + * Returns the request method. + * + * @return string The request method + */ + public function getMethod() + { + return $this->method; + } + + public function setMethod($method) + { + $this->method = $method; + } + + /** + * Returns the name. + * + * @return string The name + */ + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } + + /** + * Returns the time. + * + * @return string The time + */ + public function getTime() + { + if (null === $this->time) { + return 0; + } + + return $this->time; + } + + public function setTime($time) + { + $this->time = $time; + } + + /** + * @param int $statusCode + */ + public function setStatusCode($statusCode) + { + $this->statusCode = $statusCode; + } + + /** + * @return int + */ + public function getStatusCode() + { + return $this->statusCode; + } + + /** + * Finds children profilers. + * + * @return Profile[] An array of Profile + */ + public function getChildren() + { + return $this->children; + } + + /** + * @return mixed + */ + public function getType() + { + return $this->type; + } + + /** + * @param mixed $type + */ + public function setType($type) + { + $this->type = $type; + } + + /** + * Sets children profiler. + * + * @param Profile[] $children An array of Profile + */ + public function setChildren(array $children) + { + $this->children = array(); + foreach ($children as $child) { + $this->addChild($child); + } + } + + /** + * Adds the child token. + * + * @param Profile $child The child Profile + */ + public function addChild(Profile $child) + { + $this->children[] = $child; + $child->setParent($this); + } + + /** + * Gets a Collector by name. + * + * @param string $name A collector name + * + * @return DataCollectorInterface A DataCollectorInterface instance + * + * @throws \InvalidArgumentException if the collector does not exist + */ + public function getCollector($name) + { + if (!isset($this->collectors[$name])) { + throw new \InvalidArgumentException(sprintf('Collector "%s" does not exist.', $name)); + } + + return $this->collectors[$name]; + } + + /** + * Gets the Collectors associated with this profile. + * + * @return DataCollectorInterface[] + */ + public function getCollectors() + { + return $this->collectors; + } + + /** + * Sets the Collectors associated with this profile. + * + * @param DataCollectorInterface[] $collectors + */ + public function setCollectors(array $collectors) + { + $this->collectors = array(); + foreach ($collectors as $collector) { + $this->addCollector($collector); + } + } + + /** + * Adds a Collector. + * + * @param DataCollectorInterface $collector A DataCollectorInterface instance + */ + public function addCollector(DataCollectorInterface $collector) + { + $this->collectors[$collector->getName()] = $collector; + } + + /** + * Returns true if a Collector for the given name exists. + * + * @param string $name A collector name + * + * @return bool + */ + public function hasCollector($name) + { + return isset($this->collectors[$name]); + } + + public function __sleep() + { + return array('token', 'parent', 'children', 'collectors', 'ip', 'method', 'time', 'statusCode', 'name'); + } +} diff --git a/src/Symfony/Component/Profiler/Profiler.php b/src/Symfony/Component/Profiler/Profiler.php new file mode 100644 index 0000000000000..f7aec754ed12e --- /dev/null +++ b/src/Symfony/Component/Profiler/Profiler.php @@ -0,0 +1,252 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler; + +use Symfony\Component\HttpFoundation\Response; +use Psr\Log\LoggerInterface; +use Symfony\Component\Profiler\Context\ContextInterface; +use Symfony\Component\Profiler\DataCollector\DataCollectorInterface; +use Symfony\Component\Profiler\DataCollector\LateDataCollectorInterface; +use Symfony\Component\Profiler\Profile; +use Symfony\Component\Profiler\Storage\ProfilerStorageInterface; + +/** + * Profiler. + * + * @author Fabien Potencier + */ +class Profiler +{ + /** + * @var ProfilerStorageInterface + */ + private $storage; + + /** + * @var DataCollectorInterface[] + */ + private $collectors = array(); + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var bool + */ + private $enabled = true; + + /** + * Constructor. + * + * @param ProfilerStorageInterface $storage A ProfilerStorageInterface instance + * @param LoggerInterface $logger A LoggerInterface instance + */ + public function __construct(ProfilerStorageInterface $storage, LoggerInterface $logger = null) + { + $this->storage = $storage; + $this->logger = $logger; + } + + /** + * Disables the profiler. + */ + public function disable() + { + $this->enabled = false; + } + + /** + * Enables the profiler. + */ + public function enable() + { + $this->enabled = true; + } + + /** + * Loads the Profile for the given token. + * + * @param string $token A token + * + * @return Profile A Profile instance + */ + public function loadProfile($token) + { + return $this->storage->read($token); + } + + /** + * Saves a Profile. + * + * @param Profile $profile A Profile instance + * + * @return bool + */ + public function saveProfile(Profile $profile) + { + // late collect + foreach ($profile->getCollectors() as $collector) { + if ($collector instanceof LateDataCollectorInterface) { + $collector->lateCollect($profile); + } + } + + if (!($ret = $this->storage->write($profile)) && null !== $this->logger) { + $this->logger->warning('Unable to store the profiler information.', array('configured_storage' => get_class($this->storage))); + } + + return $ret; + } + + /** + * Purges all data from the storage. + */ + public function purge() + { + $this->storage->purge(); + } + + /** + * Finds profiler tokens for the given criteria. + * + * @param array $criteria + * @param string $limit The maximum number of tokens to return + * @param string $start The start date to search from + * @param string $end The end date to search to + * @return array An array of tokens + * + * @see http://php.net/manual/en/datetime.formats.php for the supported date/time formats + */ + public function find(array $criteria, $limit, $start, $end) + { + return $this->storage->find( + $criteria, + $limit, + $this->getTimestamp($start), + $this->getTimestamp($end) + ); + } + + public function getSearchableKeys() + { + return $this->storage->getSearchableKeys(); + } + + /** + * Collects data for the given response. + * + * @param ContextInterface $context + * @return null|Profile A Profile instance or null if the profiler is disabled + */ + public function collectData(ContextInterface $context) + { + if (false === $this->enabled) { + return null; + } + + $profile = new Profile(substr(hash('sha256', uniqid(mt_rand(), true)), 0, 6)); + $profile->setTime(time()); + $profile->setName($context->getName()); + $profile->setStatusCode($context->getStatusCode()); + $profile->setType($context->getType()); + + foreach ($this->collectors as $collector) { + if ($collector->collectData($context, $profile)) { + // we need to clone for sub-requests + $profile->addCollector(clone $collector); + } + + } + + return $profile; + } + + /** + * Gets the Collectors associated with this profiler. + * + * @return array An array of collectors + */ + public function all() + { + return $this->collectors; + } + + /** + * Sets the Collectors associated with this profiler. + * + * @param DataCollectorInterface[] $collectors An array of collectors + */ + public function set(array $collectors = array()) + { + $this->collectors = array(); + foreach ($collectors as $collector) { + $this->add($collector); + } + } + + /** + * Adds a Collector. + * + * @param DataCollectorInterface $collector A DataCollectorInterface instance + */ + public function add(DataCollectorInterface $collector) + { + $this->collectors[$collector->getName()] = $collector; + } + + /** + * Returns true if a Collector for the given name exists. + * + * @param string $name A collector name + * + * @return bool + */ + public function has($name) + { + return isset($this->collectors[$name]); + } + + /** + * Gets a Collector by name. + * + * @param string $name A collector name + * + * @return DataCollectorInterface A DataCollectorInterface instance + * + * @throws \InvalidArgumentException if the collector does not exist + */ + public function get($name) + { + if (!$this->has($name)) { + throw new \InvalidArgumentException(sprintf('Collector "%s" does not exist.', $name)); + } + + return $this->collectors[$name]; + } + + private function getTimestamp($value) + { + if (null === $value || '' == $value) { + return null; + } + + try { + $value = new \DateTime(is_numeric($value) ? '@'.$value : $value); + } catch (\Exception $e) { + return null; + } + + return $value->getTimestamp(); + } +} diff --git a/src/Symfony/Component/Profiler/Storage/FileProfilerStorage.php b/src/Symfony/Component/Profiler/Storage/FileProfilerStorage.php new file mode 100644 index 0000000000000..cac877aab64a8 --- /dev/null +++ b/src/Symfony/Component/Profiler/Storage/FileProfilerStorage.php @@ -0,0 +1,307 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\Storage; + +use Symfony\Component\Profiler\Profile; +use Symfony\Component\Profiler\Storage\ProfilerStorageInterface; + +/** + * Storage for profiler using files. + * + * @author Alexandre Salomé + */ +class FileProfilerStorage implements ProfilerStorageInterface +{ + /** + * Folder where profiler data are stored. + * + * @var string + */ + private $folder; + + /** + * Constructs the file storage using a "dsn-like" path. + * + * Example : "file:/path/to/the/storage/folder" + * + * @param string $dsn The DSN + * + * @throws \RuntimeException + */ + public function __construct($dsn) + { + if (0 !== strpos($dsn, 'file:')) { + 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)); + } + $this->folder = substr($dsn, 5); + + if (!is_dir($this->folder) && false === @mkdir($this->folder, 0777, true) && !is_dir($this->folder)) { + throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $this->folder)); + } + } + + /** + * {@inheritdoc} + */ + public function find(array $criteria, $limit, $start = null, $end = null) + { + $criteria = $this->normalizeCriteria($criteria); + $file = $this->getIndexFilename(); + + if (!file_exists($file)) { + return array(); + } + + $file = fopen($file, 'r'); + fseek($file, 0, SEEK_END); + + $result = array(); + while (count($result) < $limit && $line = $this->readLineFromFile($file)) { + $values = str_getcsv($line); + list($csvToken, $csvIp, $csvMethod, $csvName, $csvTime, $csvParent, $csvStatusCode) = $values; + $csvTime = (int) $csvTime; + + if ($criteria['ip'] && false === strpos($csvIp, $criteria['ip']) || $criteria['name'] && false === strpos($csvName, $criteria['name']) || $criteria['method'] && false === strpos($csvMethod, $criteria['method']) || $criteria['status_code'] && false === strpos($csvStatusCode, $criteria['status_code'])) { + continue; + } + + if (!empty($start) && $csvTime < $start) { + continue; + } + + if (!empty($end) && $csvTime > $end) { + continue; + } + + $result[$csvToken] = array( + 'token' => $csvToken, + 'ip' => $csvIp, + 'method' => $csvMethod, + 'name' => $csvName, + 'time' => $csvTime, + 'parent' => $csvParent, + 'status_code' => $csvStatusCode, + ); + } + + fclose($file); + + return array_values($result); + } + + /** + * {@inheritdoc} + */ + public function purge() + { + $flags = \FilesystemIterator::SKIP_DOTS; + $iterator = new \RecursiveDirectoryIterator($this->folder, $flags); + $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST); + + foreach ($iterator as $file) { + if (is_file($file)) { + unlink($file); + } else { + rmdir($file); + } + } + } + + /** + * {@inheritdoc} + */ + public function read($token) + { + if (!$token || !file_exists($file = $this->getFilename($token))) { + return; + } + + return $this->createProfileFromData($token, unserialize(file_get_contents($file))); + } + + /** + * {@inheritdoc} + * + * @throws \RuntimeException + */ + public function write(Profile $profile) + { + $file = $this->getFilename($profile->getToken()); + + $profileIndexed = is_file($file); + if (!$profileIndexed) { + // Create directory + $dir = dirname($file); + if (!is_dir($dir) && false === @mkdir($dir, 0777, true) && !is_dir($dir)) { + throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $dir)); + } + } + + // Store profile + $data = array( + 'token' => $profile->getToken(), + 'parent' => $profile->getParentToken(), + 'children' => array_map(function ($p) { return $p->getToken(); }, $profile->getChildren()), + 'data' => $profile->getCollectors(), + 'ip' => $profile->getIp(), + 'method' => $profile->getMethod(), + 'name' => $profile->getName(), + 'time' => $profile->getTime(), + 'status_code' => $profile->getStatusCode(), + ); + + if (false === file_put_contents($file, serialize($data))) { + return false; + } + + if (!$profileIndexed) { + // Add to index + if (false === $file = fopen($this->getIndexFilename(), 'a')) { + return false; + } + + fputcsv($file, array( + $profile->getToken(), + $profile->getIp(), + $profile->getMethod(), + $profile->getName(), + $profile->getTime(), + $profile->getParentToken(), + $profile->getStatusCode(), + )); + fclose($file); + } + + return true; + } + + /** + * Gets filename to store data, associated to the token. + * + * @param string $token + * + * @return string The profile filename + */ + protected function getFilename($token) + { + // Uses 4 last characters, because first are mostly the same. + $folderA = substr($token, -2, 2); + $folderB = substr($token, -4, 2); + + return $this->folder.'/'.$folderA.'/'.$folderB.'/'.$token; + } + + /** + * Gets the index filename. + * + * @return string The index filename + */ + protected function getIndexFilename() + { + return $this->folder.'/index.csv'; + } + + /** + * Reads a line in the file, backward. + * + * This function automatically skips the empty lines and do not include the line return in result value. + * + * @param resource $file The file resource, with the pointer placed at the end of the line to read + * + * @return mixed A string representing the line or null if beginning of file is reached + */ + protected function readLineFromFile($file) + { + $line = ''; + $position = ftell($file); + + if (0 === $position) { + return; + } + + while (true) { + $chunkSize = min($position, 1024); + $position -= $chunkSize; + fseek($file, $position); + + if (0 === $chunkSize) { + // bof reached + break; + } + + $buffer = fread($file, $chunkSize); + + if (false === ($upTo = strrpos($buffer, "\n"))) { + $line = $buffer.$line; + continue; + } + + $position += $upTo; + $line = substr($buffer, $upTo + 1).$line; + fseek($file, max(0, $position), SEEK_SET); + + if ('' !== $line) { + break; + } + } + + return '' === $line ? null : $line; + } + + protected function createProfileFromData($token, $data, $parent = null) + { + $profile = new Profile($token); + $profile->setName($data['name']); + $profile->setTime($data['time']); + $profile->setStatusCode($data['status_code']); + $profile->setCollectors($data['data']); + + if (!$parent && $data['parent']) { + $parent = $this->read($data['parent']); + } + + if ($parent) { + $profile->setParent($parent); + } + + foreach ($data['children'] as $token) { + if (!$token || !file_exists($file = $this->getFilename($token))) { + continue; + } + + $profile->addChild($this->createProfileFromData($token, unserialize(file_get_contents($file)), $profile)); + } + + return $profile; + } + + public function getSearchableKeys() + { + return array( + 'ip', + 'name', + 'method', + 'status_code', + ); + } + + private function normalizeCriteria(array $criteria) + { + return array_merge( + array_combine( + $this->getSearchableKeys(), + array_fill(0, count($this->getSearchableKeys()), null) + ), + $criteria + ); + } +} diff --git a/src/Symfony/Component/Profiler/Storage/ProfilerStorageInterface.php b/src/Symfony/Component/Profiler/Storage/ProfilerStorageInterface.php new file mode 100644 index 0000000000000..8387b36fd9c93 --- /dev/null +++ b/src/Symfony/Component/Profiler/Storage/ProfilerStorageInterface.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\Storage; + +use Symfony\Component\Profiler\Profile; + +/** + * ProfilerStorageInterface. + * + * @author Fabien Potencier + */ +interface ProfilerStorageInterface +{ + /** + * Finds profiler tokens for the given criteria. + * + * @param array $criteria + * @param string $limit The maximum number of tokens to return + * @param int|null $start The start date to search from + * @param int|null $end The end date to search to + * @return array An array of tokens + */ + public function find(array $criteria, $limit, $start = null, $end = null); + + /** + * Reads data associated with the given token. + * + * The method returns false if the token does not exist in the storage. + * + * @param string $token A token + * + * @return Profile The profile associated with token + */ + public function read($token); + + /** + * Saves a Profile. + * + * @param Profile $profile A Profile instance + * + * @return bool Write operation successful + */ + public function write(Profile $profile); + + /** + * Purges all data from the database. + */ + public function purge(); + + /** + * @return array list of keys you can search by in the find method + */ + public function getSearchableKeys(); +} diff --git a/src/Symfony/Component/Profiler/Summary/SummaryCollectorInterface.php b/src/Symfony/Component/Profiler/Summary/SummaryCollectorInterface.php new file mode 100644 index 0000000000000..3c5a5fb4d8d14 --- /dev/null +++ b/src/Symfony/Component/Profiler/Summary/SummaryCollectorInterface.php @@ -0,0 +1,17 @@ +collectors as $collector) { + $summary = array_merge($summary, $collector->getSummary($profile)); + } + + return $summary; + } + + public function addCollector(SummaryCollectorInterface $collector) + { + $this->collectors[] = $collector; + return $this; + } +} \ No newline at end of file pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy