Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\HttpKernel\DataCollector; Chris@0: Chris@0: use Symfony\Component\HttpFoundation\Request; Chris@0: use Symfony\Component\HttpFoundation\RequestStack; Chris@0: use Symfony\Component\HttpFoundation\Response; Chris@0: use Symfony\Component\Stopwatch\Stopwatch; Chris@0: use Symfony\Component\VarDumper\Cloner\Data; Chris@0: use Symfony\Component\VarDumper\Cloner\VarCloner; Chris@0: use Symfony\Component\VarDumper\Dumper\CliDumper; Chris@17: use Symfony\Component\VarDumper\Dumper\DataDumperInterface; Chris@0: use Symfony\Component\VarDumper\Dumper\HtmlDumper; Chris@12: use Twig\Template; Chris@0: Chris@0: /** Chris@0: * @author Nicolas Grekas Chris@0: */ Chris@0: class DumpDataCollector extends DataCollector implements DataDumperInterface Chris@0: { Chris@0: private $stopwatch; Chris@0: private $fileLinkFormat; Chris@0: private $dataCount = 0; Chris@0: private $isCollected = true; Chris@0: private $clonesCount = 0; Chris@0: private $clonesIndex = 0; Chris@0: private $rootRefs; Chris@0: private $charset; Chris@0: private $requestStack; Chris@0: private $dumper; Chris@0: private $dumperIsInjected; Chris@0: Chris@0: public function __construct(Stopwatch $stopwatch = null, $fileLinkFormat = null, $charset = null, RequestStack $requestStack = null, DataDumperInterface $dumper = null) Chris@0: { Chris@0: $this->stopwatch = $stopwatch; Chris@0: $this->fileLinkFormat = $fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); Chris@0: $this->charset = $charset ?: ini_get('php.output_encoding') ?: ini_get('default_charset') ?: 'UTF-8'; Chris@0: $this->requestStack = $requestStack; Chris@0: $this->dumper = $dumper; Chris@0: $this->dumperIsInjected = null !== $dumper; Chris@0: Chris@0: // All clones share these properties by reference: Chris@17: $this->rootRefs = [ Chris@0: &$this->data, Chris@0: &$this->dataCount, Chris@0: &$this->isCollected, Chris@0: &$this->clonesCount, Chris@17: ]; Chris@0: } Chris@0: Chris@0: public function __clone() Chris@0: { Chris@0: $this->clonesIndex = ++$this->clonesCount; Chris@0: } Chris@0: Chris@0: public function dump(Data $data) Chris@0: { Chris@0: if ($this->stopwatch) { Chris@0: $this->stopwatch->start('dump'); Chris@0: } Chris@14: if ($this->isCollected && !$this->dumper) { Chris@0: $this->isCollected = false; Chris@0: } Chris@0: Chris@0: $trace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, 7); Chris@0: Chris@0: $file = $trace[0]['file']; Chris@0: $line = $trace[0]['line']; Chris@0: $name = false; Chris@0: $fileExcerpt = false; Chris@0: Chris@0: for ($i = 1; $i < 7; ++$i) { Chris@0: if (isset($trace[$i]['class'], $trace[$i]['function']) Chris@0: && 'dump' === $trace[$i]['function'] Chris@0: && 'Symfony\Component\VarDumper\VarDumper' === $trace[$i]['class'] Chris@0: ) { Chris@0: $file = $trace[$i]['file']; Chris@0: $line = $trace[$i]['line']; Chris@0: Chris@0: while (++$i < 7) { Chris@0: if (isset($trace[$i]['function'], $trace[$i]['file']) && empty($trace[$i]['class']) && 0 !== strpos($trace[$i]['function'], 'call_user_func')) { Chris@0: $file = $trace[$i]['file']; Chris@0: $line = $trace[$i]['line']; Chris@0: Chris@0: break; Chris@12: } elseif (isset($trace[$i]['object']) && $trace[$i]['object'] instanceof Template) { Chris@0: $template = $trace[$i]['object']; Chris@0: $name = $template->getTemplateName(); Chris@0: $src = method_exists($template, 'getSourceContext') ? $template->getSourceContext()->getCode() : (method_exists($template, 'getSource') ? $template->getSource() : false); Chris@0: $info = $template->getDebugInfo(); Chris@0: if (isset($info[$trace[$i - 1]['line']])) { Chris@0: $line = $info[$trace[$i - 1]['line']]; Chris@0: $file = method_exists($template, 'getSourceContext') ? $template->getSourceContext()->getPath() : null; Chris@0: Chris@0: if ($src) { Chris@0: $src = explode("\n", $src); Chris@17: $fileExcerpt = []; Chris@0: Chris@17: for ($i = max($line - 3, 1), $max = min($line + 3, \count($src)); $i <= $max; ++$i) { Chris@0: $fileExcerpt[] = ''.$this->htmlEncode($src[$i - 1]).''; Chris@0: } Chris@0: Chris@0: $fileExcerpt = '
    '.implode("\n", $fileExcerpt).'
'; Chris@0: } Chris@0: } Chris@0: break; Chris@0: } Chris@0: } Chris@0: break; Chris@0: } Chris@0: } Chris@0: Chris@0: if (false === $name) { Chris@0: $name = str_replace('\\', '/', $file); Chris@0: $name = substr($name, strrpos($name, '/') + 1); Chris@0: } Chris@0: Chris@0: if ($this->dumper) { Chris@0: $this->doDump($data, $name, $file, $line); Chris@0: } Chris@0: Chris@0: $this->data[] = compact('data', 'name', 'file', 'line', 'fileExcerpt'); Chris@0: ++$this->dataCount; Chris@0: Chris@0: if ($this->stopwatch) { Chris@0: $this->stopwatch->stop('dump'); Chris@0: } Chris@0: } Chris@0: Chris@0: public function collect(Request $request, Response $response, \Exception $exception = null) Chris@0: { Chris@0: // Sub-requests and programmatic calls stay in the collected profile. Chris@0: if ($this->dumper || ($this->requestStack && $this->requestStack->getMasterRequest() !== $request) || $request->isXmlHttpRequest() || $request->headers->has('Origin')) { Chris@0: return; Chris@0: } Chris@0: Chris@0: // In all other conditions that remove the web debug toolbar, dumps are written on the output. Chris@0: if (!$this->requestStack Chris@0: || !$response->headers->has('X-Debug-Token') Chris@0: || $response->isRedirection() Chris@0: || ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html')) Chris@0: || 'html' !== $request->getRequestFormat() Chris@0: || false === strripos($response->getContent(), '') Chris@0: ) { Chris@0: if ($response->headers->has('Content-Type') && false !== strpos($response->headers->get('Content-Type'), 'html')) { Chris@0: $this->dumper = new HtmlDumper('php://output', $this->charset); Chris@17: $this->dumper->setDisplayOptions(['fileLinkFormat' => $this->fileLinkFormat]); Chris@0: } else { Chris@0: $this->dumper = new CliDumper('php://output', $this->charset); Chris@0: } Chris@0: Chris@0: foreach ($this->data as $dump) { Chris@0: $this->doDump($dump['data'], $dump['name'], $dump['file'], $dump['line']); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@14: public function reset() Chris@14: { Chris@14: if ($this->stopwatch) { Chris@14: $this->stopwatch->reset(); Chris@14: } Chris@17: $this->data = []; Chris@14: $this->dataCount = 0; Chris@16: $this->isCollected = true; Chris@14: $this->clonesCount = 0; Chris@14: $this->clonesIndex = 0; Chris@14: } Chris@14: Chris@0: public function serialize() Chris@0: { Chris@0: if ($this->clonesCount !== $this->clonesIndex) { Chris@0: return 'a:0:{}'; Chris@0: } Chris@0: Chris@0: $this->data[] = $this->fileLinkFormat; Chris@0: $this->data[] = $this->charset; Chris@0: $ser = serialize($this->data); Chris@17: $this->data = []; Chris@0: $this->dataCount = 0; Chris@0: $this->isCollected = true; Chris@0: if (!$this->dumperIsInjected) { Chris@0: $this->dumper = null; Chris@0: } Chris@0: Chris@0: return $ser; Chris@0: } Chris@0: Chris@0: public function unserialize($data) Chris@0: { Chris@17: $this->data = unserialize($data); Chris@0: $charset = array_pop($this->data); Chris@0: $fileLinkFormat = array_pop($this->data); Chris@17: $this->dataCount = \count($this->data); Chris@0: self::__construct($this->stopwatch, $fileLinkFormat, $charset); Chris@0: } Chris@0: Chris@0: public function getDumpsCount() Chris@0: { Chris@0: return $this->dataCount; Chris@0: } Chris@0: Chris@0: public function getDumps($format, $maxDepthLimit = -1, $maxItemsPerDepth = -1) Chris@0: { Chris@0: $data = fopen('php://memory', 'r+b'); Chris@0: Chris@0: if ('html' === $format) { Chris@0: $dumper = new HtmlDumper($data, $this->charset); Chris@17: $dumper->setDisplayOptions(['fileLinkFormat' => $this->fileLinkFormat]); Chris@0: } else { Chris@0: throw new \InvalidArgumentException(sprintf('Invalid dump format: %s', $format)); Chris@0: } Chris@17: $dumps = []; Chris@0: Chris@0: foreach ($this->data as $dump) { Chris@0: $dumper->dump($dump['data']->withMaxDepth($maxDepthLimit)->withMaxItemsPerDepth($maxItemsPerDepth)); Chris@0: $dump['data'] = stream_get_contents($data, -1, 0); Chris@0: ftruncate($data, 0); Chris@0: rewind($data); Chris@0: $dumps[] = $dump; Chris@0: } Chris@0: Chris@0: return $dumps; Chris@0: } Chris@0: Chris@0: public function getName() Chris@0: { Chris@0: return 'dump'; Chris@0: } Chris@0: Chris@0: public function __destruct() Chris@0: { Chris@0: if (0 === $this->clonesCount-- && !$this->isCollected && $this->data) { Chris@0: $this->clonesCount = 0; Chris@0: $this->isCollected = true; Chris@0: Chris@0: $h = headers_list(); Chris@17: $i = \count($h); Chris@0: array_unshift($h, 'Content-Type: '.ini_get('default_mimetype')); Chris@0: while (0 !== stripos($h[$i], 'Content-Type:')) { Chris@0: --$i; Chris@0: } Chris@0: Chris@17: if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && stripos($h[$i], 'html')) { Chris@0: $this->dumper = new HtmlDumper('php://output', $this->charset); Chris@17: $this->dumper->setDisplayOptions(['fileLinkFormat' => $this->fileLinkFormat]); Chris@0: } else { Chris@0: $this->dumper = new CliDumper('php://output', $this->charset); Chris@0: } Chris@0: Chris@0: foreach ($this->data as $i => $dump) { Chris@0: $this->data[$i] = null; Chris@0: $this->doDump($dump['data'], $dump['name'], $dump['file'], $dump['line']); Chris@0: } Chris@0: Chris@17: $this->data = []; Chris@0: $this->dataCount = 0; Chris@0: } Chris@0: } Chris@0: Chris@0: private function doDump($data, $name, $file, $line) Chris@0: { Chris@0: if ($this->dumper instanceof CliDumper) { Chris@0: $contextDumper = function ($name, $file, $line, $fmt) { Chris@0: if ($this instanceof HtmlDumper) { Chris@0: if ($file) { Chris@0: $s = $this->style('meta', '%s'); Chris@0: $f = strip_tags($this->style('', $file)); Chris@0: $name = strip_tags($this->style('', $name)); Chris@17: if ($fmt && $link = \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : $fmt->format($file, $line)) { Chris@0: $name = sprintf(''.$s.'', strip_tags($this->style('', $link)), $f, $name); Chris@0: } else { Chris@0: $name = sprintf(''.$s.'', $f, $name); Chris@0: } Chris@0: } else { Chris@0: $name = $this->style('meta', $name); Chris@0: } Chris@0: $this->line = $name.' on line '.$this->style('meta', $line).':'; Chris@0: } else { Chris@0: $this->line = $this->style('meta', $name).' on line '.$this->style('meta', $line).':'; Chris@0: } Chris@0: $this->dumpLine(0); Chris@0: }; Chris@0: $contextDumper = $contextDumper->bindTo($this->dumper, $this->dumper); Chris@0: $contextDumper($name, $file, $line, $this->fileLinkFormat); Chris@0: } else { Chris@0: $cloner = new VarCloner(); Chris@0: $this->dumper->dump($cloner->cloneVar($name.' on line '.$line.':')); Chris@0: } Chris@0: $this->dumper->dump($data); Chris@0: } Chris@0: Chris@0: private function htmlEncode($s) Chris@0: { Chris@0: $html = ''; Chris@0: Chris@0: $dumper = new HtmlDumper(function ($line) use (&$html) { $html .= $line; }, $this->charset); Chris@0: $dumper->setDumpHeader(''); Chris@0: $dumper->setDumpBoundaries('', ''); Chris@0: Chris@0: $cloner = new VarCloner(); Chris@0: $dumper->dump($cloner->cloneVar($s)); Chris@0: Chris@0: return substr(strip_tags($html), 1, -1); Chris@0: } Chris@0: }