src/Debug/Dump/BaseValue.php
<?php
/**
* This file is part of PHPDebugConsole
*
* @package PHPDebugConsole
* @author Brad Kent <bkfake-github@yahoo.com>
* @license http://opensource.org/licenses/MIT MIT
* @copyright 2014-2024 Brad Kent
* @version v3.0
*/
namespace bdk\Debug\Dump;
use bdk\Debug;
use bdk\Debug\AbstractComponent;
use bdk\Debug\Abstraction\Abstracter;
use bdk\Debug\Abstraction\Abstraction;
use bdk\Debug\Abstraction\Type;
use bdk\Debug\Dump\Base as Dumper;
use bdk\PubSub\Event;
use DateTime;
/**
* Dump values
*/
class BaseValue extends AbstractComponent
{
/** @var Debug */
public $debug;
/** @var Dumper */
protected $dumper;
/** @var bool Whether to dump array keys / property names */
protected $dumpKeys = true;
/** @var array<string,mixed> */
protected $dumpOptions = array();
/** @var list<array<string,mixed>> */
protected $dumpOptStack = array();
/** @var list<Type::TYPE_*> */
protected $simpleTypes = array(
Type::TYPE_ARRAY,
Type::TYPE_BOOL,
Type::TYPE_FLOAT,
Type::TYPE_INT,
Type::TYPE_NULL,
Type::TYPE_STRING,
);
/**
* Constructor
*
* @param Dumper $dumper "parent" dump class
*/
public function __construct(Dumper $dumper)
{
$this->dumper = $dumper;
$this->debug = $dumper->debug;
}
/**
* Dump value
*
* @param mixed $val value to dump
* @param array $opts options & info for string values
*
* @return mixed
*/
public function dump($val, $opts = array())
{
$opts = \array_merge(array(
'addQuotes' => true,
'sanitize' => true, // only applies to html
'type' => null,
'typeMore' => null,
'visualWhiteSpace' => true,
), $opts);
if ($opts['type'] === null) {
list($opts['type'], $opts['typeMore']) = $this->debug->abstracter->type->getType($val);
}
if ($opts['typeMore'] === Type::TYPE_RAW) {
if ($opts['type'] === Type::TYPE_OBJECT || $this->dumper->crateRaw) {
$val = $this->debug->abstracter->crate($val, 'dump');
}
$opts['typeMore'] = null;
}
$this->dumpOptStack[] = $opts;
$method = 'dump' . \ucfirst($opts['type']);
$return = $opts['typeMore'] === Type::TYPE_ABSTRACTION
? $this->dumpAbstraction($val)
: $this->{$method}($val);
$this->dumpOptions = \array_pop($this->dumpOptStack);
return $return;
}
/**
* Get "option" of value being dumped
*
* @param string $what (optional) name of option to get (ie sanitize, type, typeMore)
*
* @return mixed
*/
public function getDumpOpt($what = null)
{
$path = $what === null
? '__end__'
: '__end__.' . $what;
return $this->debug->arrayUtil->pathGet($this->dumpOptStack, $path);
}
/**
* Set "option" of value being dumped
*
* @param array|string $what name of value to set (or key/value array)
* @param mixed $val value
*
* @return void
*/
public function setDumpOpt($what, $val = null)
{
if (\is_array($what)) {
$this->debug->arrayUtil->pathSet($this->dumpOptStack, '__end__', $what);
return;
}
$this->debug->arrayUtil->pathSet(
$this->dumpOptStack,
'__end__.' . $what,
$val
);
}
/**
* Extend me to format classname/constant, etc
*
* @param mixed $val classname or classname(::|->)name (method/property/const)
*
* @return string
*/
public function markupIdentifier($val)
{
if ($val instanceof Abstraction) {
$val = $val['value'];
if (\is_array($val)) {
$val = $val[0] . '::' . $val[1];
}
}
return $val;
}
/**
* Is value a timestamp?
*
* @param mixed $val value to check
* @param Abstraction $abs (optional) full abstraction
*
* @return string|false
*/
protected function checkTimestamp($val, Abstraction $abs = null)
{
if ($abs && $abs['typeMore'] === Type::TYPE_TIMESTAMP) {
$datetime = new DateTime('@' . (int) $val);
$datetimeStr = $datetime->format('Y-m-d H:i:s T');
$datetimeStr = \str_replace('GMT+0000', 'GMT', $datetimeStr);
return $datetimeStr;
}
return false;
}
/**
* Dump an abstraction
*
* @param Abstraction $abs Abstraction instance
*
* @return string|null
*/
protected function dumpAbstraction(Abstraction $abs)
{
$type = $abs['type'];
$method = 'dump' . \ucfirst($type);
$opts = $this->dumpAbstractionOpts($abs);
if ($abs['options']) {
$opts = \array_merge($opts, $abs['options']);
}
$opts['typeMore'] = $abs['typeMore'];
$this->setDumpOpt($opts);
if (\method_exists($this, $method) === false) {
$event = $this->debug->publishBubbleEvent(Debug::EVENT_DUMP_CUSTOM, new Event(
$abs,
array(
'return' => '',
'valDumper' => $this,
)
));
$this->setDumpOpt('typeMore', $abs['typeMore']);
return $event['return'];
}
return \in_array($type, $this->simpleTypes, true)
? $this->{$method}($abs['value'], $abs)
: $this->{$method}($abs);
}
/**
* Dump array
*
* @param array $array array to dump
*
* @return array|string
*/
protected function dumpArray($array)
{
if ($this->getDumpOpt('isMaxDepth')) {
return 'array *MAX DEPTH*';
}
$arrayNew = array();
foreach ($array as $key => $val) {
$key = $this->dumpKeys
? $this->dump($key, array('addQuotes' => false))
: $key;
$arrayNew[$key] = $this->dump($val);
}
return $arrayNew;
}
/**
* Dump boolean
*
* @param bool $val boolean value
*
* @return bool|string
*/
protected function dumpBool($val)
{
return $val;
}
/**
* Dump callable
*
* @param Abstraction $abs array/callable abstraction
*
* @return string
*/
protected function dumpCallable(Abstraction $abs)
{
return (!$abs['hideType'] ? 'callable: ' : '')
. $this->markupIdentifier($abs);
}
/**
* Dump constant
*
* @param Abstraction $abs constant abstraction
*
* @return string
*/
protected function dumpConst(Abstraction $abs)
{
return $abs['name'];
}
/**
* Dump float value
*
* @param float|int $val float value
* @param Abstraction $abs (optional) full abstraction
*
* @return float|string
*/
protected function dumpFloat($val, Abstraction $abs = null)
{
$date = $this->checkTimestamp($val, $abs);
return $date
? $val . ' (' . $date . ')'
: $val;
}
/**
* Dump integer value
*
* @param int $val integer value
* @param Abstraction $abs (optional) full abstraction
*
* @return int|string
*/
protected function dumpInt($val, Abstraction $abs = null)
{
$val = $this->dumpFloat($val, $abs);
return \is_string($val)
? $val
: (int) $val;
}
/**
* Dump non-inspected value (likely object)
*
* @return string
*/
protected function dumpNotInspected()
{
return 'NOT INSPECTED';
}
/**
* Dump null value
*
* @return null|string
*/
protected function dumpNull()
{
return null;
}
/**
* Dump object
*
* @param Abstraction $abs Object Abstraction instance
*
* @return string|array
*/
protected function dumpObject(Abstraction $abs)
{
if ($abs['isRecursion']) {
return '(object) ' . $abs['className'] . ' *RECURSION*';
}
if ($abs['isMaxDepth']) {
return '(object) ' . $abs['className'] . ' *MAX DEPTH*';
}
if ($abs['isExcluded']) {
return '(object) ' . $abs['className'] . ' NOT INSPECTED';
}
return array(
'___class_name' => $abs['className'],
) + (array) $this->dumpObjectProperties($abs);
}
/**
* Return array of object properties (name->value)
*
* @param Abstraction $abs Object Abstraction instance
*
* @return array|string
*/
protected function dumpObjectProperties(Abstraction $abs)
{
$return = array();
$properties = $abs->sort($abs['properties'], $abs['sort']);
foreach ($properties as $name => $info) {
$name = \str_replace('debug.', '', $name);
$name = $this->dumpKeys
? $this->dump($name, array('addQuotes' => false))
: $name;
$info['isInherited'] = $info['declaredLast'] && $info['declaredLast'] !== $abs['className'];
$vis = $this->dumpPropVis($info);
$name = '(' . $vis . ') ' . $name;
$return[$name] = $this->dump($info['value']);
}
return $return;
}
/**
* Dump property visibility
*
* @param array $info Property info
*
* @return string visibility
*/
protected function dumpPropVis(array $info)
{
$vis = (array) $info['visibility'];
foreach ($vis as $i => $v) {
if (\in_array($v, array('magic', 'magic-read', 'magic-write'), true)) {
$vis[$i] = '✨ ' . $v; // "sparkles": there is no magic-wand unicode char
} elseif ($v === 'private' && $info['isInherited']) {
$vis[$i] = '🔒 ' . $v;
}
}
if ($info['debugInfoExcluded']) {
$vis[] = 'excluded';
}
return \implode(' ', $vis);
}
/**
* Dump recursion (array recursion)
*
* @return string
*/
protected function dumpRecursion()
{
return 'array *RECURSION*';
}
/**
* Dump resource
*
* @param Abstraction $abs resource abstraction
*
* @return string
*/
protected function dumpResource(Abstraction $abs)
{
return $abs['value'];
}
/**
* Dump string
*
* @param string $val string value
* @param Abstraction $abs (optional) full abstraction
*
* @return string
*/
protected function dumpString($val, Abstraction $abs = null)
{
if (\is_numeric($val)) {
$date = $this->checkTimestamp($val, $abs);
return $date
? $val . ' (' . $date . ')'
: $val;
}
return $abs
? $this->dumpStringAbs($abs)
: $this->debug->utf8->dump($val);
}
/**
* Dump undefined
*
* @return string
*/
protected function dumpUndefined()
{
return Abstracter::UNDEFINED;
}
/**
* Dump Type::TYPE_UNKNOWN
*
* @param Abstraction $abs resource abstraction
*
* @return array
*/
protected function dumpUnknown(Abstraction $abs)
{
$values = $abs->getValues();
\ksort($values);
return $values;
}
/**
* Split identifier into classname, operator, & identifier
* Identifier = classname, function, or property
*
* @param Abstraction|array|string $val classname or classname(::|->)name (method/property/const)
* @param bool $asFunction (false)
*
* @return array
*/
protected function parseIdentifier($val, $asFunction = false)
{
if ($val instanceof Abstraction) {
$val = $val['value'];
}
$parts = array(
'classname' => $val,
'identifier' => '',
'operator' => '::',
);
$matches = array();
if (\is_array($val)) {
$parts['classname'] = $val[0];
$parts['identifier'] = $val[1];
} elseif (\preg_match('/^(.+)(::|->)(.+)$/', $val, $matches)) {
$parts['classname'] = $matches[1];
$parts['operator'] = $matches[2];
$parts['identifier'] = $matches[3];
} elseif (\preg_match('/^(.+)(\\\\\{closure\})$/', $val, $matches)) {
$parts['classname'] = $matches[1];
$parts['operator'] = '';
$parts['identifier'] = $matches[2];
} elseif ($asFunction) {
$parts['classname'] = '';
$parts['identifier'] = $val;
}
return $parts;
}
/**
* Get dump options for abstraction
*
* @param Abstraction $abs Abstraction instance
*
* @return array<string,mixed>
*/
private function dumpAbstractionOpts(Abstraction $abs)
{
$opts = $this->getDumpOpt();
foreach (\array_keys($opts) as $k) {
if ($abs[$k] !== null) {
$opts[$k] = $abs[$k];
}
}
return $opts;
}
/**
* Dump string abstraction
*
* @param Abstraction $abs Abstraction instance
*
* @return string
*/
private function dumpStringAbs(Abstraction $abs)
{
if ($abs['typeMore'] === Type::TYPE_STRING_BINARY && !$abs['value']) {
return 'Binary data not collected';
}
$val = $this->debug->utf8->dump($abs['value']);
$diff = $abs['strlen']
? $abs['strlen'] - \strlen($abs['value'])
: 0;
if ($diff) {
$val .= '[' . $diff . ' more bytes (not logged)]';
}
return $val;
}
}