winter/modules/cms/twig/DebugExtension.php

480 lines
13 KiB
PHP
Raw Normal View History

<?php namespace Cms\Twig;
use Str;
use Twig_Extension;
use Twig_Environment;
use Twig_SimpleFunction;
use Cms\Classes\Controller;
2014-08-30 12:23:12 +10:00
use Cms\Classes\ComponentBase;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;
use Model;
class DebugExtension extends Twig_Extension
{
2014-08-30 12:23:12 +10:00
const PAGE_CAPTION = 'Page variables';
const OBJECT_CAPTION = 'Object variables';
/**
* @var \Cms\Classes\Controller A reference to the CMS controller.
*/
protected $controller;
/**
* @var integer Helper for rendering table row styles.
*/
protected $zebra = 1;
/**
* @var boolean If no variable is passed, true.
*/
2014-08-30 12:23:12 +10:00
protected $variablePrefix = false;
/**
* @var array Collection of method/property comments.
*/
protected $commentMap = [];
protected $blockMethods = ['componentDetails', 'defineProperties', 'getPropertyOptions', 'offsetExists', 'offsetGet', 'offsetSet', 'offsetUnset'];
/**
* Creates the extension instance.
* @param \Cms\Classes\Controller $controller The CMS controller object.
*/
public function __construct(Controller $controller)
{
$this->controller = $controller;
}
/**
* Returns a list of global functions to add to the existing list.
*
* @return array An array of global functions
*/
public function getFunctions()
{
return array(
new Twig_SimpleFunction('dump', [$this, 'runDump'], array('is_safe' => ['html'], 'needs_context' => true, 'needs_environment' => true)),
);
}
2014-08-30 12:23:12 +10:00
/**
* Processes the dump variables, if none is supplied, all the twig
* template variables are used
* @param Twig_Environment $env
* @param array $context
* @return string
*/
public function runDump(Twig_Environment $env, $context)
{
if (!$env->isDebug()) {
return;
}
2014-08-30 12:23:12 +10:00
$result = '';
$count = func_num_args();
2014-08-30 12:23:12 +10:00
if ($count == 2) {
$this->variablePrefix = true;
$vars = [];
foreach ($context as $key => $value) {
if (!$value instanceof Twig_Template) {
$vars[$key] = $value;
}
}
2014-08-30 12:23:12 +10:00
$result .= $this->dump($vars, static::PAGE_CAPTION);
}
else {
$this->variablePrefix = false;
for ($i = 2; $i < $count; $i++) {
2014-08-30 12:23:12 +10:00
$result .= $this->dump(func_get_arg($i), static::OBJECT_CAPTION);
}
}
2014-08-30 12:23:12 +10:00
return $result;
}
/**
* Returns the name of the extension.
*
* @return string The extension name
*/
public function getName()
{
return 'debug';
}
/**
* Dump information about a variable
*
* @param mixed $variable Variable to dump
* @param string $caption Caption of the dump
* @return void
*/
2014-08-30 12:23:12 +10:00
public function dump($variables = null, $caption = null)
{
2014-08-30 12:23:12 +10:00
$this->commentMap = [];
$this->zebra = 1;
$info = [];
2014-08-30 12:23:12 +10:00
if (!is_array($variables)) {
if ($variables instanceof Paginator)
$variables = $this->paginatorToArray($variables);
elseif (is_object($variables))
$variables = $this->objectToArray($variables);
else
$variables = [$variables];
}
$output = [];
$output[] = '<table>';
2014-08-30 12:23:12 +10:00
if ($caption)
$output[] = $this->makeTableHeader($caption);
foreach ($variables as $key => $item) {
$output[] = $this->makeTableRow($key, $item);
}
$output[] = '</table>';
$html = implode(PHP_EOL, $output);
2014-08-30 12:23:12 +10:00
return '<pre style="' . $this->getContainerCss() . '">' . $html . '</pre>';
}
2014-08-30 12:23:12 +10:00
/**
* Builds the HTML used for the table header.
* @param string $caption
* @return string
*/
protected function makeTableHeader($caption)
{
$output = [];
$output[] = '<tr>';
$output[] = '<th colspan="100" style="'.$this->getHeaderCss().'">'.$caption.'</td>';
$output[] = '</tr>';
return implode(PHP_EOL, $output);
}
2014-08-30 12:23:12 +10:00
/**
* Builds the HTML used for each table row.
* @param mixed $key
* @param mixed $variable
* @return string
*/
protected function makeTableRow($key, $variable)
{
$this->zebra = $this->zebra ? 0 : 1;
2014-08-30 12:23:12 +10:00
$css = $this->getDataCss($variable);
$output = [];
$output[] = '<tr>';
2014-08-30 12:23:12 +10:00
$output[] = '<td style="'.$css.'">'.$this->evalKeyLabel($key).'</td>';
$output[] = '<td style="'.$css.'">'.$this->evalVarLabel($variable).'</td>';
$output[] = '<td style="'.$css.'">'.$this->evalVarDesc($variable, $key).'</td>';
$output[] = '</tr>';
return implode(PHP_EOL, $output);
}
2014-08-30 12:23:12 +10:00
protected function evalKeyLabel($key)
{
2014-08-30 12:23:12 +10:00
if ($this->variablePrefix === true) {
$output = '{{ <span>%s</span> }}';
}
elseif (is_array($this->variablePrefix)) {
$prefix = implode('.', $this->variablePrefix);
$output = '{{ <span>'.$prefix.'.%s</span> }}';
}
elseif ($this->variablePrefix) {
$output = '{{ <span>'.$this->variablePrefix.'.%s</span> }}';
}
else {
$output = '%s';
}
return sprintf($output, $key);
}
/**
* Evaluate the variable description
* @param mixed $variable
* @return string
*/
protected function evalVarLabel($variable)
{
$type = $this->getType($variable);
switch ($type) {
case 'object':
2014-08-30 12:23:12 +10:00
return $this->evalObjLabel($variable);
case 'array':
2014-08-30 12:23:12 +10:00
return $type . '('.count($variable).')';
default:
2014-08-30 12:23:12 +10:00
return $type;
}
}
2014-08-30 12:23:12 +10:00
/**
* Evaluate an object type for label
* @param object $variable
* @return string
*/
protected function getType($variable)
{
$type = gettype($variable);
if ($type == 'string' && substr($variable, 0, 12) == '___METHOD___')
return 'method';
return $type;
}
/**
* Evaluate an object type for label
* @param object $variable
* @return string
*/
protected function evalObjLabel($variable)
{
$class = get_class($variable);
$label = Str::getRealClass($variable);
if ($variable instanceof ComponentBase)
$label = '<strong>Component</strong>';
elseif ($variable instanceof Collection)
$label = 'Collection('.$variable->count().')';
elseif ($variable instanceof Model)
$label = 'Model';
return '<abbr title="'.e($class).'">'.$label.'</abbr>';
}
/**
* Evaluate the variable description
* @param mixed $variable
* @return string
*/
protected function evalVarDesc($variable, $key)
{
2014-08-30 12:23:12 +10:00
$type = $this->getType($variable);
if ($type == 'method')
return $this->evalMethodDesc($variable);
if (isset($this->commentMap[$key]))
return $this->commentMap[$key];
if ($type == 'array')
return $this->evalArrDesc($variable);
if ($type == 'object')
return $this->evalObjDesc($variable);
return '';
}
/**
* Evaluate an method type for description
* @param object $variable
* @return string
*/
protected function evalMethodDesc($variable)
{
$parts = explode('|', $variable);
if (count($parts) < 2)
return null;
$method = $parts[1];
return isset($this->commentMap[$method]) ? $this->commentMap[$method] : null;
}
2014-08-30 12:23:12 +10:00
/**
* Evaluate an array type for description
* @param array $variable
* @return string
*/
protected function evalArrDesc($variable)
{
$output = [];
foreach ($variable as $key => $value) {
$output[] = '<abbr title="'.e(gettype($value)).'">'.$key.'</abbr>';
}
return implode(', ', $output);
}
2014-08-30 12:23:12 +10:00
/**
* Evaluate an object type for description
* @param array $variable
* @return string
*/
protected function evalObjDesc($variable)
{
$output = [];
if ($variable instanceof ComponentBase) {
$details = $variable->componentDetails();
$output[] = '<abbr title="'.array_get($details, 'description').'">';
$output[] = array_get($details, 'name');
$output[] = '</abbr>';
}
return implode('', $output);
}
//
// Object helpers
//
protected function paginatorToArray(Paginator $paginator)
{
$this->commentMap = [
'links()' => 'Renders links for navigating the collection',
'currentPage' => 'Get the current page for the request.',
'lastPage' => 'Get the last page that should be available.',
'perPage' => 'Get the number of items to be displayed per page.',
'total' => 'Get the total number of items in the complete collection.',
'from' => 'Get the number of the first item on the paginator.',
'to' => 'Get the number of the last item on the paginator.',
'count' => 'Returns the number of items in this collection',
];
return [
'links' => '___METHOD___|links()',
'currentPage' => '___METHOD___|currentPage',
'lastPage' => '___METHOD___|lastPage',
'perPage' => '___METHOD___|perPage',
'total' => '___METHOD___|total',
'from' => '___METHOD___|from',
'to' => '___METHOD___|to',
'count' => '___METHOD___|count',
];
}
protected function objectToArray($object)
{
$class = get_class($object);
$info = new \ReflectionClass($object);
$this->commentMap[$class] = [];
$methods = [];
foreach ($info->getMethods() as $method) {
if (!$method->isPublic()) continue;
$name = $method->getName();
if (in_array($name, $this->blockMethods)) continue; // Blocked methods
if (preg_match('/^on[A-Z]{1}[\w+]*$/', $name)) continue; // AJAX methods
if (preg_match('/^get[A-Z]{1}[\w+]*Options$/', $name)) continue; // getSomethingOptions
if ($method->class != $class) continue; // Only locals
if (substr($name, 0, 1) == '_') continue; // Magic/hidden method
$name .= '()';
$methods[$name] = '___METHOD___|'.$name;
$this->commentMap[$name] = $this->evalDocBlock($method);
}
$vars = [];
foreach ($info->getProperties() as $property) {
if (!$property->isPublic()) continue;
$name = $property->getName();
$vars[$name] = $object->{$name};
$this->commentMap[$name] = $this->evalDocBlock($property);
}
return $methods + $vars;
}
protected function evalDocBlock($reflectionObj)
{
$comment = $reflectionObj->getDocComment();
$comment = substr($comment, 3, -2);
$parts = explode('@', $comment);
$comment = array_shift($parts);
$comment = trim(trim($comment), '*');
$comment = implode(' ', array_map('trim', explode('*', $comment)));
return $comment;
}
//
// Style helpers
//
/**
* Get the CSS string for the output data
*
* @return string
*/
2014-08-30 12:23:12 +10:00
protected function getDataCss($variable)
{
2014-08-30 12:23:12 +10:00
$css = [
'padding' => '7px',
'background-color' => $this->zebra ? '#D8D9DB' : '#FFF',
'color' => '#405261',
2014-08-30 12:23:12 +10:00
];
$type = gettype($variable);
if ($type == 'NULL')
$css['color'] = '#999';
return $this->arrayToCss($css);
}
/**
* Get the CSS string for the output container
*
* @return string
*/
protected function getContainerCss()
{
return $this->arrayToCss([
'background-color' => '#F3F3F3',
'border' => '1px solid #bbb',
'border-radius' => '4px',
'font-size' => '12px',
'line-height' => '1.4em',
'margin' => '30px',
'padding' => '7px',
'display' => 'inline-block',
]);
}
/**
* Get the CSS string for the output header
*
* @return string
*/
protected function getHeaderCss()
{
return $this->arrayToCss([
'font-size' => '18px',
'font-weight' => 'normal',
'margin' => '0',
'padding' => '10px',
'background-color' => '#7B8892',
'color' => '#FFF',
]);
}
/**
* Convert a key/value pair array into a CSS string
*
* @param array $rules List of rules to process
* @return string
*/
protected function arrayToCss(array $rules)
{
$strings = [];
foreach ($rules as $key => $value) {
$strings[] = $key . ': ' . $value;
}
return join('; ', $strings);
}
}