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 [ new TwigSimpleFunction('dump', [$this, 'runDump'], [ 'is_safe' => ['html'], 'needs_context' => true, 'needs_environment' => true ]), ]; } /** * Processes the dump variables, if none is supplied, all the twig * template variables are used * @param TwigEnvironment $env * @param array $context * @return string */ public function runDump(TwigEnvironment $env, $context) { if (!$env->isDebug()) { return; } $result = ''; $count = func_num_args(); if ($count == 2) { $this->variablePrefix = true; $vars = []; foreach ($context as $key => $value) { if (!$value instanceof TwigTemplate) { $vars[$key] = $value; } } $result .= $this->dump($vars, static::PAGE_CAPTION); } else { $this->variablePrefix = false; for ($i = 2; $i < $count; $i++) { $var = func_get_arg($i); if ($var instanceof ComponentBase) { $caption = [static::COMPONENT_CAPTION, get_class($var)]; } elseif (is_array($var)) { $caption = static::ARRAY_CAPTION; } elseif (is_object($var)) { $caption = [static::OBJECT_CAPTION, get_class($var)]; } else { $caption = [static::OBJECT_CAPTION, gettype($var)]; } $result .= $this->dump($var, $caption); } } return $result; } /** * Dump information about a variable * @param mixed $variables Variable to dump * @param mixed $caption Caption [and subcaption] of the dump * @return void */ public function dump($variables = null, $caption = null) { $this->commentMap = []; $this->zebra = 1; $info = []; 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[] = ''; if ($caption) { $output[] = $this->makeTableHeader($caption); } $output[] = ''; foreach ($variables as $key => $item) { $output[] = $this->makeTableRow($key, $item); } $output[] = ''; $output[] = '
'; $html = implode(PHP_EOL, $output); return '
' . $html . '
'; } /** * Builds the HTML used for the table header. * @param mixed $caption Caption [and subcaption] of the dump * @return string */ protected function makeTableHeader($caption) { if (is_array($caption)) { list($caption, $subcaption) = $caption; } $output = []; $output[] = ''; $output[] = ''; $output[] = ''; $output[] = $caption; if (isset($subcaption)) { $output[] = '
'.$subcaption.'
'; } $output[] = ''; $output[] = ''; $output[] = ''; return implode(PHP_EOL, $output); } /** * 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; $css = $this->getDataCss($variable); $output = []; $output[] = ''; $output[] = ''.$this->evalKeyLabel($key).''; $output[] = ''.$this->evalVarLabel($variable).''; $output[] = ''.$this->evalVarDesc($variable, $key).''; $output[] = ''; $output[] = ''; $output[] = ''.$this->evalVarDump($variable).''; $output[] = ''; return implode(PHP_EOL, $output); } /** * Builds JavaScript for toggling the dump container * @return string */ protected function evalToggleDumpOnClick() { $output = "var d=this.parentElement.nextElementSibling.getElementsByTagName('div')[0];"; $output .= "d.style.display=='none'?d.style.display='block':d.style.display='none'"; return $output; } /** * Dumps a variable using HTML Dumper, wrapped in a hidden DIV element. * @param mixed $variable * @return string */ protected function evalVarDump($variable) { $dumper = new HtmlDumper; $cloner = new VarCloner; $output = '
'; $output .= $dumper->dump($cloner->cloneVar($variable), true); $output .= '
'; return $output; } /** * Returns a variable name as HTML friendly. * @param string $key * @return string */ protected function evalKeyLabel($key) { if ($this->variablePrefix === true) { $output = '{{ %s }}'; } elseif (is_array($this->variablePrefix)) { $prefix = implode('.', $this->variablePrefix); $output = '{{ '.$prefix.'.%s }}'; } elseif ($this->variablePrefix) { $output = '{{ '.$this->variablePrefix.'.%s }}'; } 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': return $this->evalObjLabel($variable); case 'array': return $type . '('.count($variable).')'; default: return $type; } } /** * 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 = class_basename($variable); if ($variable instanceof ComponentBase) { $label = 'Component'; } elseif ($variable instanceof Collection) { $label = 'Collection('.$variable->count().')'; } elseif ($variable instanceof Paginator) { $label = 'Paged Collection('.$variable->count().')'; } elseif ($variable instanceof Model) { $label = 'Model'; } return ''.$label.''; } /** * Evaluate the variable description * @param mixed $variable * @return string */ protected function evalVarDesc($variable, $key) { $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 $this->commentMap[$method] ?? null; } /** * Evaluate an array type for description * @param array $variable * @return string */ protected function evalArrDesc($variable) { $output = []; foreach ($variable as $key => $value) { $output[] = ''.$key.''; } return implode(', ', $output); } /** * Evaluate an object type for description * @param array $variable * @return string */ protected function evalObjDesc($variable) { $output = []; if ($variable instanceof ComponentBase) { $details = $variable->componentDetails(); $output[] = ''; $output[] = array_get($details, 'name'); $output[] = ''; } return implode('', $output); } // // Object helpers // /** * Returns default comment information for a paginator object. * @param Illuminate\Pagination\Paginator $paginator * @return array */ 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', ]; } /** * Returns a map of an object as an array, containing methods and properties. * @param mixed $object * @return array */ 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; // Only public } if ($method->class != $class) { continue; // Only locals } $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 (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->isStatic()) { continue; // Only non-static } if (!$property->isPublic()) { continue; // Only public } if ($property->class != $class) { continue; // Only locals } $name = $property->getName(); $vars[$name] = $object->{$name}; $this->commentMap[$name] = $this->evalDocBlock($property); } return $methods + $vars; } /** * Extracts the comment from a DocBlock * @param ReflectionClass $reflectionObj * @return string */ 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 * @param mixed $variable * @return string */ protected function getDataCss($variable) { $css = [ 'padding' => '7px', 'background-color' => $this->zebra ? '#D8D9DB' : '#FFF', 'color' => '#405261', ]; $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' => '18px', 'margin' => '20px', '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', ]); } /** * Get the CSS string for the output subheader * @return string */ protected function getSubheaderCss() { return $this->arrayToCss([ 'font-size' => '12px', 'font-weight' => 'normal', 'font-style' => 'italic', 'margin' => '0', 'padding' => '0', '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 implode('; ', $strings); } }