diff --git a/composer.json b/composer.json index 4791a668..ca5e10ef 100644 --- a/composer.json +++ b/composer.json @@ -46,9 +46,9 @@ } }, "replace": { - "bdk/backtrace": "2.2", + "bdk/backtrace": "2.2.1", "bdk/curl-http-message": "1.0", - "bdk/errorhandler": "3.3.1", + "bdk/errorhandler": "3.3.2", "bdk/http-message": "1.1", "bdk/promise": "1.0", "bdk/pubsub": "3.2.1", diff --git a/src/Backtrace/Normalizer.php b/src/Backtrace/Normalizer.php index a1486ae2..7d940f16 100644 --- a/src/Backtrace/Normalizer.php +++ b/src/Backtrace/Normalizer.php @@ -20,6 +20,7 @@ class Normalizer private static $frameDefault = array( 'args' => array(), + 'evalLine' => null, 'file' => null, 'function' => null, // function, Class::function, or Class->function 'line' => null, @@ -42,7 +43,6 @@ public static function normalize($backtrace) 'params' => null, 'type' => null, ); - $frameKeysKeep = \array_merge(self::$frameDefault, \array_flip(array('evalLine'))); $count = \count($backtrace); $backtrace[] = array(); // add a frame so backtrace[$i + 1] is always a thing for ($i = 0; $i < $count; $i++) { @@ -59,7 +59,7 @@ public static function normalize($backtrace) // xdebug_get_function_stack $frame['args'] = self::normalizeXdebugParams($frame['params']); } - $frame = \array_intersect_key($frame, $frameKeysKeep); + $frame = \array_intersect_key($frame, self::$frameDefault); \ksort($frame); self::$backtraceTemp[] = $frame; } @@ -89,8 +89,8 @@ private static function normalizeFrameFile(array $frame, array &$frameNext) // reported line = line within eval // line inside paren is the line `eval` is on $frame['evalLine'] = $frame['line']; - $frame['line'] = (int) $matches[2]; $frame['file'] = $matches[1]; + $frame['line'] = (int) $matches[2]; if (isset($frameNext['include_filename'])) { // xdebug_get_function_stack puts the evaled code in include_filename $frameNext['params'] = array($frameNext['include_filename']); diff --git a/src/Debug/Abstraction/AbstractObject.php b/src/Debug/Abstraction/AbstractObject.php index b6704893..9fbcdd6b 100644 --- a/src/Debug/Abstraction/AbstractObject.php +++ b/src/Debug/Abstraction/AbstractObject.php @@ -26,54 +26,52 @@ use ReflectionClass; use ReflectionEnumUnitCase; use RuntimeException; -use UnitEnum; /** * Abstracter: Methods used to abstract objects */ class AbstractObject extends AbstractComponent { - // GENERAL - const PHPDOC_COLLECT = 1; // 2^0 - const PHPDOC_OUTPUT = 2; // 2^1 - const OBJ_ATTRIBUTE_COLLECT = 4; - const OBJ_ATTRIBUTE_OUTPUT = 8; - const TO_STRING_OUTPUT = 16; // 2^4 const BRIEF = 4194304; // 2^22 - // CONSTANTS - const CONST_COLLECT = 32; - const CONST_OUTPUT = 64; - const CONST_ATTRIBUTE_COLLECT = 128; - const CONST_ATTRIBUTE_OUTPUT = 256; // 2^8 - - // CASE + // CASE (2^9 - 2^12) const CASE_COLLECT = 512; const CASE_OUTPUT = 1024; const CASE_ATTRIBUTE_COLLECT = 2048; - const CASE_ATTRIBUTE_OUTPUT = 4096; // 2^12 + const CASE_ATTRIBUTE_OUTPUT = 4096; - // PROPERTIES - const PROP_ATTRIBUTE_COLLECT = 8192; - const PROP_ATTRIBUTE_OUTPUT = 16384; // 2^14 + // CONSTANTS (2^5 - 2^8) + const CONST_COLLECT = 32; + const CONST_OUTPUT = 64; + const CONST_ATTRIBUTE_COLLECT = 128; + const CONST_ATTRIBUTE_OUTPUT = 256; - // METHODS + // METHODS (2^15 - 2^21, 2^23 - 2^24) const METHOD_COLLECT = 32768; const METHOD_OUTPUT = 65536; + const METHOD_DESC_OUTPUT = 524288; const METHOD_ATTRIBUTE_COLLECT = 131072; const METHOD_ATTRIBUTE_OUTPUT = 262144; - const METHOD_DESC_OUTPUT = 524288; + const METHOD_STATIC_VAR_COLLECT = 8388608; // 2^23 + const METHOD_STATIC_VAR_OUTPUT = 16777216; // 2^24 + + const OBJ_ATTRIBUTE_COLLECT = 4; + const OBJ_ATTRIBUTE_OUTPUT = 8; + const PARAM_ATTRIBUTE_COLLECT = 1048576; - const PARAM_ATTRIBUTE_OUTPUT = 2097152; // 2^21 + const PARAM_ATTRIBUTE_OUTPUT = 2097152; + + const PHPDOC_COLLECT = 1; // 2^0 + const PHPDOC_OUTPUT = 2; - public static $cfgFlags = array( // @phpcs:ignore SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys.IncorrectKeyOrder - // GENERAL + // PROPERTIES (2^13 - 2^14) + const PROP_ATTRIBUTE_COLLECT = 8192; // 2^13 + const PROP_ATTRIBUTE_OUTPUT = 16384; // 2^14 + + const TO_STRING_OUTPUT = 16; // 2^4 + + public static $cfgFlags = array( 'brief' => self::BRIEF, - 'objAttributeCollect' => self::OBJ_ATTRIBUTE_COLLECT, - 'objAttributeOutput' => self::OBJ_ATTRIBUTE_OUTPUT, - 'phpDocCollect' => self::PHPDOC_COLLECT, - 'phpDocOutput' => self::PHPDOC_OUTPUT, - 'toStringOutput' => self::TO_STRING_OUTPUT, // CASE 'caseAttributeCollect' => self::CASE_ATTRIBUTE_COLLECT, @@ -93,12 +91,23 @@ class AbstractObject extends AbstractComponent 'methodCollect' => self::METHOD_COLLECT, 'methodDescOutput' => self::METHOD_DESC_OUTPUT, 'methodOutput' => self::METHOD_OUTPUT, + 'methodStaticVarCollect' => self::METHOD_STATIC_VAR_COLLECT, + 'methodStaticVarOutput' => self::METHOD_STATIC_VAR_OUTPUT, + + 'objAttributeCollect' => self::OBJ_ATTRIBUTE_COLLECT, + 'objAttributeOutput' => self::OBJ_ATTRIBUTE_OUTPUT, + 'paramAttributeCollect' => self::PARAM_ATTRIBUTE_COLLECT, 'paramAttributeOutput' => self::PARAM_ATTRIBUTE_OUTPUT, + 'phpDocCollect' => self::PHPDOC_COLLECT, + 'phpDocOutput' => self::PHPDOC_OUTPUT, + // PROPERTIES 'propAttributeCollect' => self::PROP_ATTRIBUTE_COLLECT, 'propAttributeOutput' => self::PROP_ATTRIBUTE_OUTPUT, + + 'toStringOutput' => self::TO_STRING_OUTPUT, ); protected $abstracter; @@ -137,32 +146,24 @@ class AbstractObject extends AbstractComponent * * @var array Array of key/values */ - protected static $values = array( // phpcs:ignore SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys.IncorrectKeyOrder - 'type' => Abstracter::TYPE_OBJECT, + protected static $values = array( 'cfgFlags' => 0, 'className' => '', 'debugMethod' => '', - 'isAnonymous' => false, + 'interfacesCollapse' => array(), // cfg.interfacesCollapse 'isExcluded' => false, // don't exclude if we're debugging directly 'isMaxDepth' => false, 'isRecursion' => false, + // methods may be populated with __toString info, or methods with staticVars 'properties' => array(), 'scopeClass' => '', + 'sectionOrder' => array(), // cfg.objectSectionOrder 'sort' => '', // cfg.objectSort 'stringified' => null, 'traverseValues' => array(), // populated if method is table 'viaDebugInfo' => false, ); - protected static $keysTemp = array( - 'collectPropertyValues', - 'fullyQualifyPhpDocType', - 'hist', - 'isTraverseOnly', - 'propertyOverrideValues', - 'reflector', - ); - /** * Constructor * @@ -202,12 +203,12 @@ public function getAbstraction($obj, $method = null, array $hist = array()) $reflector = $reflector->getEnum(); } $values = $this->getAbstractionValues($reflector, $obj, $method, $hist); - $valueStore = $this->definition->getValueStore($obj, $values); - $abs = new ObjectAbstraction($valueStore, $values); + $definitionValueStore = $this->definition->getAbstraction($obj, $values); + $abs = new ObjectAbstraction($definitionValueStore, $values); $abs->setSubject($obj); $abs['hist'][] = $obj; $this->doAbstraction($abs); - $this->absClean($abs); + $abs->clean(); return $abs; } @@ -233,21 +234,6 @@ public static function buildObjValues(array $values = array()) ); } - /** - * Sort things and remove temporary values - * - * @param ObjectAbstraction $abs Abstraction instance - * - * @return void - */ - private function absClean(ObjectAbstraction $abs) - { - $values = \array_diff_key($abs->getInstanceValues(), \array_flip(self::$keysTemp)); - $abs - ->setSubject(null) - ->setValues($values); - } - /** * Populate rows or columns (traverseValues) if we're outputing as a table * @@ -268,9 +254,10 @@ private function addTraverseValues(ObjectAbstraction $abs) } /** - * Add attributes, constants, properties, methods, constants, etc + * Collect instance info + * Property values & static method variables * - * @param ObjectAbstraction $abs Abstraction instance + * @param ObjectAbstraction $abs Object abstraction instance * * @return void */ @@ -280,11 +267,6 @@ private function doAbstraction(ObjectAbstraction $abs) return; } if ($abs['isRecursion']) { - if ($abs->getSubject() instanceof UnitEnum) { - $abs['properties']['name'] = array( - 'value' => $abs->getSubject()->name, - ); - } return; } $abs['isTraverseOnly'] = $this->isTraverseOnly($abs); @@ -304,7 +286,8 @@ private function doAbstraction(ObjectAbstraction $abs) if ($abs['isTraverseOnly']) { $this->addTraverseValues($abs); } - $this->properties->add($abs); + $this->methods->addInstance($abs); // method static variables + $this->properties->addInstance($abs); /* Debug::EVENT_OBJ_ABSTRACT_END subscriber has free reign to modify abtraction values */ @@ -312,7 +295,7 @@ private function doAbstraction(ObjectAbstraction $abs) } /** - * Returns information about an object + * Get initial "top-level" values. * * @param ReflectionClass $reflector Reflection instance * @param object|string $obj Object (or classname) to inspect @@ -330,11 +313,12 @@ protected function getAbstractionValues(ReflectionClass $reflector, $obj, $metho 'cfgFlags' => $this->getCfgFlags(), 'className' => $this->helper->getClassName($reflector), 'debugMethod' => $method, - 'isAnonymous' => PHP_VERSION_ID >= 70000 && $reflector->isAnonymous(), + 'interfacesCollapse' => \array_values(\array_intersect($reflector->getInterfaceNames(), $this->cfg['interfacesCollapse'])), 'isExcluded' => $hist && $this->isExcluded($obj), // don't exclude if we're debugging directly 'isMaxDepth' => $this->cfg['maxDepth'] && \count($hist) === $this->cfg['maxDepth'], 'isRecursion' => \in_array($obj, $hist, true), 'scopeClass' => $this->getScopeClass($hist), + 'sectionOrder' => $this->cfg['objectSectionOrder'], 'sort' => $this->cfg['objectSort'], 'viaDebugInfo' => $this->cfg['useDebugInfo'] && $reflector->hasMethod('__debugInfo'), ), @@ -355,7 +339,7 @@ protected function getAbstractionValues(ReflectionClass $reflector, $obj, $metho * * @return int bitmask */ - private function getCfgFlags() + protected function getCfgFlags() { $flagVals = \array_intersect_key(self::$cfgFlags, \array_filter($this->cfg)); $bitmask = \array_reduce($flagVals, static function ($carry, $val) { diff --git a/src/Debug/Abstraction/Abstracter.php b/src/Debug/Abstraction/Abstracter.php index 2e9c4917..ae6ae500 100644 --- a/src/Debug/Abstraction/Abstracter.php +++ b/src/Debug/Abstraction/Abstracter.php @@ -74,12 +74,30 @@ class Abstracter extends AbstractComponent protected $cfg = array( 'brief' => false, // collect & output less details 'fullyQualifyPhpDocType' => false, + 'interfacesCollapse' => array( + 'ArrayAccess', + 'BackedEnum', + 'Countable', + 'Iterator', + 'IteratorAggregate', + 'UnitEnum', + ), 'maxDepth' => 0, // value < 1 : no max-depth + 'objectSectionOrder' => array( + 'attributes', + 'extends', + 'implements', + 'constants', + 'cases', + 'properties', + 'methods', + 'phpDoc', + ), 'objectsExclude' => array( // __NAMESPACE__ added in constructor 'DOMNode', ), - 'objectSort' => 'visibility', // none, visibility, or name + 'objectSort' => 'inheritance visibility name', 'objectsWhitelist' => null, // will be used if array 'stringMaxLen' => array( 'base64' => 156, // 2 lines of chunk_split'ed @@ -470,7 +488,6 @@ protected function postSetCfg($cfg = array(), $prev = array()) $debugClass = \get_class($this->debug); if (!\array_intersect(array('*', $debugClass), $this->cfg['objectsExclude'])) { $this->cfg['objectsExclude'][] = $debugClass; - $cfg['objectsExclude'] = $this->cfg['objectsExclude']; } if (isset($cfg['stringMaxLen'])) { if (\is_array($cfg['stringMaxLen']) === false) { @@ -478,14 +495,18 @@ protected function postSetCfg($cfg = array(), $prev = array()) 'other' => $cfg['stringMaxLen'], ); } - $cfg['stringMaxLen'] = \array_merge($prev['stringMaxLen'], $cfg['stringMaxLen']); - $this->cfg['stringMaxLen'] = $cfg['stringMaxLen']; + $this->cfg['stringMaxLen'] = \array_merge($prev['stringMaxLen'], $cfg['stringMaxLen']); } if (isset($cfg['stringMinLen'])) { - $cfg['stringMinLen'] = \array_merge($prev['stringMinLen'], $cfg['stringMinLen']); - $this->cfg['stringMinLen'] = $cfg['stringMinLen']; + $this->cfg['stringMinLen'] = \array_merge($prev['stringMinLen'], $cfg['stringMinLen']); + } + if (isset($cfg['objectSectionOrder'])) { + $oso = \array_intersect($cfg['objectSectionOrder'], $prev['objectSectionOrder']); + $oso = \array_merge($oso, $prev['objectSectionOrder']); + $oso = \array_unique($oso); + $this->cfg['objectSectionOrder'] = $oso; } - $this->setCfgDependencies($cfg); + $this->setCfgDependencies(\array_intersect_key($this->cfg, $cfg)); } /** diff --git a/src/Debug/Abstraction/Object/Abstraction.php b/src/Debug/Abstraction/Object/Abstraction.php index 3450a3ee..77ab1c49 100644 --- a/src/Debug/Abstraction/Object/Abstraction.php +++ b/src/Debug/Abstraction/Object/Abstraction.php @@ -22,19 +22,29 @@ */ class Abstraction extends BaseAbstraction { - private $definition; + protected static $keysTemp = array( + 'collectPropertyValues', + 'fullyQualifyPhpDocType', + 'hist', + 'isTraverseOnly', + 'propertyOverrideValues', + 'reflector', + ); + + /** @var ValueStore */ + private $inherited; private $sortableValues = array('attributes', 'cases', 'constants', 'methods', 'properties'); /** * Constructor * - * @param ValueStore $definition class definition values - * @param array $values abtraction values + * @param ValueStore $inherited Inherited values + * @param array $values Abtraction values */ - public function __construct(ValueStore $definition, $values = array()) + public function __construct(ValueStore $inherited, $values = array()) { - $this->definition = $definition; + $this->inherited = $inherited; parent::__construct(Abstracter::TYPE_OBJECT, $values); } @@ -43,7 +53,7 @@ public function __construct(ValueStore $definition, $values = array()) */ public function __serialize() { - return $this->getInstanceValues() + array('classDefinition' => $this->definition); + return $this->getInstanceValues() + array('inherited' => $this->inherited); } /** @@ -67,13 +77,24 @@ public function __toString() */ public function __unserialize($data) { - $this->definition = isset($data['classDefinition']) - ? $data['classDefinition'] + $this->inherited = isset($data['inherited']) + ? $data['inherited'] : new ValueStore(); - unset($data['classDefinition']); + unset($data['inherited']); $this->values = $data; } + /** + * Remove temporary values + * + * @return void + */ + public function clean() + { + $this->values = \array_diff_key($this->values, \array_flip(self::$keysTemp)); + $this->setSubject(null); + } + /** * Implements JsonSerializable * @@ -83,8 +104,9 @@ public function __unserialize($data) public function jsonSerialize() { return $this->getInstanceValues() + array( - 'classDefinition' => $this->definition['className'], 'debug' => Abstracter::ABSTRACTION, + 'inheritsFrom' => $this->inherited['className'], + 'type' => Abstracter::TYPE_OBJECT, ); } @@ -93,9 +115,11 @@ public function jsonSerialize() * * @return array */ - public function getDefinitionValues() + public function getInheritedValues() { - return $this->definition->getValues(); + $values = $this->inherited->getValues(); + unset($values['cfgFlags']); + return $values; } /** @@ -104,7 +128,7 @@ public function getDefinitionValues() public function getValues() { return \array_replace_recursive( - $this->getDefinitionValues(), + $this->getInheritedValues(), $this->values ); } @@ -118,7 +142,7 @@ public function getInstanceValues() { return ArrayUtil::diffAssocRecursive( $this->values, - $this->getDefinitionValues() + $this->getInheritedValues() ); } @@ -130,36 +154,31 @@ public function getInstanceValues() * * @return array */ - public function sort(array $array, $order = 'visibility') + public function sort(array $array, $order = 'visibility, name') { - $sortVisOrder = array('public', 'magic', 'magic-read', 'magic-write', 'protected', 'private', 'debug'); - $sortData = array( - 'name' => array(), - 'vis' => array(), + $order = \preg_split('/[,\s]+/', (string) $order); + $aliases = array( + 'name' => 'name', + 'vis' => 'vis', + 'visibility' => 'vis', ); - foreach ($array as $name => $info) { - if ($name === '__construct') { - // always place __construct at the top - $sortData['name'][$name] = -1; - $sortData['vis'][$name] = 0; + foreach ($order as $i => $what) { + if (isset($aliases[$what]) === false) { + unset($order[$i]); continue; } - $vis = isset($info['visibility']) - ? $info['visibility'] - : '?'; - if (\is_array($vis)) { - // Sort the visiblity so we use the most significant vis - ArrayUtil::sortWithOrder($vis, $sortVisOrder); - $vis = $vis[0]; - } - $sortData['name'][$name] = \strtolower($name); - $sortData['vis'][$name] = \array_search($vis, $sortVisOrder, true); + $order[$i] = $aliases[$what]; + } + if (empty($order)) { + return $array; } - if ($order === 'name') { - \array_multisort($sortData['name'], $array); - } elseif ($order === 'visibility') { - \array_multisort($sortData['vis'], $sortData['name'], $array); + $multiSortArgs = array(); + $sortData = $this->sortData($array); + foreach ($order as $what) { + $multiSortArgs[] = $sortData[$what]; } + $multiSortArgs[] = &$array; + \call_user_func_array('array_multisort', $multiSortArgs); return $array; } @@ -172,7 +191,7 @@ public function offsetExists($key) if (\array_key_exists($key, $this->values)) { return $this->values[$key] !== null; } - return isset($this->definition[$key]); + return isset($this->inherited[$key]); } /** @@ -198,10 +217,19 @@ private function getCombinedValue($key) $value = isset($this->values[$key]) ? $this->values[$key] : null; - $classVal = \in_array($key, $this->sortableValues, true) - && ($this->values['isRecursion'] || $this->values['isExcluded']) - ? array() // don't inherit - : $this->definition[$key]; + $inherit = true; + if (\in_array($key, $this->sortableValues, true)) { + $combined = \array_merge(array( + 'isExcluded' => $this->inherited['isExcluded'], + 'isRecursion' => $this->inherited['isRecursion'], + ), $this->values); + if ($combined['isExcluded'] || $combined['isRecursion']) { + $inherit = false; + } + } + $classVal = $inherit + ? $this->inherited[$key] + : array(); if ($value !== null) { return \is_array($classVal) ? \array_replace_recursive($classVal, $value) @@ -209,4 +237,39 @@ private function getCombinedValue($key) } return $classVal; } + + /** + * Collect sort data to be used by `array_multisort` + * + * @param array $array The array of methods or properties to be sorted + * + * @return array + */ + protected function sortData(array $array) + { + $sortVisOrder = array('public', 'magic', 'magic-read', 'magic-write', 'protected', 'private', 'debug'); + $sortData = array( + 'name' => array(), + 'vis' => array(), + ); + foreach ($array as $name => $info) { + if ($name === '__construct') { + // always place __construct at the top + $sortData['name'][$name] = -1; + $sortData['vis'][$name] = 0; + continue; + } + $vis = isset($info['visibility']) + ? $info['visibility'] + : '?'; + if (\is_array($vis)) { + // Sort the visiblity so we use the most significant vis + ArrayUtil::sortWithOrder($vis, $sortVisOrder); + $vis = $vis[0]; + } + $sortData['name'][$name] = \strtolower($name); + $sortData['vis'][$name] = \array_search($vis, $sortVisOrder, true); + } + return $sortData; + } } diff --git a/src/Debug/Abstraction/Object/Definition.php b/src/Debug/Abstraction/Object/Definition.php index 5e4802d3..8311dc22 100644 --- a/src/Debug/Abstraction/Object/Definition.php +++ b/src/Debug/Abstraction/Object/Definition.php @@ -15,7 +15,9 @@ 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\PubSub\ValueStore; +use ReflectionClass; /** * Abstracter: Gather class definition info common across all instances @@ -26,10 +28,11 @@ class Definition protected $debug; protected $helper; protected $methods; + protected $object; protected $properties; /** @var ValueStore|null base/default class values */ - protected static $default; + protected $default; /** * @var array Array of key/values @@ -41,15 +44,17 @@ class Definition 'className' => "\x00default\x00", 'constants' => array(), 'definition' => array( - 'extensionName' => '', - 'fileName' => '', - 'startLine' => 1, + 'extensionName' => false, + 'fileName' => false, + 'startLine' => false, ), 'extends' => array(), 'implements' => array(), + 'isAnonymous' => false, 'isFinal' => false, 'isReadOnly' => false, 'methods' => array(), + 'methodsWithStaticVars' => array(), 'phpDoc' => array( 'desc' => null, 'summary' => null, @@ -65,77 +70,41 @@ class Definition public function __construct(AbstractObject $abstractObject) { $this->debug = $abstractObject->debug; + $this->object = $abstractObject; $this->helper = $abstractObject->helper; $this->constants = $abstractObject->constants; $this->methods = $abstractObject->methods; $this->properties = $abstractObject->properties; } - /** - * Get class abstraction - * - * @param object $obj Object being abstracted - * @param array $info values already collected - * - * @return Abstraction - */ - public function getAbstraction($obj, array $info = array()) - { - $abs = $this->getAbstractionInit($info); - $abs->setSubject($obj); - $abs['fullyQualifyPhpDocType'] = $info['fullyQualifyPhpDocType']; - $abs['reflector'] = $info['reflector']; - - $this->addDefinition($abs); - $this->addAttributes($abs); - $this->constants->add($abs); - $this->constants->addCases($abs); - $this->methods->add($abs); - $this->properties->addClass($abs); - if ($abs['className'] === 'Closure') { - // __incoke is "unique" per instance - $abs['methods']['__invoke'] = array(); - } - - unset($abs['fullyQualifyPhpDocType']); - unset($abs['reflector']); - return $abs; - } - /** * Get class ValueStore obj * - * @param object $obj Object being abstracted - * @param array $info Instance abstraction info + * @param object $obj Object being abstracted + * @param array $values Instance values * * @return ValueStore */ - public function getValueStore($obj, array $info) + public function getAbstraction($obj, array $values) { - $className = $info['className']; - $reflector = $info['reflector']; - if ($info['isAnonymous']) { - if ($reflector->getParentClass() === false) { - return $this->getValueStoreDefault(); - } - $reflector = $reflector->getParentClass(); - $className = $reflector->getName(); - $info['reflector'] = $reflector; - } - $dataPath = array('classDefinitions', $className); + $className = $values['className']; + $reflector = $values['reflector']; + $valueStoreKey = PHP_VERSION_ID >= 70000 && $reflector->isAnonymous() + ? $className . '|' . \md5($reflector->getName()) + : $className; + $dataPath = array('classDefinitions', $valueStoreKey); $valueStore = $this->debug->data->get($dataPath); if ($valueStore) { return $valueStore; } - if (\array_filter(array($info['isMaxDepth'], $info['isExcluded']))) { + if (\array_filter(array($values['isMaxDepth'], $values['isExcluded']))) { return $this->getValueStoreDefault(); } - $valueStore = new ValueStore(); - $this->debug->data->set($dataPath, $valueStore); - $classAbs = $this->getAbstraction($obj, $info); - $valueStore->setValues($classAbs->getValues()); - unset($valueStore['type']); - return $valueStore; + $abs = new ObjectAbstraction($this->getValueStoreDefault(), $this->getInitValues($values)); + $abs->setSubject($obj); + $this->doAbstraction($abs); + $this->debug->data->set($dataPath, $abs); + return $abs; } /** @@ -145,12 +114,22 @@ public function getValueStore($obj, array $info) */ public function getValueStoreDefault() { - if (self::$default) { - return self::$default; + if ($this->default) { + return $this->default; } $key = self::$values['className']; - $classValueStore = new ValueStore(self::$values); - self::$default = $classValueStore; + $classValueStore = new ValueStore(\array_merge( + self::$values, + array( + 'isExcluded' => false, + 'sectionOrder' => $this->object->getCfg('objectSectionOrder'), + 'sort' => $this->object->getCfg('objectSort'), + 'stringified' => null, + 'traverseValues' => array(), + 'viaDebugInfo' => false, + ) + )); + $this->default = $classValueStore; $this->debug->data->set(array( 'classDefinitions', $key, @@ -161,22 +140,22 @@ public function getValueStoreDefault() /** * Collect class attributes * - * @param ValueStore $abs Object abstraction instance + * @param ValueStore $abs ValueStore instance * * @return void */ public function addAttributes(ValueStore $abs) { - $reflector = $abs['reflector']; - $abs['attributes'] = $abs['cfgFlags'] & AbstractObject::OBJ_ATTRIBUTE_COLLECT - ? $this->helper->getAttributes($reflector) - : array(); + if ($abs['cfgFlags'] & AbstractObject::OBJ_ATTRIBUTE_COLLECT) { + $reflector = $abs['reflector']; + $abs['attributes'] = $this->helper->getAttributes($reflector); + } } /** * Collect "definition" values * - * @param ValueStore $abs Object abstraction instance + * @param ValueStore $abs ValueStore instance * * @return void */ @@ -192,37 +171,135 @@ public function addDefinition(ValueStore $abs) ); } + /** + * Collect interfaces that object implements + * + * @param ValueStore $abs ValueStore instance + * + * @return void + */ + public function addImplements(ValueStore $abs) + { + $abs['implements'] = $this->getInterfaces($abs['reflector']); + } + + /** + * Collect phpDoc summary/description/params + * + * @param ValueStore $abs ValueStore instance + * + * @return void + */ + public function addPhpDoc(ValueStore $abs) + { + $reflector = $abs['reflector']; + $fullyQualifyType = $abs['fullyQualifyPhpDocType']; + $phpDoc = $this->helper->getPhpDoc($reflector, $fullyQualifyType); + while ( + ($reflector = $reflector->getParentClass()) + && $phpDoc === array('desc' => null, 'summary' => null) + ) { + $phpDoc = $this->helper->getPhpDoc($reflector, $fullyQualifyType); + } + unset($phpDoc['method']); + // magic properties removed via PropertiesPhpDoc::addViaPhpDocIter + $abs['phpDoc'] = $phpDoc; + } + + /** + * Collect runtime info + * attributes, constants, properties, methods, etc + * + * @param ObjectAbstraction $abs Object abstraction instance + * + * @return void + */ + protected function doAbstraction(ObjectAbstraction $abs) + { + $this->addAttributes($abs); + $this->addDefinition($abs); + $this->addImplements($abs); + $this->addPhpDoc($abs); + $this->constants->add($abs); + $this->constants->addCases($abs); + $this->methods->add($abs); + $this->properties->add($abs); + + if ($abs['className'] === 'Closure') { + // __invoke is "unique" per instance + $abs['methods']['__invoke'] = array(); + } + + $abs->clean(); + } + + /** + * Get a structured interface tree + * + * @param ReflectionClass $reflector ReflectionClass + * + * @return array + */ + protected function getInterfaces(ReflectionClass $reflector) + { + $interfaces = array(); + $remove = array(); + foreach ($reflector->getInterfaces() as $classname => $refClass) { + if (\in_array($classname, $remove, true)) { + continue; + } + $extends = $refClass->getInterfaceNames(); + if ($extends) { + $interfaces[$classname] = $this->getInterfaces($refClass); + $remove = \array_merge($remove, $extends); + continue; + } + $interfaces[] = $classname; + } + $remove = \array_unique($remove); + $interfaces = \array_diff_key($interfaces, \array_flip($remove)); + // remove values... array_diff complains about array to string conversion + foreach ($remove as $classname) { + $key = \array_search($classname, $interfaces, true); + if ($key !== false) { + unset($interfaces[$key]); + } + } + return $interfaces; + } + /** * Initialize class definition abstraction * - * @param array $info values already collected + * @param array $values values already collected * * @return Absttraction */ - protected function getAbstractionInit(array $info) + protected function getInitValues(array $values) { - $reflector = $info['reflector']; - $interfaceNames = $reflector->getInterfaceNames(); - \sort($interfaceNames); + $reflector = $values['reflector']; + $isAnonymous = PHP_VERSION_ID >= 70000 && $reflector->isAnonymous(); $values = \array_merge( self::$values, array( - 'cfgFlags' => $info['cfgFlags'], - 'className' => $reflector->getName(), - 'implements' => $interfaceNames, + 'cfgFlags' => $values['cfgFlags'], + 'className' => $isAnonymous + ? $values['className'] . '|' . \md5($reflector->getName()) + : $values['className'], + 'isAnonymous' => $isAnonymous, 'isFinal' => $reflector->isFinal(), 'isReadOnly' => PHP_VERSION_ID >= 80200 && $reflector->isReadOnly(), - 'phpDoc' => $this->helper->getPhpDoc($reflector, $info['fullyQualifyPhpDocType']), + ), + array( + // these are temporary values available during abstraction + 'fullyQualifyPhpDocType' => $values['fullyQualifyPhpDocType'], + 'hist' => array(), + 'reflector' => $values['reflector'], ) ); while ($reflector = $reflector->getParentClass()) { - if ($values['phpDoc'] === array('desc' => null, 'summary' => null)) { - $values['phpDoc'] = $this->helper->getPhpDoc($reflector, $info['fullyQualifyPhpDocType']); - } $values['extends'][] = $reflector->getName(); } - unset($values['phpDoc']['method']); - // magic properties removed via PropertiesPhpDoc::addViaPhpDocIter - return new Abstraction(Abstracter::TYPE_OBJECT, $values); + return $values; } } diff --git a/src/Debug/Abstraction/Object/Methods.php b/src/Debug/Abstraction/Object/Methods.php index 691266ed..5a7b59ac 100644 --- a/src/Debug/Abstraction/Object/Methods.php +++ b/src/Debug/Abstraction/Object/Methods.php @@ -46,6 +46,7 @@ class Methods extends Inheritable 'desc' => null, 'type' => null, ), + 'staticVars' => array(), 'visibility' => 'public', // public | private | protected | magic ); @@ -77,6 +78,28 @@ public function add(Abstraction $abs) } } + /** + * Add static variable info to abstraction + * + * @param Abstraction $abs Object Abstraction instance + * + * @return void + */ + public function addInstance(Abstraction $abs) + { + $staticVarCollect = $abs['cfgFlags'] & AbstractObject::METHOD_COLLECT + && $abs['cfgFlags'] & AbstractObject::METHOD_STATIC_VAR_COLLECT; + if ($staticVarCollect === false) { + return; + } + foreach ($abs['methodsWithStaticVars'] as $name) { + $reflector = $abs['reflector']->getMethod($name); + $abs['methods'][$name]['staticVars'] = \array_map(function ($value) use ($abs) { + return $this->abstracter->crate($value, $abs['debugMethod'], $abs['hist']); + }, $reflector->getStaticVariables()); + } + } + /** * Return method info array * @@ -138,8 +161,10 @@ private function addMethodsFull(Abstraction $abs) } $methods = $abs['methods']; foreach ($methods as &$methodInfo) { - $methodInfo['phpDoc']['desc'] = null; - $methodInfo['phpDoc']['summary'] = null; + $methodInfo['phpDoc'] = array( + 'desc' => null, + 'summary' => null, + ); foreach (\array_keys($methodInfo['params']) as $index) { $methodInfo['params'][$index]['desc'] = null; } @@ -180,19 +205,19 @@ private function addMethodsMin(Abstraction $abs) */ private function addImplements(Abstraction $abs) { - $interfaceMethods = array( - 'ArrayAccess' => array('offsetExists','offsetGet','offsetSet','offsetUnset'), - 'BackedEnum' => array('from', 'tryFrom'), - 'Countable' => array('count'), - 'Iterator' => array('current','key','next','rewind','valid'), - 'IteratorAggregate' => array('getIterator'), - 'UnitEnum' => array('cases'), - ); - $interfaces = \array_intersect($abs['reflector']->getInterfaceNames(), \array_keys($interfaceMethods)); - foreach ($interfaces as $interface) { - foreach ($interfaceMethods[$interface] as $methodName) { - // this method implements this interface - $abs['methods'][$methodName]['implements'] = $interface; + $stack = $abs['implements']; + while ($stack) { + $key = \key($stack); + $val = \array_shift($stack); + $classname = $val; + if (\is_array($val)) { + $classname = $key; + $stack = \array_merge($stack, $val); + } + $refClass = new ReflectionClass($classname); + foreach ($refClass->getMethods() as $refMethod) { + $methodName = $refMethod->getName(); + $abs['methods'][$methodName]['implements'] = $classname; } } } @@ -260,7 +285,8 @@ private function addViaPhpDocInherit(Abstraction $abs, &$declaredLast = null) private function addViaReflection(Abstraction $abs) { $methods = array(); - $this->traverseAncestors($abs['reflector'], function (ReflectionClass $reflector) use ($abs, &$methods) { + $withStaticVars = array(); + $this->traverseAncestors($abs['reflector'], function (ReflectionClass $reflector) use ($abs, &$methods, &$withStaticVars) { $className = $this->helper->getClassName($reflector); $refMethods = $reflector->getMethods(); while ($refMethods) { @@ -279,12 +305,17 @@ private function addViaReflection(Abstraction $abs) // getMethods() returns parent's private methods (#reasons).. we'll skip it continue; } + if (!empty($info['hasStaticVars'])) { + $withStaticVars[] = $name; + } + unset($info['hasStaticVars']); unset($info['phpDoc']['param']); unset($info['phpDoc']['return']); $methods[$name] = $info; } }); \ksort($methods); + $abs['methodsWithStaticVars'] = $withStaticVars; $abs['methods'] = $methods; } @@ -333,6 +364,7 @@ private function buildMethodRef(Abstraction $abs, ReflectionMethod $refMethod) 'attributes' => $abs['cfgFlags'] & AbstractObject::METHOD_ATTRIBUTE_COLLECT ? $this->helper->getAttributes($refMethod) : array(), + 'hasStaticVars' => \count($refMethod->getStaticVariables()) > 0, // temporary we don't store the values in the definition, only what methods have static vars 'isAbstract' => $refMethod->isAbstract(), 'isDeprecated' => $refMethod->isDeprecated() || isset($phpDoc['deprecated']), 'isFinal' => $refMethod->isFinal(), diff --git a/src/Debug/Abstraction/Object/Properties.php b/src/Debug/Abstraction/Object/Properties.php index dae40383..8539e39d 100644 --- a/src/Debug/Abstraction/Object/Properties.php +++ b/src/Debug/Abstraction/Object/Properties.php @@ -59,7 +59,7 @@ public function __construct(AbstractObject $abstractObject) } /** - * Add property instance info/values to abstraction + * Add declared property info * * @param Abstraction $abs Object Abstraction instance * @@ -67,34 +67,56 @@ public function __construct(AbstractObject $abstractObject) */ public function add(Abstraction $abs) { - if ($abs['isTraverseOnly']) { - return; + $this->addViaRef($abs); + $this->phpDoc->addViaPhpDoc($abs); // magic properties documented via phpDoc + + $properties = $abs['properties']; + + // note: for user-defined classes getDefaultProperties + // will return the current value for static properties + $defaultValues = $abs['reflector']->getDefaultProperties(); + foreach ($defaultValues as $name => $value) { + $properties[$name]['value'] = $value; } - $this->addValues($abs); - $obj = $abs->getSubject(); - if (\is_object($obj)) { - $this->addDebug($abs); // use __debugInfo() values if useDebugInfo' && method exists + + if ($abs['isAnonymous']) { + $properties['debug.file'] = static::buildPropValues(array( + 'type' => Abstracter::TYPE_STRING, + 'value' => $abs['definition']['fileName'], + 'valueFrom' => 'debug', + 'visibility' => 'debug', + )); + $properties['debug.line'] = static::buildPropValues(array( + 'type' => Abstracter::TYPE_INT, + 'value' => (int) $abs['definition']['startLine'], + 'valueFrom' => 'debug', + 'visibility' => 'debug', + )); } + + $abs['properties'] = $properties; + $this->crate($abs); } /** - * Add declared property info + * Add property instance info/values to abstraction * * @param Abstraction $abs Object Abstraction instance * * @return void */ - public function addClass(Abstraction $abs) + public function addInstance(Abstraction $abs) { - $this->addViaRef($abs); - $this->phpDoc->addViaPhpDoc($abs); // magic properties documented via phpDoc - $defaultValues = $abs['reflector']->getDefaultProperties(); - $properties = $abs['properties']; - foreach ($defaultValues as $name => $value) { - $properties[$name]['value'] = $value; + if ($abs['isTraverseOnly']) { + return; } - $abs['properties'] = $properties; + $this->addValues($abs); + $obj = $abs->getSubject(); + if (\is_object($obj)) { + $this->addDebug($abs); // use __debugInfo() values if useDebugInfo' && method exists + } + $this->crate($abs); } /** @@ -300,14 +322,13 @@ private function buildViaRef(Abstraction $abs, ReflectionProperty $refProperty) { $phpDoc = $this->helper->getPhpDocVar($refProperty, $abs['fullyQualifyPhpDocType']); $refProperty->setAccessible(true); // only accessible via reflection - /* - getDeclaringClass returns "LAST-declared/overriden" - */ return static::buildPropValues(array( 'attributes' => $abs['cfgFlags'] & AbstractObject::PROP_ATTRIBUTE_COLLECT ? $this->helper->getAttributes($refProperty) : array(), - 'desc' => $phpDoc['desc'], + 'desc' => $abs['cfgFlags'] & AbstractObject::PHPDOC_COLLECT + ? $phpDoc['desc'] + : null, 'isPromoted' => PHP_VERSION_ID >= 80000 ? $refProperty->isPromoted() : false, @@ -330,12 +351,8 @@ private function buildViaRef(Abstraction $abs, ReflectionProperty $refProperty) private function crate(Abstraction $abs) { $properties = $abs['properties']; - $phpDocCollect = $abs['cfgFlags'] & AbstractObject::PHPDOC_COLLECT; foreach ($properties as $name => $info) { $info['value'] = $this->abstracter->crate($info['value'], $abs['debugMethod'], $abs['hist']); - if (!$phpDocCollect) { - $info['desc'] = null; - } $properties[$name] = $info; } $abs['properties'] = $properties; diff --git a/src/Debug/Abstraction/Object/PropertiesPhpDoc.php b/src/Debug/Abstraction/Object/PropertiesPhpDoc.php index c8494cbf..cf2f48db 100644 --- a/src/Debug/Abstraction/Object/PropertiesPhpDoc.php +++ b/src/Debug/Abstraction/Object/PropertiesPhpDoc.php @@ -13,6 +13,7 @@ namespace bdk\Debug\Abstraction\Object; use bdk\Debug\Abstraction\Abstraction; +use bdk\Debug\Abstraction\AbstractObject; use bdk\Debug\Abstraction\Object\Helper; /** @@ -133,7 +134,9 @@ private function buildViaPhpDoc(Abstraction $abs, $phpDocProp, $declaredLast, $v $existing ?: Properties::buildPropValues(), // self::$basePropInfo array( 'declaredLast' => $declaredLast, - 'desc' => $phpDocProp['desc'], + 'desc' => $abs['cfgFlags'] & AbstractObject::PHPDOC_COLLECT + ? $phpDocProp['desc'] + : null, 'type' => $phpDocProp['type'], 'visibility' => $existing ? array($vis, $existing['visibility']) // we want "magic" visibility first diff --git a/src/Debug/Abstraction/Object/Subscriber.php b/src/Debug/Abstraction/Object/Subscriber.php index 1a2c0384..4de7812f 100644 --- a/src/Debug/Abstraction/Object/Subscriber.php +++ b/src/Debug/Abstraction/Object/Subscriber.php @@ -68,23 +68,28 @@ public function getSubscriptions() public function onStart(Abstraction $abs) { $obj = $abs->getSubject(); - if ($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::ISO8601); - } elseif ($obj instanceof mysqli) { - $this->onStartMysqli($abs); - } elseif ($obj instanceof Data) { - $abs['propertyOverrideValues']['data'] = Abstracter::NOT_INSPECTED; - } elseif ($obj instanceof PhpDoc) { - $abs['propertyOverrideValues']['cache'] = Abstracter::NOT_INSPECTED; - } elseif ($obj instanceof AbstractObjectDefinition) { - $abs['propertyOverrideValues']['cache'] = Abstracter::NOT_INSPECTED; - } elseif ($abs['isAnonymous']) { - $this->onStartAnonymous($abs); - } elseif ($abs['className'] === 'Closure') { - $this->onStartClosure($abs); + 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::ISO8601); + 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 AbstractObjectDefinition: + $abs['propertyOverrideValues']['cache'] = Abstracter::NOT_INSPECTED; + break; + case $abs['className'] === 'Closure': + $this->onStartClosure($abs); + break; } } @@ -164,44 +169,6 @@ private function onEndMysqli(Abstraction $abs) \restore_error_handler(); } - /** - * Add anonymous instance info - * - * * definition - * * constants - * * methods - * * add file & line debug properties - * - * @param Abstraction $abs Abstraction instance - * - * @return void - */ - private function onStartAnonymous(Abstraction $abs) - { - $this->abstractObject->definition->addDefinition($abs); - $this->abstractObject->constants->add($abs); - $this->abstractObject->methods->add($abs); - if ($abs['reflector']->getParentClass()) { - $abs['extends'] = \array_merge(array( - $abs['reflector']->getParentClass()->getName(), - ), $abs['extends']); - } - $properties = $abs['properties']; - $properties['debug.file'] = $this->abstractObject->properties->buildPropValues(array( - 'type' => Abstracter::TYPE_STRING, - 'value' => $abs['definition']['fileName'], - 'valueFrom' => 'debug', - 'visibility' => 'debug', - )); - $properties['debug.line'] = $this->abstractObject->properties->buildPropValues(array( - 'type' => Abstracter::TYPE_INT, - 'value' => (int) $abs['definition']['startLine'], - 'valueFrom' => 'debug', - 'visibility' => 'debug', - )); - $abs['properties'] = $properties; - } - /** * Set Closure definition and debug properties * diff --git a/src/Debug/Config.php b/src/Debug/Config.php index 76037aee..5dd05214 100644 --- a/src/Debug/Config.php +++ b/src/Debug/Config.php @@ -36,14 +36,18 @@ class Config 'constCollect', 'constOutput', 'fullyQualifyPhpDocType', + 'interfacesCollapse', 'maxDepth', 'methodAttributeCollect', 'methodAttributeOutput', 'methodCollect', 'methodDescOutput', 'methodOutput', + 'methodStaticVarCollect', + 'methodStaticVarOutput', 'objAttributeCollect', 'objAttributeOutput', + 'objectSectionOrder', 'objectsExclude', 'objectSort', 'objectsWhitelist', diff --git a/src/Debug/Debug.php b/src/Debug/Debug.php index 0ffff3e6..6130731a 100644 --- a/src/Debug/Debug.php +++ b/src/Debug/Debug.php @@ -111,7 +111,7 @@ class Debug extends AbstractDebug const EVENT_STREAM_WRAP = 'debug.streamWrap'; const META = "\x00meta\x00"; - const VERSION = '3.1'; + const VERSION = '3.2'; protected $cfg = array( 'channelIcon' => 'fa fa-list-ul', diff --git a/src/Debug/Dump/Html.php b/src/Debug/Dump/Html.php index 8dbf92f6..1a2a7cf0 100644 --- a/src/Debug/Dump/Html.php +++ b/src/Debug/Dump/Html.php @@ -78,11 +78,7 @@ public function processLogEntry(LogEntry $logEntry) 'data-icon' => $meta['icon'], ), $meta['attribs']); $html = parent::processLogEntry($logEntry); - $html = \strtr($html, array( - ' data-channel="null"' => '', - ' data-detect-files="null"' => '', - ' data-icon="null"' => '', - )); + $html = \preg_replace('/ data-[-\w]+="null"/', '', $html); return $html . "\n"; } @@ -226,7 +222,9 @@ protected function methodDefault(LogEntry $logEntry) if (isset($meta['file']) && $logEntry->getChannelName() !== $this->channelNameRoot . '.phpError') { // PHP errors will have file & line as one of the arguments // so no need to store file & line as data args + $meta = \array_merge(array('evalLine' => null), $meta); $attribs = \array_merge(array( + 'data-evalLine' => $meta['evalLine'], 'data-file' => $meta['file'], 'data-line' => $meta['line'], ), $attribs); diff --git a/src/Debug/Dump/Html/AbstractObjectSection.php b/src/Debug/Dump/Html/AbstractObjectSection.php index cf4893f3..fafc9509 100644 --- a/src/Debug/Dump/Html/AbstractObjectSection.php +++ b/src/Debug/Dump/Html/AbstractObjectSection.php @@ -13,6 +13,7 @@ namespace bdk\Debug\Dump\Html; use bdk\Debug\Abstraction\Abstracter; +use bdk\Debug\Abstraction\AbstractObject; use bdk\Debug\Abstraction\Object\Abstraction as ObjectAbstraction; use bdk\Debug\Dump\Html\Helper; use bdk\Debug\Dump\Html\Value as ValDumper; @@ -52,20 +53,69 @@ public function __construct(ValDumper $valDumper, Helper $helper, HtmlUtil $html */ public function dumpItems(ObjectAbstraction $abs, $what, array $cfg) { - $html = ''; $items = $abs->sort($abs[$what], $abs['sort']); - foreach ($items as $name => $info) { - $info = \array_merge(array( - 'className' => $abs['className'], - 'declaredLast' => null, - 'declaredPrev' => null, - ), $info); - $info['isInherited'] = $info['declaredLast'] && $info['declaredLast'] !== $abs['className']; - $html .= $this->dumpItem($name, $info, $cfg) . "\n"; + $cfg = \array_merge(array( + 'groupByInheritance' => \strpos($abs['sort'], 'inheritance') === 0, + 'objClassName' => $abs['className'], + 'phpDocOutput' => $abs['cfgFlags'] & AbstractObject::PHPDOC_OUTPUT, + ), $cfg); + if ($cfg['groupByInheritance'] === false) { + return $this->dumpItemsFiltered($items, $cfg); + } + // group by inheritance... with headings + // stop looping over classes when we've output everything + // no sense in showing "inherited from" when no more inherited items + // Or, we could only display the heading when itemsFiltered non-empty + $classes = $this->getInheritedClasses($abs, $what); + \array_unshift($classes, $abs['className']); + $html = ''; + $itemCount = \count($items); + $itemOutCount = 0; + while ($classes && $itemOutCount < $itemCount) { + $classname = \array_shift($classes); + $itemsFiltered = \array_filter($items, static function ($info) use ($classname) { + return !isset($info['declaredLast']) || $info['declaredLast'] === $classname; + }); + $items = \array_diff_key($items, $itemsFiltered); + $itemOutCount += \count($itemsFiltered); + $html .= \in_array($classname, array($abs['className'], 'stdClass'), true) === false + ? '