diff --git a/phpunit.xml.dist b/phpunit.xml.dist index e3e45f4a..599aab4d 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -83,6 +83,9 @@ tests/Slack + + tests/Table + tests/Teams diff --git a/src/Backtrace/Backtrace.php b/src/Backtrace/Backtrace.php index 7dcc6f6c..228062fc 100644 --- a/src/Backtrace/Backtrace.php +++ b/src/Backtrace/Backtrace.php @@ -112,7 +112,7 @@ public static function getCallerInfo($offset = 0, $options = 0) we need to collect object... we'll remove object at end if undesired */ $phpOptions = static::translateOptions($options | self::INCL_OBJECT); - $backtrace = \debug_backtrace($phpOptions, 33); + $backtrace = \debug_backtrace($phpOptions, 60); $backtrace = self::normalize($backtrace); $index = SkipInternal::getFirstIndex($backtrace, $offset); $index = \max($index, 1); // ensure we're >= 1 diff --git a/src/Container/Container.php b/src/Container/Container.php index 4bed751f..36bace51 100644 --- a/src/Container/Container.php +++ b/src/Container/Container.php @@ -25,6 +25,7 @@ * Forked from pimple/pimple * adds: * get() + * getObject() * has() * needsInvoked() * setCfg() @@ -97,7 +98,7 @@ class Container implements ArrayAccess * @param array $values The parameters or objects * @param array $cfg Config options */ - public function __construct($values = array(), $cfg = array()) + public function __construct(array $values = array(), array $cfg = array()) { $this->factories = new SplObjectStorage(); $this->protected = new SplObjectStorage(); @@ -213,16 +214,17 @@ public function get($name) * * Attempt to resolve any constructor arguments from the container. * - * @param string $classname Classname of the object to instantiate + * @param string $classname Classname of the object to instantiate + * @param bool $useContainer (true) Pull from container if available / store obj in container after build. False is similar to factory behavior. * * @return object */ - public function getObject($classname) + public function getObject($classname, $useContainer = true) { if ($this->objectBuilder === null) { $this->objectBuilder = new ObjectBuilder($this); } - return $this->objectBuilder->build($classname); + return $this->objectBuilder->build($classname, $useContainer); } /** @@ -457,7 +459,7 @@ public function setCfg($mixed, $value = null) * * @return $this */ - public function setValues($values) + public function setValues(array $values) { foreach ($values as $key => $value) { $this->offsetSet($key, $value); diff --git a/src/Container/ObjectBuilder.php b/src/Container/ObjectBuilder.php index 991d01d6..ed1faf95 100644 --- a/src/Container/ObjectBuilder.php +++ b/src/Container/ObjectBuilder.php @@ -23,19 +23,7 @@ use RuntimeException; /** - * Container - * - * Forked from pimple/pimple - * adds: - * get() - * has() - * needsInvoked() - * setCfg() - * allowOverride & onInvoke callback - * setValues() - * - * @author Fabien Potencier - * @author Brad Kent + * Instantiate objects with dependencies resolved via the container */ class ObjectBuilder { @@ -82,33 +70,31 @@ public function __construct(Container $container) * * We will look for the class in the container * If it is not found, we will try to instantiate it via reflection and obtaining dependencies via the container - * We will save the instance in the container * - * @param string $classname Fully qualified class name - * @param bool $addToContainer (true) Add instantiated object to container? + * if $save is true, we will save the instance in the container + * + * @param string $classname Fully qualified class name + * @param bool $save (true) store built object in container after build * * @return object * * @throws RuntimeException */ - public function build($classname, $addToContainer = true) + public function build($classname, $save = true) { if ($this->container->has($classname)) { return $this->container->get($classname); } $refClass = new ReflectionClass($classname); $refConstructor = $refClass->getConstructor(); - if ($refConstructor === null) { - $return = new $classname(); + $paramValues = $refConstructor + ? $this->resolveConstructorArgs($refConstructor) + : []; + $return = $refClass->newInstanceArgs($paramValues); + if ($save) { $this->container->offsetSet($classname, $return); - return $return; - } - $paramValues = $this->resolveConstructorArgs($refConstructor); - $obj = $refClass->newInstanceArgs($paramValues); - if ($addToContainer) { - $this->container->offsetSet($classname, $obj); } - return $obj; + return $return; } /** diff --git a/src/CurlHttpMessage/AbstractClient.php b/src/CurlHttpMessage/AbstractClient.php index d36fbc6a..78eb335d 100644 --- a/src/CurlHttpMessage/AbstractClient.php +++ b/src/CurlHttpMessage/AbstractClient.php @@ -53,7 +53,7 @@ abstract class AbstractClient * * @throws InvalidArgumentException */ - public function __construct($options = array()) + public function __construct(array $options = array()) { $cookieJarDefault = \tempnam(\sys_get_temp_dir(), 'curlHttpMessageCookies_') . '.txt'; $this->isTempCookieJar = isset($options['curl'][CURLOPT_COOKIEJAR]) === false; diff --git a/src/CurlHttpMessage/Factory.php b/src/CurlHttpMessage/Factory.php index 629037fb..41a9cd33 100644 --- a/src/CurlHttpMessage/Factory.php +++ b/src/CurlHttpMessage/Factory.php @@ -45,7 +45,7 @@ class Factory * * @param array $factories factory definitions */ - public function __construct($factories = array()) + public function __construct(array $factories = array()) { $this->factories = \array_merge(array( 'curlReqRes' => [$this, 'buildCurlReqRes'], @@ -82,7 +82,7 @@ public function __call($method, array $args) * * @return CurlReqRes */ - public function buildCurlReqRes(RequestInterface $request, $options = array()) + public function buildCurlReqRes(RequestInterface $request, array $options = array()) { $curlReqRes = new CurlReqRes($request, $this->factories['response']); $curlReqRes->setOptions($options); @@ -100,7 +100,7 @@ public function buildCurlReqRes(RequestInterface $request, $options = array()) * * @return Request */ - public function buildRequest($method, $uri, $headers = array(), $body = null) + public function buildRequest($method, $uri, array $headers = array(), $body = null) { $request = new Request($method, $uri); $request = $this->withHeaders($request, $headers); @@ -119,7 +119,7 @@ public function buildRequest($method, $uri, $headers = array(), $body = null) * * @return Response */ - public function buildResponse($code = 200, $reasonPhrase = '', $headers = array(), $body = null) + public function buildResponse($code = 200, $reasonPhrase = '', array $headers = array(), $body = null) { $response = new Response($code, (string) $reasonPhrase); $response = $this->withHeaders($response, $headers); diff --git a/src/CurlHttpMessage/Handler/CurlMulti.php b/src/CurlHttpMessage/Handler/CurlMulti.php index 15cddbaf..ea2d02b1 100644 --- a/src/CurlHttpMessage/Handler/CurlMulti.php +++ b/src/CurlHttpMessage/Handler/CurlMulti.php @@ -48,7 +48,7 @@ class CurlMulti extends Curl * * @throws RuntimeException */ - public function __construct($options = array()) + public function __construct(array $options = array()) { $this->options = \array_replace_recursive(array( 'curlMulti' => array(), diff --git a/src/Debug/AbstractDebug.php b/src/Debug/AbstractDebug.php index fb8d89b0..1dda392a 100644 --- a/src/Debug/AbstractDebug.php +++ b/src/Debug/AbstractDebug.php @@ -55,7 +55,7 @@ abstract class AbstractDebug * * @param array $cfg config */ - public function __construct($cfg = array()) + public function __construct(array $cfg = array()) { if (!isset(self::$instance)) { // self::getInstance() will always return initial/first instance @@ -80,7 +80,7 @@ public function __call($methodName, array $args) $this->publishBubbleEvent(Debug::EVENT_CUSTOM_METHOD, $logEntry); if ($logEntry['handled'] !== true) { $logEntry->setMeta('isCustomMethod', true); - $this->rootInstance->getPlugin('methodBasic')->log($logEntry); + $this->rootInstance->log($logEntry); } return $logEntry['return']; } diff --git a/src/Debug/Abstraction/AbstractObject.php b/src/Debug/Abstraction/AbstractObject.php index 77d4a572..18f80084 100644 --- a/src/Debug/Abstraction/AbstractObject.php +++ b/src/Debug/Abstraction/AbstractObject.php @@ -21,6 +21,7 @@ use bdk\Debug\Abstraction\Object\Properties; use bdk\Debug\Abstraction\Object\PropertiesInstance; use bdk\Debug\Abstraction\Object\Subscriber; +use bdk\Table\Element; use ReflectionClass; use ReflectionEnumUnitCase; use RuntimeException; @@ -181,11 +182,10 @@ class AbstractObject extends AbstractComponent 'keys' => array(), // methods may be populated with __toString info, or methods with staticVars 'properties' => array(), - 'scopeClass' => '', + 'scopeClass' => null, 'sectionOrder' => array(), // cfg.objectSectionOrder 'sort' => '', // cfg.objectSort 'stringified' => null, - 'traverseValues' => array(), // populated if method is table 'viaDebugInfo' => false, ); @@ -205,10 +205,11 @@ public function __construct(Abstracter $abstracter) $this->propertiesInstance = new PropertiesInstance($this); $this->definition = new Definition($this); - if ($abstracter->debug->parentInstance === null) { - // we only need to subscribe to these events from root channel - $abstracter->debug->eventManager->addSubscriberInterface(new Subscriber($this)); - } + // if we are the root instance, subscribe to events + // otherwise ensure root instance is instantiated + $abstracter->debug->parentInstance === null + ? $abstracter->debug->eventManager->addSubscriberInterface(new Subscriber($this)) + : $abstracter->debug->rootInstance->abstracter; self::$values['sectionOrder'] = $abstracter->getCfg('objectSectionOrder'); self::$values['sort'] = $abstracter->getCfg('objectSort'); @@ -221,7 +222,7 @@ public function __construct(Abstracter $abstracter) * @param string $method Method requesting abstraction * @param array $hist (@internal) array & object history * - * @return ObjectAbstraction + * @return ObjectAbstraction|mixed * @throws RuntimeException */ public function getAbstraction($obj, $method = null, array $hist = array()) @@ -236,6 +237,11 @@ public function getAbstraction($obj, $method = null, array $hist = array()) $abs->setSubject($obj); $abs['hist'][] = $obj; $this->doAbstraction($abs); + if ($abs['unstructuredValue']) { + return $abs['unstructuredValue']; + } + // Mark definition as used + $this->definition->markAsUsed($definitionValueStore); return $abs->clean(); } @@ -254,26 +260,9 @@ public static function buildValues(array $values = array()) return $carry | $val; }, 0) & ~self::BRIEF & ~self::PROP_VIRTUAL_VALUE_COLLECT; } - return \array_merge(self::$values, $values); - } - - /** - * Populate rows or columns (traverseValues) if we're outputting as a table - * - * @param ObjectAbstraction $abs Abstraction instance - * - * @return void - */ - private function addTraverseValues(ObjectAbstraction $abs) - { - if ($abs['traverseValues']) { - return; - } - $obj = $abs->getSubject(); - $abs['hist'][] = $obj; - foreach ($obj as $k => $v) { - $abs['traverseValues'][$k] = $this->abstracter->crate($v, $abs['debugMethod'], $abs['hist']); - } + $values = \array_merge(self::$values, $values); + \ksort($values); + return $values; } /** @@ -289,7 +278,6 @@ private function doAbstraction(ObjectAbstraction $abs) if ($abs['isMaxDepth'] || $abs['isRecursion']) { return; } - $abs['isTraverseOnly'] = $this->helper->isTraverseOnly($abs); /* Debug::EVENT_OBJ_ABSTRACT_START subscriber may set isExcluded @@ -297,15 +285,11 @@ private function doAbstraction(ObjectAbstraction $abs) set cfgFlags (int / bitmask) set propertyOverrideValues set stringified - set traverseValues */ $this->debug->publishBubbleEvent(Debug::EVENT_OBJ_ABSTRACT_START, $abs, $this->debug); if ($abs['isExcluded']) { return; } - if ($abs['isTraverseOnly']) { - $this->addTraverseValues($abs); - } $this->methods->addInstance($abs); // method static variables $this->propertiesInstance->add($abs); /* @@ -347,7 +331,6 @@ protected function getAbstractionValues(ReflectionClass $reflector, $obj, $metho 'collectPropertyValues' => true, 'fullyQualifyPhpDocType' => $this->cfg['fullyQualifyPhpDocType'], 'hist' => $hist, - 'isTraverseOnly' => false, 'propertyOverrideValues' => array(), 'reflector' => $reflector, ) @@ -394,6 +377,9 @@ protected function getCfgFlags(ReflectionClass $reflector) private function getScopeClass(array &$hist) { for ($i = \count($hist) - 1; $i >= 0; $i--) { + if ($hist[$i] instanceof Element) { + continue; + } if (\is_object($hist[$i])) { return \get_class($hist[$i]); } diff --git a/src/Debug/Abstraction/AbstractString.php b/src/Debug/Abstraction/AbstractString.php index 8e7db16e..6ed91685 100644 --- a/src/Debug/Abstraction/AbstractString.php +++ b/src/Debug/Abstraction/AbstractString.php @@ -370,8 +370,8 @@ private function getTypeMore($val) return Type::TYPE_STRING_FILEPATH; } $strLen = \strlen($val); - $maxlen = $this->getMaxLen('other', $strLen); - return $maxlen > -1 && $strLen > $maxlen + $maxLen = $this->getMaxLen('other', $strLen); + return $maxLen > -1 && $strLen > $maxLen ? Type::TYPE_STRING_LONG : null; } diff --git a/src/Debug/Abstraction/Abstracter.php b/src/Debug/Abstraction/Abstracter.php index ce6483ca..a1a729e6 100644 --- a/src/Debug/Abstraction/Abstracter.php +++ b/src/Debug/Abstraction/Abstracter.php @@ -17,6 +17,7 @@ use bdk\Debug\Abstraction\AbstractObject; use bdk\Debug\Abstraction\AbstractString; use bdk\Debug\Abstraction\Type; +use SensitiveParameterValue; /** * Store array/object/resource info @@ -63,6 +64,7 @@ class Abstracter extends AbstractComponent 'brief' => false, // collect & output less details // see also AbstractObject::$cfgFlags where each key // can be set to true/false as a cfg value here + 'detectFiles' => false, 'fullyQualifyPhpDocType' => false, 'interfacesCollapse' => [ 'ArrayAccess', @@ -117,7 +119,7 @@ class Abstracter extends AbstractComponent * @param Debug $debug debug instance * @param array $cfg config options */ - public function __construct(Debug $debug, $cfg = array()) + public function __construct(Debug $debug, array $cfg = array()) { $this->debug = $debug; // we need debug instance so we can bubble events up channels $this->cfg['objectsExclude'][] = __NAMESPACE__; @@ -150,7 +152,7 @@ public function __construct(Debug $debug, $cfg = array()) * * @return mixed */ - public function crate($mixed, $method = null, $hist = array()) + public function crate($mixed, $method = null, array $hist = array()) { $typeInfo = self::needsAbstraction($mixed); if (!$typeInfo) { @@ -169,10 +171,10 @@ public function crate($mixed, $method = null, $hist = array()) * * @return Abstraction */ - public function crateWithVals($mixed, $values = array()) + public function crateWithVals($mixed, array $values = array()) { /* - Note: this->crateValues is the raw values passed to this method + Note: this->crateVals is the raw values passed to this method the values may end up being processed in Abstraction::onSet ie, converting attribs.class to an array */ @@ -187,10 +189,18 @@ public function crateWithVals($mixed, $values = array()) ? \array_values($typeInfo) : array(); unset($values['type']); + $cfgRestore = array(); + if (isset($values['detectFiles'])) { + $cfgRestore['detectFiles'] = $this->abstractString->setCfg('detectFiles', $values['detectFiles']); + unset($values['detectFiles']); + } $abs = $this->getAbstraction($mixed, __FUNCTION__, $typeInfo); foreach ($values as $k => $v) { $abs[$k] = $v; } + if ($cfgRestore) { + $this->debug->setCfg($cfgRestore, Debug::CONFIG_NO_RETURN); + } $this->crateVals = array(); return $abs; } @@ -217,7 +227,7 @@ public function crateWithVals($mixed, $values = array()) * * @SuppressWarnings(PHPMD.DevelopmentCodeFragment) */ - public function getAbstraction($val, $method = null, $typeInfo = [], $hist = array()) + public function getAbstraction($val, $method = null, array $typeInfo = [], array $hist = array()) { list($type, $typeMore) = $typeInfo ?: $this->type->getType($val); switch ($type) { @@ -228,7 +238,7 @@ public function getAbstraction($val, $method = null, $typeInfo = [], $hist = arr case Type::TYPE_FLOAT: return $this->getAbstractionFloat($val, $typeMore); case Type::TYPE_OBJECT: - return $val instanceof \SensitiveParameterValue + return $val instanceof SensitiveParameterValue ? $this->abstractString->getAbstraction(\call_user_func($this->debug->getPlugin('redaction')->getCfg('redactReplace'), 'redacted')) : $this->abstractObject->getAbstraction($val, $method, $hist); case Type::TYPE_RESOURCE: @@ -280,13 +290,10 @@ public function needsAbstraction($val) return false; } list($type, $typeMore) = $this->type->getType($val); - if ($type === Type::TYPE_BOOL) { - return false; - } - if (\in_array($typeMore, [Type::TYPE_ABSTRACTION, Type::TYPE_STRING_NUMERIC], true)) { + if (\in_array($typeMore, [Type::TYPE_STRING_NUMERIC, 'false', 'true'], true)) { return false; } - return $typeMore + return $type === Type::TYPE_OBJECT || $typeMore !== null ? [$type, $typeMore] : false; } @@ -317,7 +324,7 @@ private function getAbstractionFloat($val, $typeMore) /** * {@inheritDoc} */ - protected function postSetCfg($cfg = array(), $prev = array()) + protected function postSetCfg(array $cfg = array(), array $prev = array()) { $debugClass = \get_class($this->debug); if (!\array_intersect(['*', $debugClass], $this->cfg['objectsExclude'])) { diff --git a/src/Debug/Abstraction/Abstraction.php b/src/Debug/Abstraction/Abstraction.php index 05aa00c1..55838b7b 100644 --- a/src/Debug/Abstraction/Abstraction.php +++ b/src/Debug/Abstraction/Abstraction.php @@ -32,7 +32,7 @@ class Abstraction extends Event * @param string $type value type (one of the Abstracter TYPE_XXX constants) * @param array $values Abstraction values */ - public function __construct($type, $values = array()) + public function __construct($type, array $values = array()) { $values['type'] = $type; if ($type !== Type::TYPE_OBJECT && \array_key_exists('value', $values) === false) { @@ -97,7 +97,7 @@ public function setSubject($subject = null) * * @return void */ - protected function onSet($values = array()) + protected function onSet(array $values = array()) { if ($this->values['type'] === Type::TYPE_CONST && $this->getValue('name') !== null) { \trigger_error('Deprecated: TYPE_CONST - use TYPE_IDENTIFIER instead', \E_USER_DEPRECATED); diff --git a/src/Debug/Abstraction/Object/AbstractInheritable.php b/src/Debug/Abstraction/Object/AbstractInheritable.php index e4de48b3..6d4ef9fe 100644 --- a/src/Debug/Abstraction/Object/AbstractInheritable.php +++ b/src/Debug/Abstraction/Object/AbstractInheritable.php @@ -47,9 +47,11 @@ public function __construct(AbstractObject $abstractObject) * * @return array */ - public static function buildValues($values = array()) + public static function buildValues(array $values = array()) { - return \array_merge(static::$values, $values); + $values = \array_merge(static::$values, $values); + \ksort($values); + return $values; } /** diff --git a/src/Debug/Abstraction/Object/Abstraction.php b/src/Debug/Abstraction/Object/Abstraction.php index b1ca87a3..63d22449 100644 --- a/src/Debug/Abstraction/Object/Abstraction.php +++ b/src/Debug/Abstraction/Object/Abstraction.php @@ -13,11 +13,7 @@ use bdk\Debug\Abstraction\Abstracter; use bdk\Debug\Abstraction\Abstraction as BaseAbstraction; use bdk\Debug\Abstraction\AbstractObject; -use bdk\Debug\Abstraction\Object\Constants; use bdk\Debug\Abstraction\Object\Definition; -use bdk\Debug\Abstraction\Object\MethodParams; -use bdk\Debug\Abstraction\Object\Methods; -use bdk\Debug\Abstraction\Object\Properties; use bdk\Debug\Abstraction\Type; use bdk\Debug\Utility\ArrayUtil; use bdk\PubSub\ValueStore; @@ -32,9 +28,9 @@ class Abstraction extends BaseAbstraction 'collectPropertyValues', 'fullyQualifyPhpDocType', 'hist', - 'isTraverseOnly', 'propertyOverrideValues', 'reflector', + 'unstructuredValue', ]; /** @var ValueStore */ @@ -49,7 +45,7 @@ class Abstraction extends BaseAbstraction * @param ValueStore $inherited Inherited values * @param array $values Abstraction values */ - public function __construct(ValueStore $inherited, $values = array()) + public function __construct(ValueStore $inherited, array $values = array()) { $this->inherited = $inherited; parent::__construct(Type::TYPE_OBJECT, $values); @@ -84,7 +80,7 @@ public function __toString() */ public function __unserialize(array $data) { - $data = $this->unserializeDataPrep($data); + $data['inherited'] = $this->unserializeDataInherited($data); $this->inherited = $data['inherited']; unset($data['inherited']); $this->values = $data; @@ -136,7 +132,7 @@ public function jsonSerialize() public function getInheritedValues() { $values = $this->inherited->getValues(); - unset($values['cfgFlags']); + unset($values['__isUsed']); // don't inherit __isUsed return $values; } @@ -197,7 +193,7 @@ public function sort(array $array, $order = 'visibility, name') foreach ($order as $what) { $multiSortArgs[] = $sortData[$what]; } - // array_multisort reindexes nunmeric keys, + // array_multisort reindexes numeric keys, // so... we sort the keys -> array_fill -> array_replace $multiSortArgs[] = &$sortData['key']; \call_user_func_array('array_multisort', $multiSortArgs); @@ -232,34 +228,6 @@ public function &offsetGet($key) return $this->values[$key]; } - /** - * Make sure property and method info contains expected keys - * - * @param \ArrayAccess $data Either instance data or inherited data - * - * @return array - */ - public static function unserializeBuildValues($data) - { - $data['constants'] = \array_map(static function (array $info) { - return Constants::buildValues($info); - }, $data['constants']); - - $data['properties'] = \array_map(static function (array $info) { - return Properties::buildValues($info); - }, $data['properties']); - - $data['methods'] = \array_map(static function (array $info) { - $info = Methods::buildValues($info); - $info['params'] = \array_map(static function (array $paramInfo) { - return MethodParams::buildValues($paramInfo); - }, $info['params']); - return $info; - }, $data['methods']); - - return $data; - } - /** * Get merged class & instance value * @@ -272,7 +240,7 @@ private function getCombinedValue($key) $value = isset($this->values[$key]) ? $this->values[$key] : null; - if (\in_array($key, self::$keysTemp, true)) { + if (\in_array($key, self::$keysTemp, true) || $key === '__isUsed') { return $value; } $classVal = $this->inheritValue($key) @@ -346,29 +314,6 @@ protected function sortData(array $array) return $sortData; } - /** - * Ensure data contains all expected keys - * - * @param array $data Serialized data - * - * @return array - */ - private function unserializeDataPrep(array $data) - { - if (empty($data['definition'])) { - // we are instance values - $data = AbstractObject::buildValues($data); - } - - if (empty($data['className'])) { - unset($data['className']); - } - - $data['inherited'] = $this->unserializeDataInherited($data); - - return $data; - } - /** * Get inherited ValueStore * @@ -381,16 +326,14 @@ private function unserializeDataInherited(array &$data) if (isset($data['inherited'])) { $inherited = $data['inherited']; unset($data['inherited']); - return $this->unserializeBuildValues($inherited); + return $inherited; } if (isset($data['classDefinition'])) { // maintain backwards compatibility - v3.1 used 'classDefinition' $inherited = $data['classDefinition']; unset($data['classDefinition']); - return $this->unserializeBuildValues($inherited); + return $inherited; } - // maintain backwards compatibility - v3.0 did not inherit - $data = $this->unserializeBuildValues($data); return new ValueStore(AbstractObject::buildValues(Definition::buildValues())); } } diff --git a/src/Debug/Abstraction/Object/Definition.php b/src/Debug/Abstraction/Object/Definition.php index 0a376e1a..71599a72 100644 --- a/src/Debug/Abstraction/Object/Definition.php +++ b/src/Debug/Abstraction/Object/Definition.php @@ -105,7 +105,9 @@ public function __construct(AbstractObject $abstractObject) */ public static function buildValues(array $values = array()) { - return \array_merge(self::$values, $values); + $values = \array_merge(self::$values, $values); + \ksort($values); + return $values; } /** @@ -150,13 +152,29 @@ public function getValueStoreDefault() return $this->default; } $values = $this->object->buildValues(static::buildValues()); - $classValueStore = new ValueStore($values); - $this->default = $classValueStore; - $this->debug->data->set([ - 'classDefinitions', - $values['className'], // "\x00default\x00" - ], $classValueStore); - return $classValueStore; + $this->default = new ValueStore($values); + $dataPath = ['classDefinitions', $values['className']]; + $this->debug->data->set($dataPath, $this->default); + return $this->default; + } + + /** + * Mark a definition as used (referenced by a logged ObjectAbstraction) + * + * @param ValueStore $valueStore The definition ValueStore + * + * @return void + */ + public function markAsUsed(ValueStore $valueStore) + { + if ($valueStore['__isUsed']) { + return; // already marked + } + $valueStore['__isUsed'] = true; + if ($valueStore['inherited']) { + // also mark "parent" definition as used + $this->markAsUsed($valueStore['inherited']); + } } /** diff --git a/src/Debug/Abstraction/Object/Helper.php b/src/Debug/Abstraction/Object/Helper.php index 12f78ea8..18fa0109 100644 --- a/src/Debug/Abstraction/Object/Helper.php +++ b/src/Debug/Abstraction/Object/Helper.php @@ -166,23 +166,6 @@ public function getPhpDocVar(Reflector $reflector, $fullyQualifyType = false) return $phpDoc; } - /** - * Test if only need to populate traverseValues - * - * @param ObjectAbstraction $abs Abstraction instance - * - * @return bool - */ - public function isTraverseOnly(ObjectAbstraction $abs) - { - if ($abs['debugMethod'] === 'table' && \count($abs['hist']) < 4) { - $abs['cfgFlags'] &= ~AbstractObject::CONST_COLLECT; // set collect constants to "false" - $abs['cfgFlags'] &= ~AbstractObject::METHOD_COLLECT; // set collect methods to "false" - return true; - } - return false; - } - /** * Get Constant, Property, or Parameter's type or Method's return type * Priority given to phpDoc type, followed by reflection type (if available) diff --git a/src/Debug/Abstraction/Object/MethodParams.php b/src/Debug/Abstraction/Object/MethodParams.php index 22f3fe87..c37ee599 100644 --- a/src/Debug/Abstraction/Object/MethodParams.php +++ b/src/Debug/Abstraction/Object/MethodParams.php @@ -67,9 +67,11 @@ public function __construct(AbstractObject $abstractObject) * * @return array */ - public static function buildValues($values = array()) + public static function buildValues(array $values = array()) { - return \array_merge(static::$baseParamInfo, $values); + $values = \array_merge(static::$baseParamInfo, $values); + \ksort($values); + return $values; } /** diff --git a/src/Debug/Abstraction/Object/PropertiesInstance.php b/src/Debug/Abstraction/Object/PropertiesInstance.php index 8a02454a..88cd141c 100644 --- a/src/Debug/Abstraction/Object/PropertiesInstance.php +++ b/src/Debug/Abstraction/Object/PropertiesInstance.php @@ -32,9 +32,6 @@ class PropertiesInstance extends Properties */ public function add(Abstraction $abs) { - if ($abs['isTraverseOnly']) { - return; - } $abs['isLazy'] ? $this->addValuesLazy($abs) : $this->addValues($abs); diff --git a/src/Debug/Abstraction/Object/Subscriber.php b/src/Debug/Abstraction/Object/Subscriber.php index 0244f4ae..3a65bd7f 100644 --- a/src/Debug/Abstraction/Object/Subscriber.php +++ b/src/Debug/Abstraction/Object/Subscriber.php @@ -12,11 +12,11 @@ use bdk\Debug; use bdk\Debug\Abstraction\Abstracter; +use bdk\Debug\Abstraction\Abstraction; use bdk\Debug\Abstraction\AbstractObject; -use bdk\Debug\Abstraction\Object\Definition as AbstractObjectDefinition; +use bdk\Debug\Abstraction\Object\Abstraction as ObjectAbstraction; use bdk\Debug\Abstraction\Object\PropertiesDom; -use bdk\Debug\Data; -use bdk\Debug\Utility\PhpDoc; +use bdk\Debug\Abstraction\Type; use bdk\PubSub\SubscriberInterface; use Exception; use mysqli; @@ -24,7 +24,7 @@ use UnitEnum; /** - * Internal subscriber to ABSTRACT_START and ABSTRACT_END events + * Internal subscriber to OBJ_ABSTRACT_START and OBJ_ABSTRACT_END events */ class Subscriber implements SubscriberInterface { @@ -34,6 +34,8 @@ class Subscriber implements SubscriberInterface /** @var PropertiesDom */ private $dom; + private $isAbstractingTable = false; + /** * Constructor * @@ -59,49 +61,41 @@ public function getSubscriptions() /** * Debug::EVENT_OBJ_ABSTRACT_START event subscriber * - * @param Abstraction $abs Abstraction instance + * @param ObjectAbstraction $abs Abstraction instance * * @return void */ - public function onStart(Abstraction $abs) + public function onStart(ObjectAbstraction $abs) { $obj = $abs->getSubject(); - switch (true) { - case $obj instanceof \DateTime || $obj instanceof \DateTimeImmutable: - // check for both DateTime and DateTimeImmutable - // DateTimeInterface (and DateTimeImmutable) not available until Php 5.5 - $abs['isTraverseOnly'] = false; - $abs['stringified'] = $obj->format(\DateTime::RFC3339); - break; - case $obj instanceof mysqli: - $this->onStartMysqli($abs); - break; - case $obj instanceof Data: - $abs['propertyOverrideValues']['data'] = Abstracter::NOT_INSPECTED; - break; - case $obj instanceof PhpDoc: - $abs['propertyOverrideValues']['cache'] = Abstracter::NOT_INSPECTED; - break; - case $obj instanceof UnitEnum: - $abs['isTraverseOnly'] = false; - break; - case $obj instanceof AbstractObjectDefinition: - $abs['propertyOverrideValues']['cache'] = Abstracter::NOT_INSPECTED; - break; - case $abs['className'] === 'Closure': - $this->onStartClosure($abs); + // @phpcs:ignore SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys.IncorrectKeyOrder + $handlers = array( + 'bdk\Table\Table' => [$this, 'onStartTable'], + 'bdk\Table\Element' => [$this, 'onStartElement'], + 'Closure' => [$this, 'onStartClosure'], + 'DateTime' => [$this, 'onStartDateTime'], + 'DateTimeImmutable' => [$this, 'onStartDateTime'], + 'mysqli' => [$this, 'onStartMysqli'], + 'bdk\Debug\Abstraction\Object\Definition' => [$this, 'onStartAbstractObjectDefinition'], + 'bdk\Debug\Data' => [$this, 'onStartData'], + 'bdk\Debug\Utility\PhpDoc' => [$this, 'onStartPhpDoc'], + ); + foreach ($handlers as $class => $handler) { + if (\is_a($obj, $class)) { + \call_user_func($handler, $abs); break; + } } } /** * Debug::EVENT_OBJ_ABSTRACT_END event subscriber * - * @param Abstraction $abs Abstraction instance + * @param ObjectAbstraction $abs Object abstraction instance * * @return void */ - public function onEnd(Abstraction $abs) + public function onEnd(ObjectAbstraction $abs) { $obj = $abs->getSubject(); if ($obj instanceof Exception && isset($abs['properties']['xdebug_message'])) { @@ -118,14 +112,30 @@ public function onEnd(Abstraction $abs) $this->promoteParamDescs($abs); } + /** + * If cell value is an object, set unstructuredValue for abstraction + * + * @param ObjectAbstraction $abs Object abstraction instance + * + * @return void + */ + public function tableCellValueAbstracter(ObjectAbstraction $abs) + { + if (isset($abs['stringified'])) { + $abs['unstructuredValue'] = $abs['stringified']; + } elseif (isset($abs['methods']['__toString']['returnValue'])) { + $abs['unstructuredValue'] = $abs['methods']['__toString']['returnValue']; + } + } + /** * Add enum case's @var desc (if exists) to phpDoc * - * @param Abstraction $abs Abstraction instance + * @param ObjectAbstraction $abs Object abstraction instance * * @return void */ - private function onEndEnum(Abstraction $abs) + private function onEndEnum(ObjectAbstraction $abs) { if ($abs['debugMethod'] === 'table') { $abs['cfgFlags'] |= AbstractObject::BRIEF; @@ -151,11 +161,11 @@ private function onEndEnum(Abstraction $abs) /** * Add mysqli property values * - * @param Abstraction $abs Abstraction instance + * @param ObjectAbstraction $abs Object abstraction instance * * @return void */ - private function onEndMysqli(Abstraction $abs) + private function onEndMysqli(ObjectAbstraction $abs) { $obj = $abs->getSubject(); $propsAlwaysAvail = [ @@ -175,14 +185,26 @@ private function onEndMysqli(Abstraction $abs) \restore_error_handler(); } + /** + * Don't inspect Definition cache + * + * @param ObjectAbstraction $abs Object abstraction instance + * + * @return void + */ + private function onStartAbstractObjectDefinition(ObjectAbstraction $abs) + { + $abs['propertyOverrideValues']['cache'] = Abstracter::NOT_INSPECTED; + } + /** * Set Closure definition and debug properties * - * @param Abstraction $abs Abstraction instance + * @param ObjectAbstraction $abs Object abstraction instance * * @return void */ - private function onStartClosure(Abstraction $abs) + private function onStartClosure(ObjectAbstraction $abs) { // get the per-instance __invoke signature $this->abstractObject->methods->add($abs); @@ -195,14 +217,54 @@ private function onStartClosure(Abstraction $abs) $this->abstractObject->properties->addDebugProperties($abs); } + /** + * Data - don't inspect data prop + * + * @param ObjectAbstraction $abs Object abstraction instance + * + * @return void + */ + private function onStartData(ObjectAbstraction $abs) + { + $abs['propertyOverrideValues']['data'] = Abstracter::NOT_INSPECTED; + } + + /** + * DateTime - store stringified value + * + * @param ObjectAbstraction $abs Object abstraction instance + * + * @return void + */ + private function onStartDateTime(ObjectAbstraction $abs) + { + $obj = $abs->getSubject(); + $abs['stringified'] = $obj->format(\DateTime::RFC3339); + } + + /** + * Handle element abstraction + * + * @param ObjectAbstraction $abs Object abstraction instance + * + * @return void + */ + private function onStartElement(ObjectAbstraction $abs) + { + $obj = $abs->getSubject(); + $values = $obj->jsonSerialize(); + $abs['unstructuredValue'] = $this->abstractObject->abstracter->crate($values, $abs['debugMethod'], $abs['hist']); + $abs['isExcluded'] = true; + } + /** * Test if we can collect mysqli property values * - * @param Abstraction $abs Abstraction instance + * @param ObjectAbstraction $abs Object abstraction instance * * @return void */ - private function onStartMysqli(Abstraction $abs) + private function onStartMysqli(ObjectAbstraction $abs) { /* stat() may throw an error (ie "mysqli object is not fully initialized") @@ -217,14 +279,51 @@ private function onStartMysqli(Abstraction $abs) } } + /** + * PhpDoc - don't inspect cache + * + * @param ObjectAbstraction $abs Object abstraction instance + * + * @return void + */ + private function onStartPhpDoc(ObjectAbstraction $abs) + { + $abs['propertyOverrideValues']['cache'] = Abstracter::NOT_INSPECTED; + } + + /** + * Handle table abstraction + * + * @param ObjectAbstraction $abs Object abstraction instance + * + * @return void + */ + private function onStartTable(ObjectAbstraction $abs) + { + $obj = $abs->getSubject(); + $debug = $this->abstractObject->debug; + $this->isAbstractingTable = true; + $debug->eventManager->subscribe(Debug::EVENT_OBJ_ABSTRACT_END, [$this, 'tableCellValueAbstracter']); + try { + $values = $obj->jsonSerialize(); + $values = $debug->abstracter->crate($values, $abs['debugMethod'], $abs['hist']); + } catch (Exception $e) { + $values = array(); + } + $debug->eventManager->unsubscribe(Debug::EVENT_OBJ_ABSTRACT_END, [$this, 'tableCellValueAbstracter']); + $this->isAbstractingTable = false; + $abs['unstructuredValue'] = new Abstraction(Type::TYPE_TABLE, $values); + $abs['isExcluded'] = true; + } + /** * Reuse the phpDoc description from promoted __construct params * - * @param Abstraction $abs Abstraction instance + * @param ObjectAbstraction $abs Object abstraction instance * * @return void */ - private function promoteParamDescs(Abstraction $abs) + private function promoteParamDescs(ObjectAbstraction $abs) { if (isset($abs['methods']['__construct']) === false) { return; diff --git a/src/Debug/Abstraction/Type.php b/src/Debug/Abstraction/Type.php index 8eca2720..93c3ac54 100644 --- a/src/Debug/Abstraction/Type.php +++ b/src/Debug/Abstraction/Type.php @@ -13,6 +13,8 @@ use bdk\Debug\Abstraction\Abstracter; use bdk\Debug\Abstraction\Abstraction; use bdk\Debug\Utility\Php; +use Closure; +use UnitEnum; /** * Determine value type / extended type @@ -43,13 +45,14 @@ class Type "typeMore" values (no constants for "true" & "false") */ - const TYPE_ABSTRACTION = 'abstraction'; const TYPE_RAW = 'raw'; // raw object or array const TYPE_FLOAT_INF = "\x00inf\x00"; const TYPE_FLOAT_NAN = "\x00nan\x00"; const TYPE_IDENTIFIER_CLASSNAME = 'className'; const TYPE_IDENTIFIER_CONST = 'const'; const TYPE_IDENTIFIER_METHOD = 'method'; + const TYPE_OBJ_CLOSURE = 'closure'; // subtype of object + const TYPE_OBJ_ENUM = 'enum'; // subtype of object const TYPE_STRING_BASE64 = 'base64'; // "encoded" / auto-detected const TYPE_STRING_BINARY = 'binary'; // string that contains non-utf8 const TYPE_STRING_CLASSNAME = 'classname'; // deprecated (use TYPE_IDENTIFIER) @@ -59,6 +62,7 @@ class Type const TYPE_STRING_LONG = 'maxLen'; const TYPE_STRING_NUMERIC = 'numeric'; const TYPE_STRING_SERIALIZED = 'serialized'; // encoded / auto-detected + const TYPE_TABLE = 'table'; // Table structure const TYPE_TIMESTAMP = 'timestamp'; protected $abstracter; @@ -183,9 +187,16 @@ private function getTypeInt($val) */ private function getTypeObject($object) { - return $object instanceof Abstraction - ? [$object['type'], $object['typeMore']] - : [self::TYPE_OBJECT, self::TYPE_RAW]; // raw indicates value needs abstracted + if ($object instanceof Abstraction) { + return [$object['type'], $object['typeMore']]; + } + $typeMore = null; + if ($object instanceof Closure) { + $typeMore = self::TYPE_OBJ_CLOSURE; + } elseif ($object instanceof UnitEnum) { + $typeMore = self::TYPE_OBJ_ENUM; + } + return [self::TYPE_OBJECT, $typeMore]; } /** diff --git a/src/Debug/Collector/AbstractAsyncMiddleware.php b/src/Debug/Collector/AbstractAsyncMiddleware.php index a7782cb0..330162f0 100644 --- a/src/Debug/Collector/AbstractAsyncMiddleware.php +++ b/src/Debug/Collector/AbstractAsyncMiddleware.php @@ -59,7 +59,7 @@ class AbstractAsyncMiddleware extends AbstractComponent * * @SuppressWarnings(PHPMD.StaticAccess) */ - public function __construct($cfg = array(), $debug = null) + public function __construct(array $cfg = array(), $debug = null) { \bdk\Debug\Utility\PhpType::assertType($debug, 'bdk\Debug|null', 'debug'); $this->setCfg($cfg); diff --git a/src/Debug/Collector/CurlHttpMessageMiddleware.php b/src/Debug/Collector/CurlHttpMessageMiddleware.php index 80019223..5c42d368 100644 --- a/src/Debug/Collector/CurlHttpMessageMiddleware.php +++ b/src/Debug/Collector/CurlHttpMessageMiddleware.php @@ -23,7 +23,7 @@ class CurlHttpMessageMiddleware extends AbstractAsyncMiddleware /** * {@inheritDoc} */ - public function __construct($cfg = array(), $debug = null) + public function __construct(array $cfg = array(), $debug = null) { \bdk\Debug\Utility\PhpType::assertType($debug, 'bdk\Debug|null', 'debug'); diff --git a/src/Debug/Collector/GuzzleMiddleware.php b/src/Debug/Collector/GuzzleMiddleware.php index d00eb796..cb411e60 100644 --- a/src/Debug/Collector/GuzzleMiddleware.php +++ b/src/Debug/Collector/GuzzleMiddleware.php @@ -26,7 +26,7 @@ class GuzzleMiddleware extends AbstractAsyncMiddleware /** * {@inheritDoc} */ - public function __construct($cfg = array(), $debug = null) + public function __construct(array $cfg = array(), $debug = null) { \bdk\Debug\Utility\PhpType::assertType($debug, 'bdk\Debug|null', 'debug'); diff --git a/src/Debug/Collector/MonologHandler/CompatTrait_3.0.php b/src/Debug/Collector/MonologHandler/CompatTrait_3.0.php index 74161164..c7f5dd6f 100644 --- a/src/Debug/Collector/MonologHandler/CompatTrait_3.0.php +++ b/src/Debug/Collector/MonologHandler/CompatTrait_3.0.php @@ -19,6 +19,8 @@ if (\trait_exists(__NAMESPACE__ . '\\CompatTrait', false) === false) { /** * Provide handle method (with return type-hint) + * + * @phpcs:disable Generic.Classes.DuplicateClassName.Found */ trait CompatTrait // @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter Generic.CodeAnalysis.UnusedFunctionParameter { diff --git a/src/Debug/Config.php b/src/Debug/Config.php index 3197f1eb..cc92558e 100644 --- a/src/Debug/Config.php +++ b/src/Debug/Config.php @@ -58,7 +58,7 @@ public function get($path, $forInit = false) $keys = \array_unique(\array_merge($this->normalizer->serviceKeys(), \array_keys($this->valuesPending))); $values = array(); foreach ($keys as $debugProp) { - $values[$debugProp] = $this->getPropCfg($debugProp, array(), $forInit, false); + $values[$debugProp] = $this->getPropCfg($debugProp, [], $forInit, false); } \ksort($values); return $values; @@ -165,13 +165,13 @@ private function doSetReturn(array $configs) foreach ($configs as $debugProp => $cfg) { $cfgWas = $debugProp === 'debug' ? $this->debug->getCfg(null, Debug::CONFIG_DEBUG) - : $this->getPropCfg($debugProp, array(), true, false); + : $this->getPropCfg($debugProp, [], true, false); $cfgWas = \array_intersect_key($cfgWas, $cfg); $keys = \array_keys($cfg); $keysWas = \array_keys($cfgWas); if ($debugProp !== 'debug' && \array_intersect($keys, $keysWas) !== $keys) { // we didn't get all the expected previous values... - $cfgWas = $this->getPropCfg($debugProp, array(), true, false); + $cfgWas = $this->getPropCfg($debugProp, [], true, false); $cfgWas = \array_intersect_key($cfgWas, $cfg); } $return[$debugProp] = $cfgWas; @@ -189,7 +189,7 @@ private function doSetReturn(array $configs) * * @return mixed */ - private function getPropCfg($debugProp, $path = array(), $forInit = false, $delPending = true) + private function getPropCfg($debugProp, array $path = [], $forInit = false, $delPending = true) { $val = null; if (isset($this->valuesPending[$debugProp])) { @@ -215,7 +215,7 @@ private function getPropCfg($debugProp, $path = array(), $forInit = false, $delP * * @return mixed */ - private function getPropCfgFromChildObj($debugProp, $path = array(), $forInit = false) + private function getPropCfgFromChildObj($debugProp, array $path = [], $forInit = false) { $obj = null; $matches = array(); diff --git a/src/Debug/Debug.php b/src/Debug/Debug.php index 1b38b65b..fb00dd8a 100644 --- a/src/Debug/Debug.php +++ b/src/Debug/Debug.php @@ -119,7 +119,7 @@ class Debug extends AbstractDebug const EVENT_STREAM_WRAP = 'debug.streamWrap'; const META = "\x00meta\x00"; - const VERSION = '3.5'; + const VERSION = '3.6'; /** @var array */ protected $cfg = array( @@ -270,7 +270,7 @@ class Debug extends AbstractDebug * * @param array $cfg config */ - public function __construct($cfg = array()) + public function __construct(array $cfg = array()) { $this->cfg['errorMask'] = E_ERROR | E_PARSE | E_COMPILE_ERROR | E_CORE_ERROR | E_WARNING | E_USER_ERROR | E_RECOVERABLE_ERROR; @@ -302,7 +302,7 @@ public function getCfg($path = null, $opt = null) * * @return static */ - public static function getInstance($cfg = array()) + public static function getInstance(array $cfg = array()) { if (!isset(self::$instance)) { // self::$instance set in __construct diff --git a/src/Debug/Dump/AbstractValue.php b/src/Debug/Dump/AbstractValue.php index db5be85c..e5050793 100644 --- a/src/Debug/Dump/AbstractValue.php +++ b/src/Debug/Dump/AbstractValue.php @@ -30,6 +30,11 @@ abstract class AbstractValue extends AbstractComponent /** @var array */ public $charData = array(); + /** @var array */ + protected $cfg = array( + 'undefinedAs' => 'unset', + ); + /** @var Dumper */ protected $dumper; @@ -42,6 +47,9 @@ abstract class AbstractValue extends AbstractComponent /** @var array Pointer to top of optionsStack */ protected $optionsCurrent = array(); + /** @var array Options for previously dumped value */ + protected $optionsPrevious = array(); + /** @var list */ protected $simpleTypes = [ Type::TYPE_ARRAY, @@ -100,7 +108,7 @@ public function checkTimestamp($val, $abs = null) * * @return mixed */ - public function dump($val, $opts = array()) + public function dump($val, array $opts = array()) { $opts = $this->getPerValueOptions($val, $opts); if ($opts['typeMore'] === Type::TYPE_RAW) { @@ -138,6 +146,9 @@ public function findChars($str) */ public function optionGet($what = null) { + if ($what === 'previous') { + return $this->optionsPrevious; + } return $what === null ? $this->optionsCurrent : $this->debug->arrayUtil->pathGet($this->optionsCurrent, $what); @@ -184,7 +195,7 @@ public function optionStackPush(array $options) */ public function optionStackPop() { - \array_pop($this->optionStack); + $this->optionsPrevious = \array_pop($this->optionStack); $this->optionsCurrent = &$this->optionStack[\count($this->optionStack) - 1]; } @@ -301,6 +312,15 @@ abstract protected function dumpBool($val); */ abstract protected function dumpFloat($val, $abs = null); + /** + * Dump identifier + * + * @param Abstraction $abs constant abstraction + * + * @return string + */ + abstract protected function dumpIdentifier(Abstraction $abs); + /** * Dump integer value * @@ -337,6 +357,15 @@ abstract protected function dumpObject(ObjectAbstraction $abs); */ abstract protected function dumpString($val, $abs = null); + /** + * Dump Table + * + * @param Abstraction $abs Table abstraction + * + * @return array|string + */ + abstract protected function dumpTable(Abstraction $abs); + /** * Escape hex and unicode escape sequences. * This allows us to differentiate between '\u{03c5}' and a replaced "\u{03c5}" @@ -374,14 +403,14 @@ protected function getPerValueOptions($val, $opts) * Split identifier into classname, operator, & name. * * classname may be namespace\classname - * identifier = classname, constant function, or property + * identifier = className, const, method, or property * * @param string|array $val classname or classname(::|->)name (method/property/const) - * @param string $what ("classname"), "const", or "method" + * @param string $what (Type::TYPE_IDENTIFIER_CLASSNAME), Type::TYPE_IDENTIFIER_CONST, or Type::TYPE_IDENTIFIER_METHOD * * @return array */ - protected function parseIdentifier($val, $what = 'className') + protected function parseIdentifier($val, $what = Type::TYPE_IDENTIFIER_CLASSNAME) { $parts = \array_fill_keys(['classname', 'name', 'namespace', 'operator'], ''); $parts['classname'] = $val; @@ -394,7 +423,7 @@ protected function parseIdentifier($val, $what = 'className') $parts['classname'] = $matches[1]; $parts['operator'] = $matches[2]; $parts['name'] = $matches[3]; - } elseif (\in_array($what, ['const', 'method', 'function'], true)) { + } elseif (\in_array($what, [Type::TYPE_IDENTIFIER_CONST, Type::TYPE_IDENTIFIER_METHOD, 'function'], true)) { \preg_match('/^(.+\\\\)?(.+)$/', $val, $matches); $parts['classname'] = ''; $parts['namespace'] = $matches[1]; diff --git a/src/Debug/Dump/Base.php b/src/Debug/Dump/Base.php index b56fc166..b5fd95f9 100644 --- a/src/Debug/Dump/Base.php +++ b/src/Debug/Dump/Base.php @@ -12,8 +12,6 @@ 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\Value; use bdk\Debug\Dump\Substitution; @@ -58,6 +56,14 @@ class Base extends AbstractComponent color: #8a6d3b;', ); + /** @var array */ + protected $cfg = array( + // ChromeLogger: 'unset' + // FirePhp: null + // Script: Abstracter::UNDEFINED + 'undefinedAs' => 'unset', + ); + /** @var string */ protected $channelKeyRoot; @@ -72,9 +78,6 @@ class Base extends AbstractComponent /** @var Value */ protected $valDumper; - /** @var array */ - private $tableInfo = array(); - /** * Constructor * @@ -84,7 +87,7 @@ public function __construct(Debug $debug) { $this->debug = $debug; $this->substitution = new Substitution($this); - $this->valDumper = $this->getValDumper(); + $this->valDumper = $this->initValDumper(); $this->channelKeyRoot = $this->debug->rootInstance->getCfg('channelKey', Debug::CONFIG_DEBUG); } @@ -149,12 +152,11 @@ public function substitutionAsString($val, $opts) * * @return Value */ - protected function getValDumper() + protected function initValDumper() { - if (!$this->valDumper) { - $this->valDumper = new Value($this); - } - return $this->valDumper; + $valDumper = new Value($this); + $valDumper->setCfg($this->cfg); + return $valDumper; } /** @@ -231,111 +233,27 @@ protected function methodGroup(LogEntry $logEntry) } /** - * Normalize table data - * - * Ensures each row has all key/values and that they're in the same order - * if any row is an object, each row will get a ___class_name value + * Handle the "output" of tabular methods: profileEnd, table, trace * * This builds table rows usable by ChromeLogger, FirePhp, Text, and Script * - * undefinedAs values - * ChromeLogger: 'unset' - * FirePhp: null - * Text: 'unset' - * Script: Abstracter::UNDEFINED - * * @param LogEntry $logEntry LogEntry instance * - * @return string|null + * @return void */ protected function methodTabular(LogEntry $logEntry) { - $forceArray = $logEntry->getMeta('forceArray', true); - $undefinedAs = $logEntry->getMeta('undefinedAs', 'unset'); - $this->tableInfo = $logEntry->getMeta('tableInfo'); - $processRows = $undefinedAs !== Abstracter::UNDEFINED - || $forceArray === false - || $this->tableInfo['haveObjRow']; - if (!$processRows) { - return null; - } - if ($undefinedAs === 'null') { - $undefinedAs = null; - } - - $this->tableInfo['undefinedAs'] = $undefinedAs; - $this->tableInfo['columnKeys'] = \array_map(static function (array $colInfo) { - return $colInfo['key']; - }, $logEntry['meta']['tableInfo']['columns']); - - $rows = $logEntry['args'][0]; - foreach ($rows as $rowKey => $row) { - $rows[$rowKey] = $this->methodTabularRow($row, $rowKey, $logEntry['method'], $forceArray); - } $logEntry['method'] = 'table'; - $logEntry['args'] = [$rows]; - $this->tableInfo = array(); + // copy caption to meta so that script route can access it + $logEntry->setMeta('caption', $logEntry['args'][0]['caption']); + $this->methodDefault($logEntry); } /** - * Process table row - * - * @param array $row row - * @param int|string $rowKey row's key/index - * @param string $method log method (trace|table|profileEnd) - * @param bool $forceArray whether "scalar" rows should be wrapped in array - * - * @return array - */ - private function methodTabularRow(array $row, $rowKey, $method, $forceArray) - { - $rowInfo = isset($this->tableInfo['rows'][$rowKey]) - ? $this->tableInfo['rows'][$rowKey] - : array(); - $rowInfo = \array_merge( - array( - 'class' => null, - 'isScalar' => false, - ), - $rowInfo - ); - if ($rowInfo['isScalar'] === true && $forceArray === false) { - return \current($row); - } - $row = $this->methodTabularRowPrep($row, $method); - if ($this->tableInfo['haveObjRow']) { - $row = \array_merge( - array('___class_name' => $rowInfo['class']), - $row - ); - } - return $row; - } - - /** - * Set (or unset) any undefined values - * - * @param array $row row - * @param string $method log method (trace|table|profileEnd) - * - * @return array + * {@inheritDoc} */ - private function methodTabularRowPrep(array $row, $method) + protected function postSetCfg(array $cfg = array(), array $prev = array()) { - // handle undefined values - $rowNew = array(); - \array_walk($row, function ($val, $i) use (&$rowNew) { - if ($val === Abstracter::UNDEFINED) { - $val = $this->tableInfo['undefinedAs']; - if ($this->tableInfo['undefinedAs'] === 'unset') { - return; - } - } elseif ($val instanceof Abstraction) { - $val = $this->valDumper->dump($val, array('addQuotes' => false)); - } - $k = $this->tableInfo['columnKeys'][$i]; - $rowNew[$k] = $val; - }); - return $rowNew; + $this->valDumper->setCfg($cfg); } } diff --git a/src/Debug/Dump/Base/Value.php b/src/Debug/Dump/Base/Value.php index 899e3e2b..f612daa8 100644 --- a/src/Debug/Dump/Base/Value.php +++ b/src/Debug/Dump/Base/Value.php @@ -17,6 +17,7 @@ use bdk\Debug\Dump\AbstractValue; use bdk\Debug\Dump\Base\BaseObject; use bdk\Debug\Utility\Utf8; +use bdk\Table\Table as BdkTable; /** * Dump values @@ -102,11 +103,7 @@ protected function dumpFloat($val, $abs = null) } /** - * Dump identifier - * - * @param Abstraction $abs constant abstraction - * - * @return string + * {@inheritDoc} */ protected function dumpIdentifier(Abstraction $abs) { @@ -247,6 +244,23 @@ protected function dumpStringBinary(Abstraction $abs) }, $abs['chunks'] ?: array())); } + /** + * Dump Table + * + * @param Abstraction $abs Table abstraction + * + * @return array + */ + protected function dumpTable(Abstraction $abs) + { + $data = $abs->getValues(); + $table = new BdkTable($data); + $tableAsArray = \bdk\Table\Utility::asArray($table, array( + 'undefinedAs' => $this->cfg['undefinedAs'], + )); + return $this->dumpArray($tableAsArray); + } + /** * Dump undefined * diff --git a/src/Debug/Dump/Html.php b/src/Debug/Dump/Html.php index 33e89c04..9e6eac52 100644 --- a/src/Debug/Dump/Html.php +++ b/src/Debug/Dump/Html.php @@ -14,19 +14,17 @@ use bdk\Debug\Abstraction\Type; use bdk\Debug\Dump\Html\Group; use bdk\Debug\Dump\Html\Helper; -use bdk\Debug\Dump\Html\Table; use bdk\Debug\Dump\Html\Value; use bdk\Debug\LogEntry; /** * Dump val as HTML * - * @property HtmlTable $table lazy-loaded HtmlTable... only loaded if outputting a table - * @property Value $valDumper HTML value dumper + * @property Value $valDumper HTML value dumper */ class Html extends Base { - /** @var HtmlHelper helper class */ + /** @var Helper helper class */ public $helper; /** @var Debug[] Logged channels (channelName => Debug) */ @@ -38,9 +36,6 @@ class Html extends Base /** @var \bdk\Debug\Utility\Html */ protected $html; - /** @var HtmlTable */ - protected $lazyTable; - /** @var array LogEntry meta attribs */ protected $logEntryAttribs = array(); @@ -108,7 +103,7 @@ public function substitutionAsString($val, $opts) $opts['tagName'] = null; $toStr = (string) $val; // objects __toString or its classname return $toStr === $val['className'] - ? $this->valDumper->markupIdentifier($toStr, 'className') + ? $this->valDumper->markupIdentifier($toStr, Type::TYPE_IDENTIFIER_CLASSNAME) : $this->valDumper->dump($toStr, $opts); } return $this->valDumper->dump($val); @@ -128,30 +123,14 @@ protected function getChannels() return $channels; } - /** - * Getter for this->table - * - * @return HtmlTable - */ - protected function getTable() - { - if (!$this->lazyTable) { - $this->lazyTable = new Table($this); - } - return $this->lazyTable; - } - /** * Get value dumper * * @return Value */ - protected function getValDumper() + protected function initValDumper() { - if (!$this->valDumper) { - $this->valDumper = new Value($this); - } - return $this->valDumper; + return new Value($this); } /** @@ -227,10 +206,14 @@ protected function methodDefault(LogEntry $logEntry) $append = !empty($meta['context']) ? $this->helper->buildContext($meta['context'], $meta['line']) : ''; + $argString = $this->helper->buildArgString($args, $meta); + if (\in_array($logEntry['method'], ['profileEnd', 'table', 'trace'], true)) { + $argString = "\n" . $argString . "\n"; + } return $this->html->buildTag( 'li', $this->methodDefaultAttribs($logEntry), - $this->helper->buildArgString($args, $meta) . $append + $argString . $append ); } @@ -290,42 +273,7 @@ protected function methodGroup(LogEntry $logEntry) #[\Override] protected function methodTabular(LogEntry $logEntry) { - $tableOptions = $this->debug->arrayUtil->mergeDeep(array( - 'attribs' => array( - 'class' => \array_keys(\array_filter(array( - 'sortable' => $logEntry->getMeta('sortable'), - 'table-bordered' => true, - 'trace-context' => $logEntry->getMeta('inclContext'), - ))), - ), - ), $logEntry['meta']); - return $this->html->buildTag( - 'li', - $this->logEntryAttribs, - "\n" . $this->table->build($logEntry['args'][0], $tableOptions) . "\n" - ); - } - - /** - * Handle trace methods - * - * @param LogEntry $logEntry LogEntry instance - * - * @return string - */ - protected function methodTrace(LogEntry $logEntry) - { - $inclContext = $logEntry->getMeta('inclContext', false); - if ($inclContext === false) { - // not including context... add no-quotes class to filepath column - $meta = $logEntry['meta']; - $this->debug->arrayUtil->pathSet($meta, 'tableInfo.columns.0.attribs.class.__push__', 'no-quotes'); - $logEntry['meta'] = $meta; - } - if ($inclContext) { - $this->helper->addContextRows($logEntry); - } - return $this->methodTabular($logEntry); + return $this->methodDefault($logEntry); } /** diff --git a/src/Debug/Dump/Html/Group.php b/src/Debug/Dump/Html/Group.php index 0fe4d292..296df3d2 100644 --- a/src/Debug/Dump/Html/Group.php +++ b/src/Debug/Dump/Html/Group.php @@ -10,6 +10,7 @@ namespace bdk\Debug\Dump\Html; +use bdk\Debug\Abstraction\Type; use bdk\Debug\Dump\Html as Dumper; use bdk\Debug\LogEntry; @@ -115,7 +116,7 @@ private function header(array $args, array $meta) { $label = \array_shift($args); $label = $meta['isFuncName'] - ? $this->dumper->valDumper->markupIdentifier($label, 'method') + ? $this->dumper->valDumper->markupIdentifier($label, Type::TYPE_IDENTIFIER_METHOD) : \preg_replace('#^(.+)$#s', '$1', $this->dumper->valDumper->dump($label)); $labelAttribs = array( diff --git a/src/Debug/Dump/Html/Helper.php b/src/Debug/Dump/Html/Helper.php index 2216c817..38818c9a 100644 --- a/src/Debug/Dump/Html/Helper.php +++ b/src/Debug/Dump/Html/Helper.php @@ -13,7 +13,6 @@ use bdk\Debug; use bdk\Debug\Abstraction\Type; use bdk\Debug\Dump\Html as Dumper; -use bdk\Debug\LogEntry; /** * Html dump helper methods @@ -38,35 +37,7 @@ public function __construct(Dumper $dumper) { $this->debug = $dumper->debug; $this->dumper = $dumper; - $this->html = $dumper->debug->html; - } - - /** - * Move context info from meta to table rows - * - * @param LogEntry $logEntry LogEntry instance - * - * @return void - */ - public function addContextRows(LogEntry $logEntry) - { - $rowsInfo = $logEntry['meta']['tableInfo']['rows']; - $rowsNew = []; - $rowsInfoNew = []; - foreach ($logEntry['args'][0] as $i => $row) { - $rowsNew[$i] = $row; - $rowsInfoNew[$i] = $rowsInfo[$i]; - if (isset($rowsInfo[$i]['context']) === false) { - continue; - } - - $displayContext = $i === 0; - $rowsNew[$i . '_context'] = [$this->buildContextCell($rowsInfoNew[$i], $row[1])]; - - list($rowsInfoNew[$i], $rowsInfoNew[$i . '_context']) = $this->contextRowInfo($rowsInfoNew[$i], $displayContext); - } - $logEntry['args'] = [$rowsNew]; - $logEntry['meta']['tableInfo']['rows'] = $rowsInfoNew; + $this->html = $this->debug->html; } /** @@ -120,25 +91,6 @@ public function buildContext(array $lines, $lineNumber) ); } - /** - * Build context + arguments cell data - * - * @param array $rowInfo row meta information - * @param int $lineNumber line number to highlight - * - * @return Abstraction - */ - public function buildContextCell(array $rowInfo, $lineNumber) - { - $innerHtml = $this->buildContext($rowInfo['context'], $lineNumber) - . $this->buildContextArguments($rowInfo['args']); - return $this->debug->abstracter->crateWithVals($innerHtml, array( - 'dumpType' => false, // don't add t_string css class - 'sanitize' => false, - 'visualWhiteSpace' => false, - )); - } - /** * Dump phpDoc string * @@ -222,65 +174,6 @@ private function buildArgStringArgs(array $args, array $meta) }, $args, \array_keys($args)); } - /** - * Dump context arguments - * - * @param string|array $args Arguments from backtrace - * - * @return string - */ - private function buildContextArguments($args) - { - if (\is_array($args) === false || \count($args) === 0) { - return ''; - } - $crateRawWas = $this->dumper->crateRaw; - $this->dumper->crateRaw = true; - // set maxDepth for args - $maxDepthBak = $this->debug->getCfg('maxDepth'); - if ($maxDepthBak > 0) { - $this->debug->setCfg('maxDepth', $maxDepthBak + 1, Debug::CONFIG_NO_PUBLISH); - } - $args = '
Arguments = ' . $this->dumper->valDumper->dump($args); - $this->debug->setCfg('maxDepth', $maxDepthBak, Debug::CONFIG_NO_PUBLISH | Debug::CONFIG_NO_RETURN); - $this->dumper->crateRaw = $crateRawWas; - return $args; - } - - /** - * Build context row's info - * - * @param array $rowInfo row info - * @param bool $displayContext whether context should be initially expanded - * - * @return array rowInfo and contextRowInfo - */ - private function contextRowInfo(array $rowInfo, $displayContext = true) - { - unset($rowInfo['context']); - unset($rowInfo['args']); - $rowInfo['attribs']['class']['expanded'] = $displayContext; - $rowInfo['attribs']['data-toggle'] = 'next'; - $rowInfo['columns'][0]['attribs']['class'][] = 'no-quotes'; // no quotes on filepath - - $rowInfoContext = array( - 'attribs' => array( - 'class' => ['context'], - 'style' => $displayContext ? 'display:table-row;' : null, - ), - 'columns' => [ - array( - 'attribs' => array( - 'colspan' => 4, - ), - ), - ], - 'keyOutput' => false, - ); - - return [$rowInfo, $rowInfoContext]; - } - /** * Get argument "glue" and whether to glue after first arg * diff --git a/src/Debug/Dump/Html/HtmlObject.php b/src/Debug/Dump/Html/HtmlObject.php index 83652c5d..4b34a42c 100644 --- a/src/Debug/Dump/Html/HtmlObject.php +++ b/src/Debug/Dump/Html/HtmlObject.php @@ -182,7 +182,7 @@ protected function dumpAttributes(ObjectAbstraction $abs) return '
' . $this->debug->i18n->trans('object.attributes') . '
' . "\n" . \implode(\array_map(function ($info) { return '
' - . $this->valDumper->markupIdentifier($info['name'], 'className') + . $this->valDumper->markupIdentifier($info['name'], Type::TYPE_IDENTIFIER_CLASSNAME) . $this->dumpAttributeArgs($info['arguments']) . '
' . "\n"; }, $attributes)); diff --git a/src/Debug/Dump/Html/HtmlString.php b/src/Debug/Dump/Html/HtmlString.php index bad86112..347075bb 100644 --- a/src/Debug/Dump/Html/HtmlString.php +++ b/src/Debug/Dump/Html/HtmlString.php @@ -110,8 +110,7 @@ public function dump($val, $abs = null) */ public function dumpAsSubstitution($val, $opts) { - $isBinary = $val instanceof Abstraction && $val['typeMore'] === Type::TYPE_STRING_BINARY; - if ($isBinary) { + if ($val instanceof Abstraction && $val['typeMore'] === Type::TYPE_STRING_BINARY) { $val['brief'] = true; return $this->binary->dump($val); } @@ -297,21 +296,22 @@ private function buildPrettifiedPostDump(Abstraction $abs) if ($abs['prettifiedTag'] === false) { return $dumped; } - $tagName = 'span'; - if ($opts['tagName'] === 'td') { - $tagName = 'td'; + $attribsContainer = \array_filter(array( + 'class' => 'value-container', + 'data-type' => $abs['type'], + 'data-type-more' => $abs['typeMore'], + )); + if ($opts['tagName'] === null) { + $dumped = $this->debug->html->buildTag('span', $opts['attribs'], $dumped); + } elseif ($opts['tagName'] !== 'span') { $parsed = $this->debug->html->parseTag($dumped); $dumped = $this->debug->html->buildTag('span', $parsed['attribs'], $parsed['innerhtml']); } - return $this->debug->html->buildTag( - $tagName, - \array_filter(array( - 'class' => 'value-container', - 'data-type' => $abs['type'], - 'data-type-more' => $abs['typeMore'], - )), - '(' . $this->debug->i18n->trans('word.prettified') . ') ' . $dumped - ); + $dumped = '(' . $this->debug->i18n->trans('word.prettified') . ') ' . $dumped; + $this->valDumper->optionSet('attribs', $attribsContainer); // replace attribs with new outer container attribs + return $opts['tagName'] !== null + ? $this->debug->html->buildTag($opts['tagName'], $attribsContainer, $dumped) + : $dumped; }; } diff --git a/src/Debug/Dump/Html/HtmlStringBinary.php b/src/Debug/Dump/Html/HtmlStringBinary.php index e08d2970..7c346c4b 100644 --- a/src/Debug/Dump/Html/HtmlStringBinary.php +++ b/src/Debug/Dump/Html/HtmlStringBinary.php @@ -64,7 +64,7 @@ public function dump(Abstraction $abs) } if ($abs['percentBinary'] > 33 || $abs['contentType']) { $this->valDumper->optionSet('tagName', null); - $this->valDumper->optionSet('postDump', $this->dumpPost($abs, $tagName)); + $this->valDumper->optionSet('postDump', $this->buildPostDump($abs, $tagName)); } return $str; } @@ -116,7 +116,7 @@ private function dumpBrief($str, Abstraction $abs) * * @return Closure */ - private function dumpPost(Abstraction $abs, $tagName) + private function buildPostDump(Abstraction $abs, $tagName) { return function ($dumped) use ($abs, $tagName) { $lis = []; @@ -127,7 +127,8 @@ private function dumpPost(Abstraction $abs, $tagName) $lis[] = $dumped ? '
  • ' . $dumped . '
  • ' : '
  • ' . $this->debug->i18n->trans('string.binary-not-collected') . '
  • '; - $wrapped = 'string(binary)' . "\n" + $this->valDumper->optionSet('attribs', []); // ensure attribs not output + $dumped = 'string(binary)' . "\n" . $this->debug->html->buildTag( 'ul', \array_filter(array( @@ -137,11 +138,9 @@ private function dumpPost(Abstraction $abs, $tagName) )), "\n" . \implode("\n", $lis) . "\n" ); - if ($tagName === 'td') { - // wrap with td without adding class="binary t_string" - $wrapped = '' . $wrapped . ''; - } - return $wrapped; + return $tagName === 'td' + ? '' . $dumped . '' + : $dumped; }; } } diff --git a/src/Debug/Dump/Html/Object/AbstractSection.php b/src/Debug/Dump/Html/Object/AbstractSection.php index 57c093f2..7af8f344 100644 --- a/src/Debug/Dump/Html/Object/AbstractSection.php +++ b/src/Debug/Dump/Html/Object/AbstractSection.php @@ -15,6 +15,7 @@ use bdk\Debug\Abstraction\Abstraction; use bdk\Debug\Abstraction\AbstractObject; use bdk\Debug\Abstraction\Object\Abstraction as ObjectAbstraction; +use bdk\Debug\Abstraction\Type; use bdk\Debug\Dump\Html\Helper; use bdk\Debug\Dump\Html\Value as ValDumper; use bdk\Debug\Utility\Html as HtmlUtil; @@ -97,7 +98,7 @@ protected function buildInheritedFromHeading($className) return '
    ' . $this->debug->i18n->trans('object.inherited-from') . ' ' - . $this->valDumper->markupIdentifier($className, 'className') + . $this->valDumper->markupIdentifier($className, Type::TYPE_IDENTIFIER_CLASSNAME) . '
    ' . "\n"; } diff --git a/src/Debug/Dump/Html/Object/Enum.php b/src/Debug/Dump/Html/Object/Enum.php index e363b2a7..f4ef3300 100644 --- a/src/Debug/Dump/Html/Object/Enum.php +++ b/src/Debug/Dump/Html/Object/Enum.php @@ -66,10 +66,7 @@ public function dumpBrief(ObjectAbstraction $abs) $this->valDumper->optionGet('attribs'), $parsed['attribs'] ); - if ($this->valDumper->optionGet('tagName') !== 'td') { - $this->valDumper->optionSet('tagName', 'span'); - } - $this->valDumper->optionSet('type', null); // exclude t_object classname + $this->valDumper->optionSet('dumpType', false); // exclude t_object classname $this->valDumper->optionSet('attribs', $attribs); return $parsed['innerhtml']; } diff --git a/src/Debug/Dump/Html/Table.php b/src/Debug/Dump/Html/Table.php index 213f977d..0dd2ec69 100644 --- a/src/Debug/Dump/Html/Table.php +++ b/src/Debug/Dump/Html/Table.php @@ -10,8 +10,16 @@ namespace bdk\Debug\Dump\Html; +use bdk\Debug; +use bdk\Debug\Abstraction\Abstracter; +use bdk\Debug\Abstraction\Abstraction; use bdk\Debug\Abstraction\Type; use bdk\Debug\Dump\Html as Dumper; +use bdk\Debug\Dump\Html\Helper; +use bdk\Debug\Dump\Html\Value as ValDumper; +use bdk\Table\Table as BdkTable; +use bdk\Table\TableCell; +use bdk\Table\TableRow; /** * build a table @@ -24,306 +32,267 @@ class Table /** @var Dumper */ protected $dumper; - /** @var array */ - protected $options; + /** @var Helper helper class */ + protected $helper; - /** @var array */ - private $optionsDefault = array( - 'attribs' => array(), - 'caption' => '', - 'tableInfo' => array( - 'class' => null, // class name of table object - 'columns' => array(), - 'commonRowInfo' => array( - 'attribs' => array(), - 'class' => null, - 'key' => null, - 'keyOutput' => true, - 'summary' => '', - ), - 'haveObjRow' => false, - 'indexLabel' => null, - 'rows' => array(), - 'summary' => '', // title attr on class - ), - ); + /** @var \bdk\Debug\Utility\Html */ + protected $html; + + /** @var ValDumper */ + protected $valDumper; + + /** @var int|null */ + private $classColumnIndex; + + /** @var BdkTable|null */ + private $table; /** * Constructor * - * @param Dumper $dumper html dumper + * @param Dumper $dumper Html dumper + * @param Helper $helper Html dump helpers */ - public function __construct(Dumper $dumper) + public function __construct(Dumper $dumper, Helper $helper) { $this->debug = $dumper->debug; $this->dumper = $dumper; + $this->helper = $helper; + $this->html = $this->debug->html; + $this->valDumper = $dumper->valDumper; } /** - * Formats an array as a table + * Dump table structure * - * @param mixed $rows array of \Traversable or Abstraction - * @param array $options options - * 'attribs' : key/val array (or string - interpreted as class value) - * 'caption' : optional caption - * 'tableInfo': - * 'columns' : list of columns info + * @param Abstraction $abs Table abstraction * * @return string */ - public function build($rows, $options = array()) + public function dump(Abstraction $abs) { - $this->buildInitOptions($options); + $data = $abs->getValues(); + $table = new BdkTable($data); + $classes = \array_keys(\array_filter(array( + 'sortable' => $table->getMeta('sortable'), + 'table-bordered' => true, + 'trace-context' => $table->getMeta('inclContext'), // only applies for trace tables + ))); + $table->addClass($classes); + $this->table = $table; + + $this->updateCaption(); + $this->setClassColumnIndex(); + + if ($table->getMeta('inclContext')) { + $this->addContextRows($table); + } - return $this->debug->html->buildTag( - 'table', - $this->options['attribs'], - "\n" - . $this->buildCaption() - . $this->buildHeader() - . $this->buildBody($rows) - . $this->buildFooter() - ); + TableCell::setValDumper(function (TableCell $tableCell) { + return $this->valDumper($tableCell); + }); + return $table->getOuterHtml(); } /** - * Initialize options + * Insert new table rows fot context * - * @param array $options table options and info + * @param BdkTable $table Table instance * * @return void */ - private function buildInitOptions(array $options) + private function addContextRows(BdkTable $table) { - $this->options = \array_replace_recursive($this->optionsDefault, $options); - - foreach ($this->options['tableInfo']['columns'] as $k => $colInfo) { - $this->options['tableInfo']['columns'][$k] = \array_merge(array( - 'attribs' => array(), - 'class' => null, - 'falseAs' => null, - 'key' => '', - 'total' => null, - 'trueAs' => null, - ), $colInfo); + $rows = $table->getRows(); + $table->setRows([]); + foreach ($rows as $i => $row) { + $table->appendRow($row); + if ($row->getMeta('context') === null) { + continue; + } + $contextRow = $this->buildContextRow($row, $i === 0); + $table->appendRow($contextRow); } } /** - * Builds table's body + * Dump context arguments * - * @param array $rows array of arrays or Traversable + * @param string|array $args Arguments from backtrace * * @return string */ - protected function buildBody($rows) + private function buildContextArguments($args) { - $tBody = ''; - foreach ($rows as $k => $row) { - $rowInfo = \array_merge( - $this->options['tableInfo']['commonRowInfo'], - isset($this->options['tableInfo']['rows'][$k]) - ? $this->options['tableInfo']['rows'][$k] - : array() - ); - $tBody .= $this->buildRow($row, $rowInfo, $k); + if (\is_array($args) === false || \count($args) === 0) { + return ''; } - return '' . "\n" . $tBody . '' . "\n"; + $crateRawWas = $this->dumper->crateRaw; + $this->dumper->crateRaw = true; + // set maxDepth for args + $maxDepthBak = $this->debug->getCfg('maxDepth'); + if ($maxDepthBak > 0) { + $this->debug->setCfg('maxDepth', $maxDepthBak + 1, Debug::CONFIG_NO_PUBLISH); + } + $args = '
    Arguments = ' . $this->valDumper->dump($args); + $this->debug->setCfg('maxDepth', $maxDepthBak, Debug::CONFIG_NO_PUBLISH | Debug::CONFIG_NO_RETURN); + $this->dumper->crateRaw = $crateRawWas; + return $args; } /** - * Build table caption + * Create new TableRow containing trace context * - * @return string + * @param TableRow $row TableRow instance + * @param bool $expanded Whether row should be initially expanded + * + * @return TableRow */ - private function buildCaption() + private function buildContextRow(TableRow $row, $expanded) { - $caption = $this->dumper->valDumper->dump((string) $this->options['caption'], array( - 'tagName' => null, - 'type' => Type::TYPE_STRING, // pass so dumper doesn't need to infer - )); - if (!$this->options['tableInfo']['class']) { - return $caption - ? '' . $caption . '' . "\n" - : ''; + if ($expanded) { + $row->addClass('expanded'); } - $class = $this->dumper->valDumper->markupIdentifier( - $this->options['tableInfo']['class'], - 'className', - 'span', - array( - 'title' => $this->options['tableInfo']['summary'], - ) - ); - $caption = $caption - ? $caption . ' (' . $class . ')' - : $class; - return '' . $caption . '' . "\n"; + $row->setAttrib('data-toggle', 'next'); + + $contextHtml = $this->helper->buildContext($row->getMeta('context'), $row->getCells()[2]->getValue()) + . $this->buildContextArguments($row->getMeta('args')); + $contextHtml = $this->debug->abstracter->crateWithVals($contextHtml, array( + 'dumpType' => false, // don't add t_string css class + 'sanitize' => false, + 'visualWhiteSpace' => false, + )); + $tableCell = new TableCell($contextHtml); + $tableCell->setAttrib('colspan', \count($row->getCells())); + + $tableRow = new TableRow(); + $tableRow->setAttribs(array( + 'class' => ['context'], + 'style' => $expanded ? 'display:table-row;' : null, + )); + + $tableRow->appendCell($tableCell); + return $tableRow; } /** - * Builds table's tfoot + * Determine TableCell's row "type" (thead, tbody, or tfoot) + * + * @param TableCell $tableCell TableCell instance * * @return string */ - protected function buildFooter() + private function getRowType(TableCell $tableCell) { - $haveTotal = false; - $cells = \array_map(function ($colInfo) use (&$haveTotal) { - if (isset($colInfo['total']) === false) { - return ''; - } - $haveTotal = true; - $totalVal = $colInfo['total']; - if (\is_float($totalVal)) { - $totalVal = \round($totalVal, 6); + $parent = $tableCell->getParent(); + while ($parent !== null) { + $tagName = $parent->getTagName(); + if (\in_array($tagName, ['thead', 'tbody', 'tfoot'], true)) { + return $tagName; } - return $this->dumper->valDumper->dump($totalVal, array( - 'attribs' => $colInfo['attribs'], - 'tagName' => 'td', - )); - }, $this->options['tableInfo']['columns']); - if (!$haveTotal) { - return ''; + $parent = $parent->getParent(); } - return '' . "\n" - . '' - . ($this->options['tableInfo']['commonRowInfo']['keyOutput'] ? ' ' : '') - . ($this->options['tableInfo']['haveObjRow'] ? ' ' : '') - . \implode('', $cells) - . '' . "\n" - . '' . "\n"; + return 'tbody'; } /** - * Returns table's thead + * Determine & set the column index of ___class_name column * - * @return string + * @return void */ - protected function buildHeader() + private function setClassColumnIndex() { - $labels = \array_map([$this, 'buildHeaderLabel'], $this->options['tableInfo']['columns']); - $keyLabel = $this->options['tableInfo']['commonRowInfo']['keyOutput'] - ? ($this->options['tableInfo']['indexLabel'] - ? '' . $this->options['tableInfo']['indexLabel'] . '' - : ' ') - : ''; - return '' . "\n" - . '' - . $keyLabel - . ($this->options['tableInfo']['haveObjRow'] - ? ' ' - : '') - . \implode('', $labels) - . '' . "\n" - . '' . "\n"; + $this->classColumnIndex = null; + foreach ($this->table->getMeta('columns', []) as $i => $colMeta) { + if ($colMeta['key'] === '___class_name') { + $this->classColumnIndex = $i; + break; + } + } } /** - * Build header label th tag + * Dump a TableCell value * - * @param array $colInfo Column information + * @param TableCell $tableCell TableCell instance * * @return string */ - protected function buildHeaderLabel($colInfo) + private function valDumper(TableCell $tableCell) { - $type = $this->debug->abstracter->type->getType($colInfo['key']); - $label = $this->dumper->valDumper->dump($colInfo['key'], array( + $index = $tableCell->getIndex(); + $value = $tableCell->getValue(); + $rowType = $this->getRowType($tableCell); + + if ($rowType === 'tbody' && $index === $this->classColumnIndex && \in_array($value, [null, Abstracter::UNDEFINED], true) === false) { + return $this->valDumperClassName($tableCell); + } + + $columnMeta = \array_merge(array( + 'class' => null, + 'falseAs' => null, + 'trueAs' => null, + ), $this->table->getMeta('columns', [])[$index]); + $dumpOpts = \array_merge($columnMeta, array( + 'attribs' => array(), // don't use columnMeta attribs 'tagName' => null, )); - if (!empty($colInfo['class'])) { - $label .= ' ' . $this->dumper->valDumper->markupIdentifier($colInfo['class'], 'className'); + + $dumped = $this->valDumper->dump($value, $dumpOpts); + $optionsPrev = $this->valDumper->optionGet('previous'); + if ($optionsPrev['attribs']) { + // update tableCell attribs + $attribs = $this->debug->arrayUtil->mergeDeep($tableCell->getAttribs(), $optionsPrev['attribs']); + $tableCell->setAttribs($attribs); } - return $this->debug->html->buildTag('th', array( - 'class' => $type[0] !== 'string' - ? 't_' . $type[0] - : null, - 'scope' => 'col', - ), $label); - } - /** - * Returns table row - * - * @param mixed $row should be array or object abstraction - * @param array $rowInfo row info / meta - * @param string|int $rowKey row key - * - * @return string - */ - protected function buildRow($row, array $rowInfo, $rowKey) - { - $str = ''; - $rowKey = $rowInfo['key'] ?: $rowKey; - $str .= 'debug->html->buildAttribString($rowInfo['attribs']) . '>'; - $str .= $rowInfo['keyOutput'] ? $this->buildRowKey($rowKey) : ''; - /* - Output row's classname (if row is an object) - */ - if ($this->options['tableInfo']['haveObjRow']) { - $str .= $rowInfo['class'] - ? $this->dumper->valDumper->markupIdentifier($rowInfo['class'], 'className', 'td', array( - 'title' => $rowInfo['summary'], - )) - : ''; + if ($rowType === 'thead' && $columnMeta['class']) { + $dumped .= ' ' . $this->valDumper->markupIdentifier($columnMeta['class'], Type::TYPE_IDENTIFIER_CLASSNAME); } - /* - Output values - */ - $str .= $this->buildRowCells($row, $rowInfo); - $str .= '' . "\n"; - return $str; + + return $dumped; } /** - * Build the row's value cells + * Dump the className column value * - * @param mixed $row should be array or object abstraction - * @param array $rowInfo row info / meta + * @param TableCell $tableCell TableCell instance * * @return string */ - private function buildRowCells($row, array $rowInfo) + private function valDumperClassName(TableCell $tableCell) { - $cells = \array_map(function ($val, $i) use ($rowInfo) { - $colInfo = \array_merge( - $this->options['tableInfo']['columns'][$i], - isset($rowInfo['columns'][$i]) - ? $rowInfo['columns'][$i] - : array() - ); - $td = $this->dumper->valDumper->dump($val, array( - 'attribs' => $colInfo['attribs'], - 'tagName' => 'td', - )); - if ($val === true && $colInfo['trueAs'] !== null) { - $td = \str_replace('>true<', '>' . $colInfo['trueAs'] . '<', $td); - } elseif ($val === false && $colInfo['falseAs'] !== null) { - $td = \str_replace('>false<', '>' . $colInfo['falseAs'] . '<', $td); - } - return $td; - }, \array_values($row), \range(0, \count($row) - 1)); - return \implode('', $cells); + $value = $tableCell->getValue(); + $dumped = $this->valDumper->markupIdentifier($value, Type::TYPE_IDENTIFIER_CLASSNAME); + $parsed = $this->html->parseTag($dumped); + $tableCell->setAttribs($parsed['attribs']); + return $parsed['innerhtml']; } /** - * Build row's key/index th tag - * - * @param string|int $rowKey Row's index + * Sanitize the caption and with classname (if applicable) * - * @return string + * @return void */ - private function buildRowKey($rowKey) + private function updateCaption() { - $rowKeyParsed = $this->debug->html->parseTag($this->dumper->valDumper->dump($rowKey)); - return $this->debug->html->buildTag( - 'th', - $this->debug->arrayUtil->mergeDeep($rowKeyParsed['attribs'], array( - 'class' => ['t_key'], - 'scope' => 'row', - )), - $rowKeyParsed['innerhtml'] - ); + $caption = ''; + $captionElement = $this->table->getCaption(); + if ($captionElement) { + $caption = $captionElement->getHtml(); + $caption = $this->valDumper->dump($caption, array( + 'tagName' => null, + 'type' => Type::TYPE_STRING, // pass so dumper doesn't need to infer + )); + } + $class = $this->table->getMeta('class'); + if ($class) { + $caption = \trim(\sprintf( + '%s (%s)', + $caption, + $this->valDumper->markupIdentifier($class, Type::TYPE_IDENTIFIER_CLASSNAME) + )); + } + $this->table->setCaption($caption ?: null); } } diff --git a/src/Debug/Dump/Html/Value.php b/src/Debug/Dump/Html/Value.php index 453661cf..785b8335 100644 --- a/src/Debug/Dump/Html/Value.php +++ b/src/Debug/Dump/Html/Value.php @@ -11,6 +11,7 @@ namespace bdk\Debug\Dump\Html; use bdk\Debug\Abstraction\Abstraction; +use bdk\Debug\Abstraction\AbstractObject; use bdk\Debug\Abstraction\Object\Abstraction as ObjectAbstraction; use bdk\Debug\Abstraction\Type; use bdk\Debug\Dump\Base\Value as BaseValue; @@ -18,11 +19,13 @@ use bdk\Debug\Dump\Html\HtmlArray; use bdk\Debug\Dump\Html\HtmlObject; use bdk\Debug\Dump\Html\HtmlString; +use bdk\Debug\Dump\Html\Table; /** * Dump val as HTML * * @property HtmlObject $object lazy-loaded HtmlObject... only loaded if dumping an object + * @property HtmlTable $table lazy-loaded HtmlTable... only loaded if outputting a table */ class Value extends BaseValue { @@ -38,6 +41,9 @@ class Value extends BaseValue /** @var HtmlObject */ protected $lazyObject; + /** @var Table */ + protected $lazyTable; + /** * Constructor * @@ -78,14 +84,15 @@ public function checkTimestamp($val, $abs = null) 'class' => ['timestamp', 'value-container'], 'title' => $date, ); - if ($opts['tagName'] === 'td') { - return $this->html->buildTag( - 'td', - $attribsContainer, - $this->html->buildTag('span', $opts['attribs'], $val) - ); + if ($opts['tagName'] !== 'span') { + // if tagName is not 'span' or even if null, go ahead and wrap/rebuild in + $dumped = $this->html->buildTag('span', $opts['attribs'], $val); } - return $this->html->buildTag('span', $attribsContainer, $dumped); + $this->optionSet('attribs', $attribsContainer); // replace attribs with new outer container attribs + // dumped is now : 1767751464 + return $opts['tagName'] !== null + ? $this->html->buildTag($opts['tagName'], $attribsContainer, $dumped) + : $dumped; }); } return $date; @@ -100,11 +107,11 @@ public function checkTimestamp($val, $abs = null) * * @return string */ - public function dump($val, $opts = array()) + public function dump($val, array $opts = array()) { $opts = $this->getPerValueOptions($val, $opts); $this->optionStackPush($opts); // sets optionsCurrent - $val = $this->doDump($val); + $dumped = $this->doDump($val); if ($this->optionsCurrent['type'] && $this->optionsCurrent['dumpType']) { $this->optionsCurrent['attribs']['class'][] = 't_' . $this->optionsCurrent['type']; } @@ -113,13 +120,13 @@ public function dump($val, $opts = array()) } $tagName = $this->optionsCurrent['tagName']; if ($tagName) { - $val = $this->html->buildTag($tagName, $this->optionsCurrent['attribs'], $val); + $dumped = $this->html->buildTag($tagName, $this->optionsCurrent['attribs'], $dumped); } if ($this->optionsCurrent['postDump']) { - $val = \call_user_func($this->optionsCurrent['postDump'], $val, $this->optionsCurrent); + $dumped = \call_user_func($this->optionsCurrent['postDump'], $dumped, $this->optionsCurrent); } $this->optionStackPop(); - return $val; + return $dumped; } /** @@ -128,14 +135,15 @@ public function dump($val, $opts = array()) * if namespaced additionally wrap namespace in span.namespace * * @param string|array $val classname or classname(::|->)name (method/property/const) - * @param string $what ("className"), "const", or "function" - specify what we're marking if ambiguous + * @param string $what (Type::TYPE_IDENTIFIER_CLASSNAME), Type::TYPE_IDENTIFIER_CONST, or Type::TYPE_IDENTIFIER_METHOD + * specify what we're marking if ambiguous * @param string $tagName ("span") html tag to use - * @param array $attribs (optional) additional html attributes for classname span (such as title) + * @param array|null $attribs (optional) additional html attributes for classname span (such as title) * @param bool $wbr (false) whether to add a after the classname * * @return string html snippet */ - public function markupIdentifier($val, $what = 'className', $tagName = 'span', $attribs = array(), $wbr = false) + public function markupIdentifier($val, $what = Type::TYPE_IDENTIFIER_CLASSNAME, $tagName = 'span', $attribs = array(), $wbr = false) { $parts = \array_map([$this->string, 'dump'], $this->parseIdentifier($val, $what)); $class = 'classname'; @@ -197,6 +205,13 @@ protected function dumpArray(array $array, $abs = null) */ protected function dumpBool($val) { + $options = $this->optionGet(); + if ($val === true && isset($options['trueAs'])) { + return $options['trueAs']; + } + if ($val === false && isset($options['falseAs'])) { + return $options['falseAs']; + } return $val ? 'true' : 'false'; } @@ -215,7 +230,7 @@ protected function dumpCallable(Abstraction $abs) } return 'callable ' . '' - . $this->markupIdentifier($abs['value'], 'callable') + . $this->markupIdentifier($abs['value'], Type::TYPE_IDENTIFIER_METHOD) . ''; } @@ -300,6 +315,19 @@ protected function dumpString($val, $abs = null) return $this->string->dump($val, $abs); } + /** + * Dump Table + * + * @param Abstraction $abs Table abstraction + * + * @return string + */ + protected function dumpTable(Abstraction $abs) + { + $this->optionSet('tagName', null); + return $this->getTable()->dump($abs); + } + /** * Dump undefined * @@ -335,6 +363,19 @@ protected function getObject() return $this->lazyObject; } + /** + * Getter for this->table + * + * @return HtmlTable + */ + protected function getTable() + { + if (!$this->lazyTable) { + $this->lazyTable = new Table($this->dumper, $this->dumper->helper); + } + return $this->lazyTable; + } + /** * Get dump options * @@ -347,6 +388,7 @@ protected function getObject() protected function getPerValueOptions($val, $opts) { $parentOptions = parent::getPerValueOptions($val, $opts); + $isAbstraction = $val instanceof Abstraction; return $this->debug->arrayUtil->mergeDeep( array( 'attribs' => array( @@ -354,13 +396,13 @@ protected function getPerValueOptions($val, $opts) ), 'dumpType' => true, 'postDump' => null, - 'tagName' => $parentOptions['type'] === Type::TYPE_OBJECT + 'tagName' => $parentOptions['type'] === Type::TYPE_OBJECT && (!$isAbstraction || !($val['cfgFlags'] & AbstractObject::BRIEF)) ? 'div' : 'span', ), $parentOptions, array( - 'attribs' => $val instanceof Abstraction && \is_array($val['attribs']) + 'attribs' => $isAbstraction && \is_array($val['attribs']) ? $val['attribs'] : [], ) diff --git a/src/Debug/Dump/Substitution.php b/src/Debug/Dump/Substitution.php index fc4c03d5..8c38adcc 100644 --- a/src/Debug/Dump/Substitution.php +++ b/src/Debug/Dump/Substitution.php @@ -58,7 +58,7 @@ public function __construct(Dumper $dumper) * @see https://console.spec.whatwg.org/#formatter * @see https://developer.mozilla.org/en-US/docs/Web/API/console#Using_string_substitutions */ - public function process($args, $options = array()) + public function process(array $args, array $options = array()) { if (\is_string($args[0]) === false) { return $args; diff --git a/src/Debug/Dump/Text.php b/src/Debug/Dump/Text.php index bca953ce..ee191406 100644 --- a/src/Debug/Dump/Text.php +++ b/src/Debug/Dump/Text.php @@ -179,12 +179,9 @@ private function getArgGlue(array $args, $metaGlue) * * @return Value */ - protected function getValDumper() + protected function initValDumper() { - if (!$this->valDumper) { - $this->valDumper = new Value($this); - } - return $this->valDumper; + return new Value($this); } /** @@ -280,7 +277,7 @@ private function methodGroupBuildOutput(LogEntry $logEntry) ), $logEntry['meta']); $label = \array_shift($args); $label = $meta['isFuncName'] - ? $this->valDumper->markupIdentifier($label, 'method') + ? $this->valDumper->markupIdentifier($label, Type::TYPE_IDENTIFIER_METHOD) : $this->valDumper->dump($label, array('addQuotes' => false)); foreach ($args as $k => $v) { $args[$k] = $this->valDumper->dump($v); @@ -304,12 +301,6 @@ private function methodGroupBuildOutput(LogEntry $logEntry) */ protected function methodTabular(LogEntry $logEntry) { - $meta = $logEntry['meta']; - $logEntry->setMeta('forceArray', false); - parent::methodTabular($logEntry); - if (!empty($meta['caption'])) { - \array_unshift($logEntry['args'], $meta['caption']); - } - return $this->buildArgString($logEntry['args'], $meta); + return $this->methodDefault($logEntry); } } diff --git a/src/Debug/Dump/Text/TextObject.php b/src/Debug/Dump/Text/TextObject.php index 896d6e36..9a52fc00 100644 --- a/src/Debug/Dump/Text/TextObject.php +++ b/src/Debug/Dump/Text/TextObject.php @@ -12,6 +12,7 @@ use bdk\Debug\Abstraction\AbstractObject; use bdk\Debug\Abstraction\Object\Abstraction as ObjectAbstraction; +use bdk\Debug\Abstraction\Type; use bdk\Debug\Dump\Base\BaseObject; use bdk\Debug\Dump\Text\Value as ValDumper; @@ -32,7 +33,7 @@ class TextObject extends BaseObject */ public function dump(ObjectAbstraction $abs) { - $className = $this->valDumper->markupIdentifier($abs['className'], 'className'); + $className = $this->valDumper->markupIdentifier($abs['className'], Type::TYPE_IDENTIFIER_CLASSNAME); $str = $this->dumpSpecialCases($abs, $className); if ($str) { return $str; diff --git a/src/Debug/Dump/Text/Value.php b/src/Debug/Dump/Text/Value.php index 1975223d..6c87f46e 100644 --- a/src/Debug/Dump/Text/Value.php +++ b/src/Debug/Dump/Text/Value.php @@ -15,6 +15,7 @@ use bdk\Debug\Abstraction\Type; use bdk\Debug\Dump\Base\Value as BaseValue; use bdk\Debug\Dump\Text\TextObject; +use bdk\Table\Table as BdkTable; /** * Dump val as plain text @@ -182,6 +183,28 @@ protected function dumpString($val, $abs = null) : $val; } + /** + * Dump Table + * + * @param Abstraction $abs Table abstraction + * + * @return string + */ + protected function dumpTable(Abstraction $abs) + { + $data = $abs->getValues(); + $table = new BdkTable($data); + $tableAsArray = \bdk\Table\Utility::asArray($table); + $caption = $table->getCaption() + ? $this->dump($table->getCaption()->getHtml(), array( + 'addQuotes' => false, + )) + : ''; + return $caption + ? $caption . "\n" . \str_repeat('-', \strlen($caption)) . "\n" . $this->dumpArray($tableAsArray) + : $this->dumpArray($tableAsArray); + } + /** * Dump undefined * diff --git a/src/Debug/Dump/TextAnsi.php b/src/Debug/Dump/TextAnsi.php index e5858648..76709040 100644 --- a/src/Debug/Dump/TextAnsi.php +++ b/src/Debug/Dump/TextAnsi.php @@ -32,7 +32,7 @@ class TextAnsi extends Text 'excluded' => "\e[38;5;9m", // red 'false' => "\e[91m", // red 'keyword' => "\e[38;5;45m", // blue - 'maxlen' => "\e[30;48;5;41m", // black foreground / light-green background + 'maxLen' => "\e[30;48;5;41m", // black foreground / light-green background 'muted' => "\e[38;5;250m", // dark grey 'numeric' => "\e[96m", // blue 'operator' => "\e[38;5;224m", // "misty rose" @@ -99,13 +99,11 @@ public function processLogEntry(LogEntry $logEntry) * * @return Value */ - protected function getValDumper() + protected function initValDumper() { - if (!$this->valDumper) { - $this->valDumper = new Value($this); - $this->valDumper->setCfg($this->cfg); - } - return $this->valDumper; + $valDumper = new Value($this); + $valDumper->setCfg($this->cfg); + return $valDumper; } /** diff --git a/src/Debug/Dump/TextAnsi/TextAnsiObject.php b/src/Debug/Dump/TextAnsi/TextAnsiObject.php index 45ff0e43..abac42ae 100644 --- a/src/Debug/Dump/TextAnsi/TextAnsiObject.php +++ b/src/Debug/Dump/TextAnsi/TextAnsiObject.php @@ -12,6 +12,7 @@ use bdk\Debug\Abstraction\AbstractObject; use bdk\Debug\Abstraction\Object\Abstraction as ObjectAbstraction; +use bdk\Debug\Abstraction\Type; use bdk\Debug\Dump\Text\TextObject; use bdk\Debug\Dump\TextAnsi\Value as ValDumper; @@ -20,6 +21,9 @@ */ class TextAnsiObject extends TextObject { + const ANSI_UNDERLINE = "\e[4m"; + const ANSI_NO_UNDERLINE = "\e[24m"; + /** @var ValDumper */ public $valDumper; @@ -32,7 +36,7 @@ class TextAnsiObject extends TextObject */ public function dump(ObjectAbstraction $abs) { - $className = $this->valDumper->markupIdentifier($abs['className'], 'className'); + $className = $this->valDumper->markupIdentifier($abs['className'], Type::TYPE_IDENTIFIER_CLASSNAME); $str = $this->dumpSpecialCases($abs, $className); if ($str) { return $str; @@ -90,7 +94,7 @@ protected function dumpMethods(ObjectAbstraction $abs, array $cfg) . $escapeCodes['numeric'] . $count . $escapeReset . "\n"; }, \array_keys($counts), $counts); $header = $counts - ? "\e[4m" . $this->debug->i18n->trans('object.methods') . ":\e[24m" + ? self::ANSI_UNDERLINE . $this->debug->i18n->trans('object.methods') . ':' . self::ANSI_NO_UNDERLINE : $this->debug->i18n->trans('object.methods.none'); return ' ' . $header . "\n" . \implode('', $counts); } diff --git a/src/Debug/Dump/TextAnsi/Value.php b/src/Debug/Dump/TextAnsi/Value.php index 347a0f0d..2303f9bb 100644 --- a/src/Debug/Dump/TextAnsi/Value.php +++ b/src/Debug/Dump/TextAnsi/Value.php @@ -17,6 +17,7 @@ use bdk\Debug\Dump\TextAnsi as Dumper; use bdk\Debug\Dump\TextAnsi\TextAnsiObject; use bdk\Debug\Utility\Utf8; +use bdk\Table\Table as BdkTable; /** * Base output plugin @@ -78,10 +79,9 @@ public function setEscapeReset($escapeReset = "\e[0m") * * @return string */ - public function markupIdentifier($val, $what = 'className') + public function markupIdentifier($val, $what = Type::TYPE_IDENTIFIER_CLASSNAME) { $parts = $this->parseIdentifier($val, $what); - $escapeReset = $this->escapeReset; $operator = $this->cfg['escapeCodes']['operator'] . $parts['operator'] . $this->escapeReset; $identifier = ''; $classnameOut = $parts['classname'] @@ -91,9 +91,7 @@ public function markupIdentifier($val, $what = 'className') ? $this->markupIdentifierNamespace($parts['namespace']) : ''; if ($parts['name']) { - $this->escapeReset = "\e[0;1m"; - $identifier = self::ANSI_BOLD . $this->highlightChars($parts['name']) . self::ANSI_BOLD_DIM_RESET; - $this->escapeReset = $escapeReset; + $identifier = self::ANSI_BOLD . $this->highlightChars($parts['name'], "\e[0;1m") . self::ANSI_BOLD_DIM_RESET; } $parts = \array_filter([$namespaceOut, $classnameOut, $identifier], 'strlen'); return \implode($operator, $parts); @@ -125,8 +123,8 @@ protected function dumpArray(array $array, $abs = null) $isNested = $this->valDepth > 0; $escapeCodes = $this->cfg['escapeCodes']; if ($this->optionGet('isMaxDepth')) { - return $this->cfg['escapeCodes']['keyword'] . 'array ' - . $this->cfg['escapeCodes']['recursion'] . '*' . $this->debug->i18n->trans('abs.max-depth') . '*' + return $escapeCodes['keyword'] . 'array ' + . $escapeCodes['recursion'] . '*' . $this->debug->i18n->trans('abs.max-depth') . '*' . $this->escapeReset; } $absKeys = isset($abs['keys']) @@ -183,9 +181,8 @@ private function dumpArrayValues(array $array, array $absKeys) */ protected function dumpBool($val) { - return $val - ? $this->cfg['escapeCodes']['true'] . 'true' . $this->escapeReset - : $this->cfg['escapeCodes']['false'] . 'false' . $this->escapeReset; + $key = $val ? 'true' : 'false'; + return $this->cfg['escapeCodes'][$key] . $key . $this->escapeReset; } /** @@ -207,9 +204,7 @@ protected function dumpFloat($val, $abs = null) } $date = $this->checkTimestamp($val, $abs); $val = $this->cfg['escapeCodes']['numeric'] . $val . $this->escapeReset; - return $date - ? '📅 ' . $val . ' ' . $this->cfg['escapeCodes']['muted'] . '(' . $date . ')' . $this->escapeReset - : $val; + return $this->formatWithDate($val, $date); } /** @@ -271,7 +266,9 @@ protected function dumpString($val, $abs = null) \bdk\Debug\Utility\PhpType::assertType($abs, self::TYPE_ABSTRACTION, 'abs'); if (\is_numeric($val)) { - return $this->dumpStringNumeric($val, $abs); + $date = $this->checkTimestamp($val, $abs); + $val = $this->cfg['escapeCodes']['numeric'] . $val . $this->escapeReset; + return $this->formatWithDate($this->addQuotes($val), $date); } if ($abs) { return $this->dumpStringAbs($abs); @@ -298,7 +295,7 @@ protected function dumpStringAbs(Abstraction $abs) $strLenDiff = $abs['strlen'] - $abs['strlenValue']; } if ($strLenDiff) { - $val .= $this->cfg['escapeCodes']['maxlen'] + $val .= $this->cfg['escapeCodes']['maxLen'] . '[' . $this->debug->i18n->trans('string.more-bytes', array('bytes' => $strLenDiff)) . ']' . $this->escapeReset; } @@ -330,24 +327,28 @@ protected function dumpStringBinary(Abstraction $abs) } /** - * Dump numeric string + * Dump table * - * @param string $val numeric string value - * @param Abstraction|null $abs (optional) full abstraction + * @param Abstraction $abs Table abstraction * * @return string */ - private function dumpStringNumeric($val, $abs = null) + protected function dumpTable(Abstraction $abs) { - \bdk\Debug\Utility\PhpType::assertType($abs, self::TYPE_ABSTRACTION, 'abs'); - - $escapeCodes = $this->cfg['escapeCodes']; - $date = $this->checkTimestamp($val, $abs); - $val = $escapeCodes['numeric'] . $val . $this->escapeReset; - $val = $this->addQuotes($val); - return $date - ? '📅 ' . $val . ' ' . $escapeCodes['muted'] . '(' . $date . ')' . $this->escapeReset - : $val; + $table = new BdkTable($abs->getValues()); + $tableAsArray = \bdk\Table\Utility::asArray($table); + $caption = $table->getCaption(); + if (!$caption) { + return $this->dumpArray($tableAsArray); + } + $caption = $this->dump($table->getCaption()->getHtml(), array( + 'addQuotes' => false, + )); + return self::ANSI_BOLD + . $caption . "\n" + . \str_repeat('-', \strlen($caption)) + . $this->escapeReset . "\n" + . $this->dumpArray($tableAsArray); } /** @@ -376,23 +377,39 @@ protected function getObject() /** * Highlight confusable and other characters * - * @param string $str HTML String to update + * @param string $str HTML String to update + * @param string|null $reset (optional) specify reset sequence * * @return string */ - protected function highlightChars($str) + protected function highlightChars($str, $reset = null) { $chars = $this->findChars($str); $charReplace = $this->optionGet('charReplace'); foreach ($chars as $char) { $replacement = $this->cfg['escapeCodes']['char'] . $this->charReplacement($char, $charReplace) - . $this->escapeReset; + . ($reset ?: $this->escapeReset); $str = \str_replace($char, $replacement, $str); } return $str; } + /** + * Format value with optional date annotation + * + * @param string $val formatted value + * @param string|null $date date string or null + * + * @return string + */ + private function formatWithDate($val, $date) + { + return $date + ? '📅 ' . $val . ' ' . $this->cfg['escapeCodes']['muted'] . '(' . $date . ')' . $this->escapeReset + : $val; + } + /** * Markup classname (namespace & classname) portion of identifier string * @@ -409,11 +426,7 @@ private function markupIdentifierClassname($classname) $classname = \substr($classname, $idx + 1); $classnameOut = $this->markupIdentifierNamespace($namespace); } - $escapeReset = $this->escapeReset; - $this->escapeReset = "\e[0;1m"; - $classnameOut .= self::ANSI_BOLD . $this->highlightChars($classname) . self::ANSI_BOLD_DIM_RESET; - $this->escapeReset = $escapeReset; - return $classnameOut; + return $classnameOut . self::ANSI_BOLD . $this->highlightChars($classname, "\e[0;1m") . self::ANSI_BOLD_DIM_RESET; } /** @@ -425,10 +438,6 @@ private function markupIdentifierClassname($classname) */ private function markupIdentifierNamespace($namespace) { - $escapeReset = $this->escapeReset; - $this->escapeReset = $this->cfg['escapeCodes']['muted']; - $namespace = $this->cfg['escapeCodes']['muted'] . $this->highlightChars($namespace) . $escapeReset; - $this->escapeReset = $escapeReset; - return $namespace; + return $this->cfg['escapeCodes']['muted'] . $this->highlightChars($namespace, $this->cfg['escapeCodes']['muted']) . $this->escapeReset; } } diff --git a/src/Debug/Framework/Laravel/CacheEventsSubscriber.php b/src/Debug/Framework/Laravel/CacheEventsSubscriber.php index 7de06605..206b577b 100644 --- a/src/Debug/Framework/Laravel/CacheEventsSubscriber.php +++ b/src/Debug/Framework/Laravel/CacheEventsSubscriber.php @@ -50,7 +50,7 @@ class CacheEventsSubscriber * * @SuppressWarnings(PHPMD.StaticAccess) */ - public function __construct($options = array(), $debug = null) + public function __construct(array $options = array(), $debug = null) { \bdk\Debug\Utility\PhpType::assertType($debug, 'bdk\Debug|null', 'debug'); diff --git a/src/Debug/Framework/Laravel/EventsSubscriber.php b/src/Debug/Framework/Laravel/EventsSubscriber.php index d29bfff4..18a7c16c 100644 --- a/src/Debug/Framework/Laravel/EventsSubscriber.php +++ b/src/Debug/Framework/Laravel/EventsSubscriber.php @@ -63,7 +63,7 @@ public function __construct($debug = null) * * @return void */ - public function onWildcardEvent($name = null, $payload = array()) + public function onWildcardEvent($name = null, array $payload = array()) { $groupParams = [$name]; if (\preg_match('/^(\S+):\s+(\S+)$/', $name, $matches)) { diff --git a/src/Debug/Framework/Laravel/LogViews.php b/src/Debug/Framework/Laravel/LogViews.php index 5a1e599a..d34a3e9f 100644 --- a/src/Debug/Framework/Laravel/LogViews.php +++ b/src/Debug/Framework/Laravel/LogViews.php @@ -100,7 +100,7 @@ protected function logView(View $view) ? 'blade' : \pathinfo($path, PATHINFO_EXTENSION)), )); - $this->viewChannel->log('view', $info, $this->viewChannel->meta('detectFiles')); + $this->viewChannel->log('view', $info); } /** diff --git a/src/Debug/Framework/Laravel/ServiceProvider.php b/src/Debug/Framework/Laravel/ServiceProvider.php index 2c7a77c0..ee350682 100644 --- a/src/Debug/Framework/Laravel/ServiceProvider.php +++ b/src/Debug/Framework/Laravel/ServiceProvider.php @@ -18,10 +18,10 @@ use bdk\Debug\Framework\Laravel\Middleware; use bdk\Debug\LogEntry; use bdk\Debug\Utility\ArrayUtil; -use bdk\Debug\Utility\TableRow; use bdk\ErrorHandler\Error; use Illuminate\Contracts\Http\Kernel; use Illuminate\Support\ServiceProvider as BaseServiceProvider; +use ReflectionClass; /** * PhpDebugConsole @@ -105,19 +105,18 @@ public function onOutput() 'nested' => false, ); $debug = $this->debug->getChannel($channelKey, $channelOptions); - $tableInfoRows = array(); - $modelCounts = $this->buildModelCountTable($tableInfoRows); + $modelCounts = $this->buildModelCountTable(); $debug->table('Model Usage', $modelCounts, $debug->meta(array( - 'columnNames' => array( - TableRow::INDEX => 'model', - TableRow::SCALAR => 'count', - ), - 'detectFiles' => true, - 'sortable' => true, - 'tableInfo' => array( - 'rows' => $tableInfoRows, + 'columnMeta' => array( + 'model' => array( + 'attribs' => array( + 'scope' => 'row', + ), + 'tagName' => 'th', + ), ), - 'totalCols' => [TableRow::SCALAR], + 'inclIndex' => false, + 'totalCols' => ['count'], ))); } @@ -137,25 +136,22 @@ public function shouldCollect($name, $default = false) /** * Process the stored model counts for outputting as table * - * @param array $tableInfoRows gets updated with tableInfo.rows for table - * * @return array */ - private function buildModelCountTable(&$tableInfoRows) + private function buildModelCountTable() { $modelCounts = array(); - $tableInfoRows = array(); foreach ($this->modelCounts as $class => $count) { - $ref = new \ReflectionClass($class); - $modelCounts[] = $count; - $tableInfoRows[] = array( - 'key' => $this->debug->abstracter->crateWithVals($class, array( + $ref = new ReflectionClass($class); + $modelCounts[] = array( + 'model' => $this->debug->abstracter->crateWithVals($class, array( 'attribs' => array( 'data-file' => $ref->getFileName(), ), 'type' => Type::TYPE_IDENTIFIER, 'typeMore' => Type::TYPE_IDENTIFIER_CLASSNAME, )), + 'count' => $count, ); } return $modelCounts; diff --git a/src/Debug/Framework/WordPress/Hooks.php b/src/Debug/Framework/WordPress/Hooks.php index 12fd6f8f..e74902b2 100644 --- a/src/Debug/Framework/WordPress/Hooks.php +++ b/src/Debug/Framework/WordPress/Hooks.php @@ -89,20 +89,18 @@ public function onOutput(Event $event) private function hookTableMeta() { return array( - 'columns' => ['isFilter', 'count'], - 'tableInfo' => array( - 'columns' => array( - 'count' => array( - 'key' => \_x('count', 'hooks.count.label', 'debug-console-php'), - ), - 'isFilter' => array( - 'attribs' => array('class' => ['text-center']), - 'falseAs' => '', - 'key' => \_x('isFilter', 'hooks.isFilter.label', 'debug-console-php'), - 'trueAs' => '', - ), + 'columnLabels' => array( + 'count' => \_x('count', 'hooks.count.label', 'debug-console-php'), + 'isFilter' => \_x('isFilter', 'hooks.isFilter.label', 'debug-console-php'), + ), + 'columnMeta' => array( + 'isFilter' => array( + 'attribs' => array('class' => ['text-center']), + 'falseAs' => '', + 'trueAs' => '', ), ), + 'columns' => ['isFilter', 'count'], 'totalCols' => ['count'], ); } @@ -110,7 +108,7 @@ private function hookTableMeta() /** * {@inheritDoc} */ - protected function postSetCfg($cfg = array(), $prev = array()) + protected function postSetCfg(array $cfg = array(), array $prev = array()) { $isFirstConfig = empty($this->cfg['configured']); $enabledChanged = isset($cfg['enabled']) && $cfg['enabled'] !== $prev['enabled']; diff --git a/src/Debug/Framework/WordPress/ObjectCache.php b/src/Debug/Framework/WordPress/ObjectCache.php index 7fb3a995..db729e87 100644 --- a/src/Debug/Framework/WordPress/ObjectCache.php +++ b/src/Debug/Framework/WordPress/ObjectCache.php @@ -57,12 +57,10 @@ public function onOutput(Event $event) $this->debug->log(\_x('Cache Misses', 'cache.misses', 'debug-console-php'), $GLOBALS['wp_object_cache']->cache_misses); $cacheInfo = $this->getCacheInfo(); - $this->debug->table($cacheInfo, $this->debug->meta('tableInfo', array( - 'columns' => array( - 'size' => array( - 'attribs' => array('class' => ['no-quotes']), - 'total' => $this->debug->utility->getBytes($this->totalCacheSize), - ), + $this->debug->table($cacheInfo, $this->debug->meta('columnMeta', array( + 'size' => array( + 'attribs' => array('class' => ['no-quotes']), + 'total' => $this->debug->utility->getBytes($this->totalCacheSize), ), ))); } diff --git a/src/Debug/Framework/WordPress/Shortcodes.php b/src/Debug/Framework/WordPress/Shortcodes.php index 4a24317a..899875bc 100644 --- a/src/Debug/Framework/WordPress/Shortcodes.php +++ b/src/Debug/Framework/WordPress/Shortcodes.php @@ -6,9 +6,10 @@ use bdk\Debug\AbstractComponent; use bdk\Debug\Abstraction\Type; use bdk\Debug\LogEntry; -use bdk\Debug\Utility\TableRow; use bdk\PubSub\Event; use bdk\PubSub\SubscriberInterface; +use bdk\Table\TableCell; +use bdk\Table\TableRow; /** * WordPress "shortcode" info @@ -61,14 +62,24 @@ public function onOutput() return; } - $this->debug->eventManager->subscribe(Debug::EVENT_LOG, [$this, 'onLog'], 1000); - $this->debug->table('shortcodes', $this->getShortCodeData(), $this->debug->meta(array( - 'columnNames' => array( - 'links' => \_x('links', 'shortcode.links', 'debug-console-php'), - TableRow::INDEX => 'shortcode', - ), - ))); - $this->debug->eventManager->unsubscribe(Debug::EVENT_LOG, [$this, 'onLog']); + $logEntry = new LogEntry( + $this->debug, + 'table', + [ + 'shortcodes', + $this->getShortCodeData(), + ], + array( + 'columnLabels' => array( + 'links' => \_x('links', 'shortcode.links', 'debug-console-php'), + \bdk\Table\Factory::KEY_INDEX => 'shortcode', + ), + 'sortable' => true, + ) + ); + $this->debug->rootInstance->getPlugin('methodTable')->doTable($logEntry); + $this->modifyShortcodeTable($logEntry); + $this->debug->log($logEntry); } /** @@ -79,31 +90,53 @@ public function onOutput() * * @return void */ - public function onLog(LogEntry $logEntry) + protected function modifyShortcodeTable(LogEntry $logEntry) { - if ($logEntry['method'] !== 'table') { - return; + $table = $logEntry['args'][0]; + $rowsNew = []; + foreach ($table->getRows() as $row) { + $rowsNew[] = $row; + $cells = $row->getCells(); + $shortcode = $cells[0]->getValue(); + $links = $cells[2]->getValue(); + $cells[2]->setValue( + $links . ' ' + ); + $rowsNew[] = $this->buildInfoRow($row); } + $table->setRows($rowsNew); + } - $dataNew = array(); - $rowsMeta = $logEntry['meta']['tableInfo']['rows']; - foreach ($logEntry['args'][0] as $shortcode => $row) { - $phpDoc = $this->debug->phpDoc->getParsed($row[0] . '()'); - $row[1] .= ' '; - $dataNew[$shortcode] = $row; - $dataNew[$shortcode . ' info'] = [$this->debug->abstracter->crateWithVals( - $this->buildPhpDoc($shortcode, $phpDoc), - array( - 'addQuotes' => false, - 'sanitize' => false, - 'visualWhiteSpace' => false, - ) - )]; - $rowsMeta[$shortcode . ' info'] = $this->buildInfoMeta($shortcode); - } - $logEntry['args'][0] = $dataNew; - $logEntry['meta']['tableInfo']['rows'] = $rowsMeta; - unset($logEntry['meta']['tableInfo']['columns'][2]); + /** + * Build the phpDoc info row for the shortcode + * + * @param TableRow $row TableRow instance + * + * @return TableRow + */ + private function buildInfoRow(TableRow $row) + { + $cells = $row->getCells(); + $shortcode = $cells[0]->getValue(); + $method = $cells[1]->getValue(); + $phpDoc = $this->debug->phpDoc->getParsed($method . '()'); + $infoCell = new TableCell($this->debug->abstracter->crateWithVals( + $this->buildPhpDoc($shortcode, $phpDoc), + array( + 'addQuotes' => false, + 'sanitize' => false, + 'visualWhiteSpace' => false, + ) + )); + $infoCell->setAttribs(array( + 'colspan' => 3, + )); + return (new TableRow()) + ->setAttribs(array( + 'id' => 'shortcode_' . $shortcode . '_doc', + 'style' => 'display: none;', + )) + ->appendCell($infoCell); } /** @@ -129,31 +162,6 @@ private function buildCodexLink($shortcode) : ''; } - /** - * Build the table meta info for the phpdoc row - * - * @param string $shortcode shortcode - * - * @return array - */ - private function buildInfoMeta($shortcode) - { - return array( - 'attribs' => array( - 'id' => 'shortcode_' . $shortcode . '_doc', - 'style' => 'display: none;', - ), - 'columns' => array( - array( - 'attribs' => array( - 'colspan' => 3, - ), - ), - ), - 'keyOutput' => false, - ); - } - /** * Build the phpDoc output for the shortcode * @@ -186,7 +194,7 @@ private function buildPhpDoc($shortCode, array $phpDoc) */ private function getShortCodeData() { - $tags = $GLOBALS['shortcode_tags']; // + array('whatgives' => [$this, 'onBootstrap'] + $tags = $GLOBALS['shortcode_tags']; // + array('whatGives' => [$this, 'onBootstrap'] $tags['embed'] = [$GLOBALS['wp_embed'], 'shortcode']; $shortcodeData = $this->debug->arrayUtil->mapWithKeys(function ($callable, $shortcode) { @@ -217,7 +225,7 @@ private function getShortCodeData() /** * {@inheritDoc} */ - protected function postSetCfg($cfg = array(), $prev = array()) + protected function postSetCfg(array $cfg = array(), array $prev = array()) { $isFirstConfig = empty($this->cfg['configured']); $enabledChanged = isset($cfg['enabled']) && $cfg['enabled'] !== $prev['enabled']; diff --git a/src/Debug/Framework/WordPress/WordPress.php b/src/Debug/Framework/WordPress/WordPress.php index 06e547bd..4a1bf107 100644 --- a/src/Debug/Framework/WordPress/WordPress.php +++ b/src/Debug/Framework/WordPress/WordPress.php @@ -197,7 +197,7 @@ private function logShowOnFront() /** * {@inheritDoc} */ - protected function postSetCfg($cfg = array(), $prev = array()) + protected function postSetCfg(array $cfg = array(), array $prev = array()) { $isFirstConfig = empty($this->cfg['configured']); $enabledChanged = isset($cfg['enabled']) && $cfg['enabled'] !== $prev['enabled']; diff --git a/src/Debug/Framework/WordPress/WpDb.php b/src/Debug/Framework/WordPress/WpDb.php index 68920c0f..f136280e 100644 --- a/src/Debug/Framework/WordPress/WpDb.php +++ b/src/Debug/Framework/WordPress/WpDb.php @@ -155,7 +155,7 @@ private function logRuntime() /** * {@inheritDoc} */ - protected function postSetCfg($cfg = array(), $prev = array()) + protected function postSetCfg(array $cfg = array(), array $prev = array()) { $isFirstConfig = empty($this->cfg['configured']); $enabledChanged = isset($cfg['enabled']) && $cfg['enabled'] !== $prev['enabled']; diff --git a/src/Debug/Framework/WordPress/WpHttp.php b/src/Debug/Framework/WordPress/WpHttp.php index 9b59ea82..d52577e5 100644 --- a/src/Debug/Framework/WordPress/WpHttp.php +++ b/src/Debug/Framework/WordPress/WpHttp.php @@ -158,7 +158,7 @@ private function isResponseError($response) /** * {@inheritDoc} */ - protected function postSetCfg($cfg = array(), $prev = array()) + protected function postSetCfg(array $cfg = array(), array $prev = array()) { $isFirstConfig = empty($this->cfg['configured']); $enabledChanged = isset($cfg['enabled']) && $cfg['enabled'] !== $prev['enabled']; diff --git a/src/Debug/Framework/Yii1_1/LogRoute.php b/src/Debug/Framework/Yii1_1/LogRoute.php index a6c58560..a4fa5700 100644 --- a/src/Debug/Framework/Yii1_1/LogRoute.php +++ b/src/Debug/Framework/Yii1_1/LogRoute.php @@ -65,7 +65,7 @@ class LogRoute extends CLogRoute * * @SuppressWarnings(PHPMD.StaticAccess) */ - public function __construct($debug = null, $opts = array()) + public function __construct($debug = null, array $opts = array()) { \bdk\Debug\Utility\PhpType::assertType($debug, 'bdk\Debug|null', 'debug'); diff --git a/src/Debug/Framework/Yii2/LogTarget.php b/src/Debug/Framework/Yii2/LogTarget.php index d6337b32..f7bdc6e8 100644 --- a/src/Debug/Framework/Yii2/LogTarget.php +++ b/src/Debug/Framework/Yii2/LogTarget.php @@ -47,7 +47,7 @@ class LogTarget extends Target * * @SuppressWarnings(PHPMD.StaticAccess) */ - public function __construct($debug = null, $config = array()) + public function __construct($debug = null, array $config = array()) { \bdk\Debug\Utility\PhpType::assertType($debug, 'bdk\Debug|null', 'debug'); diff --git a/src/Debug/LogEntry.php b/src/Debug/LogEntry.php index 9885161e..d705d0ab 100644 --- a/src/Debug/LogEntry.php +++ b/src/Debug/LogEntry.php @@ -51,7 +51,7 @@ class LogEntry extends Event implements JsonSerializable * @param array $defaultArgs default arguments (key/value array) * @param array $argsToMeta move specified keys to meta */ - public function __construct(Debug $subject, $method, $args = array(), $meta = array(), $defaultArgs = array(), $argsToMeta = array()) + public function __construct(Debug $subject, $method, array $args = [], array $meta = array(), array $defaultArgs = array(), array $argsToMeta = []) { $this->subject = $subject; $this->values = array( @@ -230,7 +230,7 @@ private function crateSetCfg() } $detectFiles = $this->getMeta('detectFiles', false); $detectFilesWas = $detectFiles !== null - ? $this->subject->abstracter->abstractString->setCfg('detectFiles', true) + ? $this->subject->abstracter->abstractString->setCfg('detectFiles', $detectFiles) : $detectFiles; if ($detectFilesWas !== $detectFiles) { $cfgRestore['detectFiles'] = $detectFilesWas; @@ -306,7 +306,7 @@ private function metaExtract(&$array) * * @return void */ - protected function onSet($values = array()) + protected function onSet(array $values = array()) { if (isset($values['meta']) === false) { return; diff --git a/src/Debug/Plugin/LogFiles.php b/src/Debug/Plugin/LogFiles.php index 8fd4a180..54a77bc3 100644 --- a/src/Debug/Plugin/LogFiles.php +++ b/src/Debug/Plugin/LogFiles.php @@ -257,10 +257,7 @@ private function logFilesAsTree($files) 'expand' => true, ), ) - ), - $this->debug->meta(array( - 'detectFiles' => true, - )) + ) ); $this->debug->setCfg('maxDepth', $maxDepthBak, Debug::CONFIG_NO_PUBLISH | Debug::CONFIG_NO_RETURN); } diff --git a/src/Debug/Plugin/LogPhp.php b/src/Debug/Plugin/LogPhp.php index 47fcb1d0..c2612ca3 100644 --- a/src/Debug/Plugin/LogPhp.php +++ b/src/Debug/Plugin/LogPhp.php @@ -283,12 +283,12 @@ private function logPhpIni() $this->debug->abstracter->crateWithVals( $iniFiles, array( + 'detectFiles' => true, 'options' => array( 'showListKeys' => false, ), ) - ), - $this->debug->meta('detectFiles') + ) ); } diff --git a/src/Debug/Plugin/LogRequest.php b/src/Debug/Plugin/LogRequest.php index f28db289..c12af745 100644 --- a/src/Debug/Plugin/LogRequest.php +++ b/src/Debug/Plugin/LogRequest.php @@ -50,10 +50,8 @@ public function getSubscriptions() */ public function onBootstrap(Event $event) { - $this->debug = $event->getSubject()->getChannel($this->cfg['channelKey'], $this->cfg['channelOptions']); - $collectWas = $this->debug->setCfg('collect', true); + $this->debug = $event->getSubject(); $this->logRequest(); - $this->debug->setCfg('collect', $collectWas, Debug::CONFIG_NO_RETURN); } /** @@ -66,6 +64,8 @@ public function logRequest() if ($this->testLogRequest() === false) { return; } + $this->debug = $this->debug->rootInstance->getChannel($this->cfg['channelKey'], $this->cfg['channelOptions']); + $collectWas = $this->debug->setCfg('collect', true); $this->debug->log( $this->debug->i18n->trans('request'), $this->debug->meta(array( @@ -87,6 +87,7 @@ public function logRequest() $this->logRequestCookies(); $this->logPostOrInput(); $this->logFiles(); + $this->debug->setCfg('collect', $collectWas, Debug::CONFIG_NO_RETURN); } /** @@ -116,7 +117,7 @@ private function logFiles() 'type' => $uploadedFile->getClientMediaType(), ); }, $files); - $this->debug->log('$_FILES', $files); + $this->debug->log('$_FILES', $files, $this->debug->meta('detectFiles')); } /** diff --git a/src/Debug/Plugin/LogResponse.php b/src/Debug/Plugin/LogResponse.php index bc03182b..ba873111 100644 --- a/src/Debug/Plugin/LogResponse.php +++ b/src/Debug/Plugin/LogResponse.php @@ -55,7 +55,8 @@ public function getSubscriptions() */ public function onBootstrap(Event $event) { - $this->debug = $event->getSubject()->getChannel($this->cfg['channelKey'], $this->cfg['channelOptions']); + // logResponse will update to request/response channel + $this->debug = $event->getSubject(); } /** @@ -97,6 +98,7 @@ public function logResponse() if ($this->debug->rootInstance->getCfg('logResponse', Debug::CONFIG_DEBUG) === false) { return; } + $this->debug = $this->debug->rootInstance->getChannel($this->cfg['channelKey'], $this->cfg['channelOptions']); $this->debug->log( $this->debug->i18n->trans('response'), $this->debug->meta(array( diff --git a/src/Debug/Plugin/Method/General.php b/src/Debug/Plugin/Method/General.php index 3a019296..500119a8 100644 --- a/src/Debug/Plugin/Method/General.php +++ b/src/Debug/Plugin/Method/General.php @@ -103,23 +103,24 @@ public function errorStats() } /** - * Get dumper + * Get dumper from container * - * @param string $name classname - * @param bool $checkOnly (false) only check if initialized + * @param string $name classname + * @param string|null $route (null) additional identifier to distinguish dumpers + * @param bool $checkOnly (false) only check if initialized * * @return \bdk\Debug\Dump\Base|bool * * @psalm-return ($checkOnly is true ? bool : \bdk\Debug\Dump\Base) */ - public function getDump($name, $checkOnly = false) + public function getDump($name, $route = null, $checkOnly = false) { /** @var \bdk\Debug\Dump\Base|bool */ - return $this->getDumpRoute('dump', $name, $checkOnly); + return $this->getDumpRoute('dump', $name, $route, $checkOnly); } /** - * Get route + * Get route from container * * @param string $name classname * @param bool $checkOnly (false) only check if initialized @@ -131,7 +132,7 @@ public function getDump($name, $checkOnly = false) public function getRoute($name, $checkOnly = false) { /** @var \bdk\Debug\Route\RouteInterface|bool */ - return $this->getDumpRoute('route', $name, $checkOnly); + return $this->getDumpRoute('route', $name, null, $checkOnly); } /** @@ -207,19 +208,20 @@ public function setErrorCaller($caller = null) } /** - * Get Dump or Route instance + * Get Dump or Route instance from container * * @param 'dump'|'route' $cat "Category" (dump or route) - * @param string $name html, text, etc) + * @param string $name base, html, text, etc + * @param string|null $nameMore additional identifier to distinguish dumpers * @param bool $checkOnly Only check if initialized? * * @return \bdk\Debug\Dump\Base|RouteInterface|bool * * @psalm-return ($checkOnly is true ? bool : \bdk\Debug\Dump\Base|RouteInterface) */ - private function getDumpRoute($cat, $name, $checkOnly) + private function getDumpRoute($cat, $name, $nameMore = null, $checkOnly = false) { - $property = $cat . \ucfirst($name); + $property = $cat . \ucfirst($name) . ($nameMore ? \ucfirst($nameMore) : ''); $isDefined = isset($this->debug->{$property}); if ($checkOnly) { return $isDefined; @@ -252,8 +254,8 @@ private function getDumpRoute($cat, $name, $checkOnly) private function getDumpRouteInit($property, $classname) { /** @var \bdk\Debug\Dump\Base|RouteInterface */ - $val = $this->debug->container->getObject($classname); - $this->debug->container->addAlias($property, $classname); + $val = $this->debug->container->getObject($classname, false); + $this->debug->container[$property] = $val; if ($val instanceof ConfigurableInterface) { $cfg = $this->debug->getCfg($property, Debug::CONFIG_INIT); $val->setCfg($cfg); diff --git a/src/Debug/Plugin/Method/Group.php b/src/Debug/Plugin/Method/Group.php index fccc10f0..0694fbdc 100644 --- a/src/Debug/Plugin/Method/Group.php +++ b/src/Debug/Plugin/Method/Group.php @@ -284,7 +284,7 @@ private function doGroup(LogEntry $logEntry) * * @return array */ - private function autoArgs($caller = array()) + private function autoArgs(array $caller = array()) { $args = array(); if (isset($caller['function']) === false) { diff --git a/src/Debug/Plugin/Method/GroupCleanup.php b/src/Debug/Plugin/Method/GroupCleanup.php index 19568805..29d89c2d 100644 --- a/src/Debug/Plugin/Method/GroupCleanup.php +++ b/src/Debug/Plugin/Method/GroupCleanup.php @@ -31,7 +31,6 @@ class GroupCleanup implements SubscriberInterface /** @var array */ private $cleanupInfo = array( 'stack' => array(), - 'stackCount' => 0, ); /** @var Debug|null */ @@ -167,7 +166,6 @@ private function onOutputCleanup() 'groupCount' => 0, ), ), - 'stackCount' => 1, ); $reindex = false; for ($i = 0, $count = \count($this->log); $i < $count; $i++) { @@ -224,7 +222,7 @@ private function outputCleanupPLE($index) { $logEntry = $this->log[$index]; $method = $logEntry['method']; - $stackCount = $this->cleanupInfo['stackCount']; + $stackCount = \count($this->cleanupInfo['stack']); if (\in_array($method, ['group', 'groupCollapsed'], true)) { $this->cleanupInfo['stack'][] = array( 'childCount' => 0, // includes any child groups @@ -236,13 +234,11 @@ private function outputCleanupPLE($index) ); $this->cleanupInfo['stack'][$stackCount - 1]['childCount']++; $this->cleanupInfo['stack'][$stackCount - 1]['groupCount']++; - $this->cleanupInfo['stackCount']++; return false; } if ($method === 'groupEnd') { $group = \array_pop($this->cleanupInfo['stack']); $group['indexEnd'] = $index; - $this->cleanupInfo['stackCount']--; return $this->outputCleanupGroup($group); } $this->cleanupInfo['stack'][$stackCount - 1]['childCount']++; diff --git a/src/Debug/Plugin/Method/Profile.php b/src/Debug/Plugin/Method/Profile.php index 0d2d0846..d44e955f 100644 --- a/src/Debug/Plugin/Method/Profile.php +++ b/src/Debug/Plugin/Method/Profile.php @@ -19,6 +19,7 @@ use bdk\Debug\Utility\Profile as ProfileInstance; use bdk\PubSub\Event; use bdk\PubSub\SubscriberInterface; +use bdk\Table\Table as BdkTable; /** * Handle Debug's profile methods @@ -161,7 +162,9 @@ public function profileEnd($name = null) // @phpcs:ignore Generic.CodeAnalysis.U $this->debug, __FUNCTION__, \func_get_args(), - array(), + array( + 'sortable' => true, + ), $this->debug->rootInstance->reflection->getMethodDefaultArgs(__METHOD__), ['name'] ); @@ -224,6 +227,7 @@ private function doProfileEnd(LogEntry $logEntry) : 'profileEnd: ' . $this->debug->i18n->trans('method.profile.not-active') ); $debug->rootInstance->getPlugin('methodTable')->doTable($logEntry); + $this->updateTableKeyColumn($logEntry); $debug->log($logEntry); unset($this->instances[$name]); } @@ -244,18 +248,8 @@ private function doProfileEndArgs(LogEntry $logEntry) if (!$data) { return [$caption, $this->debug->i18n->trans('method.profile.no-data')]; } - $tableInfo = \array_replace_recursive(array( - 'rows' => \array_fill_keys(\array_keys($data), array()), - ), $logEntry->getMeta('tableInfo', array())); - foreach (\array_keys($data) as $k) { - $tableInfo['rows'][$k]['key'] = new Abstraction(Type::TYPE_IDENTIFIER, array( - 'typeMore' => Type::TYPE_IDENTIFIER_METHOD, - 'value' => $k, - )); - } $logEntry->setMeta(array( 'caption' => $caption, - 'tableInfo' => $tableInfo, 'totalCols' => ['ownTime'], )); return [$data]; @@ -296,4 +290,28 @@ private function onCfgEnableProfiling($val, $key, Event $event) // phpcs:ignore } return $val; } + + /** + * Update table key column (method name) to use method abstraction + * + * @param LogEntry $logEntry LogEntry instance + * + * @return void + */ + private function updateTableKeyColumn(LogEntry $logEntry) + { + $table = $logEntry['args'][0]; + if (($table instanceof BdkTable) === false) { + return; + } + foreach ($table->getRows() as $row) { + $cells = $row->getCells(); + $method = $cells[0]->getValue(); + $methodAbs = new Abstraction(Type::TYPE_IDENTIFIER, array( + 'typeMore' => Type::TYPE_IDENTIFIER_METHOD, + 'value' => $method, + )); + $cells[0]->setValue($methodAbs); + } + } } diff --git a/src/Debug/Plugin/Method/Table.php b/src/Debug/Plugin/Method/Table.php index d00b4831..d1b0564b 100644 --- a/src/Debug/Plugin/Method/Table.php +++ b/src/Debug/Plugin/Method/Table.php @@ -12,11 +12,13 @@ use bdk\Debug; use bdk\Debug\Abstraction\Abstracter; +use bdk\Debug\Abstraction\Abstraction; +use bdk\Debug\Abstraction\Type; use bdk\Debug\LogEntry; use bdk\Debug\Plugin\CustomMethodTrait; -use bdk\Debug\Utility\Table as TableProcessor; -use bdk\Debug\Utility\TableRow; use bdk\PubSub\SubscriberInterface; +use bdk\Table\Factory as TableFactory; +use bdk\Table\Table as BdkTable; /** * Table method @@ -28,12 +30,26 @@ class Table implements SubscriberInterface /** @var LogEntry */ private $logEntry; + /** @var array */ + private $tableMeta = [ + 'caption' => 'setter', + 'columnLabels' => 'option', // key => label + 'columnMeta' => 'option', // key => meta array + 'columns' => 'option', // list of keys + 'inclIndex' => 'option', + 'sortable' => 'meta', + 'totalCols' => 'option', + ]; + /** @var string[] */ protected $methods = [ 'table', 'doTable', ]; + /** @var TableFactory */ + protected $tableFactory; + /** * Constructor * @@ -46,7 +62,7 @@ public function __construct() /** * Output an array or object as a table * - * Accepts array of arrays or array of objects + * Accepts array/object of array/objects/values * * Parameters: * 1st encountered array (or traversable) is the data @@ -68,7 +84,10 @@ public function table() $logEntry = new LogEntry( $this->debug, __FUNCTION__, - \func_get_args() + \func_get_args(), + array( + 'sortable' => true, + ) ); $this->doTable($logEntry); $this->debug->log($logEntry); @@ -106,42 +125,89 @@ public function doTable(LogEntry $logEntry) } } + /** + * "getValInfo" callback used by TableFactory + * + * @param mixed $value Value to get type of + * @param bool $isRow Does value represent a row + * + * @return array + */ + public function getValInfo($value, $isRow = false) + { + $type = $this->debug->abstracter->type->getType($value)[0]; + $nonIterable = [ + 'UnitEnum', + 'Closure', + 'DateTime', + 'DateTimeImmutable', + ]; + $isIterable = true; + foreach ($nonIterable as $nonIterableType) { + if ($value instanceof $nonIterableType) { + $isIterable = false; + break; + } + } + return array( + 'className' => $type === Type::TYPE_OBJECT + ? ($value instanceof Abstraction + ? $value['className'] + : \get_class($value)) + : null, + 'iterable' => $isIterable, + 'type' => $type, + ); + } + /** * Process table log entry * - * @param LogEntry $logEntry log entry instance + * @param LogEntry $logEntry Log entry instance * * @return void */ private function doTableLogEntry(LogEntry $logEntry) { $this->initLogEntry($logEntry); - $table = new TableProcessor( + + $table = $this->getTableFactory()->create( isset($logEntry['args'][0]) ? $logEntry['args'][0] : null, - \array_replace_recursive(array( - 'columnNames' => array( - TableRow::SCALAR => $this->debug->i18n->trans('word.value'), - ), - ), $logEntry['meta']), - $this->debug + $this->optionsFromLogEntry($logEntry) ); - if ($table->haveRows()) { - $logEntry['args'] = [$table->getRows()]; - $logEntry['meta'] = $table->getMeta(); + if ($table->getRows()) { + $this->valsFromLogEntry($table, $logEntry); + $this->removeTableMetaFromLogEntry($logEntry); + $logEntry['args'] = [$table]; return; } - // no data... create log method logEntry instead + // no table rows... create log method logEntry instead $logEntry['method'] = 'log'; if ($logEntry->getMeta('caption')) { \array_unshift($logEntry['args'], $logEntry->getMeta('caption')); } elseif (\count($logEntry['args']) === 0) { $logEntry['args'] = [$this->debug->i18n->trans('method.table.no-args')]; } - $logEntry['meta'] = $table->getMeta(); + $this->removeTableMetaFromLogEntry($logEntry); + } + + /** + * Get or create TableFactory instance + * + * @return TableFactory + */ + private function getTableFactory() + { + if ($this->tableFactory === null) { + $this->tableFactory = new TableFactory(array( + 'getValInfo' => [$this, 'getValInfo'], + )); + } + return $this->tableFactory; } /** @@ -169,6 +235,64 @@ private function initLogEntry(LogEntry $logEntry) } } + /** + * Remove table related meta info from logEntry + * + * @param LogEntry $logEntry Log entry instance + * + * @return void + */ + private function removeTableMetaFromLogEntry(LogEntry $logEntry) + { + foreach (\array_keys($this->tableMeta) as $key) { + $logEntry->setMeta($key, null); + } + $logEntry['meta'] = \array_filter($logEntry['meta'], static function ($val) { + return $val !== null; + }); + } + + /** + * Get table options from logEntry meta + * + * @param LogEntry $logEntry Log entry instance + * + * @return array + */ + private function optionsFromLogEntry(LogEntry $logEntry) + { + $keys = \array_keys($this->tableMeta, 'option', true); + $meta = \array_intersect_key($logEntry['meta'], \array_flip($keys)); + return \array_replace_recursive(array( + 'columnLabels' => array( + TableFactory::KEY_SCALAR => $this->debug->i18n->trans('word.value'), + ), + ), $meta); + } + + /** + * Update table with values from logEntry meta + * + * @param BdkTable $table Table instance + * @param LogEntry $logEntry Log entry instance + * + * @return void + */ + private function valsFromLogEntry(BdkTable $table, LogEntry $logEntry) + { + $keys = \array_keys($this->tableMeta, 'setter', true); + foreach ($keys as $key) { + $val = $logEntry->getMeta($key); + $setter = 'set' . \ucfirst($key); + $table->$setter($val); + } + $keys = \array_keys($this->tableMeta, 'meta', true); + foreach ($keys as $key) { + $val = $logEntry->getMeta($key); + $table->setMeta($key, $val); + } + } + /** * Place argument as "data", "caption", "columns", or "other" * diff --git a/src/Debug/Plugin/Method/Trace.php b/src/Debug/Plugin/Method/Trace.php index db7aed95..87361294 100644 --- a/src/Debug/Plugin/Method/Trace.php +++ b/src/Debug/Plugin/Method/Trace.php @@ -18,6 +18,7 @@ use bdk\Debug\LogEntry; use bdk\Debug\Plugin\CustomMethodTrait; use bdk\PubSub\SubscriberInterface; +use bdk\Table\Table as BdkTable; /** * Trace method @@ -92,17 +93,21 @@ public function trace($inclContext = false, $caption = null, $limit = 0, $trace */ public function doTrace(LogEntry $logEntry) { - $this->debug = $logEntry->getSubject(); - $meta = $this->getMeta($logEntry); - $trace = $this->getTrace($logEntry); - if ($meta['inclContext']) { + $this->debug = $logEntry->getSubject(); // setting debug here as doTrace may be called directly + $this->setMetaDefaults($logEntry); + if ($logEntry->getMeta('inclContext')) { $this->debug->addPlugin($this->debug->pluginHighlight, 'highlight'); } - unset($meta['trace']); - $logEntry['args'] = [$trace]; - $logEntry['meta'] = $meta; - $this->evalRows($logEntry); + $trace = $this->getTrace($logEntry); + $logEntry['args'] = [$trace]; // $trace is still array here $this->debug->rootInstance->getPlugin('methodTable')->doTable($logEntry); + $this->updateMeta($logEntry); + /** @var \bdk\Table\Table */ + $table = $logEntry['args'][0]; + $this->updateTable($table, $trace); + $columnMeta = $table->getMeta('columns'); + $columnMeta[1]['attribs']['class'][] = 'no-quotes'; // file column + $table->setMeta('columns', $columnMeta); } /** @@ -114,7 +119,7 @@ public function doTrace(LogEntry $logEntry) */ private function getTrace(LogEntry $logEntry) { - $meta = $this->getMeta($logEntry); + $meta = $logEntry['meta']; $getOptions = $meta['inclArgs'] ? Backtrace::INCL_ARGS : 0; $getOptions |= $meta['inclInternal'] ? Backtrace::INCL_INTERNAL : 0; $trace = \is_array($meta['trace']) @@ -133,115 +138,6 @@ private function getTrace(LogEntry $logEntry) return $this->getTraceFinish($trace, $meta); } - /** - * Apply final adjustments to the trace - * - * @param array $trace Backtrace frames - * - * @return array - */ - private function getTraceFinish(array $trace) - { - $trace = $this->mapFilepaths($trace); - $this->setCommonFilePrefix($trace); - return \array_map(function ($frame) { - if ($frame['file'] === null) { - $frame['file'] = Abstracter::UNDEFINED; - } elseif ($frame['file'] !== 'eval()\'d code') { - $frame['file'] = $this->parseFilePath($frame['file'], $this->commonFilePrefix); - } - $frame['function'] = isset($frame['function']) - ? new Abstraction(Type::TYPE_IDENTIFIER, array( - 'typeMore' => Type::TYPE_IDENTIFIER_METHOD, - 'value' => $frame['function'], - )) - : Abstracter::UNDEFINED; // either not set or null - return $frame; - }, $trace); - } - - /** - * Remove Internal Frames - * - * @param array $trace Backtrace frames - * - * @return array - */ - private function removeMinInternal(array $trace) - { - $count = \count($trace); - $internalClasses = [ - __CLASS__, - 'bdk\PubSub\Manager', - ]; - for ($i = 3; $i < $count; $i++) { - $frame = $trace[$i]; - \preg_match('/^(?P.+)->(?P.+)$/', $frame['function'], $matches); - $isInternal = \in_array($matches['classname'], $internalClasses, true) || $matches['function'] === 'publishBubbleEvent'; - if ($isInternal === false) { - break; - } - } - return \array_slice($trace, $i); - } - - /** - * Set default meta values - * - * @param LogEntry $logEntry LogEntry instance - * - * @return array meta values - */ - private function getMeta(LogEntry $logEntry) - { - $meta = \array_merge(array( - 'caption' => $this->debug->i18n->trans('method.trace'), - 'columns' => ['file','line','function'], - 'inclArgs' => null, // incl arguments with context? - // will default to $inclContext - // may want to set meta['cfg']['objectsExclude'] = '*' - 'inclContext' => false, - 'inclInternal' => false, - 'limit' => 0, - 'sortable' => false, - 'trace' => null, // set to array or Exception to specify trace - ), $logEntry['meta']); - - if ($meta['inclArgs'] === null) { - $meta['inclArgs'] = $meta['inclContext']; - } - return $meta; - } - - /** - * Handle "eval()'d code" frames - * - * @param LogEntry $logEntry LogEntry instance - * - * @return void - */ - private function evalRows(LogEntry $logEntry) - { - $meta = \array_replace_recursive(array( - 'tableInfo' => array( - 'rows' => array(), - ), - ), $logEntry['meta']); - $trace = $logEntry['args'][0]; - foreach ($trace as $i => $frame) { - if (!empty($frame['evalLine'])) { - $meta['tableInfo']['rows'][$i]['attribs'] = array( - 'data-file' => (string) $frame['file'], - 'data-line' => $frame['line'], - ); - $trace[$i]['file'] = 'eval()\'d code'; - $trace[$i]['line'] = $frame['evalLine']; - } - } - $logEntry['meta'] = $meta; - $logEntry['args'] = [$trace]; - } - /** * Get trace args * @@ -275,25 +171,39 @@ private function getTraceArgs(array $argsPassed, array $argsDefault) ? $this->debug->i18n->trans('method.trace') . ' (' . $this->debug->i18n->trans('method.trace.limited', array('limit' => $argsTyped['limit'])) . ')' : $this->debug->i18n->trans('method.trace'); - $args = \array_merge($argsDefault, $argsTyped); - return \array_values($args); + return \array_values(\array_merge($argsDefault, $argsTyped)); } /** - * Update filepaths in trace according to filepathMap config + * Apply final adjustments to the trace * * @param array $trace Backtrace frames * * @return array */ - private function mapFilepaths(array $trace) + private function getTraceFinish(array $trace) { - return \array_map(function ($frame) { + $trace = \array_map(function ($frame) { if (isset($frame['file'])) { $frame['file'] = $this->debug->filepathMap($frame['file']); } return $frame; }, $trace); + $this->setCommonFilePrefix($trace); + return \array_map(function ($frame) { + if ($frame['file'] === null) { + $frame['file'] = Abstracter::UNDEFINED; + } elseif ($frame['file'] !== 'eval()\'d code') { + $frame['file'] = $this->parseFilePath($frame['file'], $this->commonFilePrefix); + } + $frame['function'] = isset($frame['function']) + ? new Abstraction(Type::TYPE_IDENTIFIER, array( + 'typeMore' => Type::TYPE_IDENTIFIER_METHOD, + 'value' => $frame['function'], + )) + : Abstracter::UNDEFINED; // either not set or null + return $frame; + }, $trace); } /** @@ -333,6 +243,27 @@ private function parseFilePath($filePath, $commonPrefix) )); } + /** + * Remove Internal Frames + * + * @param array $trace Backtrace frames + * + * @return array + */ + private function removeMinInternal(array $trace) + { + $count = \count($trace); + $internalClasses = [__CLASS__, 'bdk\PubSub\Manager']; + for ($i = 3; $i < $count; $i++) { + \preg_match('/^(?P.+)->(?P.+)$/', $trace[$i]['function'], $matches); + $isInternal = \in_array($matches['classname'], $internalClasses, true) || $matches['function'] === 'publishBubbleEvent'; + if ($isInternal === false) { + break; + } + } + return \array_slice($trace, $i); + } + /** * Determine the common file prefix for the given trace * @@ -346,10 +277,9 @@ private function setCommonFilePrefix(array $trace) if (\count($trace) < 2) { return; } - $trace = \array_filter($trace, static function ($frame) { + $files = \array_column(\array_filter($trace, static function ($frame) { return isset($frame['file']) && empty($frame['evalLine']); - }); - $files = \array_column($trace, 'file'); + }), 'file'); $commonPrefix = $this->debug->stringUtil->commonPrefix($files); // don't treat "/" as common prefix $this->commonFilePrefix = \strlen($commonPrefix) > 1 @@ -357,6 +287,34 @@ private function setCommonFilePrefix(array $trace) : ''; } + /** + * Set default meta values + * + * @param LogEntry $logEntry LogEntry instance + * + * @return void + */ + private function setMetaDefaults(LogEntry $logEntry) + { + $meta = \array_merge(array( + 'caption' => $this->debug->i18n->trans('method.trace'), + 'columns' => ['file','line','function'], + 'inclArgs' => null, // incl arguments with context? + // will default to $inclContext + // may want to set meta['cfg']['objectsExclude'] = '*' + 'inclContext' => false, + 'inclInternal' => false, + 'limit' => 0, + 'sortable' => false, + 'trace' => null, // set to array or Exception to specify trace + ), $logEntry['meta']); + + if ($meta['inclArgs'] === null) { + $meta['inclArgs'] = $meta['inclContext']; + } + $logEntry['meta'] = $meta; + } + /** * Remove internal frames and limit the number of frames * @@ -367,12 +325,60 @@ private function setCommonFilePrefix(array $trace) */ private function slice(array $trace, array $meta) { - if ($meta['inclInternal']) { - $trace = $this->removeMinInternal($trace); - } - if ($meta['limit'] > 0) { - $trace = \array_slice($trace, 0, $meta['limit']); - } + $trace = $meta['inclInternal'] + ? $this->removeMinInternal($trace) + : $trace; + $trace = $meta['limit'] > 0 + ? \array_slice($trace, 0, $meta['limit']) + : $trace; return $trace; } + + /** + * Move some meta vals from LogEntry to table arg + * + * @param LogEntry $logEntry LogEntry instance + * + * @return void + */ + private function updateMeta(LogEntry $logEntry) + { + $table = $logEntry['args'][0]; + $meta = $logEntry['meta']; + unset($meta['trace']); + $keys = ['inclArgs', 'inclContext']; + foreach ($keys as $key) { + $table->setMeta($key, $meta[$key]); + unset($meta[$key]); + } + $logEntry['meta'] = $meta; + } + + /** + * Add context and eval info to table rows + * + * @param BdkTable $table Table instance + * @param array $trace Backtrace frames + * + * @return void + */ + private function updateTable(BdkTable $table, array $trace) + { + $inclContext = $table->getMeta('inclContext'); + $rows = $table->getRows(); + \array_walk($rows, static function ($row, $i) use ($inclContext, $trace) { + $cells = $row->getCells(); + $frame = $trace[$i]; + if ($frame['evalLine']) { + $row->setAttrib('data-file', (string) $frame['file']); + $row->setAttrib('data-line', $frame['line']); + $cells[1]->setValue('eval()\'d code'); + $cells[2]->setValue($frame['evalLine']); + } + if ($inclContext && !empty($frame['context'])) { + $row->setMeta('context', $frame['context']); + $row->setMeta('args', $frame['args']); + } + }); + } } diff --git a/src/Debug/Plugin/Prettify.php b/src/Debug/Plugin/Prettify.php index 2d635fa3..3670e1ee 100644 --- a/src/Debug/Plugin/Prettify.php +++ b/src/Debug/Plugin/Prettify.php @@ -141,7 +141,7 @@ private function onPrettifyDo(Event $event, $type) $string = $this->debug->stringUtil->prettySql($string, $prettified); break; case 'xml': - $string = $this->debug->stringUtil->prettyXml($string); + $string = \rtrim($this->debug->stringUtil->prettyXml($string)); $prettified = true; } $event['highlightLang'] = $lang; diff --git a/src/Debug/Psr15/Middleware.php b/src/Debug/Psr15/Middleware.php index 0fcc65d0..99efc2cd 100644 --- a/src/Debug/Psr15/Middleware.php +++ b/src/Debug/Psr15/Middleware.php @@ -36,7 +36,7 @@ class Middleware extends AbstractComponent implements MiddlewareInterface * * @SuppressWarnings(PHPMD.StaticAccess) */ - public function __construct($debug = null, $cfg = array()) + public function __construct($debug = null, array $cfg = array()) { \bdk\Debug\Utility\PhpType::assertType($debug, 'bdk\Debug|null', 'debug'); diff --git a/src/Debug/Route/AbstractRoute.php b/src/Debug/Route/AbstractRoute.php index 98dff1af..576a5ede 100644 --- a/src/Debug/Route/AbstractRoute.php +++ b/src/Debug/Route/AbstractRoute.php @@ -355,7 +355,7 @@ protected function shouldInclude(LogEntry $logEntry) * * @return bool */ - protected function testChannelKeyMatch($channelKey, $channelKeys = array()) + protected function testChannelKeyMatch($channelKey, array $channelKeys = array()) { foreach ($channelKeys as $channelKeyTest) { if ($this->testChannelKey($channelKey, $channelKeyTest)) { diff --git a/src/Debug/Route/ChromeLogger.php b/src/Debug/Route/ChromeLogger.php index fb24de97..57715f4a 100644 --- a/src/Debug/Route/ChromeLogger.php +++ b/src/Debug/Route/ChromeLogger.php @@ -85,7 +85,7 @@ class ChromeLogger extends AbstractRoute public function __construct(Debug $debug) { parent::__construct($debug); - $this->dumper = $debug->getDump('base'); + $this->dumper = $debug->getDump('base', 'chromeLogger'); } /** @@ -297,7 +297,7 @@ private function reduceDataSummary() * * @return void */ - protected function reduceDataFill($logBack = array()) + protected function reduceDataFill(array $logBack = array()) { $indexes = \array_reverse(\array_keys($logBack)); $this->depth = 0; diff --git a/src/Debug/Route/Firephp.php b/src/Debug/Route/Firephp.php index 6d91bb17..f85aa0ec 100644 --- a/src/Debug/Route/Firephp.php +++ b/src/Debug/Route/Firephp.php @@ -13,6 +13,7 @@ use bdk\Debug; use bdk\Debug\LogEntry; use bdk\PubSub\Event; +use bdk\Table\Table as BdkTable; /** * Output log via FirePHP @@ -61,7 +62,8 @@ class Firephp extends AbstractRoute public function __construct(Debug $debug) { parent::__construct($debug); - $this->dumper = $debug->getDump('base'); + $this->dumper = $debug->getDump('base', 'firephp'); + $this->dumper->setCfg('undefinedAs', null); } /** @@ -215,31 +217,29 @@ private function methodGroup(LogEntry $logEntry) */ private function methodTabular(LogEntry $logEntry) { - $logEntry->setMeta('undefinedAs', 'null'); - $this->dumper->processLogEntry($logEntry); + $data = $logEntry['args'][0]->getValues(); + $table = new BdkTable($data); $logEntry['firephpMeta']['Type'] = $this->firephpMethods['table']; - $caption = $logEntry->getMeta('caption'); - if ($caption) { - $logEntry['firephpMeta']['Label'] = $caption; + if ($table->getCaption()) { + $logEntry['firephpMeta']['Label'] = $table->getCaption()->getHtml(); } - $args = $logEntry['args']; - $firephpTable = true; - if (!$firephpTable) { - return $this->dumper->valDumper->dump($args[0]); - } - $value = array(); - $keys = \array_map(static function (array $colInfo) { - return $colInfo['key']; - }, $logEntry['meta']['tableInfo']['columns']); - if ($logEntry['meta']['tableInfo']['haveObjRow']) { - \array_unshift($keys, '___class_name'); - } - \array_unshift($keys, ''); // key/index - $value[] = $keys; - foreach ($args[0] as $k => $row) { - $value[] = \array_merge([$k], \array_values($row)); + + $headerVals = \array_map(static function ($cell) { + return $cell->getValue(); + }, $table->getHeader()->getChildren()); + // initiate rows with header row + $rows = [ + $headerVals, + ]; + $tableAsArray = \bdk\Table\Utility::asArray($table, array( + 'forceArray' => true, + 'undefinedAs' => null, + )); + $tableAsArray = $this->dumper->valDumper->dump($tableAsArray); + foreach ($tableAsArray as $k => $row) { + $rows[] = \array_merge([$k], \array_values($row)); } - return $this->dumper->valDumper->dump($value); + return $this->dumper->valDumper->dump($rows); } /** diff --git a/src/Debug/Route/Html.php b/src/Debug/Route/Html.php index f12a1fe2..46818a00 100644 --- a/src/Debug/Route/Html.php +++ b/src/Debug/Route/Html.php @@ -313,7 +313,7 @@ private function buildOutput() /** * {@inheritDoc} */ - protected function postSetCfg($cfg = array(), $prev = array()) + protected function postSetCfg(array $cfg = array(), array $prev = array()) { $assetTypeAndPriority = array( 'css' => ['css', 0], diff --git a/src/Debug/Route/Html/FatalError.php b/src/Debug/Route/Html/FatalError.php index acea0338..687376c9 100644 --- a/src/Debug/Route/Html/FatalError.php +++ b/src/Debug/Route/Html/FatalError.php @@ -112,6 +112,7 @@ protected function buildBacktrace(array $trace) ); $this->debug->rootInstance->getPlugin('methodTrace')->doTrace($logEntry); $this->debug->setCfg($cfgWas, Debug::CONFIG_NO_PUBLISH | Debug::CONFIG_NO_RETURN); + $logEntry->crate(); return $this->routeHtml->dumper->processLogEntry($logEntry); } diff --git a/src/Debug/Route/Script.php b/src/Debug/Route/Script.php index ff9cba0f..f16a0b5d 100644 --- a/src/Debug/Route/Script.php +++ b/src/Debug/Route/Script.php @@ -56,7 +56,8 @@ class Script extends AbstractRoute public function __construct(Debug $debug) { parent::__construct($debug); - $this->dumper = $debug->getDump('base'); + $this->dumper = $debug->getDump('base', 'script'); + $this->dumper->setCfg('undefinedAs', Abstracter::UNDEFINED); } /** @@ -90,32 +91,14 @@ public function processLogEntries($event = null) */ public function processLogEntry(LogEntry $logEntry) { - $method = $logEntry['method']; - if (\in_array($method, ['table', 'trace'], true)) { - $logEntry->setMeta(array( - 'forceArray' => false, - 'undefinedAs' => Abstracter::UNDEFINED, - )); - } $this->dumper->processLogEntry($logEntry); $str = $this->buildConsoleCall($logEntry); - $str = \str_replace( - [ - // ensure that doesn't appear inside our ', - \json_encode(Type::TYPE_FLOAT_INF), - \json_encode(Type::TYPE_FLOAT_NAN), - \json_encode(Abstracter::UNDEFINED), - ], - [ - '<\\/script>', - 'Infinity', - 'NaN', - 'undefined', - ], - $str - ); - return $str; + return \strtr($str, array( + '' => '<\\/script>', + \json_encode(Abstracter::UNDEFINED) => 'undefined', + \json_encode(Type::TYPE_FLOAT_INF) => 'Infinity', + \json_encode(Type::TYPE_FLOAT_NAN) => 'NaN', + )); } /** diff --git a/src/Debug/Route/Stream.php b/src/Debug/Route/Stream.php index 9b4f29fa..ff3dde0d 100644 --- a/src/Debug/Route/Stream.php +++ b/src/Debug/Route/Stream.php @@ -303,7 +303,7 @@ private function setFilehandle($stream) /** * {@inheritDoc} */ - protected function postSetCfg($cfg = array(), $prev = array()) + protected function postSetCfg(array $cfg = array(), array $prev = array()) { if (\array_key_exists('stream', $cfg)) { // changing stream? diff --git a/src/Debug/Route/Text.php b/src/Debug/Route/Text.php index 729d8901..e4e29032 100644 --- a/src/Debug/Route/Text.php +++ b/src/Debug/Route/Text.php @@ -25,8 +25,6 @@ class Text extends AbstractRoute public function __construct(Debug $debug) { parent::__construct($debug); - if (!$this->dumper) { - $this->dumper = $debug->getDump('text'); - } + $this->dumper = $debug->getDump('text'); } } diff --git a/src/Debug/Route/WampCrate.php b/src/Debug/Route/WampCrate.php index 061fa2d5..85341f11 100644 --- a/src/Debug/Route/WampCrate.php +++ b/src/Debug/Route/WampCrate.php @@ -105,6 +105,9 @@ private function crateAbstraction(Abstraction $abs) $clone['valueDecoded'] = $this->crate($clone['valueDecoded']); } return $clone; + case Type::TYPE_TABLE: + $clone['rows'] = $this->crateArray($clone['rows']); + return $clone; } return $clone; } @@ -204,6 +207,7 @@ private function getErrorTraceMeta(LogEntry $logEntry) $meta ); $this->debug->rootInstance->getPlugin('methodTrace')->doTrace($logEntryTmp); + $logEntryTmp->crate(); return \array_replace_recursive( $logEntryTmp['meta'], array( diff --git a/src/Debug/Utility/FileStreamWrapper.php b/src/Debug/Utility/FileStreamWrapper.php index 7a193110..87502475 100644 --- a/src/Debug/Utility/FileStreamWrapper.php +++ b/src/Debug/Utility/FileStreamWrapper.php @@ -10,8 +10,8 @@ namespace bdk\Debug\Utility; -use bdk\Debug\Utility\Php; use bdk\Debug\Utility; +use bdk\Debug\Utility\Php; /** * Generic stream-wrapper which publishes Debug::EVENT_STREAM_WRAP when file is required/included diff --git a/src/Debug/Utility/FileTree.php b/src/Debug/Utility/FileTree.php index 3879179c..8c3d9ff5 100644 --- a/src/Debug/Utility/FileTree.php +++ b/src/Debug/Utility/FileTree.php @@ -28,7 +28,7 @@ class FileTree * * @return array */ - public function filesToTree($files, $excludedCounts = array(), $condense = false) + public function filesToTree(array $files, array $excludedCounts = array(), $condense = false) { $tree = array(); foreach ($files as $filepath) { diff --git a/src/Debug/Utility/HtmlBuild.php b/src/Debug/Utility/HtmlBuild.php index bdf95a85..c5cd170f 100644 --- a/src/Debug/Utility/HtmlBuild.php +++ b/src/Debug/Utility/HtmlBuild.php @@ -92,7 +92,7 @@ private static function buildAttribVal($name, $val) * * @return string|null */ - private static function buildAttribValArray($name, $values = array()) + private static function buildAttribValArray($name, array $values = array()) { if ($name === 'style') { $keyValues = []; diff --git a/src/Debug/Utility/PhpDoc.php b/src/Debug/Utility/PhpDoc.php index b8528ace..a6bc8028 100644 --- a/src/Debug/Utility/PhpDoc.php +++ b/src/Debug/Utility/PhpDoc.php @@ -76,7 +76,7 @@ class PhpDoc * * @param array $cfg Configuration */ - public function __construct($cfg = array()) + public function __construct(array $cfg = array()) { $this->cfg = \array_merge(array( 'sanitizer' => ['bdk\Debug\Utility\HtmlSanitize', 'sanitize'], diff --git a/src/Debug/Utility/PhpType.php b/src/Debug/Utility/PhpType.php index ccb6b625..4170832b 100644 --- a/src/Debug/Utility/PhpType.php +++ b/src/Debug/Utility/PhpType.php @@ -11,6 +11,7 @@ namespace bdk\Debug\Utility; use bdk\Debug\Utility\Php; +use Closure; use Exception; use InvalidArgumentException; use UnitEnum; @@ -108,7 +109,7 @@ public static function getDebugType($val, $opts = 0, &$isObject = false) * @param int $opts Bitmask of ENUM_AS_OBJECT flag * @param bool $isObject Whether the value is an object * Closure: false - * UnitEnum: true if Php::ENUM_AS_OBJECT flag passed + * UnitEnum: true/false depending on Php::ENUM_AS_OBJECT flag passed * other objects: true * * @return string @@ -120,7 +121,7 @@ public static function getDebugTypeObject($obj, $opts = 0, &$isObject = false) if ($obj instanceof UnitEnum && !$enumAsObject) { return \get_class($obj) . '::' . $obj->name; } - $isObject = $obj instanceof \Closure === false; + $isObject = $obj instanceof Closure === false; $class = \is_object($obj) ? \get_class($obj) : $obj; diff --git a/src/Debug/Utility/Profile.php b/src/Debug/Utility/Profile.php index 531910ea..132b7ea4 100644 --- a/src/Debug/Utility/Profile.php +++ b/src/Debug/Utility/Profile.php @@ -37,7 +37,7 @@ class Profile * * @param array $namespacesIgnore array of namespaces who's methods will be excluded from profile */ - public function __construct($namespacesIgnore = array()) + public function __construct(array $namespacesIgnore = array()) { $namespacesIgnore = \array_merge([$this->namespace], (array) $namespacesIgnore); $namespacesIgnore = \array_unique($namespacesIgnore); diff --git a/src/Debug/Utility/SerializeLog.php b/src/Debug/Utility/SerializeLog.php index ea9eff96..0dc4fa0b 100644 --- a/src/Debug/Utility/SerializeLog.php +++ b/src/Debug/Utility/SerializeLog.php @@ -11,14 +11,8 @@ namespace bdk\Debug\Utility; use bdk\Debug; -use bdk\Debug\Abstraction\Abstracter; -use bdk\Debug\Abstraction\Abstraction; -use bdk\Debug\Abstraction\AbstractObject; -use bdk\Debug\Abstraction\Object\Abstraction as ObjectAbstraction; -use bdk\Debug\Abstraction\Type; use bdk\Debug\LogEntry; -use bdk\Debug\Utility\Php; -use bdk\Debug\Utility\StringUtil; +use bdk\Debug\Utility\UnserializeLog; /** * Serialize / compress / base64 encode log data @@ -27,8 +21,6 @@ class SerializeLog { /** @var Debug */ protected static $debug; - /** @var bool */ - protected static $isLegacyData = false; /** * Import the config and data into the debug instance @@ -37,29 +29,12 @@ class SerializeLog * @param Debug|null $debug (optional) Debug instance * * @return Debug + * + * @deprecated 3.6 use UnserializeLog::import instead */ public static function import(array $data, $debug = null) { - \bdk\Debug\Utility\PhpType::assertType($debug, 'bdk\Debug|null', 'debug'); - - if (!$debug) { - $debug = new Debug(); - } - self::$isLegacyData = \version_compare($data['version'], '3.0', '<'); - self::$debug = $debug; - // set config for any channels already present in debug - foreach (\array_intersect_key($debug->getChannels(true, true), $data['config']['channels']) as $fqn => $channel) { - $channel->setCfg($data['config']['channels'][$fqn], Debug::CONFIG_NO_RETURN); - } - $debug->setCfg($data['config'], Debug::CONFIG_NO_RETURN); - unset($data['config'], $data['version']); - foreach (['alerts', 'log', 'logSummary'] as $cat) { - $data[$cat] = self::importGroup($cat, $data[$cat]); - } - foreach ($data as $k => $v) { - $debug->data->set($k, $v); - } - return $debug; + return UnserializeLog::import($data, $debug); } /** @@ -93,178 +68,12 @@ public static function serialize($data) * @param string $str serialized log data * * @return array|false - */ - public static function unserialize($str) - { - $str = self::extractLog($str); - $str = self::unserializeDecodeAndInflate($str); - $data = self::unserializeSafe($str); - if (!$data) { - return false; - } - $data = \array_merge(array( - 'config' => array( - 'channels' => array(), - ), - 'version' => '2.3', // prior to 3.0, we didn't include version - ), $data); - if (isset($data['rootChannel'])) { - $data['config']['channelName'] = $data['rootChannel']; - $data['config']['channels'] = array(); - unset($data['rootChannel']); - } - return $data; - } - - /** - * Extract serialized/encoded log data from between "START DEBUG" & "END DEBUG" - * - * @param string $str string containing serialized log - * - * @return string - */ - private static function extractLog($str) - { - $strStart = 'START DEBUG'; - $strEnd = 'END DEBUG'; - $regex = '/' . $strStart . '[\r\n]+(.+)[\r\n]+' . $strEnd . '/s'; - $matches = []; - if (\preg_match($regex, $str, $matches)) { - $str = $matches[1]; - } - return $str; - } - - /** - * Unserialize Log entry - * - * @param array $vals method, args, & meta values - * - * @return LogEntry - */ - private static function importLogEntry(array $vals) - { - $vals = \array_replace(['', array(), array()], $vals); - $vals[1] = self::importLegacy($vals[1]); - $logEntry = new LogEntry(self::$debug, $vals[0], $vals[1], $vals[2]); - if (self::$isLegacyData && $vals[0] === 'table') { - self::$debug->rootInstance->getPlugin('methodTable')->doTable($logEntry); - } - return $logEntry; - } - - /** - * "unserialize" log data - * - * @param string $cat ('alerts'|'log'|'logSummary') - * @param array $data data to unserialize * - * @return array + * @deprecated 3.6 use UnserializeLog::unserialize instead */ - private static function importGroup($cat, $data) - { - foreach ($data as $i => $val) { - if ($cat !== 'logSummary') { - $data[$i] = self::importLogEntry($val); - continue; - } - foreach ($val as $priority => $val2) { - $data[$i][$priority] = self::importLogEntry($val2); - } - } - return $data; - } - - /** - * Convert pre 3.0 serialized log entry args to 3.0 - * - * Prior to to v3.0, abstractions were stored as an array - * Find these arrays and convert them to Abstraction objects - * - * @param array $vals values or properties - * - * @return array - */ - private static function importLegacy(array $vals) - { - return \array_map(static function ($val) { - if (\is_array($val) === false) { - return $val; - } - return isset($val['debug']) && $val['debug'] === Abstracter::ABSTRACTION - ? self::importLegacyAbstraction($val) - : self::importLegacy($val); - }, $vals); - } - - /** - * Import legacy abstraction - * - * @param array $absValues Abstraction values - * - * @return Abstraction - */ - private static function importLegacyAbstraction(array $absValues) - { - $type = $absValues['type']; - unset($absValues['debug'], $absValues['type']); - return $type === Type::TYPE_OBJECT - ? self::importLegacyObj($absValues) - : new Abstraction($type, $absValues); - } - - /** - * Convert legacy object abstraction data - * - * @param array $absValues Abstraction values - * - * @return ObjectAbstraction - */ - private static function importLegacyObj(array $absValues) - { - $absValues['properties'] = self::importLegacy($absValues['properties']); - $absValues = self::importLegacyObjConvert($absValues); - $absValues = AbstractObject::buildValues($absValues); - $absValues = ObjectAbstraction::unserializeBuildValues($absValues); - - $absValues['methods'] = \array_map(static function (array $methodInfo) { - $methodInfo['phpDoc']['desc'] = $methodInfo['phpDoc']['description']; - unset($methodInfo['phpDoc']['description']); - return $methodInfo; - }, $absValues['methods']); - - $valueStore = self::$debug->abstracter->abstractObject->definition->getValueStoreDefault(); - return new ObjectAbstraction($valueStore, $absValues); - } - - /** - * Convert values - * - * @param array $absValues Object abstraction values - * - * @return array - */ - private static function importLegacyObjConvert($absValues) + public static function unserialize($str) { - if (isset($absValues['collectMethods'])) { - if ($absValues['collectMethods'] === false) { - $absValues['cfgFlags'] &= ~AbstractObject::METHOD_COLLECT; - } - unset($absValues['collectMethods']); - } - if (\array_key_exists('inheritedFrom', $absValues)) { - $absValues['declaredLast'] === $absValues['inheritedFrom']; - unset($absValues['inheritedFrom']); - } - if (\array_key_exists('overrides', $absValues)) { - $absValues['declaredPrev'] === $absValues['overrides']; - unset($absValues['overrides']); - } - if (\array_key_exists('originallyDeclared', $absValues)) { - $absValues['declaredOrig'] === $absValues['originallyDeclared']; - unset($absValues['originallyDeclared']); - } - return $absValues; + return UnserializeLog::unserialize($str); } /** @@ -313,12 +122,28 @@ private static function serializeGetData(Debug $debug) 'requestId', 'runtime', ])); + // Filter out orphaned definitions (not referenced by any logged abstraction) + $data['classDefinitions'] = self::filterOrphanedDefinitions($data['classDefinitions']); foreach (['alerts', 'log', 'logSummary'] as $cat) { $data[$cat] = self::serializeGroup($data[$cat]); } return $data; } + /** + * Filter out orphaned class definitions (not referenced by any logged abstraction) + * + * @param array $classDefinitions Class definitions + * + * @return array + */ + private static function filterOrphanedDefinitions(array $classDefinitions) + { + return \array_filter($classDefinitions, static function ($definition) { + return !empty($definition['__isUsed']); + }); + } + /** * "serialize" log data * @@ -341,50 +166,4 @@ private static function serializeGroup(array $data) } return $data; } - - /** - * base64 decode and gzinflate - * - * @param string $str compressed / encoded log data - * - * @return string|false - */ - private static function unserializeDecodeAndInflate($str) - { - $str = StringUtil::isBase64Encoded($str) - ? \base64_decode($str, true) - : false; - if ($str && \function_exists('gzinflate')) { - $strInflated = \gzinflate($str); - if ($strInflated) { - $str = $strInflated; - } - } - return $str; - } - - /** - * Safely unserialize data - * Handle legacy data - * - * @param string $serialized serialized array - * - * @return array|false - */ - private static function unserializeSafe($serialized) - { - $serialized = \preg_replace( - '/O:33:"bdk\\\Debug\\\Abstraction\\\Abstraction":((?:\d+):{s:4:"type";s:6:"object")/', - 'O:40:"bdk\\Debug\\Abstraction\\Object\Abstraction":$1', - (string) $serialized - ); - if (!$serialized) { - return false; - } - return Php::unserializeSafe($serialized, [ - 'bdk\\Debug\\Abstraction\\Abstraction', - 'bdk\\Debug\\Abstraction\\Object\\Abstraction', - 'bdk\\PubSub\\ValueStore', - ]); - } } diff --git a/src/Debug/Utility/StopWatch.php b/src/Debug/Utility/StopWatch.php index 465602af..4db8389f 100644 --- a/src/Debug/Utility/StopWatch.php +++ b/src/Debug/Utility/StopWatch.php @@ -33,7 +33,7 @@ class StopWatch * * @param array{requestTime?:float} $vals Initial values */ - public function __construct($vals = array()) + public function __construct(array $vals = array()) { $requestTimeDefault = isset($_SERVER['REQUEST_TIME_FLOAT']) ? (float) $_SERVER['REQUEST_TIME_FLOAT'] diff --git a/src/Debug/Utility/Table.php b/src/Debug/Utility/Table.php deleted file mode 100644 index c18f3038..00000000 --- a/src/Debug/Utility/Table.php +++ /dev/null @@ -1,477 +0,0 @@ - - * @license http://opensource.org/licenses/MIT MIT - * @copyright 2014-2025 Brad Kent - * @since 2.1 - */ - -namespace bdk\Debug\Utility; - -use bdk\Debug; -use bdk\Debug\Abstraction\Type; -use bdk\Debug\Utility\TableRow; - -/** - * Tablefy data. - * Ensure all row fields are in the same order - * - * @psalm-type meta = array{ - * columns: list, - * columnNames: array, - * tableInfo: array{ - * class: string|null, - * columns: array>, - * rows: array>, - * ..., - * }, - * totalCols: list, - * ..., - * } - */ -class Table -{ - /** @var Debug */ - private $debug; - /** @var meta */ - private $meta = array( - 'caption' => null, - 'columnNames' => array( - // specify column header label (may also specify via tableInfo/columns) - TableRow::SCALAR => 'value', - ), - 'columns' => [], // specify columns to collect/output - 'inclContext' => false, // for trace tables - 'sortable' => true, - 'tableInfo' => array( - 'class' => null, - 'columns' => array( - /* - array( - attribs - key // specify column header label (defaults to actual key or name specified in columnNames) - class // populated if all col values of the same class - falseAs: '' - total - trueAs: '' - ) - */ - ), - /* - 'commonRowInfo' => array( - // common info used for all rows.. only utilized when outputting - 'attribs' => array(), - 'class' => null, - 'keyOutput' => true, - 'summary' => '', - ), - */ - 'haveObjRow' => false, // if any row is an object (any object row will have rows[key]['class]) - 'indexLabel' => null, - 'rows' => array( - /* - key/index => array( - 'args' for traces - 'attribs - 'class' populated if row is an object - 'columns' - attribs - 'context' for traces - 'isScalar' - 'key' alternate key to display - 'summary' - ) - */ - ), - 'summary' => '', // if table is an obj... phpDoc summary - ), - 'totalCols' => array(), - ); - /** @var array */ - private $rows = array(); - - /** - * Constructor - * - * @param mixed $rows Table data - * @param array $meta Meta info / options - * @param Debug|null $debug Debug instance - */ - public function __construct($rows = array(), array $meta = array(), $debug = null) - { - \bdk\Debug\Utility\PhpType::assertType($debug, 'bdk\Debug|null', 'debug'); - - $this->debug = $debug ?: Debug::getInstance(); - $this->initMeta($meta); - $this->processRows($rows); - $this->setMeta(); - } - - /** - * Go through all the "rows" of array to determine what the keys are and their order - * - * @param array[]|TableRow[]|mixed[] $rows Array rows - * - * @return list - */ - public static function colKeys(array $rows) - { - $colKeys = array(); - foreach ($rows as $row) { - if (!$row instanceof TableRow) { - $row = new TableRow($row); - } - $curRowKeys = $row->keys(); - if ($curRowKeys !== $colKeys) { - $colKeys = self::colKeysMerge($curRowKeys, $colKeys); - } - } - return $colKeys; - } - - /** - * Get table rows - * - * @return array - */ - public function getRows() - { - return $this->rows; - } - - /** - * Get meta info - * - * @return meta - */ - public function getMeta() - { - return $this->meta; - } - - /** - * Do we have table data? - * - * @return bool - */ - public function haveRows() - { - return \is_array($this->rows) && \count($this->rows) > 0; - } - - /** - * Merge current row's keys with merged keys - * - * @param list $curRowKeys current row's keys - * @param list $colKeys all col keys - * - * @return list - */ - private static function colKeysMerge(array $curRowKeys, array $colKeys) - { - /** @var list */ - $newKeys = array(); - $count = \count($curRowKeys); - for ($i = 0; $i < $count; $i++) { - $curKey = $curRowKeys[$i]; - $position = \array_search($curKey, $colKeys, true); - if ($position !== false) { - $segment = \array_splice($colKeys, 0, (int) $position + 1); - /** @psalm-var list $newKeys */ - \array_splice($newKeys, \count($newKeys), 0, $segment); - } elseif (\in_array($curKey, $newKeys, true) === false) { - /** @psalm-var list $newKeys */ - $newKeys[] = $curKey; - } - } - // put on remaining colKeys - \array_splice($newKeys, \count($newKeys), 0, $colKeys); - /** @psalm-var list */ - return \array_values(\array_unique($newKeys)); - } - - /** - * Merge / initialize meta values - * - * @param array $meta Meta info / options - * - * @return void - */ - private function initMeta(array $meta) - { - /* - columns, columnNames, & totalCols will be moved to - tableInfo['columns'] structure - */ - /** @psalm-var meta */ - $this->meta = $this->debug->arrayUtil->mergeDeep($this->meta, $meta); - } - - /** - * Initialize this->meta['tableInfo']['columns'] - * - * @return void - */ - private function initTableInfoColumns() - { - $columnNames = $this->meta['columnNames']; - $keys = $this->meta['columns'] ?: self::colKeys($this->rows); - $columns = \array_fill_keys($keys, array()); - \array_walk($columns, function (&$column, $key) use ($columnNames) { - $default = array( - 'key' => isset($columnNames[$key]) - ? $columnNames[$key] - : $key, - ); - $column = \array_merge($default, isset($this->meta['tableInfo']['columns'][$key]) - ? $this->meta['tableInfo']['columns'][$key] - : array()); - }); - foreach ($this->meta['totalCols'] as $i => $key) { - if (isset($columns[$key]) === false) { - unset($this->meta['totalCols'][$i]); - continue; - } - $columns[$key]['total'] = null; - } - /** - * @psalm-suppress MixedArrayAssignment - * @psalm-suppress MixedPropertyTypeCoercion - */ - $this->meta['tableInfo']['columns'] = \array_map(static function (array $column) { - \ksort($column); - return $column; - }, $columns); - } - - /** - * Reduce each row to the columns specified - * Do this so we don't needlessly crate values that we won't output - * - * @param mixed $rows Table rows - * - * @return mixed - */ - private function preCrate($rows) - { - if (\is_array($rows) === false || empty($this->meta['columns'])) { - return $rows; - } - $colFlip = \array_flip($this->meta['columns']); - foreach ($rows as $i => $row) { - if (\is_array($row)) { - $rows[$i] = \array_intersect_key($row, $colFlip); - } - } - return $rows; - } - - /** - * non-array - * empty array - * array - * object / traversable - * - * @param mixed $rows Row data to process - * - * @return void - */ - private function processRows($rows) - { - if ($rows === null) { - return; - } - $rows = $this->processRowsGet($rows); - if (\is_array($rows) === false) { - return; - } - $this->rows = \array_map(static function ($row) { - return new TableRow($row); - }, $rows); - $this->initTableInfoColumns(); - foreach ($this->rows as $rowKey => $row) { - $this->rows[$rowKey] = $this->processRow($row, $rowKey); - } - } - - /** - * Get table rows - * - * @param mixed $rows Row data to process - * - * @return mixed - */ - private function processRowsGet($rows) - { - if ($this->meta['inclContext'] === false) { - $rows = $this->preCrate($rows); - } - $rows = $this->debug->abstracter->crate($rows, 'table'); - if ($this->debug->abstracter->isAbstraction($rows, Type::TYPE_OBJECT)) { - /** - * @psalm-var array{ - * classname: string, - * phpDoc: array{summary: string}, - * properties: array>, - * traverseValues?: array, - * } $rows - * - * - * @psalm-suppress MixedArrayAssignment - * @psalm-suppress MixedPropertyTypeCoercion pslam bug tableInfo becomes mixed - */ - $this->meta['tableInfo']['class'] = $rows['className']; - /** - * @psalm-suppress MixedArrayAssignment - * @psalm-suppress MixedPropertyTypeCoercion pslam bug tableInfo becomes mixed - * @psalm-suppress MixedArrayAccess - */ - $this->meta['tableInfo']['summary'] = $rows['phpDoc']['summary']; - /** @psalm-suppress MixedArgument */ - return $rows['traverseValues'] - ? $rows['traverseValues'] - : \array_map( - /** - * @param array{value:mixed,...} $info - */ - static function ($info) { - return $info['value']; - }, - \array_filter( - $rows['properties'], - /** - * @param array $prop - */ - static function ($prop) { - return \in_array('public', (array) $prop['visibility'], true); - } - ) - ); - } - return $rows; - } - - /** - * Process table row - * - * @param TableRow $row TableRow instance - * @param string|int $rowKey index of row - * - * @return array key => value - */ - private function processRow(TableRow $row, $rowKey) - { - $columns = $this->meta['tableInfo']['columns']; - $keys = \array_keys($columns); - $valsTemp = $row->keyValues($keys); - $rowInfo = $row->getInfo(); - if ($this->meta['inclContext']) { - $rowInfo['args'] = $row->getValue('args'); - $rowInfo['context'] = $row->getValue('context'); - } - $this->updateTableInfo($rowKey, $valsTemp, $rowInfo); - return \array_values($valsTemp); - } - - /** - * Set meta info - * - * @return void - */ - private function setMeta() - { - $this->setMetaTableInfoColumns(); - - if (isset($this->meta['columnNames'][TableRow::INDEX])) { - $this->meta['tableInfo']['indexLabel'] = $this->meta['columnNames'][TableRow::INDEX]; - } - - $this->meta = \array_diff_key($this->meta, \array_flip(['columnNames', 'columns', 'totalCols'])); - if (!$this->meta['inclContext']) { - unset($this->meta['inclContext']); - } - if (!$this->haveRows()) { - $this->meta = \array_diff_key($this->meta, \array_flip(['caption', 'inclContext', 'sortable', 'tableInfo'])); - } - } - - /** - * Set tableInfo['columns'] removing empty values - * - * @return void - */ - private function setMetaTableInfoColumns() - { - $this->meta['tableInfo']['columns'] = \array_values(\array_map(static function ($colInfo) { - return \array_filter($colInfo, static function ($val) { - return \is_array($val) - ? !empty($val) - : $val !== null && $val !== false; - }); - }, $this->meta['tableInfo']['columns'])); - } - - /** - * Update collected table info - * - * @param int|string $rowKey row's key/index - * @param array $rowValues row's values - * @param array $rowInfo Row info - * - * @return void - */ - private function updateTableInfo($rowKey, array $rowValues, array $rowInfo) - { - $this->meta['tableInfo']['haveObjRow'] = $this->meta['tableInfo']['haveObjRow'] || $rowInfo['class']; - foreach ($this->meta['totalCols'] as $key) { - /** - * @psalm-suppress MixedPropertyTypeCoercion - * @psalm-suppress MixedOperand - * @psalm-suppress PossiblyFalseOperand - */ - $this->meta['tableInfo']['columns'][$key]['total'] += $rowValues[$key]; - } - /** @var array-key $key */ - foreach ($rowInfo['classes'] as $key => $class) { - if (!isset($this->meta['tableInfo']['columns'][$key]['class'])) { - /** @psalm-suppress MixedPropertyTypeCoercion */ - $this->meta['tableInfo']['columns'][$key]['class'] = $class; - } elseif ($this->meta['tableInfo']['columns'][$key]['class'] !== $class) { - // column values not of the same type - /** @psalm-suppress MixedPropertyTypeCoercion */ - $this->meta['tableInfo']['columns'][$key]['class'] = false; - } - } - $this->updateTableInfoRow($rowKey, $rowInfo); - } - - /** - * Merge rowInfo into tableInfo['rows'][$rowKey] - * - * @param int|string $rowKey row's key/index - * @param array $rowInfo Row info - * - * @return void - */ - private function updateTableInfoRow($rowKey, array $rowInfo) - { - unset($rowInfo['classes']); - $rowInfo = \array_filter($rowInfo, static function ($val) { - return \in_array($val, [null, false, ''], true) === false; - }); - if (!$rowInfo) { - return; - } - // non-null/false values - $rowInfoExisting = isset($this->meta['tableInfo']['rows'][$rowKey]) - ? $this->meta['tableInfo']['rows'][$rowKey] - : array(); - /** - * @psalm-suppress MixedArrayAssignment - * @psalm-suppress MixedPropertyTypeCoercion - */ - $this->meta['tableInfo']['rows'][$rowKey] = \array_merge($rowInfoExisting, $rowInfo); - } -} diff --git a/src/Debug/Utility/TableRow.php b/src/Debug/Utility/TableRow.php deleted file mode 100644 index 3d20cbfc..00000000 --- a/src/Debug/Utility/TableRow.php +++ /dev/null @@ -1,208 +0,0 @@ - - * @license http://opensource.org/licenses/MIT MIT - * @copyright 2014-2025 Brad Kent - * @since 3.0b1 - */ - -namespace bdk\Debug\Utility; - -use bdk\Debug\Abstraction\Abstracter; -use bdk\Debug\Abstraction\Abstraction; -use bdk\Debug\Abstraction\Type; - -/** - * Represent a table row - * - * @psalm-type rowInfo = array{ - * class: string|null, - * classes: array, - * isScalar: bool, - * summary: string, - * } - */ -class TableRow -{ - const INDEX = "\x00index\x00"; - const SCALAR = "\x00scalar\x00"; - - /** @var array */ - private $row = array(); - - /** - * Will be populated with object info - * if row is an object, $info will be populated with - * 'class' & 'summary' - * if a value is an object being displayed as a string, - * $info['classes'][key] will be populated with className - * - * @var rowInfo - */ - private $info = array( - 'class' => null, - 'classes' => array(), // key => classname (or false if not stringified class) - 'isScalar' => false, - 'summary' => '', - ); - - /** - * Constructor - * - * @param mixed $row May be "scalar", array, abstraction (array, Traversable, object) - */ - public function __construct($row) - { - if (\is_array($row)) { - $this->row = $row; - return; - } - if ($row instanceof Abstraction) { - $this->row = $this->valuesAbs($row); - return; - } - $this->info['isScalar'] = true; - $this->row = array( - self::SCALAR => $row, - ); - } - - /** - * Get the collected row information - * - * @return rowInfo - */ - public function getInfo() - { - return $this->info; - } - - /** - * Get column value - * - * @param string|int $name column name - * @param bool $stringified return "stringified" value? - * - * @return mixed - */ - public function getValue($name, $stringified = true) - { - $value = \array_key_exists($name, $this->row) - ? $this->row[$name] - : Abstracter::UNDEFINED; - if ($stringified && $value instanceof Abstraction) { - // just return the stringified / __toString value in a table - if (isset($value['stringified'])) { - $this->info['classes'][$name] = $value['className']; - $value = $value['stringified']; - } elseif (isset($value['methods']['__toString']['returnValue'])) { - $this->info['classes'][$name] = $value['className']; - $value = $value['methods']['__toString']['returnValue']; - } - } - return $value; - } - - /** - * Get the row's keys - * - * @return list - */ - public function keys() - { - return \array_keys($this->row); - } - - /** - * Get values for passed keys - * - * @param array $keys column keys - * - * @return array key => value array - */ - public function keyValues($keys) - { - $values = array(); - foreach ($keys as $key) { - $this->info['classes'][$key] = false; - $values[$key] = $this->getValue($key); - } - if (\array_keys($values) === [self::SCALAR]) { - $this->info['isScalar'] = true; - } - return $values; - } - - /** - * Get values from abstraction - * - * @param Abstraction $abs Abstraction instance - * - * @return array - */ - private function valuesAbs(Abstraction $abs) - { - if ($abs['type'] !== Type::TYPE_OBJECT) { - // resource, callable, string, etc - $this->info['isScalar'] = true; - return array(self::SCALAR => $abs); - } - // we are an object - if (\strpos(\json_encode($abs['implements']), '"UnitEnum"') !== false) { - $this->info['isScalar'] = true; - return array(self::SCALAR => $abs); - } - if ($abs['className'] === 'Closure') { - $this->info['isScalar'] = true; - return array(self::SCALAR => $abs); - } - $this->info['class'] = $abs['className']; - $this->info['summary'] = $abs['phpDoc']['summary']; - $values = self::valuesAbsObj($abs); - if (\is_array($values) === false) { - // ie stringified value - $this->info['class'] = null; - $this->info['isScalar'] = true; - $values = array(self::SCALAR => $values); - } - return $values; - } - - /** - * Get object abstraction's values - * if, object has a stringified or __toString value, it will be returned - * - * @param Abstraction $abs Object Abstraction instance - * - * @return array|string - */ - private static function valuesAbsObj(Abstraction $abs) - { - if ($abs['traverseValues']) { - // probably Traversable - return $abs['traverseValues']; - } - if ($abs['stringified']) { - return $abs['stringified']; - } - if (isset($abs['methods']['__toString']['returnValue'])) { - return $abs['methods']['__toString']['returnValue']; - } - $values = \array_map( - static function ($info) { - return $info['value']; - }, - \array_filter($abs['properties'], static function ($prop) { - return \in_array('public', (array) $prop['visibility'], true); - }) - ); - /* - Reflection doesn't return properties in any given order - so, we'll sort for consistency - */ - \ksort($values, SORT_NATURAL | SORT_FLAG_CASE); - return $values; - } -} diff --git a/src/Debug/Utility/UnserializeLog.php b/src/Debug/Utility/UnserializeLog.php new file mode 100644 index 00000000..337b4d7b --- /dev/null +++ b/src/Debug/Utility/UnserializeLog.php @@ -0,0 +1,205 @@ + + * @license http://opensource.org/licenses/MIT MIT + * @copyright 2014-2026 Brad Kent + * @since 3.6 + */ + +namespace bdk\Debug\Utility; + +use bdk\Debug; +use bdk\Debug\LogEntry; +use bdk\Debug\Utility\Php; +use bdk\Debug\Utility\StringUtil; +use bdk\Debug\Utility\UnserializeLogBackwards; + +/** + * Unserialize / decompress / base64 decode log data + */ +class UnserializeLog +{ + /** @var Debug */ + protected static $debug; + + /** + * Import the config and data into the debug instance + * + * @param array $data Unpacked / Unserialized log data + * @param Debug|null $debug (optional) Debug instance + * + * @return Debug + */ + public static function import(array $data, $debug = null) + { + \bdk\Debug\Utility\PhpType::assertType($debug, 'bdk\Debug|null', 'debug'); + + if (!$debug) { + $debug = new Debug(); + } + self::$debug = $debug; + + // set config for any channels already present in debug + foreach (\array_intersect_key($debug->getChannels(true, true), $data['config']['channels']) as $fqn => $channel) { + $channel->setCfg($data['config']['channels'][$fqn], Debug::CONFIG_NO_RETURN); + } + $debug->setCfg($data['config'], Debug::CONFIG_NO_RETURN); + + if ($data['classDefinitions'] && empty($data['classDefinitions']["\x00default\x00"])) { + $data['classDefinitions']["\x00default\x00"] = self::$debug->abstracter->abstractObject->definition->getValueStoreDefault(); + } + $data['classDefinitions'] = \array_map(static function ($def) { + return UnserializeLogBackwards::updateClassDefinition($def, self::$debug); + }, $data['classDefinitions']); + // set classDefinitions early so can reference them when importing log entries + $debug->data->set('classDefinitions', $data['classDefinitions']); + + foreach (['alerts', 'log', 'logSummary'] as $cat) { + $data[$cat] = self::importGroup($cat, $data[$cat]); + } + + unset($data['config']); + unset($data['version']); + unset($data['classDefinitions']); // already set above (or when processing log) + foreach ($data as $k => $v) { + $debug->data->set($k, $v); + } + return $debug; + } + + /** + * Unserialize log data serialized by emailLog + * + * @param string $str serialized log data + * + * @return array|false + */ + public static function unserialize($str) + { + $str = self::extractLog($str); + $str = self::unserializeDecodeAndInflate($str); + $data = self::unserializeSafe($str); + if (!$data) { + return false; + } + $data = \array_merge(array( + 'classDefinitions' => array(), + 'config' => array( + 'channels' => array(), + ), + 'version' => '2.3', // prior to 3.0, we didn't include version + ), $data); + \ksort($data); + \ksort($data['classDefinitions']); + \ksort($data['runtime']); // 2.3 runtime not sorted + if (isset($data['rootChannel'])) { + $data['config']['channelName'] = $data['rootChannel']; + $data['config']['channels'] = array(); + unset($data['rootChannel']); + } + return $data; + } + + /** + * Extract serialized/encoded log data from between "START DEBUG" & "END DEBUG" + * + * @param string $str string containing serialized log + * + * @return string + */ + private static function extractLog($str) + { + $strStart = 'START DEBUG'; + $strEnd = 'END DEBUG'; + $regex = '/(?:' . $strStart . '[\r\n]+)(.+)(?:[\r\n]+' . $strEnd . ')/s'; + $matches = []; + if (\preg_match($regex, $str, $matches)) { + $str = $matches[1]; + } + return $str; + } + + /** + * "unserialize" log data + * + * @param string $cat ('alerts'|'log'|'logSummary') + * @param array $data data to import + * + * @return array + */ + private static function importGroup($cat, $data) + { + foreach ($data as $i => $val) { + if ($cat !== 'logSummary') { + $data[$i] = self::importLogEntry($val); + continue; + } + foreach ($val as $priority => $val2) { + $data[$i][$priority] = self::importLogEntry($val2); + } + } + return $data; + } + + /** + * Unserialize Log entry + * + * @param array $vals method, args, & meta values + * + * @return LogEntry + */ + private static function importLogEntry(array $vals) + { + $vals = \array_replace(['', array(), array()], $vals); + $logEntry = new LogEntry(self::$debug, $vals[0], $vals[1], $vals[2]); + return UnserializeLogBackwards::updateLogEntry($logEntry); + } + + /** + * base64 decode and gzinflate + * + * @param string $str compressed / encoded log data + * + * @return string|false + */ + private static function unserializeDecodeAndInflate($str) + { + $str = StringUtil::isBase64Encoded($str) + ? \base64_decode($str, true) + : false; + if ($str && \function_exists('gzinflate')) { + $strInflated = \gzinflate($str); + if ($strInflated) { + $str = $strInflated; + } + } + return $str; + } + + /** + * Safely unserialize data + * Handle legacy data + * + * @param string $serialized serialized array + * + * @return array|false + */ + private static function unserializeSafe($serialized) + { + $serialized = \preg_replace( + '/O:33:"bdk\\\Debug\\\Abstraction\\\Abstraction":((?:\d+):{s:4:"type";s:6:"object")/', + 'O:40:"bdk\\Debug\\Abstraction\\Object\Abstraction":$1', + (string) $serialized + ); + if (!$serialized) { + return false; + } + return Php::unserializeSafe($serialized, [ + 'bdk\\Debug\\Abstraction\\Abstraction', + 'bdk\\Debug\\Abstraction\\Object\\Abstraction', + 'bdk\\PubSub\\ValueStore', + ]); + } +} diff --git a/src/Debug/Utility/UnserializeLogBackwards.php b/src/Debug/Utility/UnserializeLogBackwards.php new file mode 100644 index 00000000..23b29c8b --- /dev/null +++ b/src/Debug/Utility/UnserializeLogBackwards.php @@ -0,0 +1,372 @@ + + * @license http://opensource.org/licenses/MIT MIT + * @copyright 2014-2026 Brad Kent + * @since 3.6 + */ + +namespace bdk\Debug\Utility; + +use bdk\Debug; +use bdk\Debug\Abstraction\Abstracter; +use bdk\Debug\Abstraction\Abstraction; +use bdk\Debug\Abstraction\AbstractObject; +use bdk\Debug\Abstraction\Object\Abstraction as ObjectAbstraction; +use bdk\Debug\Abstraction\Object\Definition; +use bdk\Debug\Abstraction\Object\MethodParams; +use bdk\Debug\Abstraction\Object\Methods; +use bdk\Debug\Abstraction\Object\Properties; +use bdk\Debug\Abstraction\Type; +use bdk\Debug\LogEntry; +use bdk\Debug\Utility\ArrayUtil; +use bdk\PubSub\ValueStore; + +/** + * "Normalize" log entries and values + */ +class UnserializeLogBackwards +{ + /** @var Debug */ + protected static $debug; + + /** + * Update class definition + * + * @param ValueStore $def Class definition to update + * + * @return ValueStore + */ + public static function updateClassDefinition(ValueStore $def, Debug $debug) + { + self::$debug = $debug; + $values = $def->getValues(); + $values = \array_filter($values, static function ($val) { + return $val !== null; + }); + if ($values['className'] === "\x00default\x00") { + $values['cfgFlags'] = self::$debug->abstracter->abstractObject->definition->getValueStoreDefault()->getValue('cfgFlags'); + $values['scopeClass'] = null; + } + $values['__isUsed'] = true; + $values['methods'] = self::updateObjectMethods($values['methods'], true); + $values['phpDoc'] = self::updatePhpDoc($values['phpDoc']); + $values['properties'] = self::updateObjectProperties($values['properties'], true); + if ( + isset($values['scopeClass']) + && \in_array($values['scopeClass'], [ + '', + 'bdk\\Debug', + 'bdk\\Debug\\Abstraction\\AbstractObject', + ], true) + ) { + $values['scopeClass'] = null; + } + unset($values['traverseValues']); + $values = AbstractObject::buildValues(Definition::buildValues($values)); + if ($values['className'] !== "\x00default\x00") { + $valueStoreDefault = self::$debug->abstracter->abstractObject->definition->getValueStoreDefault(); + $def = new ObjectAbstraction($valueStoreDefault, $values); + self::$debug->abstracter->abstractObject->definition->markAsUsed($valueStoreDefault); + return $def; + } + $def->setValues($values); + return $def; + } + + /** + * Update LogEntry with any necessary changes to work with current version of debug + * + * @param LogEntry $logEntry LogEntry instance + * + * @return LogEntry + */ + public static function updateLogEntry(LogEntry $logEntry) + { + self::$debug = $logEntry->getSubject(); + $method = $logEntry['method']; + if ($method === 'alert') { + $logEntry = self::updateLogEntryAlert($logEntry); + } + if (\in_array($method, ['profileEnd', 'table', 'trace'], true) && !($logEntry['args'][0] instanceof Abstraction)) { + self::updateLogEntryTabular($logEntry); + return $logEntry; + } + self::updateLogEntryDefault($logEntry); + return $logEntry; + } + + private static function updateLogEntryAlert(LogEntry $logEntry) + { + if ($logEntry->getMeta('class')) { + $level = $logEntry->getMeta('class'); + $levelTrans = array( + 'danger' => 'error', + 'warning' => 'warn', + ); + $level = \str_replace(\array_keys($levelTrans), $levelTrans, $level); + $logEntry->setMeta('level', $level); + $logEntry->setMeta('class', null); + $logEntry->crate(); // removes the null meta value + } + return $logEntry; + } + + /** + * Update log entry + * + * @param LogEntry $logEntry Log entry to update + * + * @return void + */ + private static function updateLogEntryDefault(LogEntry $logEntry) + { + $logEntry['args'] = self::updateValues($logEntry['args']); + } + + /** + * Update table, trace, profile end logEntry + * + * @param LogEntry $logEntry Log entry to update + * + * @return void + */ + private static function updateLogEntryTabular(LogEntry $logEntry) + { + $tableData = $logEntry['args'][0]; + $tableInfo = \array_merge(array( + 'columns' => array(), + 'rows' => array(), + ), $logEntry->getMeta('tableInfo', array())); + $columnKeys = \array_column($tableInfo['columns'], 'key'); + $logEntry->setMeta('tableInfo', null); + if ($logEntry->getMeta('caption')) { + $logEntry['args'][1] = $logEntry->getMeta('caption'); + $logEntry->setMeta('caption', null); + } + foreach ($tableData as $k => $row) { + if (!empty($tableInfo['rows'][$k]['isScalar'])) { + $tableData[$k] = array( + \bdk\Table\Factory::KEY_SCALAR => \reset($row), + ); + } elseif (\count($columnKeys) === \count($row)) { + $tableData[$k] = \array_combine($columnKeys, $row); + } + } + $logEntry['args'][0] = $tableData; + self::$debug->rootInstance->getPlugin('methodTable')->doTable($logEntry); + $logEntry->crate(); + } + + /** + * Update abstraction + * + * @param Abstraction $abs Abstraction instance + * + * @return Abstraction + */ + private static function updateAbstraction(Abstraction $abs) + { + if ($abs->getValue('type') === Type::TYPE_OBJECT) { + return self::updateObjectAbstraction($abs); + } + $values = $abs->getValues(); + $values = \array_diff_assoc($values, array( + 'strlen' => null, + )); + $abs->setValues($values); + return $abs; + } + + /** + * Update object abstraction + * + * @param Abstraction $abs Object Abstraction + * + * @return AbstractObject + */ + private static function updateObjectAbstraction(Abstraction $abs) + { + $values = $abs->getValues(); + + if (isset($values['collectMethods'])) { + if ($values['collectMethods'] === false) { + $values['cfgFlags'] &= ~AbstractObject::METHOD_COLLECT; + } + unset($values['collectMethods']); + } + + $values = \array_filter($values, static function ($val) { + return $val !== null; + }); + + if (isset($values['scopeClass']) && \in_array($values['scopeClass'], ['', 'bdk\\Debug\\Abstraction\\AbstractObject'], true)) { + // prior to 3.4 scopeClass settled on AbstractObject + $values['scopeClass'] = null; + } + + $values['methods'] = self::updateObjectMethods($values['methods']); + $values['phpDoc'] = self::updatePhpDoc($values['phpDoc']); + $values['properties'] = self::updateObjectProperties($values['properties']); + $values = AbstractObject::buildValues($values); + + unset($values['sort']); + unset($values['traverseValues']); + + $classDefinition = self::$debug->data->get('classDefinitions.' . $values['className']); // ?: self::$debug->abstracter->abstractObject->definition->getValueStoreDefault(); + if ($classDefinition === null) { + $valuesDef = \array_diff_key($values, \array_flip(['debugMethod'])); + $classDefinition = new ValueStore($valuesDef); + $classDefinition = self::updateClassDefinition($classDefinition, self::$debug); + self::$debug->data->set('classDefinitions.' . $values['className'], $classDefinition); + } + $values = ArrayUtil::diffDeep($values, $classDefinition->getValues()); + return new ObjectAbstraction($classDefinition, $values); + } + + /** + * Convert old object inheritance keys to current names + * + * @param array $values Object abstraction values + * + * @return array + */ + private static function updateObjectInheritance(array $values) + { + if (\array_key_exists('inheritedFrom', $values)) { + $values['declaredLast'] = $values['inheritedFrom']; + unset($values['inheritedFrom']); + } + if (\array_key_exists('overrides', $values)) { + $values['declaredPrev'] = $values['overrides']; + unset($values['overrides']); + } + if (\array_key_exists('originallyDeclared', $values)) { + $values['declaredOrig'] = $values['originallyDeclared']; + unset($values['originallyDeclared']); + } + return $values; + } + + /** + * Update object method info + * + * @param array $methods Abstracted methods + * @param bool $isDefinition Whether the method info is for a class definition (vs method info on an object instance) + * + * @return array + */ + private static function updateObjectMethods(array $methods, $isDefinition = false) + { + return ArrayUtil::mapWithKeys(static function (array $info, $methodName) use ($isDefinition) { + if ($methodName === '__toString') { + $info['implements'] = 'Stringable'; + $info['return']['type'] = 'string'; + } + $info = self::updateObjectInheritance($info); + $info = Methods::buildValues($info); + $info['params'] = \array_map(static function ($paramInfo) { + $paramInfo = MethodParams::buildValues($paramInfo); + $paramInfo['name'] = \trim($paramInfo['name'], '&$.'); + unset($paramInfo['constantName']); // v2.3 + return $paramInfo; + }, \array_values($info['params'])); + $info['phpDoc'] = self::updatePhpDoc($info['phpDoc']); + if (isset($info['phpDoc']['return'])) { + $info['return'] = $info['phpDoc']['return']; + unset($info['phpDoc']['return']); + } + if (isset($info['return']['desc']) === false) { + $info['return']['desc'] = ''; + } + if ($isDefinition && isset($info['returnValue'])) { + $info['returnValue'] = null; + } + \ksort($info['return']); + return $info; + }, $methods); + } + + /** + * Update object property info + * + * @param array $properties Abstracted properties + * @param bool $isDefinition Whether the property info is for a class definition (vs property info on an object instance) + * + * @return array + */ + private static function updateObjectProperties(array $properties, $isDefinition = false) + { + return \array_map(static function (array $info) use ($isDefinition) { + $info = self::updateValues($info); + $info = self::updateObjectInheritance($info); + $info = Properties::buildValues($info); + $info['visibility'] = (array) $info['visibility']; + if (isset($info['desc'])) { + $info['phpDoc']['desc'] = $info['desc']; + } + if ($isDefinition) { + unset($info['scopeClass']); + } + $info = \array_diff_assoc($info, array( + 'isExcluded' => false, + )); + unset($info['desc']); + return $info; + }, $properties); + } + + /** + * Update phpDoc values + * prior to 3.3 null was used vs '' + * + * @param array $phpDoc phpDoc values + * + * @return array + */ + private static function updatePhpDoc(array $phpDoc) + { + if (isset($phpDoc['description'])) { + $phpDoc['desc'] = $phpDoc['description']; + $phpDoc['description'] = null; + } + $phpDoc = \array_filter($phpDoc, static function ($val) { + return $val !== null; + }); + $phpDoc = \array_merge(array( + 'desc' => '', + 'summary' => '', + ), $phpDoc); + return $phpDoc; + } + + /** + * Walk / Iterate over array and update values as needed + * + * @param array $values Values to iterate over + * + * @return array + */ + private static function updateValues(array $values) + { + return \array_map(static function ($val) { + $isAbsArray = \is_array($val) && isset($val['debug']) && $val['debug'] === Abstracter::ABSTRACTION; + if ($isAbsArray && $val['type'] === Type::TYPE_OBJECT) { + unset($val['debug'], $val['type']); + $valueStore = self::$debug->abstracter->abstractObject->definition->getValueStoreDefault(); + $val = new ObjectAbstraction($valueStore, $val); + } elseif ($isAbsArray) { + unset($val['debug']); + $val = new Abstraction($val['type'], $val); + } + if ($val instanceof Abstraction) { + return self::updateAbstraction($val); + } elseif (\is_array($val)) { + return self::updateValues($val); + } + return $val; + }, $values); + } +} diff --git a/src/Debug/Utility/Utility.php b/src/Debug/Utility/Utility.php index f91e512a..694b9ad1 100644 --- a/src/Debug/Utility/Utility.php +++ b/src/Debug/Utility/Utility.php @@ -285,7 +285,7 @@ public static function sortFiles($files) * Convenience wrapper for Debug's trans method * * @param string $str string to translate - * @param array $args optional arguments + * @param array $args optional arguments (may omit param) * @param string $domain optional domain (defaults to defaultDomain) * * @return string diff --git a/src/Debug/js/Debug.js b/src/Debug/js/Debug.js index abc29453..16256111 100644 --- a/src/Debug/js/Debug.js +++ b/src/Debug/js/Debug.js @@ -711,7 +711,7 @@ var phpDebugConsole = (function (exports, $) { } function buildFileHref (file, line, docRoot) { - // console.warn('buildfileHref', {file, line, docRoot}) + // console.warn('buildFileHref', {file, line, docRoot}) var data = { file: docRoot ? file.replace(/^DOCUMENT_ROOT\b/, docRoot) @@ -780,9 +780,16 @@ var phpDebugConsole = (function (exports, $) { $entry.find('table tr:not(.context) > *:last-child').remove(); return } - $entry.find('table tbody tr').each(function () { + $entry.find('table tbody tr:not(.context)').each(function () { createFileLinksTraceProcessTr($(this), isUpdate); }); + if (isUpdate) { + return; + } + $entry.find('table tbody tr.context').each(function () { + var $td0 = $(this).find('> td').eq(0); + $td0.attr('colspan', parseInt($td0.attr('colspan'), 10) + 1); + }); } function createFileLinksTraceProcessTr($tr, isUpdate) { @@ -801,11 +808,7 @@ var phpDebugConsole = (function (exports, $) { }); if (isUpdate) { $tr.find('.file-link').replaceWith($a); - return // continue - } - if ($tr.hasClass('context')) { - $tds.eq(0).attr('colspan', parseInt($tds.eq(0).attr('colspan'), 10) + 1); - return // continue + return } $tds.last().after($('', { class: 'text-center', @@ -6316,9 +6319,9 @@ var phpDebugConsole = (function (exports, $) { var $dl = $('
    '); for (i = 0, count = throws.length; i < count; i++) { info = throws[i]; - $dl.append($('
    ').html(markupClassname(info.type))); + $dl.append($('
    ').html(markupClassname(info.type))); if (info.desc) { - $dl.append($('
    ').html(info.desc)); + $dl.append($('
    ').html(info.desc)); } } return title + $dl[0].outerHTML diff --git a/src/Debug/js/Debug.min.js b/src/Debug/js/Debug.min.js index 0f93ab6a..2a3b3501 100644 --- a/src/Debug/js/Debug.min.js +++ b/src/Debug/js/Debug.min.js @@ -1 +1 @@ -var phpDebugConsole=function(e,t){"use strict";var n,a,i;function r(e){var a=e.find("> .array-inner");e.find(" > .t_array-expand").length>0||((a.html()||"").trim().length<1?e.addClass("expanded").find("br").hide():(!function(e){var a,i=e.find("> .array-inner");if(e.closest(".array-file-tree").length)return e.find("> .t_keyword, > .t_punct").remove(),i.find("> li > .t_operator, > li > .t_key.t_int").remove(),void e.prevAll(".t_key").each((function(){var n=t(this).attr("data-toggle","array");e.prepend(n),e.prepend('')}));a=t('array( ··· )'),e.find("> .t_keyword").first().wrap('').after('( ').parent().next().remove(),e.prepend(a)}(e),t.each(n.iconsArray,(function(t,n){e.find(n).prepend(t)})),e.debugEnhance(function(e){var t=e.data("expand"),n=e.parentsUntil(".m_group",".t_object, .t_array").length,a=0===n;void 0===t&&0!==n&&(t=e.closest(".t_array[data-expand]").data("expand"));void 0===t&&(t=a);return t||e.hasClass("array-file-tree")}(e)?"expand":"collapse")))}function o(e){a=e.data("config").get(),i=e.data("config").dict,e.on("click","[data-toggle=vis]",(function(){return function(e){var n=t(e),a=n.data("vis"),i=n.closest(".t_object"),r=i.find("> .object-inner"),o=r.find("[data-toggle=vis][data-vis="+a+"]"),s="inherited"===a?"dd[data-inherited-from], .private-ancestor":"."+a,c=r.find(s),l=n.hasClass("toggle-off");o.html(n.html().replace(l?"show ":"hide ",l?"hide ":"show ")).addClass(l?"toggle-on":"toggle-off").removeClass(l?"toggle-off":"toggle-on"),l?function(e){e.each((function(){var e=t(this),n=e.closest(".object-inner"),a=!0;n.find("> .vis-toggles [data-toggle]").each((function(){var n=t(this),i=n.hasClass("toggle-on"),r=n.data("vis"),o="inherited"===r?"dd[data-inherited-from], .private-ancestor":"."+r;if(!i&&1===e.filter(o).length)return a=!1,!1})),a&&e.show()}))}(c):c.hide(),f(i,!0)}(this),!1})),e.on("click","[data-toggle=interface]",(function(){return l(this),!1}))}function s(e){t.each(a.iconsObject,(function(n,a){var r=function(e,n){var a=n.match(/(?:parent(:\S+)\s)?(?:context(\S+)\s)?(.*)$/);if(null===a)return e.find(n);if(a[1]&&0===e.parent().filter(a[1]).length)return t();if(n=a[3],a[2])return e.filter(a[2]).find(n);return e.find(n)}(e,a),o="string"==typeof n?/^([ap])\s*:(.+)$/.exec(n):null,s=!o||"p"===o[1];if(o&&(n=o[2]),"function"==typeof n){var c=n;n=function(){return"object"==typeof(n=c.apply(this,arguments))&&(n=n[0].outerHTML),i.replaceTokens(n)}}else n=i.replaceTokens(n);s?function(e,t){var n=e.find("> i:first-child + i").after(t);e=e.not(n.parent()),n=e.find("> i:first-child").after(t),(e=e.not(n.parent())).prepend(t)}(r,n):r.append(n)}))}function c(e){var n=e.find("> .t_identifier").length?["> .t_identifier"]:["> .classname","> .t_const"],i=e.find("> .object-inner");e.find(n.join(",")).each((function(){var n=t(this),r="object"===n.data("toggle");i.is(".t_maxDepth, .t_recursion, .excluded, .t_punct")?n.addClass("empty"):r||0!==i.length&&(!function(e,n){if(!1===e.hasClass("prop-only"))return void n.wrap('').after(' ');var i=t(''+n[0].outerHTML+'( ··· )');n.wrap('').after('( ').parent().next(".t_punct").remove(),e.prepend(i)}(e,n),i.hide())})),e.debugEnhance(function(e){var t=e.data("expand"),n=e.parentsUntil(".m_group",".t_object, .t_array").length,a=0===n&&e.hasClass("prop-only");void 0===t&&0!==n&&(t=e.closest(".t_object[data-expand]").data("expand"));return void 0!==t?t:a}(e)?"expand":"collapse")}function l(e){var n=t(e),a=n.closest(".t_object");(n=n.is(".toggle-off")?n.add(n.next().find(".toggle-off")):n.add(n.next().find(".toggle-on"))).each((function(){var e=t(this),n=e.data("interface"),i=d(a,n);e.is(".toggle-off")?(e.addClass("toggle-on").removeClass("toggle-off"),i.show()):(e.addClass("toggle-off").removeClass("toggle-on"),i.hide())})),f(a)}function d(e,t){var n='> .object-inner > dd[data-implements="'+CSS.escape(t)+'"]';return e.find(n)}function f(e,n){var a=n?".object-inner > dt":"> .object-inner > dt",i=n?".object-inner > .heading":"> .object-inner > .heading";e.find(a).each((function(e){var n=t(e).nextUntil("dt"),a=n.not(".heading").filter((function(e){return"none"!==t(e).style("display")})),i=n.length>0&&0===a.length;t(e).toggleClass("text-muted",i)})),e.find(i).each((function(e){var n=t(e).nextUntil("dt, .heading"),a=n.filter((function(e){return"none"!==t(e).style("display")})),i=n.length>0&&0===a.length;t(e).toggleClass("text-muted",i)})),e.trigger("expanded.debug.object")}Object.keys=Object.keys||function(e){if(e!==Object(e))throw new TypeError("Object.keys called on a non-object");var t,n=[];for(t in e)Object.hasOwn(e,t)&&n.push(t);return n};var u,p,g,h,m,b,v=Object.freeze({__proto__:null,enhance:c,enhanceInner:function(e){var n=e.find("> .object-inner"),a=e.data("accessible"),i=null;e.is(".enhanced")||(n.find("> .private, > .protected").filter(".magic, .magic-read, .magic-write").removeClass("private protected"),"public"===a&&(n.find(".private, .protected").hide(),i="allDesc"),function(e){var n=e.find("> .object-inner");n.find("> dd.interface, > dd.implements .interface").each((function(){var n=t(this).text();0!==d(e,n).length&&t(this).addClass("toggle-on").prop("title","toggle interface methods").attr("data-toggle","interface").attr("data-interface",n)})).filter(".toggle-off").removeClass("toggle-off").each((function(){l(this)}))}(e),function(e,n){var a={hasProtected:e.children(".protected").not(".magic, .magic-read, .magic-write").length>0,hasPrivate:e.children(".private").not(".magic, .magic-read, .magic-write").length>0,hasExcluded:e.children(".debuginfo-excluded").hide().length>0,hasInherited:e.children("dd[data-inherited-from]").length>0},i=function(e,n){var a=t('
    '),i="public"===n?"toggle-off":"toggle-on",r="public"===n?"show":"hide",o={hasProtected:''+r+" protected",hasPrivate:''+r+" private",hasExcluded:'show excluded',hasInherited:'hide inherited'};return t.each(e,(function(e,t){e&&a.append(o[t])})),a}(a,n);if(e.find("> dd[class*=t_modifier_]").length)return void e.find("> dd[class*=t_modifier_]").last().after(i);e.prepend(i)}(n,a),s(n),n.find("> .property.forceShow").show().find("> .t_array").debugEnhance("expand"),i&&f(e,"allDesc"===i),e.addClass("enhanced"))},init:o});function y(){var e=t(this).closest(".show-more-container");e.find(".show-more-wrapper").style("display","block").animate({height:"70px"}),e.find(".show-more-fade").fadeIn(),e.find(".show-more").show(),e.find(".show-less").hide()}function w(){var e=t(this).closest(".show-more-container");e.find(".show-more-wrapper").animate({height:e.find(".t_string").height()},400,"swing",(function(){t(this).style("display","inline")})),e.find(".show-more-fade").fadeOut(),e.find(".show-more").hide(),e.find(".show-less").show()}function x(e){var n=t(this).data("codePoint"),a="https://symbl.cc/en/"+n;e.stopPropagation(),n&&window.open(a,"unicode").focus()}function k(e){var n=t(e.target),a=n.closest("li[class*=m_]");e.stopPropagation(),n.find("> .array-inner > li > :last-child, > .array-inner > li[class]").each((function(){g(this,a)}))}function _(e){var n=t(e.target);e.stopPropagation(),n.find("> .group-body").debugEnhance()}function C(e){var n=t(e.target),a=n.closest("li[class*=m_]");e.stopPropagation(),n.is(".enhanced")||(n.find("> .object-inner").find("> .constant > :last-child,> .property > :last-child,> .method > ul > li > :last-child").each((function(){g(this,a)})),p.enhanceInner(n))}function O(e){var n=t(e.target);(n.hasClass("t_array")?n.find("> .array-inner").find("> li > .t_string, > li.t_string"):n.hasClass("m_group")?n.find("> .group-body > li > .t_string"):n.hasClass("group-body")?n.find("> li > .t_string"):n.hasClass("t_object")?n.find("> .object-inner").find(["> dd.constant > .t_string","> dd.property > .t_string","> dd.method > ul > li > .t_string.return-value"].join(", ")).filter(":visible"):t()).not("[data-type-more=numeric]").each((function(){var e,n,a;(e=t(this)).height()-70>35&&((a=e.wrap('
    ').parent()).append('
    '),(n=a.wrap('
    ').parent()).append('"),n.append('"))}))}function E(e){var n=t(e),a=n.find("> thead");return n.is("table.sortable")?(n.addClass("table-sort"),a.on("click","th",(function(){var e=t(this),a=t(this).closest("tr").children(),i=a.index(e),r="desc"===(e.is(".sort-asc")?"asc":"desc")?"asc":"desc";a.removeClass("sort-asc sort-desc"),e.addClass("sort-"+r),e.find(".sort-arrows").length||(a.find(".sort-arrows").remove(),e.append('')),function(e,t,n){var a,i=e.tBodies[0],r=i.rows,o="function"==typeof Intl.Collator?new Intl.Collator([],{numeric:!0,sensitivity:"base"}):null;for(n="desc"===n?-1:1,r=(r=Array.prototype.slice.call(r,0)).sort(function(e,t,n){var a=/^([+-]?(?:0|[1-9]\d*)(?:\.\d*)?)(?:[eE]([+-]?\d+))?$/;return function(i,r){var o=i.cells[e],s=r.cells[e],c=o.textContent.trim(),l=s.textContent.trim(),d=o.getAttribute("data-type-more"),f=s.getAttribute("data-type-more"),u=c.match(a),p=l.match(a);return["true","false"].indexOf(d)>-1&&(c=d),["true","false"].indexOf(f)>-1&&(l=f),u&&p?t*function(e,t){if(et)return 1;return 0}(T(u),T(p)):t*function(e,t,n){return n?n.compare(e,t):e.localeCompare(t)}(c,l,n)}}(t,n,o)),a=0;a0;if(a){if(n)return void e.find("table tr:not(.context) > *:last-child").remove()}else e.find("table thead tr > *:last-child").after("");e.find("table tbody tr").each((function(){!function(e,n){var a=e.find("> td"),i={file:e.data("file")||a.eq(0).text(),line:e.data("line")||a.eq(1).text()},r=m.data("meta").DOCUMENT_ROOT??"",o=t("",{class:"file-link",href:A(i.file,i.line,r),html:'',style:"vertical-align: bottom",title:"Open in editor"});if(n)return void e.find(".file-link").replaceWith(o);if(e.hasClass("context"))return void a.eq(0).attr("colspan",parseInt(a.eq(0).attr("colspan"),10)+1);a.last().after(t("",{class:"text-center",html:o}))}(t(this),a)}))}(e,a):e.is("[data-file]")?function(e,n){var a=m.data("meta").DOCUMENT_ROOT;if(e.find("> .file-link").remove(),n)return;e.append(t("",{html:'',href:A(e.data("file"),e.data("line"),a),title:"Open in editor",class:"file-link lpad"})[0].outerHTML)}(e,a):t.each(n||t(),(function(){!function(e,n){var a=t(e),i="create";n?i="remove":a.hasClass("file-link")&&(i="update");if(a.closest(".m_trace").length)return;!function(e,n){var a=function(e,n){var a,i=m.data("meta").DOCUMENT_ROOT,r=function(e){var n=[],a=e.text().trim();if(e.data("file"))return[null,e.data("file"),e.data("line")||1];if(e.parent(".property.debug-value").find("> .t_identifier").text().match(/^file$/))return n={line:1},e.parent().parent().find("> .property.debug-value").each((function(){var e=t(this).find("> .t_identifier").text().trim(),a=t(this).find("> *:last-child").text().trim();n[e]=a})),[null,a,n.line];return a.match(/^(.+?)(?: \(.+? (\d+)(, .+ \d+)?\))?$/)||[]}(e);"update"===n?a=e.prop("href",A(r[1],r[2],i)):"create"===n?a=t("",{class:"file-link",href:A(r[1],r[2],i),html:e.html()+' ',title:"Open in editor"}):(e.find("i.fa-external-link").remove(),e.removeClass("file-link"),a=t("",{html:e.html()}));return function(e,n){var a=e[0].attributes;t.each(a,(function(){if(void 0!==this){var t=this.name;["html","href","title"].indexOf(t)>-1||(e.removeAttr(t),"class"!==t?n.attr(t,this.value):n.addClass(this.value))}}))}(e,a),a}(e,n);if(!1===e.is("li, td, th"))return void e.replaceWith(a);e.html("remove"===n?a.html():a)}(a,i)}(this,a)})))}function A(e,t,n){var a={file:n?e.replace(/^DOCUMENT_ROOT\b/,n):e,line:t||1};return h.linkFilesTemplate.replace(/%(\w*)\b/g,(function(e,t){return Object.hasOwn(a,t)?a[t]:""}))}var L,M,S,H,I=[],P=!1;function F(e){b=e.data("config").get(),function(e){n=e.data("config").get()}(e),o(e),function(e,n,a){u=e.data("config").dict,g=n,p=a,e.on("click",".close[data-dismiss=alert]",(function(){t(this).parent().remove()})),e.on("click",".show-more-container .show-less",y),e.on("click",".show-more-container .show-more",w),e.on("click",".char-ws, .unicode",x),e.on("expand.debug.array",k),e.on("expand.debug.group",_),e.on("expand.debug.object",C),e.on("expanded.debug.next",".context",(function(e){g(t(e.target).find("> td > .t_array"),t(e.target).closest("li"))})),e.on("expanded.debug.array expanded.debug.group expanded.debug.object",O)}(e,N,v),j(e)}function R(e){var n=e.parent(),a=!n.hasClass("m_group")||n.hasClass("expanded");e.hide(),e.children().each((function(){$(t(this))})),a&&e.show().trigger("expanded.debug.group"),function(){if(P)return;P=!0;for(;I.length;)I.shift().debugEnhance("expand");P=!1}(),!1===e.parent().hasClass("m_group")&&e.addClass("enhanced")}function $(e){if(!e.hasClass("enhanced")){if(e.hasClass("m_group"))!function(e){var n=e.find("> .group-header"),a=n.next();if(V(e),V(n),n.attr("data-toggle","group"),n.find(".t_array, .t_object").each((function(){t(this).data("expand",!1),N(this,e)})),e.hasClass("expanded")||a.find(".m_error, .m_warn").not(".filter-hidden").not("[data-uncollapse=false]").length)return void I.push(n);n.debugEnhance("collapse",!0)}(e);else{if(e.hasClass("filter-hidden"))return;e.is(".m_table, .m_trace")?function(e){D(e),V(e),e.hasClass("m_table")&&e.find("> table > tbody > tr > td").each((function(){N(this,e)}));e.find("tbody > tr.expanded").next().trigger("expanded.debug.next"),E(e.find("> table"))}(e):function(e){var t;e.data("file")&&(e.attr("title")||(t=e.data("file")+": line "+e.data("line"),e.data("evalline")&&(t+=" (eval'd line "+e.data("evalline")+")"),e.attr("title",t)),D(e));(e.hasClass("m_error")||e.hasClass("m_warn"))&&e.find(".m_trace").debugEnhance();V(e),e.children().each((function(){N(this,e)})),e.hasClass("have-fatal")&&D(e,e.find("[data-type-more=filepath]"))}(e)}e.addClass("enhanced"),e.trigger("enhanced.debug")}}function N(e,n){var a=t(e);a.is(".t_array")?r(a):a.is(".t_object")?c(a):a.is("table")?E(a):a.is(".string-encoded.tabs-container")?N(a.find("> .tab-pane.active > *"),n):a.is("[data-type-more=filepath], .t_string[data-file")&&D(n,a)}function V(e){var n=function(e){var n,a;if(e.data("icon"))return e.data("icon").match("<")?t(e.data("icon")):t("").addClass(e.data("icon"));if(e.hasClass("m_group"))return n;return a=e.hasClass("group-header")?e.parent():e,function(e){var n,a;for(a in b.iconsMethods)if(e.is(a)){n=t(b.iconsMethods[a]);break}return n}(a)}(e);!function(e){var n;for(n in b.iconsMisc)e.find(n).each((function(){var e=t(this),a=t(b.iconsMisc[n]);e.find("> i:first-child").hasClass(a.attr("class"))||e.prepend(a)}))}(e),n&&(e.hasClass("m_group")?e=e.find("> .group-header .group-label").eq(0):e.find("> table").length&&(e=function(e){var n=e.parent(".no-indent").length>0,a=e.find("> table > caption");0===a.length&&!1===n&&(a=t(""),e.find("> table").prepend(a));return a}(e)),e.find("> i:first-child").hasClass(n.attr("class"))||e.prepend(n))}function W(e){var t;(M=(L=e).data("config")).get("drawer")&&(L.addClass("debug-drawer debug-enhanced-ui"),(t=L.find(".debug-menu-bar")).before('
    PHP
    '),t.find(".float-right").append(''),L.find(".tab-panes").scrollLock(),L.find(".debug-resize-handle").on("mousedown",U),L.find(".debug-pull-tab").on("click",B),L.find(".debug-menu-bar .close").on("click",q),M.get("persistDrawer")&&M.get("openDrawer")&&B())}function B(e){e&&(L=t(e.target).closest(".debug-drawer")),L.addClass("debug-drawer-open"),L.debugEnhance(),G(),t(window).on("resize",G),M.get("persistDrawer")&&M.set("openDrawer",!0)}function q(e){e&&(L=t(e.target).closest(".debug-drawer")),L.removeClass("debug-drawer-open"),t(window).off("resize",G),M.get("persistDrawer")&&M.set("openDrawer",!1),G(0)}function U(e){t(e.target).closest(".debug-drawer").is(".debug-drawer-open")&&(S=L.find(".tab-panes").height(),H=e.pageY,t("html").addClass("debug-resizing"),L.parents().on("mousemove",z).on("mouseup",K),e.preventDefault())}function z(e){G(S+(H-e.pageY),!0)}function K(){t("html").removeClass("debug-resizing"),L.parents().off("mousemove",z).off("mouseup",K)}function G(e,n){var a=L.find(".tab-panes"),i=L.find(".debug-menu-bar").outerHeight(),r=window.innerHeight-i-50;if(0===e)return t("body").style("marginBottom",""),void L.trigger("resize.debug");e=function(e){var t=L.find(".tab-panes");if(e&&"object"!=typeof e)return e;e=parseInt(t[0].style.height,10),!e&&M.get("persistDrawer")&&(e=M.get("height"));return e||100}(e),e=Math.min(e,r),e=Math.max(e,20),a.height(e),t("body").style("marginBottom",L.height()+8+"px"),L.trigger("resize.debug"),n&&M.get("persistDrawer")&&M.set("height",e)}t.fn.scrollLock=function(e){return(e=void 0===e||e)?void t(this).on("DOMMouseScroll mousewheel wheel",(function(e){var n=t(this),a=this.scrollTop,i=this.scrollHeight,r=n.innerHeight(),o=e.wheelDelta,s=o>0,c=function(){return e.stopPropagation(),e.preventDefault(),e.returnValue=!1,!1};return!s&&-o>i-r-a?(n.scrollTop(i),c()):s&&o>a?(n.scrollTop(0),c()):void 0})):this.off("DOMMouseScroll mousewheel wheel")};var J,Y,X=[],Q=!0,Z=[function(e){var t=e.data("channel")||e.closest(".debug").data("channelKeyRoot");if(Q){if(e.is(".m_warn, .m_error")&&!1!==e.data("uncollapse"))return!0;if(e.is(".m_group")&&e.find(".m_error, .m_warn").not(".filter-hidden").length)return!0}return X.indexOf(t)>-1}],ee=[function(e){var n=e.find("input[data-toggle=channel]");0!==n.length?(X=[],n.filter(":checked").each((function(){X.push(t(this).val())}))):X=[e.data("channelKeyRoot")]}];function te(){var e=t(this),n=e.is(":checked"),a=e.closest("label").next("ul").find("input"),i=e.closest(".debug");e.closest(".debug-options").length>0||"error"!==e.data("toggle")&&(a.prop("checked",n),ae(i))}function ne(){var e=t(this),n=e.is(":checked"),a=e.closest(".debug"),i=".group-body .error-"+e.val();a.find(i).toggleClass("filter-hidden",!n),a.find(".m_error, .m_warn").parents(".m_group").trigger("collapsed.debug.group"),oe(a),re(a.find("> .tab-panes > .tab-pane.active"))}function ae(e){var n,a,i=e.data("channelKeyRoot"),r=[];for(n in ee)ee[n](e);for(e.find("> .tab-panes > .tab-primary > .tab-body").find(".m_alert, .group-body > *:not(.m_groupSummary)").each((function(){r.push({depth:t(this).parentsUntil(".tab_body").length,node:t(this)})})),r.sort((function(e,t){return e.depth .tab-panes > .tab-pane.active")),oe(e)}function ie(e,t){var n,a=e.is(".filter-hidden");e.data("channel")!==t+".phpError"&&(n=function(e){var t,n=!0;for(t in Z)if(!(n=Z[t](e)))break;return n}(e),e.toggleClass("filter-hidden",!n),n&&a?function(e){var t=e.parent().closest(".m_group");t.length&&!t.hasClass("expanded")||e.debugEnhance()}(e):n||a||function(e){e.hasClass("m_group")&&e.find("> .group-body").debugEnhance()}(e),n&&e.hasClass("m_group")&&e.trigger("collapsed.debug.group"))}function re(e){e.find("> .tab-body > hr").toggleClass("filter-hidden",e.find("> .tab-body").find(" > .debug-log-summary, > .debug-log").filter((function(){return t(this).height()<1})).length>0)}function oe(e){var t=e.find(".debug-sidebar input:checkbox:not(:checked)").length>0;e.toggleClass("filter-active",t)}function se(e){var t=e+"=",n=document.cookie.split(";"),a=null,i=0;for(i=0;i1?n[t[1]]:n}var de,fe,ue,pe='
    ';function ge(e){var t;Y=(J=e).data("config"),(t=J.find(".debug-menu-bar")).find(".float-right").prepend(''),pe=Y.dict.replaceTokens(pe),t.append(pe),Y.get("drawer")||t.find("input[name=persistDrawer]").closest("label").remove(),J.find(".debug-options-toggle").on("click",ve),J.find("select[name=theme]").on("change",ke).val(Y.get("theme")),J.find("input[name=debugCookie]").on("change",be).prop("checked",Y.get("debugKey")&&se("debug")===Y.get("debugKey")).prop("disabled",!Y.get("debugKey")).closest("label").toggleClass("disabled",!Y.get("debugKey")),J.find("input[name=persistDrawer]").on("change",xe).prop("checked",Y.get("persistDrawer")),J.find("input[name=linkFiles]").on("change",ye).prop("checked",Y.get("linkFiles")).trigger("change"),J.find("input[name=linkFilesTemplate]").on("change",we).val(Y.get("linkFilesTemplate"))}function he(e){0===J.find(".debug-options").find(e.target).length&&_e()}function me(e){27===e.keyCode&&_e()}function be(){t(this).is(":checked")?ce("debug",Y.get("debugKey"),7):ce("debug","",-1)}function ve(e){var n=t(this).closest(".debug-bar").find(".debug-options").is(".show");J=t(this).closest(".debug"),n?_e():(J.find(".debug-options").addClass("show"),t("body").on("click",he),t("body").on("keyup",me)),e.stopPropagation()}function ye(){var e=t(this).prop("checked"),n=t(this).closest(".debug-options").find("input[name=linkFilesTemplate]").closest(".form-group");e?n.slideDown():n.slideUp(),Y.set("linkFiles",e),t("input[name=linkFilesTemplate]").trigger("change")}function we(){var e=t(this).val();Y.set("linkFilesTemplate",e),J.trigger("config.debug.updated","linkFilesTemplate")}function xe(){var e=t(this).is(":checked");Y.set({persistDrawer:e,openDrawer:e,openSidebar:!0})}function ke(){Y.set("theme",t(this).val()),J.attr("data-theme",Y.themeGet())}function _e(){J.find(".debug-options").removeClass("show"),t("body").off("click",he),t("body").off("keyup",me)}var Ce,Oe,Ee,Te=!1,je={alert:'{string:side.alert}',error:'{string:side.error}',warn:'{string:side.warning}',info:'{string:side.info}',other:'{string:side.other}'};function De(e){var n;(de=e.data("config")||t("body").data("config"),(fe=e.find("> .tab-panes > .tab-primary").data("options")||{}).sidebar&&Ie(e),de.get("persistDrawer")&&!de.get("openSidebar")&&Pe(e),e.on("click",".close[data-dismiss=alert]",Le),e.on("click",".sidebar-toggle",Me),e.on("change",".debug-sidebar input[type=checkbox]",Ae),Te)||(n=He,ee.push(n),function(e){Z.push(e)}(Se),Te=!0)}function Ae(e){var n=t(this),a=n.closest(".toggle"),i=a.next("ul").find(".toggle"),r=n.is(":checked"),o=t(".m_alert.error-summary.have-fatal");a.toggleClass("active",r),i.toggleClass("active",r),"fatal"===n.val()&&(o.find(".error-fatal").toggleClass("filter-hidden",!r),o.toggleClass("filter-hidden",0===o.children().not(".filter-hidden").length))}function Le(e){var n=t(e.delegateTarget);setTimeout((function(){0===n.find(".tab-primary > .tab-body > .m_alert").length&&n.find(".debug-sidebar input[data-toggle=method][value=alert]").parent().addClass("disabled")}))}function Me(){var e=t(this).closest(".debug");e.find(".debug-sidebar").is(".show")?Pe(e):Fe(e)}function Se(e){var t=e[0].className.match(/\bm_(\S+)\b/),n=t?t[1]:null;return!fe.sidebar||("group"===n&&e.find("> .group-body")[0].className.match(/level-(error|info|warn)/)&&(n=e.find("> .group-body")[0].className.match(/level-(error|info|warn)/)[1],e.toggleClass("filter-hidden-body",ue.indexOf(n)<0)),["alert","error","warn","info"].indexOf(n)>-1?ue.indexOf(n)>-1:ue.indexOf("other")>-1)}function He(e){var n=e.find(".tab-pane.active .debug-sidebar");ue=[],0===n.length&&(ue=Object.keys(je)),n.find("input[data-toggle=method]:checked").each((function(){ue.push(t(this).val())}))}function Ie(e){var n=t(de.dict.replaceTokens('
    ')),a=e.find(".tab-panes > .tab-primary > .tab-body > .expand-all");e.find(".tab-panes > .tab-primary > .tab-body").before(n),function(e){var t=e.closest(".debug").find(".m_alert.error-summary"),n=t.find(".in-console");n.prev().remove(),n.remove(),0===t.children().length&&t.remove()}(e),function(e){var n=e.find(".debug-sidebar .php-errors ul"),a=["fatal","error","warning","deprecated","notice","strict"];t.each(a,(function(a){var i="fatal"===a?e.find(".m_alert.error-summary.have-fatal").length:e.find(".error-"+a).filter(".m_error,.m_warn").length;0!==i&&n.append(t("
  • ").append(t('
  • ").append(t('