<?php
/**
 * This file is part of PHPDebugConsole
 *
 * @package   PHPDebugConsole
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2019 Brad Kent
 * @version   v2.3
 */

namespace bdk\Debug;

use bdk\PubSub\Event;

/**
 * Abstracter:  Methods used to abstract objects
 */
class AbstractObject
{

    static private $basePropInfo = array(
        'desc' => null,
        'inheritedFrom' => null,        // populated only if inherited
        'isExcluded' => false,          // true if not included in __debugInfo
        'isStatic' => false,
        'originallyDeclared' => null,   // populated only if originally declared in ancestor
        'overrides' => null,            // populated only if we're overriding
        'forceShow' => false,           // initially show the property/value (even if protected or private)
                                        //    if value is an array, expand it
        'type' => null,
        'value' => null,
        'valueFrom' => 'value',         // 'value' | 'debugInfo' | 'debug'
        'visibility' => 'public',       // public, private, protected, magic, magic-read, magic-write, debug
                                        //   may also be an array (ie: ['private', 'magic-read'])
    );
    static private $methodCache = array();
	protected $abstracter;
	protected $phpDoc;

    /**
     * Constructor
     *
     * @param Abstracter $abstracter abstracter obj
     * @param PhpDoc     $phpDoc     phpDoc obj
     */
    public function __construct(Abstracter $abstracter, PhpDoc $phpDoc)
    {
        $this->abstracter = $abstracter;
        $this->phpDoc = $phpDoc;
        $abstracter->debug->eventManager->subscribe('debug.objAbstractStart', array($this, 'onStart'));
        $abstracter->debug->eventManager->subscribe('debug.objAbstractEnd', array($this, 'onEnd'));
    }

    /**
     * returns information about an object
     *
     * @param object $obj    Object to inspect
     * @param string $method Method requesting abstraction
     * @param array  $hist   (@internal) array & object history
     *
     * @return array
     */
    public function getAbstraction($obj, $method = null, &$hist = array())
    {
        if (!\is_object($obj)) {
            return $obj;
        }
        $reflector = new \ReflectionObject($obj);
        $className = $reflector->getName();
        $isTableTop = $method === 'table' && \count($hist) < 2;  // rows (traversable) || row (traversable)
        $abs = new Event($obj, array(
            'className' => $className,
            'collectMethods' => !$isTableTop && $this->abstracter->getCfg('collectMethods') || $className == 'Closure',
            'constants' => array(),
            'debug' => $this->abstracter->ABSTRACTION,
            'debugMethod' => $method,
            'definition' => array(
                'fileName' => $reflector->getFileName(),
                'startLine' => $reflector->getStartLine(),
                'extensionName' => $reflector->getExtensionName(),
            ),
            'extends' => array(),
            'implements' => $reflector->getInterfaceNames(),
            'isExcluded' => $this->isObjExcluded($obj),
            'isRecursion' => \in_array($obj, $hist, true),
            'methods' => array(),   // if !collectMethods, may still get ['__toString']['returnValue']
            'phpDoc' => array(
                'summary' => null,
                'description' => null,
                // additional tags
            ),
            'properties' => array(),
            'scopeClass' => $this->getScopeClass($hist),
            'stringified' => null,
            'type' => 'object',
            'traverseValues' => array(),    // populated if method is table && traversable
            'viaDebugInfo' => $this->abstracter->getCfg('useDebugInfo') && $reflector->hasMethod('__debugInfo'),
            // these are temporary values available during abstraction
            'collectPropertyValues' => true,
            'hist' => $hist,
            'propertyOverrideValues' => array(),
            'reflector' => $reflector,
        ));
        $keysTemp = \array_flip(array('collectPropertyValues','hist','propertyOverrideValues','reflector'));
        if ($abs['isRecursion']) {
            return \array_diff_key($abs->getValues(), $keysTemp);
        }
        /*
            debug.objAbstractStart subscriber may
            set isExcluded
            set collectPropertyValues (boolean)
            set collectMethods (boolean)
            set propertyOverrideValues
            set stringified
            set traverseValues
        */
        $abs = $this->abstracter->debug->internal->publishBubbleEvent('debug.objAbstractStart', $abs);
        if (\array_filter(array($abs['isExcluded'], $abs->isPropagationStopped()))) {
            return \array_diff_key($abs->getValues(), $keysTemp);
        }
        $this->getAbstractionDetails($abs);
        /*
            debug.objAbstractEnd subscriber has free reign to modify abtraction array
        */
        $return = $this->abstracter->debug->internal->publishBubbleEvent('debug.objAbstractEnd', $abs)->getValues();
        $this->sort($return['properties']);
        $this->sort($return['methods']);
        return \array_diff_key($return, $keysTemp);
    }

    /**
     * Populate constants, extends, methods, phpDoc, properties, etc
     *
     * @param Event $abs Abstraction event object
     *
     * @return void
     */
    private function getAbstractionDetails(Event $abs)
    {
        $reflector = $abs['reflector'];
        $abs['phpDoc'] = $this->phpDoc->getParsed($reflector);
        $traversed = false;
        if ($abs['debugMethod'] === 'table' && \count($abs['hist']) < 2) {
            // this is either rows (traversable), or a row (traversable)
            $obj = $abs->getSubject();
            if ($obj instanceof \Traversable && !$abs['traverseValues']) {
                $traversed = true;
                $abs['hist'][] = $obj;
                foreach ($obj as $k => $v) {
                    $abs['traverseValues'][$k] = $this->abstracter->needsAbstraction($v)
                        ? $this->abstracter->getAbstraction($v, $abs['debugMethod'], $abs['hist'])
                        : $v;
                }
            }
        }
        if (!$traversed) {
            $this->addConstants($abs);
            while ($reflector = $reflector->getParentClass()) {
                $abs['extends'][] = $reflector->getName();
            }
            $this->addProperties($abs);
            $this->addMethods($abs);
        }
    }

    /**
     * debug.objAbstractStart event subscriber
     *
     * @param Event $event event object
     *
     * @return void
     */
    public function onStart(Event $event)
    {
        $obj = $event->getSubject();
        if ($obj instanceof \DateTime || $obj instanceof \DateTimeImmutable) {
            $event['stringified'] = $obj->format(\DateTime::ISO8601);
        } elseif ($obj instanceof \mysqli && ($obj->connect_errno || !$obj->stat)) {
            // avoid "Property access is not allowed yet"
            $event['collectPropertyValues'] = false;
        }
    }

    /**
     * debug.objAbstractEnd event subscriber
     *
     * @param Event $event event object
     *
     * @return void
     */
    public function onEnd(Event $event)
    {
        $obj = $event->getSubject();
        if ($obj instanceof \DOMNodeList) {
            // for reasons unknown, DOMNodeList's properties are invisible to reflection
            $event['properties']['length'] = \array_merge(static::$basePropInfo, array(
                'type' => 'integer',
                'value' => $obj->length,
            ));
        } elseif ($obj instanceof \Exception) {
            if (isset($event['properties']['xdebug_message'])) {
                $event['properties']['xdebug_message']['isExcluded'] = true;
            }
        } elseif ($obj instanceof \mysqli && !$event['collectPropertyValues']) {
            $propsAlwaysAvail = array(
                'client_info','client_version','connect_errno','connect_error','errno','error','stat'
            );
            $reflectionObject = $event['reflector'];
            foreach ($propsAlwaysAvail as $name) {
                $reflectionProperty = $reflectionObject->getProperty($name);
                $event['properties'][$name]['value'] = $reflectionProperty->getValue($obj);
            }
        }
    }

    /**
     * Get object's constants
     *
     * @param Event $abs Abstraction event object
     *
     * @return void
     */
    public function addConstants(Event $abs)
    {
        if (!$this->abstracter->getCfg('collectConstants')) {
            return;
        }
        $reflector = $abs['reflector'];
        $constants = $reflector->getConstants();
        while ($reflector = $reflector->getParentClass()) {
            $constants = \array_merge($reflector->getConstants(), $constants);
        }
        if ($this->abstracter->getCfg('objectSort') == 'name') {
            \ksort($constants);
        }
        $abs['constants'] = $constants;
    }

    /**
     * Adds methods to abstraction
     *
     * @param Event $abs Abstraction event object
     *
     * @return void
     */
    private function addMethods(Event $abs)
    {
        $obj = $abs->getSubject();
        if (!$abs['collectMethods']) {
            $this->addMethodsMin($abs);
            return;
        }
        if ($this->abstracter->getCfg('cacheMethods') && isset(static::$methodCache[$abs['className']])) {
            $abs['methods'] = static::$methodCache[$abs['className']];
        } else {
            $methodArray = array();
            $methods = $abs['reflector']->getMethods();
            $interfaceMethods = array(
                'ArrayAccess' => array('offsetExists','offsetGet','offsetSet','offsetUnset'),
                'Countable' => array('count'),
                'Iterator' => array('current','key','next','rewind','void'),
                'IteratorAggregate' => array('getIterator'),
                // 'Throwable' => array('getMessage','getCode','getFile','getLine','getTrace','getTraceAsString','getPrevious','__toString'),
            );
            $interfacesHide = \array_intersect($abs['implements'], \array_keys($interfaceMethods));
            foreach ($methods as $reflectionMethod) {
                $info = $this->methodInfo($obj, $reflectionMethod);
                $methodName = $reflectionMethod->getName();
                if ($info['visibility'] === 'private' && $info['inheritedFrom']) {
                    /*
                        getMethods() returns parent's private methods (must be a reason... but we'll skip it)
                    */
                    continue;
                }
                foreach ($interfacesHide as $interface) {
                    if (\in_array($methodName, $interfaceMethods[$interface])) {
                        // this method implements this interface
                        $info['implements'] = $interface;
                        break;
                    }
                }
                $methodArray[$methodName] = $info;
            }
            $abs['methods'] = $methodArray;
            $this->addMethodsPhpDoc($abs);
            static::$methodCache[$abs['className']] = $abs['methods'];
        }
        if (isset($abs['methods']['__toString'])) {
            $abs['methods']['__toString']['returnValue'] = $obj->__toString();
        }
        return;
    }

    /**
     * Add minimal method information to abstraction
     *
     * @param Event $abs Abstraction event object
     *
     * @return void
     */
    private function addMethodsMin(Event $abs)
    {
        $obj = $abs->getSubject();
        if (\method_exists($obj, '__toString')) {
            $abs['methods']['__toString'] = array(
                'returnValue' => \call_user_func(array($obj, '__toString')),
                'visibility' => 'public',
            );
        }
        if (\method_exists($obj, '__get')) {
            $abs['methods']['__get'] = array('visibility' => 'public');
        }
        if (\method_exists($obj, '__set')) {
            $abs['methods']['__set'] = array('visibility' => 'public');
        }
        return;
    }

    /**
     * "Magic" methods may be defined in a class' doc-block
     * If so... move this information to the properties array
     *
     * @param Event $abs Abstraction event object
     *
     * @return void
     *
     * @see http://docs.phpdoc.org/references/phpdoc/tags/method.html
     */
    private function addMethodsPhpDoc(Event $abs)
    {
        $inheritedFrom = null;
        if (empty($abs['phpDoc']['method'])) {
            // phpDoc doesn't contain any @method tags,
            if (\array_intersect_key($abs['methods'], \array_flip(array('__call', '__callStatic')))) {
                // we've got __call and/or __callStatic method:  check if parent classes have @method tags
                $reflector = $abs['reflector'];
                while ($reflector = $reflector->getParentClass()) {
                    $parsed = $this->phpDoc->getParsed($reflector);
                    if (isset($parsed['method'])) {
                        $inheritedFrom = $reflector->getName();
                        $abs['phpDoc']['method'] = $parsed['method'];
                        break;
                    }
                }
            }
            if (empty($abs['phpDoc']['method'])) {
                // still empty
                return;
            }
        }
        foreach ($abs['phpDoc']['method'] as $phpDocMethod) {
            $className = $inheritedFrom ? $inheritedFrom : $abs['className'];
            $abs['methods'][$phpDocMethod['name']] = array(
                'implements' => null,
                'inheritedFrom' => $inheritedFrom,
                'isAbstract' => false,
                'isDeprecated' => false,
                'isFinal' => false,
                'isStatic' => $phpDocMethod['static'],
                'params' => \array_map(function ($param) use ($className) {
                    $info = $this->phpDocParam($param, $className);
                    return array(
                        'constantName' => $info['constantName'],
                        'defaultValue' => $info['defaultValue'],
                        'desc' => null,
                        'name' => $param['name'],
                        'optional' => false,
                        'type' => $param['type'],
                    );
                }, $phpDocMethod['param']),
                'phpDoc' => array(
                    'summary' => $phpDocMethod['desc'],
                    'description' => null,
                    'return' => array(
                        'type' => $phpDocMethod['type'],
                        'desc' => null,
                    )
                ),
                'visibility' => 'magic',
            );
        }
        unset($abs['phpDoc']['method']);
        return;
    }

    /**
     * Adds properties to abstraction
     *
     * @param Event $abs Abstraction event object
     *
     * @return void
     */
    private function addProperties(Event $abs)
    {
        if ($abs['debugMethod'] === 'table' && $abs['traverseValues']) {
            return;
        }
        $obj = $abs->getSubject();
        $reflectionObject = $abs['reflector'];
        /*
            We trace our ancestory to learn where properties are inherited from
        */
        while ($reflectionObject) {
            $className = $reflectionObject->getName();
            $properties = $reflectionObject->getProperties();
            $isDebugObj = $className == __NAMESPACE__;
            while ($properties) {
                $reflectionProperty = \array_shift($properties);
                $name = $reflectionProperty->getName();
                if (isset($abs['properties'][$name])) {
                    // already have info... we're in an ancestor
                    $abs['properties'][$name]['overrides'] = $this->propOverrides(
                        $reflectionProperty,
                        $abs['properties'][$name],
                        $className
                    );
                    $abs['properties'][$name]['originallyDeclared'] = $className;
                    continue;
                }
                if ($isDebugObj && $name == 'data') {
                    $abs['properties']['data'] = \array_merge(self::$basePropInfo, array(
                        'value' => array('NOT INSPECTED'),
                        'visibility' => 'protected',
                    ));
                    continue;
                }
                $abs['properties'][$name] = $this->getPropInfo($abs, $reflectionProperty);
            }
            $reflectionObject = $reflectionObject->getParentClass();
        }
        $this->addPropertiesPhpDoc($abs);   // magic properties documented via phpDoc
        $this->addPropertiesDebug($abs);    // use __debugInfo() values if useDebugInfo' && method exists
        $properties = $abs['properties'];
        $abs['hist'][] = $obj;
        foreach ($properties as $name => $info) {
            if ($this->abstracter->needsAbstraction($info['value'])) {
                $properties[$name]['value'] = $this->abstracter->getAbstraction($info['value'], $abs['debugMethod'], $abs['hist']);
            }
        }
        $abs['properties'] = $properties;
        return;
    }

    /**
     * Add/Update properties with info from __debugInfo method
     *
     * @param Event $abs Abstraction event object
     *
     * @return void
     */
    private function addPropertiesDebug(Event $abs)
    {
        if (!$abs['collectPropertyValues']) {
            return;
        }
        if (!$abs['viaDebugInfo']) {
            // using __debugInfo is disabled, or object does not have __debugInfo method
            return;
        }
        $obj = $abs->getSubject();
        $debugInfo = \call_user_func(array($obj, '__debugInfo'));
        $properties = $abs['properties'];
        foreach ($properties as $name => $info) {
            if (\array_key_exists($name, $abs['propertyOverrideValues'])) {
                // we're using override value
                unset($debugInfo[$name]);
                continue;
            }
            if (\array_key_exists($name, $debugInfo)) {
                if ($debugInfo[$name] !== $info['value']) {
                    $properties[$name]['value'] = $debugInfo[$name];
                    $properties[$name]['valueFrom'] = 'debugInfo';
                }
                unset($debugInfo[$name]);
                continue;
            }
            $isPrivateAncestor = \in_array('private', (array) $info['visibility']) && $info['inheritedFrom'];
            if ($isPrivateAncestor) {
                // exempt from isExcluded
                continue;
            }
            $properties[$name]['isExcluded'] = true;
        }
        foreach ($debugInfo as $name => $value) {
            $properties[$name] = \array_merge(
                static::$basePropInfo,
                array(
                    'value' => $value,
                    'valueFrom' => 'debugInfo',
                    'visibility' => 'debug',    // indicates this property is exclusive to debugInfo
                )
            );
        }
        $abs['properties'] = $properties;
    }

    /**
     * "Magic" properties may be defined in a class' doc-block
     * If so... move this information to the properties array
     *
     * @param Event $abs Abstraction event object
     *
     * @return void
     *
     * @see http://docs.phpdoc.org/references/phpdoc/tags/property.html
     */
    private function addPropertiesPhpDoc(Event $abs)
    {
        // tag => visibility
        $tags = array(
            'property' => 'magic',
            'property-read' => 'magic-read',
            'property-write' => 'magic-write',
        );
        $inheritedFrom = null;
        if (!\array_intersect_key($abs['phpDoc'], $tags)) {
            // phpDoc doesn't contain any @property tags
            $found = false;
            $obj = $abs->getSubject();
            if (!\method_exists($obj, '__get')) {
                // don't have magic getter... don't bother searching ancestor phpDocs
                return;
            }
            // we've got __get method:  check if parent classes have @property tags
            $reflector = $abs['reflector'];
            while ($reflector = $reflector->getParentClass()) {
                $parsed = $this->phpDoc->getParsed($reflector);
                $tagIntersect = \array_intersect_key($parsed, $tags);
                if (!$tagIntersect) {
                    continue;
                }
                $found = true;
                $inheritedFrom = $reflector->getName();
                $abs['phpDoc'] = \array_merge(
                    $abs['phpDoc'],
                    $tagIntersect
                );
                break;
            }
            if (!$found) {
                return;
            }
        }
        $properties = $abs['properties'];
        foreach ($tags as $tag => $vis) {
            if (!isset($abs['phpDoc'][$tag])) {
                continue;
            }
            foreach ($abs['phpDoc'][$tag] as $phpDocProp) {
                $exists = isset($properties[ $phpDocProp['name'] ]);
                $properties[ $phpDocProp['name'] ] = \array_merge(
                    $exists
                        ? $properties[ $phpDocProp['name'] ]
                        : self::$basePropInfo,
                    array(
                        'desc' => $phpDocProp['desc'],
                        'type' => $phpDocProp['type'],
                        'inheritedFrom' => $inheritedFrom,
                        'visibility' => $exists
                            ? array($properties[ $phpDocProp['name'] ]['visibility'], $vis)
                            : $vis,
                    )
                );
                if (!$exists) {
                    $properties[ $phpDocProp['name'] ]['value'] = $this->abstracter->UNDEFINED;
                }
            }
            unset($abs['phpDoc'][$tag]);
        }
        $abs['properties'] = $properties;
        return;
    }

    /**
     * Get parameter details
     *
     * returns array of
     *     [
     *         'constantName'   populated only if php >= 5.4.6 & default is a constant
     *         'defaultValue'   value or UNDEFINED
     *         'desc'           description (from phpDoc)
     *         'isOptional'
     *         'name'           name
     *         'type'           type hint
     *     ]
     *
     * @param \ReflectionMethod $reflectionMethod method object
     * @param array             $phpDoc           method's parsed phpDoc comment
     *
     * @return array
     */
    private function getParams(\ReflectionMethod $reflectionMethod, $phpDoc = array())
    {
        $paramArray = array();
        $params = $reflectionMethod->getParameters();
        if (empty($phpDoc)) {
            $phpDoc = $this->phpDoc->getParsed($reflectionMethod);
        }
        foreach ($params as $i => $reflectionParameter) {
            $nameNoPrefix = $reflectionParameter->getName();
            $name = '$'.$nameNoPrefix;
            if (\method_exists($reflectionParameter, 'isVariadic') && $reflectionParameter->isVariadic()) {
                $name = '...'.$name;
            }
            if ($reflectionParameter->isPassedByReference()) {
                $name = '&'.$name;
            }
            $constantName = null;
            $defaultValue = $this->abstracter->UNDEFINED;
            if ($reflectionParameter->isDefaultValueAvailable()) {
                $defaultValue = $reflectionParameter->getDefaultValue();
                if (\version_compare(PHP_VERSION, '5.4.6', '>=') && $reflectionParameter->isDefaultValueConstant()) {
                    /*
                        php may return something like self::CONSTANT_NAME
                        hhvm will return WhateverTheClassNameIs::CONSTANT_NAME
                    */
                    $constantName = $reflectionParameter->getDefaultValueConstantName();
                }
            }
            $paramInfo = array(
                'constantName' => $constantName,
                'defaultValue' => $defaultValue,
                'desc' => null,
                'isOptional' => $reflectionParameter->isOptional(),
                'name' => $name,
                'type' => $this->getParamTypeHint($reflectionParameter),
            );
            /*
                Incorporate phpDoc info
            */
            if (isset($phpDoc['param'][$i])) {
                $paramInfo['desc'] = $phpDoc['param'][$i]['desc'];
                if (!isset($paramInfo['type'])) {
                    $paramInfo['type'] = $phpDoc['param'][$i]['type'];
                }
            }
            $paramArray[$nameNoPrefix] = $paramInfo;
        }
        return $paramArray;
    }

    /**
     * Get true typehint (not phpDoc typehint)
     *
     * @param \ReflectionParameter $reflectionParameter reflectionParameter
     *
     * @return string|null
     */
    private function getParamTypeHint(\ReflectionParameter $reflectionParameter)
    {
        $return = null;
        if ($reflectionParameter->isArray()) {
            $return = 'array';
        } elseif (\preg_match('/\[\s\<\w+?>\s([\w\\\\]+)/s', $reflectionParameter->__toString(), $matches)) {
            $return = $matches[1];
        }
        return $return;
    }

    /**
     * Get property info
     *
     * @param Event               $abs                Abstraction event object
     * @param \ReflectionProperty $reflectionProperty reflection property
     *
     * @return array
     */
    private function getPropInfo(Event $abs, \ReflectionProperty $reflectionProperty)
    {
        $obj = $abs->getSubject();
        $reflectionProperty->setAccessible(true); // only accessible via reflection
        $className = \get_class($obj); // prop->class is equiv to getDeclaringClass
        // get type and comment from phpdoc
        $commentInfo = $this->getPropCommentInfo($reflectionProperty);
        /*
            getDeclaringClass returns "LAST-declared/overriden"
        */
        $declaringClassName = $reflectionProperty->getDeclaringClass()->getName();
        $propInfo = \array_merge(static::$basePropInfo, array(
            'desc' => $commentInfo['desc'],
            'inheritedFrom' => $declaringClassName !== $className
                ? $declaringClassName
                : null,
            'isStatic' => $reflectionProperty->isStatic(),
            'type' => $commentInfo['type'],
        ));
        if ($reflectionProperty->isPrivate()) {
            $propInfo['visibility'] = 'private';
        } elseif ($reflectionProperty->isProtected()) {
            $propInfo['visibility'] = 'protected';
        }
        if ($abs['collectPropertyValues']) {
            $propName = $reflectionProperty->getName();
            if (\array_key_exists($propName, $abs['propertyOverrideValues'])) {
                $propInfo['value'] = $abs['propertyOverrideValues'][$propName];
                $propInfo['valueFrom'] = 'debug';
            } else {
                $propInfo['value'] = $reflectionProperty->getValue($obj);
            }
        }
        return $propInfo;
    }

    /**
     * Get property type and description from phpDoc comment
     *
     * @param \ReflectionProperty $reflectionProperty reflection property object
     *
     * @return array
     */
    private function getPropCommentInfo(\ReflectionProperty $reflectionProperty)
    {
        $name = $reflectionProperty->name;
        $phpDoc = $this->phpDoc->getParsed($reflectionProperty);
        $info = array(
            'type' => null,
            'desc' => $phpDoc['summary']
                ? $phpDoc['summary']
                : null,
        );
        if (isset($phpDoc['var'])) {
            if (\count($phpDoc['var']) == 1) {
                $var = $phpDoc['var'][0];
            } else {
                /*
                    php's getDocComment doesn't play nice with compound statements
                    https://www.phpdoc.org/docs/latest/references/phpdoc/tags/var.html
                */
                foreach ($phpDoc['var'] as $var) {
                    if ($var['name'] == $name) {
                        break;
                    }
                }
            }
            $info['type'] = $var['type'];
            if (!$info['desc']) {
                $info['desc'] = $var['desc'];
            } elseif ($var['desc']) {
                $info['desc'] = $info['desc'].': '.$var['desc'];
            }
        }
        return $info;
    }

    /**
     * for nested objects (ie object is a property of an object), returns the parent class
     *
     * @param array $hist Array & object history
     *
     * @return null|string
     */
    private function getScopeClass(&$hist)
    {
        $className = null;
        for ($i = \count($hist) - 1; $i >= 0; $i--) {
            if (\is_object($hist[$i])) {
                $className = \get_class($hist[$i]);
                break;
            }
        }
        if ($i < 0) {
            $backtrace = \version_compare(PHP_VERSION, '5.4.0', '>=')
                ? \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)
                : \debug_backtrace(false);   // don't provide object
            foreach ($backtrace as $i => $frame) {
                if (!isset($frame['class']) || \strpos($frame['class'], __NAMESPACE__) !== 0) {
                    break;
                }
            }
            $className = isset($backtrace[$i]['class'])
                ? $backtrace[$i]['class']
                : null;
        }
        return $className;
    }

    /**
     * Is the passed object excluded from debugging?
     *
     * @param object $obj object to test
     *
     * @return boolean
     */
    private function isObjExcluded($obj)
    {
        if (\in_array(\get_class($obj), $this->abstracter->getCfg('objectsExclude'))) {
            return true;
        }
        foreach ($this->abstracter->getCfg('objectsExclude') as $exclude) {
            if (\is_subclass_of($obj, $exclude)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Get method info
     *
     * @param object            $obj              object method belongs to
     * @param \ReflectionMethod $reflectionMethod ReflectionMethod instance
     *
     * @return array
     */
    private function methodInfo($obj, \ReflectionMethod $reflectionMethod)
    {
        // getDeclaringClass() returns LAST-declared/overridden
        $declaringClassName = $reflectionMethod->getDeclaringClass()->getName();
        $phpDoc = $this->phpDoc->getParsed($reflectionMethod);
        $vis = 'public';
        if ($reflectionMethod->isPrivate()) {
            $vis = 'private';
        } elseif ($reflectionMethod->isProtected()) {
            $vis = 'protected';
        }
        $info = array(
            'implements' => null,
            'inheritedFrom' => $declaringClassName != \get_class($obj)
                ? $declaringClassName
                : null,
            'isAbstract' => $reflectionMethod->isAbstract(),
            'isDeprecated' => $reflectionMethod->isDeprecated() || isset($phpDoc['deprecated']),
            'isFinal' => $reflectionMethod->isFinal(),
            'isStatic' => $reflectionMethod->isStatic(),
            'params' => $this->getParams($reflectionMethod, $phpDoc),
            'phpDoc' => $phpDoc,
            'visibility' => $vis,   // public | private | protected | debug | magic
        );
        unset($info['phpDoc']['param']);
        return $info;
    }

    /**
     * Get defaultValue and constantName info from phpDoc param
     *
     * Converts the defaultValue string to php scalar
     *
     * @param array  $param     parsed param in from @method tag
     * @param string $className className where phpDoc was found
     *
     * @return array
     */
    private function phpDocParam($param, $className)
    {
        $constantName = null;
        $defaultValue = $this->abstracter->UNDEFINED;
        if (\array_key_exists('defaultValue', $param)) {
            $defaultValue = $param['defaultValue'];
            if (\in_array($defaultValue, array('true','false','null'))) {
                $defaultValue = \json_decode($defaultValue);
            } elseif (\is_numeric($defaultValue)) {
                // there are no quotes around value
                $defaultValue = $defaultValue * 1;
            } elseif (\preg_match('/^array\(\s*\)|\[\s*\]$/i', $defaultValue)) {
                // empty array...
                // we're not going to eval non-empty arrays...
                //    non empty array will appear as a string
                $defaultValue = array();
            } elseif (\preg_match('/^(self::)?([^\(\)\[\]]+)$/i', $defaultValue, $matches)) {
                // appears to be a constant
                if ($matches[1]) {
                    // self
                    if (\defined($className.'::'.$matches[2])) {
                        $constantName = $matches[0];
                        $defaultValue = \constant($className.'::'.$matches[2]);
                    }
                } elseif (\defined($defaultValue)) {
                    $constantName = $defaultValue;
                    $defaultValue = \constant($defaultValue);
                }
            } else {
                $defaultValue = \trim($defaultValue, '\'"');
            }
        }
        return array(
            'constantName' => $constantName,
            'defaultValue' => $defaultValue,
        );
    }

    /**
     * Determine propInfo['overrides'] value
     *
     * This is the classname of previous ancestor where property is defined
     *
     * @param \ReflectionProperty $reflectionProperty Reflection Property
     * @param array               $propInfo           Property Info
     * @param string              $className          className of object being inspected
     *
     * @return string|null
     */
    private function propOverrides(\ReflectionProperty $reflectionProperty, $propInfo, $className)
    {
        if (empty($propInfo['overrides'])
            && empty($propInfo['inheritedFrom'])
            && $reflectionProperty->getDeclaringClass()->getName() == $className
        ) {
            return $className;
        }
        return null;
    }

    /**
     * Sorts property/method array by visibility or name
     *
     * @param array $array array to sort
     *
     * @return void
     */
    protected function sort(&$array)
    {
        if (!$array) {
            return;
        }
        $sort = $this->abstracter->getCfg('objectSort');
        if ($sort == 'name') {
            // rather than a simple key sort, use array_multisort so that __construct is always first
            $sortData = array();
            foreach (\array_keys($array) as $name) {
                $sortData[$name] = $name == '__construct'
                    ? '0'
                    : $name;
            }
            \array_multisort($sortData, $array);
        } elseif ($sort == 'visibility') {
            $sortVisOrder = array('public', 'protected', 'private', 'magic', 'magic-read', 'magic-write', 'debug');
            $sortData = array();
            foreach ($array as $name => $info) {
                $sortData['name'][$name] = $name == '__construct'
                    ? '0'     // always place __construct at the top
                    : $name;
                $vis = \is_array($info['visibility'])
                    ? $info['visibility'][0]
                    : $info['visibility'];
                $sortData['vis'][$name] = \array_search($vis, $sortVisOrder);
            }
            \array_multisort($sortData['vis'], $sortData['name'], $array);
        }
    }
}


<?php
/**
 * This file is part of PHPDebugConsole
 *
 * @package   PHPDebugConsole
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2019 Brad Kent
 * @version   v2.3
 */

namespace bdk\Debug;

/**
 * Utility to convert error level mask to user friendly string
 *
 * @see http://php.net/manual/en/errorfunc.constants.php
 * @see https://github.com/maximivanov/php-error-reporting-calculator javascript code inspiration
 */
class ErrorLevel
{

    /**
     * Get error level constants understood by specified php version
     *
     * @param string $phpVer (PHP_VERSION) PHP verion
     *
     * @return array
     */
    public static function getConstants($phpVer = null)
    {
        $phpVer = $phpVer ?: PHP_VERSION;
        /*
            version_compare considers 1 < 1.0 < 1.0.0
        */
        $phpVer = \preg_match('/^\d+\.\d+$/', $phpVer)
            ? $phpVer.'.0'
            : $phpVer;
        $constants = array(
            'E_ERROR' => 1,
            'E_WARNING' => 2,
            'E_PARSE' => 4,
            'E_NOTICE' => 8,
            'E_CORE_ERROR' => 16,
            'E_CORE_WARNING' =>32,
            'E_COMPILE_ERROR' => 64,
            'E_COMPILE_WARNING' => 128,
            'E_USER_ERROR' => 256,
            'E_USER_WARNING' => 512,
            'E_USER_NOTICE' => 1024,
            'E_STRICT' => \version_compare($phpVer, '5.0.0', '>=') ? 2048 : null,
            'E_RECOVERABLE_ERROR' => \version_compare($phpVer, '5.2.0', '>=') ? 4096 : null,
            'E_DEPRECATED' => \version_compare($phpVer, '5.3.0', '>=') ? 8192 : null,
            'E_USER_DEPRECATED' => \version_compare($phpVer, '5.3.0', '>=') ? 16384 : null,
            'E_ALL' => null, // calculated below
        );
        $constants = \array_filter($constants);
        /*
            E_ALL value:
            >= 5.4: 32767
               5.3: 30719 (doesn't include E_STRICT)
               5.2: 6143 (doesn't include E_STRICT)
             < 5.2: 2047 (doesn't include E_STRICT)
        */
        $constants['E_ALL'] = \array_sum($constants);
        if (isset($constants['E_STRICT']) && \version_compare($phpVer, '5.4.0', '<')) {
            // E_STRICT not included in E_ALL until 5.4
            $constants['E_ALL'] -= $constants['E_STRICT'];
        }
        return $constants;
    }

    /**
     * [toConstantString description]
     *
     * @param integer $level          Error Level (bitmask) value
     * @param string  $phpVer         (PHP_VERSION) php Version
     * @param boolean $explicitStrict (true) if level === E_ALL, always include/exclude E_STRICT for disambiguation / portability
     *
     * @return string
     */
    public static function toConstantString($level = null, $phpVer = null, $explicitStrict = true)
    {
        $string = '';
        $level = $level === null
            ? \error_reporting()
            : $level;
        $allConstants = self::getConstants($phpVer); // includes E_ALL
        $levelConstants = self::filterConstantsByLevel($allConstants, $level); // excludes E_ALL
        $eAll = $allConstants['E_ALL'];
        unset($allConstants['E_ALL']);
        if (\count($levelConstants) > \count($allConstants) / 2) {
            $string = 'E_ALL';
            foreach ($allConstants as $constantName => $value) {
                $isExplicit = $explicitStrict
                    && $constantName == 'E_STRICT';
                if (self::inBitmask($value, $level)) {
                    if (!self::inBitmask($value, $eAll) || $isExplicit) {
                        // only thing that wouldn't be in E_ALL is E_STRICT
                        $string .= ' | ' . $constantName;
                    }
                } else {
                    if (self::inBitmask($value, $eAll) || $isExplicit) {
                        $string .= ' & ~' . $constantName;
                    }
                }
            }
        } else {
            $string = \implode(' | ', \array_keys($levelConstants));
        }
        return $string ?: '0';
    }

    /**
     * Get all constants included in specified error level
     * excludes E_ALL
     *
     * @param array   $constants constantName => value array
     * @param integer $level     error level
     *
     * @return array
     */
    private static function filterConstantsByLevel($constants, $level)
    {
        foreach ($constants as $constantName => $value) {
            if (!self::inBitmask($value, $level)) {
                unset($constants[$constantName]);
            }
        }
        unset($constants['E_ALL']);
        return $constants;
    }

    /**
     * [isConstantSetInLevel description]
     *
     * @param integer $value   [description]
     * @param integer $bitmask [description]
     *
     * @return boolean
     */
    private static function inBitmask($value, $bitmask)
    {
        return ($bitmask & $value) === $value;
    }
}


<?php
/**
 * This file is part of PHPDebugConsole
 *
 * @package   PHPDebugConsole
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2019 Brad Kent
 * @version   v2.3
 */

namespace bdk\Debug;

use bdk\PubSub\Event;

/**
 * Table helper methods
 */
class MethodTable
{

    const SCALAR = "\x00scalar\x00";

    /**
     * Go through all the "rows" of array to determine what the keys are and their order
     *
     * @param array $rows array (or traversable abstraction)
     *
     * @return array
     */
    public static function colKeys($rows)
    {
        if (!\is_array($rows)) {
            return array();
        }
        if (Abstracter::isAbstraction($rows) && $rows['traverseValues']) {
            $rows = $rows['traverseValues'];
        }
        $lastKeys = array();
        $newKeys = array();
        $curKeys = array();
        foreach ($rows as $row) {
            $curKeys = self::keys($row);
            if (empty($lastKeys)) {
                $lastKeys = $curKeys;
            } elseif ($curKeys != $lastKeys) {
                $newKeys = array();
                $count = \count($curKeys);
                for ($i = 0; $i < $count; $i++) {
                    $curKey = $curKeys[$i];
                    if ($lastKeys && $curKey === $lastKeys[0]) {
                        \array_push($newKeys, $curKey);
                        \array_shift($lastKeys);
                    } elseif (false !== $position = \array_search($curKey, $lastKeys, true)) {
                        $segment = \array_splice($lastKeys, 0, $position+1);
                        \array_splice($newKeys, \count($newKeys), 0, $segment);
                    } elseif (!\in_array($curKey, $newKeys, true)) {
                        \array_push($newKeys, $curKey);
                    }
                }
                // put on remaining from lastKeys
                \array_splice($newKeys, \count($newKeys), 0, $lastKeys);
                $lastKeys = \array_unique($newKeys);
            }
        }
        return $lastKeys;
    }

    /**
     * Get values for passed keys
     *
     * Used by table method
     *
     * @param array $row     should be array or abstraction
     * @param array $keys    column keys
     * @param array $objInfo Will be populated with className and phpDoc
     *                           if row is an objects, $objInfo['row'] will be populated
     *                           if a value is an object being displayed as a string,
     *                               $objInfo['cols'][key] will be populated
     *
     * @return array
     */
    public static function keyValues($row, $keys, &$objInfo)
    {
        $objInfo = array(
            'row' => false,
            'cols' => array(),
        );
        $rowIsAbstraction = Abstracter::isAbstraction($row);
        if ($rowIsAbstraction) {
            if ($row['type'] == 'object') {
                $objInfo['row'] = array(
                    'className' => $row['className'],
                    'phpDoc' => $row['phpDoc'],
                );
                $row = self::objectValues($row);
                if (!\is_array($row)) {
                    // ie stringified value
                    $objInfo['row'] = false;
                    $row = array(self::SCALAR => $row);
                } elseif (Abstracter::isAbstraction($row)) {
                    // still an abstraction (ie closure)
                    $objInfo['row'] = false;
                    $row = array(self::SCALAR => $row);
                }
            } else {
                // resource & callable
                $row = array(self::SCALAR => $row);
            }
        }
        if (!\is_array($row)) {
            $row = array(self::SCALAR =>  $row);
        }
        $values = array();
        foreach ($keys as $key) {
            if (\array_key_exists($key, $row)) {
                $value = $row[$key];
                if ($value !== null) {
                    // by setting to false :
                    //    indicate that the column is not populated by objs of the same type
                    //    if stringified abstraction, we'll set cols[key] below
                    $objInfo['cols'][$key] = false;
                }
                if (Abstracter::isAbstraction($value)) {
                    // just return the stringified / __toString value in a table
                    if (isset($value['stringified'])) {
                        $objInfo['cols'][$key] = $value['className'];
                        $value = $value['stringified'];
                    } elseif (isset($value['__toString']['returnValue'])) {
                        $objInfo['cols'][$key] = $value['className'];
                        $value = $value['__toString']['returnValue'];
                    }
                }
            } else {
                $value = Abstracter::UNDEFINED;
            }
            $values[$key] = $value;
        }
        return $values;
    }

    /**
     * Handle table() call
     *
     * @param Event $event event object
     *
     * @return Event
     */
    public function onLog(Event $event)
    {
        $args = $event['args'];
        $meta = \array_merge(array(
            'caption' => null,
            'columns' => array(),
            'sortable' => true,
            'totalCols' => array(),
        ), $event['meta']);
        $argCount = \count($args);
        $data = null;
        for ($i = 0; $i < $argCount; $i++) {
            if (\is_array($args[$i])) {
                if ($data === null) {
                    $data = $args[$i];
                } elseif (!$meta['columns']) {
                    $meta['columns'] = $args[$i];
                }
            } elseif ($args[$i] instanceof \Traversable) {
                if ($data === null) {
                    $data = $args[$i];
                }
            } elseif (\is_string($args[$i]) && !$meta['caption']) {
                $meta['caption'] = $args[$i];
            }
            unset($args[$i]);
        }
        $event->setValues(array(
            'method' => $event['method'],
            'args' => array($data),
            'meta' => $meta,
        ));
        return $event;
    }

    /**
     * Get the keys contained in value
     *
     * @param mixed $val scalar value or abstraction
     *
     * @return string[]
     */
    private static function keys($val)
    {
        if (Abstracter::isAbstraction($val)) {
            // abstraction
            if ($val['type'] == 'object') {
                if ($val['traverseValues']) {
                    // probably Traversable
                    $val = $val['traverseValues'];
                } elseif ($val['stringified']) {
                    $val = null;
                } elseif (isset($val['methods']['__toString']['returnValue'])) {
                    $val = null;
                } else {
                    $val = \array_filter($val['properties'], function ($prop) {
                        return $prop['visibility'] === 'public';
                    });
                    /*
                        Reflection doesn't return properties in any given order
                        so, we'll sort for consistency
                    */
                    \ksort($val, SORT_NATURAL | SORT_FLAG_CASE);
                }
            } else {
                // ie callable or resource
                $val = null;
            }
        }
        return \is_array($val)
            ? \array_keys($val)
            : array(self::SCALAR);
    }

    /**
     * Get object abstraction's values
     * if, object has a stringified or __toString value, it will bereturned
     *
     * @param array $abs object abstraction
     *
     * @return array|string
     */
    private static function objectValues($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'];
        }
        if ($abs['className'] === 'Closure') {
            return $abs;
        }
        $values = $abs['properties'];
        foreach ($values as $k => $info) {
            if ($info['visibility'] !== 'public') {
                unset($values[$k]);
            } else {
                $values[$k] = $info['value'];
            }
        }
        return $values;
    }
}


<?php
/**
 * This file is part of PHPDebugConsole
 *
 * @package   PHPDebugConsole
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2019 Brad Kent
 * @version   v2.3
 */

namespace bdk\Debug;

use bdk\Debug;
use bdk\PubSub\Event;

/**
 * Clear method
 */
class MethodClear
{

    private $data;
    private $debug;
    private $channelName = null;
    private $channelRegex;
    private $isRootInstance = false;

    /**
     * Constructor
     *
     * @param \bdk\Debug $debug Debug instance
     * @param array      $data  debug data
     */
    public function __construct(Debug $debug, &$data)
    {
        $this->debug = $debug;
        $this->data = &$data;
    }

    /**
     * Handle clear() call
     *
     * @param Event $event event object
     *
     * @return Event
     */
    public function onLog(Event $event)
    {
        $this->channelName = $this->debug->parentInstance
            ? $event['meta']['channel'] // just clear this specific channel
            : null;
        $this->channelRegex = '#^'.\preg_quote($this->channelName, '#').'(\.|$)#';
        $this->isRootInstance = $this->debug->rootInstance === $this->debug;
        $bitmask = $event['meta']['bitmask'];
        $callerInfo = $this->debug->utilities->getCallerInfo();
        $cleared = array();
        $cleared[] = $this->clearAlerts($bitmask);
        $cleared[] = $this->clearLog($bitmask);
        $cleared[] = $this->clearSummary($bitmask);
        $this->clearErrors($bitmask);
        if (($bitmask & Debug::CLEAR_ALL) === Debug::CLEAR_ALL) {
            $cleared = array('everything');
        }
        $args = $this->getLogArgs($cleared);
        $event->setValues(array(
            'method' => 'clear',
            'args' => $args,
            'meta' =>  \array_merge(array(
                'file' => $callerInfo['file'],
                'line' => $callerInfo['line'],
                'bitmask' => $bitmask,
                'flags' => array(
                    'alerts' => (bool) ($bitmask & Debug::CLEAR_ALERTS),
                    'log' => (bool) ($bitmask & Debug::CLEAR_LOG),
                    'logErrors' => (bool) ($bitmask & Debug::CLEAR_LOG_ERRORS),
                    'summary' => (bool) ($bitmask & Debug::CLEAR_SUMMARY),
                    'summaryErrors' => (bool) ($bitmask & Debug::CLEAR_SUMMARY_ERRORS),
                    'silent' =>  (bool) ($bitmask & Debug::CLEAR_SILENT),
                ),
            ), $event['meta']),
            'log' => $args && !($bitmask & Debug::CLEAR_SILENT),
            'publish' => (bool) $args,
        ));
        return $event;
    }

    /**
     * Test channel for inclussion
     *
     * @param string $channel channel name to test against
     *
     * @return boolean
     */
    private function channelTest($channel)
    {
        return $this->isRootInstance || \preg_match($this->channelRegex, $channel);
    }

    /**
     * Clear alerts
     *
     * @param integer $flags flags passed to clear()
     *
     * @return string|null
     */
    private function clearAlerts($flags)
    {
        $clearAlerts = $flags & Debug::CLEAR_ALERTS;
        if (!$clearAlerts) {
            return null;
        }
        if ($this->channelName) {
            foreach ($this->data['alerts'] as $i => $entry) {
                $channel = isset($entry[2]['channel']) ? $entry[2]['channel'] : null;
                if ($this->channelTest($channel)) {
                    unset($this->data['alerts'][$i]);
                }
            }
            $this->data['alerts'] = \array_values($this->data['alerts']);
        } else {
            $this->data['alerts'] = array();
        }
        return 'alerts';
    }

    /**
     * Remove error & warn from summary & log
     *
     * @param integer $flags flags passed to clear()
     *
     * @return void
     */
    private function clearErrors($flags)
    {
        $clearErrors = $flags & Debug::CLEAR_LOG_ERRORS || $flags & Debug::CLEAR_SUMMARY_ERRORS;
        if (!$clearErrors) {
            return;
        }
        $errorsNotCleared = array();
        /*
            Clear Log Errors
        */
        $errorsNotCleared = $this->clearErrorsHelper(
            $this->data['log'],
            $flags & Debug::CLEAR_LOG_ERRORS
        );
        /*
            Clear Summary Errors
        */
        foreach (\array_keys($this->data['logSummary']) as $priority) {
            $errorsNotCleared = \array_merge($this->clearErrorsHelper(
                $this->data['logSummary'][$priority],
                $flags & Debug::CLEAR_SUMMARY_ERRORS
            ));
        }
        $errorsNotCleared = \array_unique($errorsNotCleared);
        $errors = $this->debug->errorHandler->get('errors');
        foreach ($errors as $error) {
            if (!\in_array($error['hash'], $errorsNotCleared)) {
                $error['inConsole'] = false;
            }
        }
    }

    /**
     * clear errors for given log
     *
     * @param array   $log   reference to log to clear of errors
     * @param boolean $clear clear errors, or return errors?
     *
     * @return string[] array of error-hashes not cleared
     */
    private function clearErrorsHelper(&$log, $clear = true)
    {
        $errorsNotCleared = array();
        foreach ($log as $k => $entry) {
            if (!\in_array($entry[0], array('error','warn'))) {
                continue;
            }
            $clear2 = $clear;
            if ($this->channelName) {
                $channel = isset($entry[2]['channel']) ? $entry[2]['channel'] : null;
                $clear2 = $clear && $channel === $this->channelName;
            }
            if ($clear2) {
                unset($log[$k]);
            } elseif (isset($entry[2]['errorHash'])) {
                $errorsNotCleared[] = $entry[2]['errorHash'];
            }
        }
        $log = \array_values($log);
        return $errorsNotCleared;
    }

    /**
     * Clear log entries
     *
     * @param integer $flags flags passed to clear()
     *
     * @return string|null
     */
    private function clearLog($flags)
    {
        $return = null;
        $clearErrors = $flags & Debug::CLEAR_LOG_ERRORS;
        if ($flags & Debug::CLEAR_LOG) {
            $return = 'log ('.($clearErrors ? 'incl errors' : 'sans errors').')';
            $curDepth = 0;
            foreach ($this->data['groupStacks']['main'] as $group) {
                $curDepth += (int) $group['collect'];
            }
            $entriesKeep = $this->debug->internal->getCurrentGroups($this->data['log'], $curDepth);
            $this->clearLogHelper($this->data['log'], $clearErrors, $entriesKeep);
        } elseif ($clearErrors) {
            $return = 'errors';
        }
        return $return;
    }

    /**
     * [clearLogHelper description]
     *
     * @param array   $log         log to clear (passed by reference)
     * @param boolean $clearErrors whether or not to clear errors
     * @param array   $entriesKeep log entries to keep
     *
     * @return void
     */
    private function clearLogHelper(&$log, $clearErrors = false, $entriesKeep = array())
    {
        $keep = $clearErrors
            ? array()
            : array('error','warn');
        if ($keep || $this->channelName) {
            // we need to go through and filter based on method and/or channel
            foreach ($log as $k => $entry) {
                $channel = isset($entry[2]['channel']) ? $entry[2]['channel'] : null;
                $channelMatch = !$this->channelName || $channel === $this->channelName;
                if (\in_array($entry[0], $keep) || !$channelMatch) {
                    $entriesKeep[$k] = $entry;
                }
            }
        }
        \ksort($entriesKeep);
        $log = \array_values($entriesKeep);
    }

    /**
     * Clear summary entries
     *
     * @param integer $flags flags passed to clear()
     *
     * @return string|null
     */
    private function clearSummary($flags)
    {
        $return = null;
        $clearErrors = $flags & Debug::CLEAR_SUMMARY_ERRORS;
        if ($flags & Debug::CLEAR_SUMMARY) {
            $return = 'summary ('.($clearErrors ? 'incl errors' : 'sans errors').')';
            $curPriority = \end($this->data['groupPriorityStack']);  // false if empty
            foreach (\array_keys($this->data['logSummary']) as $priority) {
                $entriesKeep = array();
                if ($priority === $curPriority) {
                    $curDepth = 0;
                    foreach ($this->data['groupStacks'][$priority] as $group) {
                        $curDepth += (int) $group['collect'];
                    }
                    $entriesKeep = $this->debug->internal->getCurrentGroups(
                        $this->data['logSummary'][$priority],
                        $curDepth
                    );
                } else {
                    $this->data['groupStacks'][$priority] = array();
                }
                $this->clearLogHelper($this->data['logSummary'][$priority], $clearErrors, $entriesKeep);
            }
        } elseif ($clearErrors) {
            $return = 'summary errors';
        }
        return $return;
    }

    /**
     * Build message that gets appended to log
     *
     * @param array $cleared array of things that were cleared
     *
     * @return string
     */
    private function getLogArgs($cleared)
    {
        $cleared = \array_filter($cleared);
        if (!$cleared) {
            return array();
        }
        $count = \count($cleared);
        $glue = $count == 2
            ? ' and '
            : ', ';
        if ($count > 2) {
            $cleared[$count-1] = 'and '.$cleared[$count-1];
        }
        $msg = 'Cleared '.\implode($glue, $cleared);
        if ($this->channelName) {
            return array(
                $msg.' %c(%s)',
                'background-color:#c0c0c0; padding:0 .33em;',
                $this->channelName,
            );
        }
        return array($msg);
    }
}


<?php
/**
 * This file is part of PHPDebugConsole
 *
 * @package   PHPDebugConsole
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2019 Brad Kent
 * @version   v2.1.0
 */

namespace bdk\Debug;

/**
 * Abstracter:  Methods used de=refrence and store arrays
 */
class AbstractArray
{

	protected $abstracter;

    /**
     * Constructor
     *
     * @param Abstracter $abstracter abstracter obj
     */
    public function __construct(Abstracter $abstracter)
    {
        $this->abstracter = $abstracter;
    }

    /**
     * returns information about an array
     *
     * @param array  $array  Array to inspect
     * @param string $method Method requesting abstraction
     * @param array  $hist   (@internal) array/object history
     *
     * @return array
     */
    public function getAbstraction(&$array, $method = null, &$hist = array())
    {
        if (\in_array($array, $hist, true)) {
            return $this->abstracter->RECURSION;
        }
        if (self::isCallable($array)) {
            // this appears to be a "callable"
            return array(
                'debug' => $this->abstracter->ABSTRACTION,
                'type' => 'callable',
                'values' => array(\get_class($array[0]), $array[1]),
            );
        }
        $return = array();
        $hist[] = $array;
        foreach ($array as $k => $v) {
            if ($this->abstracter->needsAbstraction($v)) {
                $v = $this->abstracter->getAbstraction($array[$k], $method, $hist);
            }
            $return[$k] = $v;
        }
        return $return;
    }

    /**
     * Is array a callable?
     *
     * @param array $array array to check
     *
     * @return boolean
     */
    public static function isCallable($array)
    {
        return \array_keys($array) == array(0,1)
            && \is_object($array[0])
            && \is_string($array[1])
            && \method_exists($array[0], $array[1]);
    }
}


<?php
/**
 * @package   bdk\ErrorHandler
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2019 Brad Kent
 * @version   v2.3
 */

namespace bdk;

use bdk\PubSub\Event;
use bdk\PubSub\Manager as EventManager;
use ReflectionObject;

/**
 * General-purpose error handler which supports fatal errors
 *
 * Able to register multiple onError "callback" functions
 */
class ErrorHandler
{

    public $eventManager;
    protected $cfg = array();
    protected $data = array(
        'errorCaller'   => array(),
        'errors'        => array(),
        'lastError'     => null,
    );
    protected $errTypes = array(
        E_ERROR             => 'Fatal Error',       // handled via shutdown function
        E_WARNING           => 'Warning',
        E_PARSE             => 'Parsing Error',     // handled via shutdown function
        E_NOTICE            => 'Notice',
        E_CORE_ERROR        => 'Core Error',        // handled via shutdown function
        E_CORE_WARNING      => 'Core Warning',      // handled?
        E_COMPILE_ERROR     => 'Compile Error',     // handled via shutdown function
        E_COMPILE_WARNING   => 'Compile Warning',   // handled?
        E_USER_ERROR        => 'User Error',
        E_USER_WARNING      => 'User Warning',
        E_USER_NOTICE       => 'User Notice',
        E_ALL               => 'E_ALL',             // listed here for completeness
        E_STRICT            => 'Runtime Notice (E_STRICT)', // php 5.0 :  2048
        E_RECOVERABLE_ERROR => 'Recoverable Error',         // php 5.2 :  4096
        E_DEPRECATED        => 'Deprecated',                // php 5.3 :  8192
        E_USER_DEPRECATED   => 'User Deprecated',           // php 5.3 : 16384
    );
    protected $errCategories = array(
        'deprecated'    => array( E_DEPRECATED, E_USER_DEPRECATED ),
        'error'         => array( E_USER_ERROR, E_RECOVERABLE_ERROR ),
        'notice'        => array( E_NOTICE, E_USER_NOTICE ),
        'strict'        => array( E_STRICT ),
        'warning'       => array( E_WARNING, E_CORE_WARNING, E_COMPILE_WARNING, E_USER_WARNING ),
        'fatal'         => array( E_ERROR, E_PARSE, E_COMPILE_ERROR, E_CORE_ERROR ),
    );
    protected $userErrors = array(
        E_USER_DEPRECATED,
        E_USER_ERROR,
        E_USER_NOTICE,
        E_USER_WARNING,
    );
    protected $registered = false;
    protected $prevDisplayErrors = null;
    protected $prevErrorHandler = null;
    protected $prevExceptionHandler = null;
    protected $uncaughtException;
    private static $instance;

    /**
     * Constructor
     *
     * @param EventManager $eventManager event manager
     * @param array        $cfg          config
     */
    public function __construct(EventManager $eventManager, $cfg = array())
    {
        $this->eventManager = $eventManager;
        $this->cfg = array(
            'continueToPrevHandler' => true,    // whether to continue to previously defined handler (if there is/was a prev error handler)
                                                //   will not continue if error event propagation stopped
            'errorReporting' => E_ALL | E_STRICT,   // what errors are handled by handler? bitmask or "system" to use runtime value
                                                    //   note that if using "system", suppressed errors (via @ operator) will not be handled (we'll still handle fatal category)
            // shortcut for subscribing to errorHandler.error Event
            //   will receive error Event object
            'onError' => null,
            'onEUserError' => 'normal', // only applicable if we're not continuing to a prev error handler
                                    // (continueToPrevHandler = false, there's no previous handler, or propagation stopped)
                                    //   'continue' : forces continueToNormal = false (script will continue)
                                    //   'log' : if propagation not stopped, call error_log()
                                    //         continue script execution
                                    //   'normal' : forces continueToNormal = true;
                                    //   null : use error's continueToNormal value
        );
        // Initialize self::$instance if not set
        //    so that self::getInstance() will always return original instance
        //    as opposed the the last instance created with new ErrorHandler()
        if (!isset(self::$instance)) {
            self::$instance = $this;
        }
        $this->setCfg($cfg);
        $this->register();
        // easier to maintain subscription to php.shutdown event and check this->registered value
        // than to subscribe with register() and unsub with unRegister()
        $this->eventManager->subscribe('php.shutdown', array($this, 'onShutdown'), PHP_INT_MAX);
        return;
    }

    /**
     * Get backtrace
     *
     * To get trace from within shutdown function utilizes xdebug_get_function_stack() if available
     *
     * @param array|Event|Exception $error (optional) error details if getting error backtrace
     *
     * @return array
     */
    public function backtrace($error = null)
    {
        $exception = null;
        $isFatalError = false;
        if ($error instanceof \Exception) {
            $exception = $error;
        } elseif ($error) {
            // array or Event
            $exception = $error['exception'];
            $isFatalError = \in_array($error['type'], $this->errCategories['fatal']);
        }
        if ($exception) {
            $backtrace = $exception->getTrace();
            \array_unshift($backtrace, array(
                'file' => $exception->getFile(),
                'line' => $exception->getLine(),
            ));
        } elseif ($isFatalError && \extension_loaded('xdebug')) {
            $backtrace = \xdebug_get_function_stack();
            $backtrace = \array_reverse($backtrace);
            $backtrace = $this->backtraceRemoveInternal($backtrace);
            $errorFileLine = array(
                'file' => $error['file'],
                'line' => $error['line'],
            );
            \array_pop($backtrace);   // pointless entry that xdebug_get_function_stack() includes
            if (empty($backtrace)) {
                return array();
            }
            if (\array_intersect_assoc($errorFileLine, $backtrace[0]) !== $errorFileLine) {
                \array_unshift($backtrace, $errorFileLine);
            }
        } elseif ($isFatalError) {
            // backtrace unavailable
            $backtrace = array();
        } else {
            $backtrace = \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
            $backtrace = $this->backtraceRemoveInternal($backtrace);
        }
        return $this->normalizeTrace($backtrace);
    }

    /**
     * Retrieve a data value or property
     *
     * @param string $key what to get
     *
     * @return mixed
     */
    public function get($key = null)
    {
        if ($key == 'lastError') {
            return isset($this->data['lastError'])
                ? $this->data['lastError']->getValues()
                : null;
        }
        if (isset($this->data[$key])) {
            return $this->data[$key];
        }
        if (isset($this->{$key})) {
            return $this->{$key};
        }
        return null;
    }

    /**
     * Retrieve a configuration value
     *
     * @param string $key what to get
     *
     * @return mixed
     */
    public function getCfg($key = null)
    {
        if (!\strlen($key)) {
            return $this->cfg;
        }
        if (isset($this->cfg[$key])) {
            return $this->cfg[$key];
        }
        return null;
    }

    /**
     * Returns the *Singleton* instance of this class (IF INSTANCE EXISTS)
     *
     * @param array $cfg optional config
     *
     * @return object|false
     */
    public static function getInstance($cfg = array())
    {
        if (!isset(self::$instance)) {
            return false;
        } elseif ($cfg) {
            self::$instance->setCfg($cfg);
        }
        return self::$instance;
    }

    /**
     * Error handler
     *
     * @param integer $errType the level of the error
     * @param string  $errMsg  the error message
     * @param string  $file    filepath the error was raised in
     * @param string  $line    the line the error was raised in
     * @param array   $vars    active symbol table at point error occured
     *
     * @return boolean
     * @link   http://php.net/manual/en/function.set-error-handler.php
     * @link   http://php.net/manual/en/language.operators.errorcontrol.php
     */
    public function handleError($errType, $errMsg, $file, $line, $vars = array())
    {
        $error = $this->buildError($errType, $errMsg, $file, $line, $vars);
        $errorReporting = $this->cfg['errorReporting'] === 'system'
            ? \error_reporting() // note:  will return 0 if error suppression is active in call stack (via @ operator)
                                //  our shutdown function unsupresses fatal errors
            : $this->cfg['errorReporting'];
        $isHandledType = $errType & $errorReporting;
        if (!$isHandledType) {
            // not handled
            //   if cfg['errorReporting'] == 'system', error could simply be suppressed
            if ($error['continueToPrevHandler']) {
                return \call_user_func(
                    $this->prevErrorHandler,
                    $error['type'],
                    $error['message'],
                    $error['file'],
                    $error['line'],
                    $vars
                );
            }
            return false;   // return false to continue to "normal" error handler
        }
        if (!$error['isSuppressed']) {
            // suppressed error should not clear error caller
            $this->data['lastError'] = $error;
            $this->data['errorCaller'] = array();
            $this->eventManager->publish('errorHandler.error', $error);
        }
        $this->data['errors'][ $error['hash'] ] = $error;
        if ($error['continueToPrevHandler'] && $this->prevErrorHandler && !$error->isPropagationStopped()) {
            return \call_user_func(
                $this->prevErrorHandler,
                $error['type'],
                $error['message'],
                $error['file'],
                $error['line'],
                $vars
            );
        }
        if (\in_array($error['type'], array(E_USER_ERROR, E_RECOVERABLE_ERROR))) {
            $this->onUserError($error);
        }
        if ($error['continueToNormal']) {
            // PHP will log the error
            // if E_USER_ERROR, php will exit()
            return false;
        }
        return true;
    }

    /**
     * Handle uncaught exceptions
     *
     * This isn't strictly necesssary...  uncaught exceptions  are a fatal error, which we can handle...
     * However..  An backtrace sure is nice...
     *    a) catching backtrace via shutdown function only possible if xdebug installed
     *    b) xdebug_get_function_stack's magic doesn't seem to powerless for uncaught exceptions!
     *
     * @param Exception|Throwable $exception exception to handle
     *
     * @return void
     */
    public function handleException($exception)
    {
        // lets store the exception so we can use the backtrace it provides
        $this->uncaughtException = $exception;
        \http_response_code(500);
        $this->handleError(
            E_ERROR,
            'Uncaught exception \''.\get_class($exception).'\' with message '.$exception->getMessage(),
            $exception->getFile(),
            $exception->getLine()
        );
        $this->uncaughtException = null;
        if ($this->cfg['continueToPrevHandler'] && $this->prevExceptionHandler) {
            \call_user_func($this->prevErrorHandler, $exception);
        }
    }

    /**
     * Send string to error_log()
     *
     * @param Event $error Error event to log
     *
     * @return boolean
     */
    public function log($error)
    {
        $str = 'PHP User Error:  '.$error['message'].' in '.$error['file'].' on line '.$error['line'];
        return \error_log($str);
    }

    /**
     * php.shutdown event subscriber
     *
     * Used to handle fatal errors
     *
     * Test fatal error handling by publishing 'php.shutdown' event with error value
     *
     * @param Event $event php.shutdown event
     *
     * @return void
     */
    public function onShutdown(Event $event)
    {
        if (!$this->registered) {
            return;
        }
        $error = $event['error'] ?: \error_get_last();
        if (!$error) {
            return;
        }
        if (\in_array($error['type'], $this->errCategories['fatal'])) {
            /*
                found in wild:
                @include(some_file_with_parse_error)
                which will trigger a fatal error (here we are),
                but error_reporting() will return 0 due to the @ operator
                unsupress fatal error here
            */
            \error_reporting(E_ALL | E_STRICT);
            $this->handleError($error['type'], $error['message'], $error['file'], $error['line']);
        }
        /*
            Find the fatal error/uncaught-exception and attach to shutdown event
        */
        foreach ($this->data['errors'] as $error) {
            if ($error['category'] === 'fatal') {
                $event['error'] = $error;
                break;
            }
        }
        return;
    }

    /**
     * Register this error handler and shutdown function
     *
     * @return void
     */
    public function register()
    {
        if ($this->registered) {
            return;
        }
        $this->prevDisplayErrors = \ini_set('display_errors', 0);
        $this->prevErrorHandler = \set_error_handler(array($this, 'handleError'));
        $this->prevExceptionHandler = \set_exception_handler(array($this, 'handleException'));
        $this->registered = true;   // used by this->onShutdown()
        return;
    }

    /**
     * Set one or more config values
     *
     * If setting a single value via method a or b, old value is returned
     *
     *    setCfg('key', 'value')
     *    setCfg(array('k1'=>'v1', 'k2'=>'v2'))
     *
     * @param string $mixed  key=>value array or key
     * @param mixed  $newVal value
     *
     * @return mixed
     */
    public function setCfg($mixed, $newVal = null)
    {
        $ret = null;
        $values = array();
        if (\is_string($mixed)) {
            $key = $mixed;
            $ret = isset($this->cfg[$key])
                ? $this->cfg[$key]
                : null;
            $values = array(
                $key => $newVal,
            );
        } elseif (\is_array($mixed)) {
            $values = $mixed;
        }
        if (isset($values['onError'])) {
            /*
                Replace - not append - subscriber set via setCfg
            */
            if (isset($this->cfg['onError'])) {
                $this->eventManager->unsubscribe('errorHandler.error', $this->cfg['onError']);
            }
            $this->eventManager->subscribe('errorHandler.error', $values['onError']);
        }
        $this->cfg = \array_merge($this->cfg, $values);
        return $ret;
    }

    /**
     * Set data value
     *
     * @param string $key   what
     * @param mixed  $value value
     *
     * @return void
     */
    public function setData($key, $value)
    {
        $this->data[$key] = $value;
    }

    /**
     * Set the calling file/line for next error
     * this override will apply until cleared or error occurs
     *
     * Example:  some wrapper function that is called often:
     *     Rather than reporting that an error occurred within the wrapper, you can use
     *     setErrorCaller() to report the error originating from the file/line that called the function
     *
     * @param array   $caller (default) null : determine automatically
     *                        empty value (false, "", 0, array(): clear current value
     *                        array() : manually set value
     * @param integer $offset (optional) if determining automatically : adjust how many frames to go back
     *
     * @return void
     */
    public function setErrorCaller($caller = null, $offset = 0)
    {
        if ($caller === null) {
            $backtrace = \version_compare(PHP_VERSION, '5.4.0', '>=')
                ? \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $offset + 3)
                : \debug_backtrace(false);   // don't provide object
            $i = isset($backtrace[$offset+1])
                ? $offset + 1
                : \count($backtrace) - 1;
            $caller = isset($backtrace[$i]['file'])
                ? $backtrace[$i]
                : $backtrace[$i+1]; // likely called via call_user_func.. need to go one more to get calling file & line
            $caller = array(
                'file' => $caller['file'],
                'line' => $caller['line'],
            );
        } elseif (empty($caller)) {
            // clear errorCaller
            $caller = array();
        }
        $this->data['errorCaller'] = $caller;
        return;
    }

    /**
     * un-register this error handler and shutdown function
     *
     * Note:  PHP conspicuously lacks an unregister_shutdown_function function.
     *     Technically this will still be registered, however:
     *     $this->registered will be used to keep track of whether
     *     we're "registered" or not and behave accordingly
     *
     * @return void
     */
    public function unregister()
    {
        if (!$this->registered) {
            return;
        }
        /*
            set and restore error handler to determine the current error handler
        */
        $errHandlerCur = \set_error_handler(array($this, 'handleError'));
        \restore_error_handler();
        if ($errHandlerCur == array($this, 'handleError')) {
            // we are the current error handler
            \restore_error_handler();
        }
        /*
            set and restore exception handler to determine the current error handler
        */
        $exHandlerCur = \set_exception_handler(array($this, 'handleException'));
        \restore_exception_handler();
        if ($exHandlerCur == array($this, 'handleException')) {
            // we are the current exception handler
            \restore_exception_handler();
        }
        \ini_set('display_errors', $this->prevDisplayErrors);
        $this->prevErrorHandler = null;
        $this->prevExceptionHandler = null;
        $this->registered = false;  // used by $this->onShutdown()
        return;
    }

    /**
     * Remove internal frames from backtrace
     *
     * @param array $backtrace backtrace
     *
     * @return array
     */
    protected function backtraceRemoveInternal($backtrace)
    {
        for ($i = \count($backtrace) - 1; $i > 0; $i--) {
            $frame = $backtrace[$i];
            if (isset($frame['class']) && $frame['class'] === __CLASS__) {
                break;
            }
        }
        if ($backtrace[$i]['function'] == 'onShutdown') {
            /*
                We got here via php.shutdown event (fatal error)
                skip over PubSub internals
            */
            $refObj = new ReflectionObject($this->eventManager);
            $filepath = $refObj->getFilename();
            while (isset($backtrace[$i+1]['file']) && $backtrace[$i+1]['file'] == $filepath) {
                $i++;
            }
        }
        $i++;
        return \array_slice($backtrace, $i);
    }

    /**
     * "Normalize" backtrace from debug_backtrace() or xdebug_get_function_stack();
     *
     * @param array $backtrace trace/stack from debug_backtrace() or xdebug_Get_function_stack()
     *
     * @return array
     */
    protected function normalizeTrace($backtrace)
    {
        $backtraceNew = array();
        $frameDefault = array(
            'file' => null,
            'line' => null,
            'function' => null,
            'class' => null,
            'type' => null,
        );
        $funcsSkip = array('call_user_func','call_user_func_array');
        $funcsSkipRegex = '/^('.\implode('|', $funcsSkip).')[:\(\{]/';
        for ($i = 0, $count=\count($backtrace); $i < $count; $i++) {
            $frame = \array_merge($frameDefault, $backtrace[$i]);
            $frame = \array_intersect_key($frame, $frameDefault);
            if (\in_array($frame['function'], $funcsSkip) || \preg_match($funcsSkipRegex, $frame['function'])) {
                $backtraceNew[count($backtraceNew) - 1]['file'] = $frame['file'];
                $backtraceNew[count($backtraceNew) - 1]['line'] = $frame['line'];
                continue;
            }
            if (\in_array($frame['type'], array('dynamic','static'))) {
                // xdebug_get_function_stack
                $frame['type'] = $frame['type'] === 'dynamic' ? '->' : '::';
            }
            if (isset($backtrace[$i]['include_filename'])) {
                // xdebug_get_function_stack
                $frame['function'] = 'include or require';
            } else {
                $frame['function'] = \preg_match('/\{closure\}$/', $frame['function'])
                    ? $frame['function']
                    : $frame['class'].$frame['type'].$frame['function'];
            }
            if (!$frame['function']) {
                unset($frame['function']);
            }
            unset($frame['class'], $frame['type']);
            $backtraceNew[] = $frame;
        }
        return $backtraceNew;
    }

    /**
     * Build error object
     *
     * Error object is simply an event object
     *
     * @param integer $errType the level of the error
     * @param string  $errMsg  the error message
     * @param string  $file    filepath the error was raised in
     * @param string  $line    the line the error was raised in
     * @param array   $vars    active symbol table at point error occured
     *
     * @return Event
     */
    protected function buildError($errType, $errMsg, $file, $line, $vars)
    {
        // determine $category
        foreach ($this->errCategories as $category => $errTypes) {
            if (\in_array($errType, $errTypes)) {
                break;
            }
        }
        $errorValues = array(
            'type'      => $errType,                    // int
            'typeStr'   => $this->errTypes[$errType],   // friendly string version of 'type'
            'category'  => $category,
            'message'   => $errMsg,
            'file'      => $file,
            'line'      => $line,
            'vars'      => $vars,
            'backtrace' => array(), // only for fatal type errors, and only if xdebug is enabled
            'continueToNormal' => false,    // aka, let PHP do its thing (log error)
            'continueToPrevHandler' => $this->cfg['continueToPrevHandler'] && $this->prevErrorHandler,
            'exception' => $this->uncaughtException,  // non-null if error is uncaught-exception
            'hash'          => null,
            'isFirstOccur'  => true,
            'isHtml'        => \filter_var(\ini_get('html_errors'), FILTER_VALIDATE_BOOLEAN)
                && !\in_array($errType, $this->userErrors) && !$this->uncaughtException,
            'isSuppressed'  => false,
        );
        $hash = $this->errorHash($errorValues);
        $isFirstOccur = !isset($this->data['errors'][$hash]);
        // if any instance of this error was not supprssed, reflect that
        if ($errorValues['isHtml']) {
            $errorValues['message'] = \str_replace('<a ', '<a target="phpRef" ', $errorValues['message']);
        }
        $isSuppressed = !$isFirstOccur && !$this->data['errors'][$hash]['isSuppressed']
            ? false
            : \error_reporting() === 0;
        if (!empty($this->data['errorCaller'])) {
            $errorValues['file'] = $this->data['errorCaller']['file'];
            $errorValues['line'] = $this->data['errorCaller']['line'];
        }
        if (\in_array($errType, array(E_ERROR, E_USER_ERROR))) {
            // will return empty unless xdebug extension installed/enabled
            $errorValues['backtrace'] = $this->backtrace($errorValues);
        }
        $errorValues = \array_merge($errorValues, array(
            'continueToNormal' => !$isSuppressed && $isFirstOccur,
            'hash' => $hash,
            'isFirstOccur' => $isFirstOccur,
            'isSuppressed' => $isSuppressed,
        ));
        return new Event($this, $errorValues);
    }

    /**
     * Generate hash used to uniquely identify this error
     *
     * @param array $errorValues error array
     *
     * @return string hash
     */
    protected function errorHash($errorValues)
    {
        $errMsg = $errorValues['message'];
        // (\(.*?)\d+(.*?\))    "(tried to allocate 16384 bytes)" -> "(tried to allocate xxx bytes)"
        $errMsg = \preg_replace('/(\(.*?)\d+(.*?\))/', '\1x\2', $errMsg);
        // "blah123" -> "blahxxx"
        $errMsg = \preg_replace('/\b([a-z]+\d+)+\b/', 'xxx', $errMsg);
        // "-123.123" -> "xxx"
        $errMsg = \preg_replace('/\b[\d.-]{4,}\b/', 'xxx', $errMsg);
        // remove "comments"..  this allows throttling email, while still adding unique info to user errors
        $errMsg = \preg_replace('/\s*##.+$/', '', $errMsg);
        $hash = \md5($errorValues['file'].$errorValues['line'].$errorValues['type'].$errMsg);
        return $hash;
    }

    /**
     * Handle E_USER_ERROR
     *
     * Should script terminate, or continue?
     *
     * @param Event $error errorHandler.error event
     *
     * @return void
     */
    protected function onUserError(Event $error)
    {
        switch ($this->cfg['onEUserError']) {
            case 'continue':
                $error['continueToNormal'] = false;
                break;
            case 'log':
                // log the error, but continue script
                if (!$error->isPropagationStopped() && $error['continueToNormal']) {
                    $this->log($error);
                }
                $error['continueToNormal'] = false;
                break;
            case 'normal':
                // force continueToNormal
                $error['continueToNormal'] = true;
                break;
            default:
                /*
                    no special consideration
                    unless errorHandler.error subscriber changes `continueToNormal` value,
                    script will be halted
                */
        }
    }
}