diff --git a/README.md b/README.md index 7be7b20..1455554 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ class Amazeballs { public function doStuff() { throw \Glitch::{'ENotFound,EFailedService'}( - 'Server "doStuff" cannot be found' + 'Service "doStuff" cannot be found' ); } } @@ -115,7 +115,7 @@ composer require decodelabs/glitch Register base paths for easier reading of file names ```php -\Glitch\PathHandler::registerAlias('app', '/path/to/my/app'); +\Glitch\Context::getDefault()->registerPathAlias('app', '/path/to/my/app'); /* /path/to/my/app/models/MyModel.php @@ -129,7 +129,7 @@ becomes ### Usage -Throw Glitches rather than Exceptions, passing mixed in interfaces as the method name (error interfaces must begin with E) +Throw Glitches rather than Exceptions, passing mixed in interfaces as the method name (generated error interfaces must begin with E) ```php throw \Glitch::EOutOfBounds('This is out of bounds'); @@ -139,13 +139,31 @@ throw \Glitch::{'ENotFound,EBadMethodCall'}( ); // You can associate a http code too.. -throw \Glitch::ECompletelyMadeUpMeaning([ - 'message' => 'My message', +throw \Glitch::ECompletelyMadeUpMeaning('My message', [ 'code' => 1234, 'http' => 501 ]); + +throw \Glitch::{'EInvalidArgument,Psr\\Cache\\InvalidArgumentException'}( + 'Cache items must implement Cache\\IItem', + ['http' => 500], // params + $item // data +); ``` +Catch a Glitch in the normal way using whichever scope you require: + +```php +try { + throw \Glitch::{'ENotFound,EBadMethodCall'}( + 'Didn\'t find a thing, couldn\'t call the other thing' + ); +} catch(\EGlitch | \ENotFound | MyLibrary\EGlitch | MyLibrary\AThingThatDoesStuff\EBadMethodCall $e) { + // All these types will catch +} +``` + + ## Licensing Glitch is licensed under the MIT License. See [LICENSE](https://github.com/decodelabs/glitch/blob/master/LICENSE) for the full license text. diff --git a/composer.json b/composer.json index 130d0c7..f47f39c 100644 --- a/composer.json +++ b/composer.json @@ -10,8 +10,11 @@ } ], "require": { - "php": "^7.1.3", - "decodelabs/df-base": "dev-develop" + "php": "^7.2", + "symfony/polyfill-mbstring": "~1.7", + + "components/jquery": "~3.3", + "components/bootstrap": "~4.3" }, "autoload": { "psr-4": { diff --git a/src/DecodeLabs/Glitch/Context.php b/src/DecodeLabs/Glitch/Context.php new file mode 100644 index 0000000..96c7bb2 --- /dev/null +++ b/src/DecodeLabs/Glitch/Context.php @@ -0,0 +1,459 @@ +startTime = microtime(true); + $this->pathAliases['glitch'] = dirname(__DIR__); + + $this->registerStatGatherer('default', [$this, 'gatherDefaultStats']); + } + + + + /** + * Set active run mode + */ + public function setRunMode(string $mode): Context + { + switch ($mode) { + case 'production': + case 'testing': + case 'development': + $this->runMode = $mode; + break; + + default: + throw \Glitch::EInvalidArgument('Invalid run mode', null, $mode); + } + + return $this; + } + + /** + * Get current run mode + */ + public function getRunMode(): string + { + return $this->runMode; + } + + /** + * Is Glitch in development mode? + */ + public function isDevelopment(): bool + { + return $this->runMode == 'development'; + } + + /** + * Is Glitch in testing mode? + */ + public function isTesting(): bool + { + return $this->runMode == 'testing' + || $this->runMode == 'development'; + } + + /** + * Is Glitch in production mode? + */ + public function isProduction(): bool + { + return $this->runMode == 'production'; + } + + + + + /** + * Send variables to dump, carry on execution + */ + public function dump(array $values, int $rewind=null): void + { + $trace = Trace::create($rewind + 1); + $inspector = new Inspector($this); + $dump = new Dump($trace); + + foreach ($this->statGatherers as $gatherer) { + $gatherer($dump, $this); + } + + foreach ($values as $value) { + $dump->addEntity($inspector($value)); + } + + $dump->setTraceEntity($inspector($trace, function ($entity) { + $entity->setOpen(false); + })); + + $packet = $this->getDumpRenderer()->render($dump, true); + $this->getTransport()->sendDump($packet); + } + + /** + * Send variables to dump, exit and render + */ + public function dumpDie(array $values, int $rewind=null): void + { + $this->dump($values, $rewind + 1); + exit(1); + } + + + + /** + * Quit a stubbed method + */ + public function incomplete($data=null, int $rewind=null): void + { + $frame = Frame::create($rewind + 1); + + throw \Glitch::EImplementation( + $frame->getSignature().' has not been implemented yet', + null, + $data + ); + } + + + + /** + * Log an exception... somewhere :) + */ + public function logException(\Throwable $e): void + { + // TODO: put this somewhere + } + + + + /** + * Override app start time + */ + public function setStartTime(float $time): Context + { + $this->startTime = $time; + return $this; + } + + /** + * Get app start time + */ + public function getStartTime(): float + { + return $this->startTime; + } + + + + + + /** + * Register path replacement alias + */ + public function registerPathAlias(string $name, string $path): Context + { + $this->pathAliases[$name] = $path; + + uasort($this->pathAliases, function ($a, $b) { + return strlen($b) - strlen($a); + }); + + return $this; + } + + /** + * Register list of path replacement aliases + */ + public function registerPathAliases(array $aliases): Context + { + foreach ($aliases as $name => $path) { + $this->pathAliases[$name] = $path; + } + + uasort($this->pathAliases, function ($a, $b) { + return strlen($b) - strlen($a); + }); + + return $this; + } + + /** + * Inspect list of registered path aliases + */ + public function getPathAliases(): array + { + return $this->pathAliases; + } + + /** + * Lookup and replace path prefix + */ + public function normalizePath(string $path): string + { + $path = str_replace('\\', '/', $path); + + foreach ($this->pathAliases as $name => $test) { + if (0 === strpos($path, $test)) { + return $name.'://'.ltrim(substr($path, strlen($test)), '/'); + } + } + + return $path; + } + + + + /** + * Register stat gatherer + */ + public function registerStatGatherer(string $name, callable $gatherer): Context + { + $this->statGatherers[$name] = $gatherer; + return $this; + } + + /** + * Get stat gatherers + */ + public function getStatGatherers(): array + { + return $this->statGatherers; + } + + /** + * Default stat gatherer + */ + public function gatherDefaultStats(Dump $dump, Context $context): void + { + $frame = $dump->getTrace()->getFirstFrame(); + + $dump->addStats( + // Time + (new Stat('time', 'Running time', microtime(true) - $this->getStartTime())) + ->applyClass(function ($value) { + switch (true) { + case $value > 0.1: + return 'danger'; + + case $value > 0.025: + return 'warning'; + + default: + return 'success'; + } + }) + ->setRenderer('text', function ($time) { + return self::formatMicrotime($time); + }), + + // Memory + (new Stat('memory', 'Memory usage', memory_get_usage())) + ->applyClass($memApp = function ($value) { + $mb = 1024 * 1024; + + switch (true) { + case $value > (10 * $mb): + return 'danger'; + + case $value > (5 * $mb): + return 'warning'; + + default: + return 'success'; + } + }) + ->setRenderer('text', function ($memory) { + return self::formatFilesize($memory); + }), + + // Peak memory + (new Stat('peakMemory', 'Peak memory usage', memory_get_peak_usage())) + ->applyClass($memApp) + ->setRenderer('text', function ($memory) { + return self::formatFilesize($memory); + }), + + // Location + (new Stat('location', 'Dump location', $frame)) + ->setRenderer('text', function ($frame) { + return $this->normalizePath($frame->getFile()).' : '.$frame->getLine(); + }) + ); + } + + /** + * TODO: move these to a shared location + */ + private static function formatFilesize($bytes) + { + $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']; + + for ($i = 0; $bytes > 1024; $i++) { + $bytes /= 1024; + } + + return round($bytes, 2).' '.$units[$i]; + } + + private static function formatMicrotime($time) + { + return number_format($time * 1000, 2).' ms'; + } + + + + /** + * Register callable inspector for a specific class + */ + public function registerObjectInspector(string $class, callable $inspector): Context + { + $this->objectInspectors[$class] = $inspector; + return $this; + } + + /** + * Get list of registered inspectors + */ + public function getObjectInspectors(): array + { + return $this->objectInspectors; + } + + + /** + * Register callable inspector for a specific resource type + */ + public function registerResourceInspector(string $type, callable $inspector): Context + { + $this->resourceInspectors[$type] = $inspector; + return $this; + } + + /** + * Get list of registered inspectors + */ + public function getResourceInspectors(): array + { + return $this->resourceInspectors; + } + + + + + /** + * Get composer vendor path + */ + public function getVendorPath(): string + { + static $output; + + if (!isset($output)) { + $ref = new \ReflectionClass(ClassLoader::class); + $output = dirname(dirname($ref->getFileName())); + } + + return $output; + } + + + /** + * Set dump renderer + */ + public function setDumpRenderer(Renderer $renderer): Context + { + $this->dumpRenderer = $renderer; + return $this; + } + + /** + * Get dump renderer + */ + public function getDumpRenderer(): Renderer + { + if (!$this->dumpRenderer) { + $this->dumpRenderer = new Renderer\Html($this); + } + + return $this->dumpRenderer; + } + + + /** + * Set transport + */ + public function setTransport(Transport $transport): Context + { + $this->transport = $transport; + return $this; + } + + /** + * Get transport + */ + public function getTransport(): Transport + { + if (!$this->transport) { + $this->transport = new Transport\Stdout($this); + } + + return $this->transport; + } +} diff --git a/src/DecodeLabs/Glitch/Dumper/Dump.php b/src/DecodeLabs/Glitch/Dumper/Dump.php new file mode 100644 index 0000000..568aebe --- /dev/null +++ b/src/DecodeLabs/Glitch/Dumper/Dump.php @@ -0,0 +1,130 @@ +trace = $trace; + } + + + /** + * Set named statistic + */ + public function addStats(Stat ...$stats): Dump + { + foreach ($stats as $stat) { + $this->stats[$stat->getKey()] = $stat; + } + + return $this; + } + + /** + * Get named statistic + */ + public function getStat(string $key): ?Stat + { + return $this->stats[$name] ?? null; + } + + /** + * Remove named statistic + */ + public function removeStat(string $key): Dump + { + unset($this->stats[$key]); + return $this; + } + + /** + * Get all named statistics + */ + public function getStats(): array + { + return $this->stats; + } + + /** + * Clear all named statistics + */ + public function clearStats(): Dump + { + $this->stats = []; + return $this; + } + + + /** + * Get active trace + */ + public function getTrace(): Trace + { + return $this->trace; + } + + + /** + * Add an entity to the list + */ + public function addEntity($entity): Dump + { + $this->entities[] = $entity; + return $this; + } + + /** + * Get list of entities + */ + public function getEntities(): array + { + return $this->entities; + } + + + /** + * Set trace entity + */ + public function setTraceEntity(Entity $entity): Dump + { + $this->traceEntity = $entity; + return $this; + } + + /** + * Get trace entity + */ + public function getTraceEntity(): ?Entity + { + return $this->traceEntity; + } + + + /** + * Loop all entities + */ + public function getIterator() + { + return new ArrayIterator($this->entities); + } +} diff --git a/src/DecodeLabs/Glitch/Dumper/Entity.php b/src/DecodeLabs/Glitch/Dumper/Entity.php new file mode 100644 index 0000000..ead3364 --- /dev/null +++ b/src/DecodeLabs/Glitch/Dumper/Entity.php @@ -0,0 +1,512 @@ +type = $type; + $this->id = str_replace('.', '-', uniqid($type.'-', true)); + } + + /** + * Static entity type name + */ + public function getType(): string + { + return $this->type; + } + + + + /** + * Set entity instance name + */ + public function setName(?string $name): Entity + { + $this->name = $name; + return $this; + } + + /** + * Get entity instance name + */ + public function getName(): ?string + { + return $this->name; + } + + + /** + * Set default open state + */ + public function setOpen(bool $open): Entity + { + $this->open = $open; + return $this; + } + + /** + * Get default open state + */ + public function isOpen(): bool + { + return $this->open; + } + + + /** + * Set object id + */ + public function setId(?string $id): Entity + { + $this->id = $id; + return $this; + } + + + /** + * Get object id + */ + public function getId(): ?string + { + return $this->id; + } + + + /** + * Set object id + */ + public function setObjectId(?int $id): Entity + { + $this->objectId = $id; + return $this; + } + + /** + * Get object id + */ + public function getObjectId(): ?int + { + return $this->objectId; + } + + /** + * Set object / array hash + */ + public function setHash(?string $hash): Entity + { + $this->hash = $hash; + return $this; + } + + /** + * Get object / array hash + */ + public function getHash(): ?string + { + return $this->hash; + } + + + /** + * Set object class + */ + public function setClass(?string $class): Entity + { + $this->class = $class; + return $this; + } + + /** + * Get object class + */ + public function getClass(): ?string + { + return $this->class; + } + + + /** + * Set parent classes + */ + public function setParentClasses(string ...$parents): Entity + { + if (empty($parents)) { + $parents = null; + } + + $this->parents = $parents; + return $this; + } + + /** + * Get parent classes + */ + public function getParentClasses(): ?array + { + return $this->parents; + } + + + /** + * Set interfaces + */ + public function setInterfaces(string ...$interfaces): Entity + { + if (empty($interfaces)) { + $interfaces = null; + } + + $this->interfaces = $interfaces; + return $this; + } + + /** + * Get interfaces + */ + public function getInterfaces(): ?array + { + return $this->interfaces; + } + + + /** + * Set traits + */ + public function setTraits(string ...$traits): Entity + { + if (empty($traits)) { + $traits = null; + } + + $this->traits = $traits; + return $this; + } + + /** + * Get traits + */ + public function getTraits(): ?array + { + return $this->traits; + } + + + + /** + * Set source file + */ + public function setFile(?string $file): Entity + { + $this->file = $file; + return $this; + } + + /** + * Get source file + */ + public function getFile(): ?string + { + return $this->file; + } + + /** + * Set source line + */ + public function setStartLine(?int $line): Entity + { + $this->startLine = $line; + return $this; + } + + /** + * Get source line + */ + public function getStartLine(): ?int + { + return $this->startLine; + } + + /** + * Set source end line + */ + public function setEndLine(?int $line): Entity + { + $this->endLine = $line; + return $this; + } + + /** + * Get source end line + */ + public function getEndLine(): ?int + { + return $this->endLine; + } + + + + + /** + * Set object text + */ + public function setText(?string $text): Entity + { + $this->text = $text; + return $this; + } + + /** + * Get object text + */ + public function getText(): ?string + { + return $this->text; + } + + + + /** + * Set definition code + */ + public function setDefinition(?string $definition): Entity + { + $this->definition = $definition; + return $this; + } + + /** + * Get definition code + */ + public function getDefinition(): ?string + { + return $this->definition; + } + + + + /** + * Set item length + */ + public function setLength(?int $length): Entity + { + $this->length = $length; + return $this; + } + + /** + * Get item length + */ + public function getLength(): ?int + { + return $this->length; + } + + + + + /** + * Set meta value + */ + public function setMeta(string $key, $value): Entity + { + $this->checkValidity($value); + + $this->meta[$key] = $value; + return $this; + } + + /** + * Get meta value + */ + public function getMeta(string $key) + { + return $this->meta[$key] ?? null; + } + + /** + * Get all meta data + */ + public function getAllMeta(): ?array + { + return $this->meta; + } + + + + /** + * Set values list + */ + public function setValues(?array $values): Entity + { + if ($values !== null) { + foreach ($values as $value) { + $this->checkValidity($value); + } + } + + $this->values = $values; + return $this; + } + + /** + * Get values list + */ + public function getValues(): ?array + { + return $this->values; + } + + + /** + * Set show keys + */ + public function setShowKeys(bool $show): Entity + { + $this->showValueKeys = $show; + return $this; + } + + /** + * Should show values keys? + */ + public function shouldShowKeys(): bool + { + return $this->showValueKeys; + } + + + + /** + * Set properties + */ + public function setProperties(array $properties): Entity + { + foreach ($properties as $key => $value) { + $this->setProperty($key, $value); + } + + return $this; + } + + /** + * Get properties + */ + public function getProperties(): ?array + { + return $this->properties; + } + + + /** + * Set property + */ + public function setProperty(string $key, $value): Entity + { + $this->checkValidity($value); + $this->properties[$key] = $value; + return $this; + } + + /** + * Get property + */ + public function getProperty(string $key) + { + return $this->properties[$key] ?? null; + } + + /** + * Has property + */ + public function hasProperty(string $key): bool + { + if (empty($this->properties)) { + return false; + } + + return array_key_exists($key, $this->properties); + } + + + + /** + * Set stack trace + */ + public function setStackTrace(Trace $trace): Entity + { + $this->stackTrace = $trace; + return $this; + } + + /** + * Get stack trace + */ + public function getStackTrace(): ?Trace + { + return $this->stackTrace; + } + + + + /** + * Check value for Entity validity + */ + protected function checkValidity($value): void + { + switch (true) { + case $value === null: + case is_bool($value): + case is_int($value): + case is_float($value): + case is_string($value): + case $value instanceof Entity: + return; + + default: + throw \Glitch::EUnexpectedValue( + 'Invalid sub-entity type - must be scalar or Entity', + null, + $value + ); + } + } +} diff --git a/src/DecodeLabs/Glitch/Dumper/Inspect/Core.php b/src/DecodeLabs/Glitch/Dumper/Inspect/Core.php new file mode 100644 index 0000000..f643b2f --- /dev/null +++ b/src/DecodeLabs/Glitch/Dumper/Inspect/Core.php @@ -0,0 +1,58 @@ +setDefinition(Reflection::getFunctionDefinition($reflection)) + ->setFile($reflection->getFileName()) + ->setStartLine($reflection->getStartLine()) + ->setEndLine($reflection->getEndLine()); + } + + /** + * Inspect Generator + */ + public static function inspectGenerator(\Generator $generator, Entity $entity, Inspector $inspector): void + { + try { + $reflection = new \ReflectionGenerator($generator); + } catch (\Exception $e) { + return; + } + + $function = $reflection->getFunction(); + + $entity + ->setDefinition(Reflection::getFunctionDefinition($function)) + ->setFile($function->getFileName()) + ->setStartLine($function->getStartLine()) + ->setEndLine($function->getEndLine()); + } + + /** + * Inspect __PHP_Incomplete_Class + */ + public static function inspectIncompleteClass(\__PHP_Incomplete_Class $class, Entity $entity, Inspector $inspector): void + { + $vars = (array)$class; + $entity->setDefinition($vars['__PHP_Incomplete_Class_Name']); + unset($vars['__PHP_Incomplete_Class_Name']); + $entity->setValues($inspector->inspectValues($vars)); + } +} diff --git a/src/DecodeLabs/Glitch/Dumper/Inspect/Curl.php b/src/DecodeLabs/Glitch/Dumper/Inspect/Curl.php new file mode 100644 index 0000000..6608e4b --- /dev/null +++ b/src/DecodeLabs/Glitch/Dumper/Inspect/Curl.php @@ -0,0 +1,23 @@ + $value) { + $entity->setMeta($key, $inspector->inspectValue($value)); + } + } +} diff --git a/src/DecodeLabs/Glitch/Dumper/Inspect/Dba.php b/src/DecodeLabs/Glitch/Dumper/Inspect/Dba.php new file mode 100644 index 0000000..11b3802 --- /dev/null +++ b/src/DecodeLabs/Glitch/Dumper/Inspect/Dba.php @@ -0,0 +1,22 @@ +setMeta('file', $inspector->inspectValue($list[(int)$resource])); + } +} diff --git a/src/DecodeLabs/Glitch/Dumper/Inspect/Gd.php b/src/DecodeLabs/Glitch/Dumper/Inspect/Gd.php new file mode 100644 index 0000000..7f168fe --- /dev/null +++ b/src/DecodeLabs/Glitch/Dumper/Inspect/Gd.php @@ -0,0 +1,33 @@ +setMeta('width', $inspector->inspectValue(imagesx($resource))) + ->setMeta('height', $inspector->inspectValue(imagesy($resource))); + } + + /** + * Inspect GD font resource + */ + public static function inspectGdFont($resource, Entity $entity, Inspector $inspector): void + { + $entity + ->setMeta('width', $inspector->inspectValue(imagesfontwidth($resource))) + ->setMeta('height', $inspector->inspectValue(imagesfontheight($resource))); + } +} diff --git a/src/DecodeLabs/Glitch/Dumper/Inspect/Process.php b/src/DecodeLabs/Glitch/Dumper/Inspect/Process.php new file mode 100644 index 0000000..044fe9c --- /dev/null +++ b/src/DecodeLabs/Glitch/Dumper/Inspect/Process.php @@ -0,0 +1,23 @@ + $value) { + $entity->setMeta($key, $inspector->inspectValue($value)); + } + } +} diff --git a/src/DecodeLabs/Glitch/Dumper/Inspect/Reflection.php b/src/DecodeLabs/Glitch/Dumper/Inspect/Reflection.php new file mode 100644 index 0000000..ea8b840 --- /dev/null +++ b/src/DecodeLabs/Glitch/Dumper/Inspect/Reflection.php @@ -0,0 +1,311 @@ +setDefinition(Reflection::getClassDefinition($reflection)); + + if (!$reflection->isInternal()) { + $entity + ->setFile($reflection->getFileName()) + ->setStartLine($reflection->getStartLine()) + ->setEndLine($reflection->getEndLine()); + } + } + + /** + * Inspect ReflectionClassConstant + */ + public static function inspectReflectionClassConstant(\ReflectionClassConstant $reflection, Entity $entity, Inspector $inspector): void + { + $entity + ->setDefinition(Reflection::getConstantDefinition($reflection)) + ->setProperties([ + 'class' => $reflection->class + ]); + } + + /** + * Inspect ReflectionZendExtension + */ + public static function inspectReflectionZendExtension(\ReflectionZendExtension $reflection, Entity $entity, Inspector $inspector): void + { + $entity->setProperties($inspector->inspectValues([ + 'version' => $reflection->getVersion(), + 'author' => $reflection->getAuthor(), + 'copyright' => $reflection->getCopyright(), + 'url' => $reflection->getURL() + ])); + } + + /** + * Inspect ReflectionExtension + */ + public static function inspectReflectionExtension(\ReflectionExtension $reflection, Entity $entity, Inspector $inspector): void + { + $entity->setProperties($inspector->inspectValues([ + 'version' => $reflection->getVersion(), + 'dependencies' => $reflection->getDependencies(), + 'iniEntries' => $reflection->getIniEntries(), + 'isPersistent' => $reflection->isPersistent(), + 'isTemporary' => $reflection->isTemporary(), + 'constants' => $reflection->getConstants(), + 'functions' => $reflection->getFunctions(), + 'classes' => $reflection->getClasses() + ])); + } + + /** + * Inspect ReflectionFunction + */ + public static function inspectReflectionFunction(\ReflectionFunctionAbstract $reflection, Entity $entity, Inspector $inspector): void + { + $entity + ->setDefinition(Reflection::getFunctionDefinition($reflection)) + ->setFile($reflection->getFileName()) + ->setStartLine($reflection->getStartLine()) + ->setEndLine($reflection->getEndLine()); + } + + /** + * Inspect ReflectionMethod + */ + public static function inspectReflectionMethod(\ReflectionMethod $reflection, Entity $entity, Inspector $inspector): void + { + self::inspectReflectionFunction($reflection, $entity, $inspector); + + $entity->setProperties([ + 'class' => $reflection->getDeclaringClass()->getName() + ]); + } + + /** + * Inspect ReflectionParameter + */ + public static function inspectReflectionParameter(\ReflectionParameter $reflection, Entity $entity, Inspector $inspector): void + { + $entity->setDefinition(self::getParameterDefinition($reflection)); + } + + /** + * Inspect ReflectionProperty + */ + public static function inspectReflectionProperty(\ReflectionProperty $reflection, Entity $entity, Inspector $inspector): void + { + $entity + ->setDefinition(self::getPropertyDefinition($reflection)) + ->setProperties([ + 'class' => $reflection->getDeclaringClass()->getName() + ]); + } + + /** + * Inspect ReflectionType + */ + public static function inspectReflectionType(\ReflectionType $reflection, Entity $entity, Inspector $inspector): void + { + $entity->setProperties([ + 'name' => $reflection->getName(), + 'allowsNull' => $reflection->allowsNull(), + 'isBuiltin' => $reflection->isBuiltin() + ]); + } + + /** + * Inspect ReflectionGenerator + */ + public static function inspectReflectionGenerator(\ReflectionGenerator $reflection, Entity $entity, Inspector $inspector): void + { + $function = $reflection->getFunction(); + + $entity + ->setDefinition(Reflection::getFunctionDefinition($function)) + ->setFile($function->getFileName()) + ->setStartLine($function->getStartLine()) + ->setEndLine($function->getEndLine()); + } + + + + + /** + * Export class definitoin + */ + public static function getClassDefinition(\ReflectionClass $reflection): string + { + $output = 'class '; + $name = $reflection->getName(); + + if (0 === strpos($name, "class@anonymous\x00")) { + $output .= '() '; + } else { + $output .= $name.' '; + } + + if ($parent = $reflection->getParentClass()) { + $output .= 'extends '.$parent->getName(); + } + + $interfaces = []; + + foreach ($reflection->getInterfaces() as $interface) { + $interfaces[] = $interface->getName(); + } + + if (!empty($interfaces)) { + $output .= 'implements '.implode(', ', $interfaces).' '; + } + + $output .= '{'."\n"; + + foreach ($reflection->getReflectionConstants() as $const) { + $output .= ' '.self::getConstantDefinition($const)."\n"; + } + + foreach ($reflection->getProperties() as $property) { + $output .= ' '.self::getPropertyDefinition($property)."\n"; + } + + foreach ($reflection->getMethods() as $method) { + $output .= ' '.self::getFunctionDefinition($method)."\n"; + } + + $output .= '}'; + + return $output; + } + + + /** + * Export property definition + */ + public static function getPropertyDefinition(\ReflectionProperty $reflection): string + { + $output = implode(' ', \Reflection::getModifierNames($reflection->getModifiers())); + $name = $reflection->getName(); + $output .= ' $'.$name.' = '; + $reflection->setAccessible(true); + $props = $reflection->getDeclaringClass()->getDefaultProperties(); + $value = $prop[$name] ?? null; + + if (is_array($value)) { + $output .= '[...]'; + } else { + $output .= Inspector::scalarToString($value); + } + + return $output; + } + + + /** + * Export class constant definition + */ + public static function getConstantDefinition(\ReflectionClassConstant $reflection): string + { + $output = implode(' ', \Reflection::getModifierNames($reflection->getModifiers())); + $output .= ' const '.$reflection->getName().' = '; + $value = $reflection->getValue(); + + if (is_array($value)) { + $output .= '[...]'; + } else { + $output .= Inspector::scalarToString($value); + } + + return $output; + } + + + /** + * Export function definition + */ + public static function getFunctionDefinition(\ReflectionFunctionAbstract $reflection): string + { + $output = ''; + + if ($reflection instanceof \ReflectionMethod) { + $output = implode(' ', \Reflection::getModifierNames($reflection->getModifiers())); + + if (!empty($output)) { + $output .= ' '; + } + } + + $output .= 'function '; + + if ($reflection->returnsReference()) { + $output .= '& '; + } + + if (!$reflection->isClosure()) { + $output .= $reflection->getName().' '; + } + + $output .= '('; + $params = []; + + foreach ($reflection->getParameters() as $parameter) { + $params[] = self::getParameterDefinition($parameter); + } + + $output .= implode(', ', $params).')'; + + if ($returnType = $reflection->getReturnType()) { + $output .= ': '; + + if ($returnType->allowsNull()) { + $output .= '?'; + } + + $output .= $returnType->getName(); + } + + return $output; + } + + /** + * Export parameter definition + */ + public static function getParameterDefinition(\ReflectionParameter $parameter): string + { + $output = ''; + + if ($parameter->allowsNull()) { + $output .= '?'; + } + + if ($type = $parameter->getType()) { + $output .= $type->getName().' '; + } + + if ($parameter->isPassedByReference()) { + $output .= '& '; + } + + if ($parameter->isVariadic()) { + $output .= '...'; + } + + $output .= '$'.$parameter->getName(); + + if ($parameter->isDefaultValueAvailable()) { + $output .= '='.(Inspector::scalarToString($parameter->getDefaultValue()) ?? '??'); + } + + return $output; + } +} diff --git a/src/DecodeLabs/Glitch/Dumper/Inspect/Spl.php b/src/DecodeLabs/Glitch/Dumper/Inspect/Spl.php new file mode 100644 index 0000000..1897fbd --- /dev/null +++ b/src/DecodeLabs/Glitch/Dumper/Inspect/Spl.php @@ -0,0 +1,11 @@ + $value) { + $entity->setMeta($key, $inspector->inspectValue($value)); + } + + self::inspectStreamContext($resource, $entity, $inspector); + } + + /** + * Inspect stream context resource + */ + public static function inspectStreamContext($resource, Entity $entity, Inspector $inspector): void + { + if (!$params = @stream_context_get_params($resource)) { + return; + } + + foreach ($params as $key => $value) { + $entity->setMeta($key, $inspector->inspectValue($value)); + } + } +} diff --git a/src/DecodeLabs/Glitch/Dumper/Inspect/Xml.php b/src/DecodeLabs/Glitch/Dumper/Inspect/Xml.php new file mode 100644 index 0000000..657eeaa --- /dev/null +++ b/src/DecodeLabs/Glitch/Dumper/Inspect/Xml.php @@ -0,0 +1,25 @@ +setMeta('current_byte_index', $inspector->inspectValue(xml_get_current_byte_index($resource))) + ->setMeta('current_column_number', $inspector->inspectValue(xml_get_current_column_number($resource))) + ->setMeta('current_line_number', $inspector->inspectValue(xml_get_current_line_number($resource))) + ->setMeta('error_code', $inspector->inspectValue(xml_get_error_code($resource))); + } +} diff --git a/src/DecodeLabs/Glitch/Dumper/Inspector.php b/src/DecodeLabs/Glitch/Dumper/Inspector.php new file mode 100644 index 0000000..535cd68 --- /dev/null +++ b/src/DecodeLabs/Glitch/Dumper/Inspector.php @@ -0,0 +1,589 @@ + [Inspect\Core::class, 'inspectClosure'], + 'Generator' => [Inspect\Core::class, 'inspectGenerator'], + '__PHP_Incomplete_Class' => [Inspect\Core::class, 'inspectIncompleteClass'], + + // Reflection + 'ReflectionClass' => [Inspect\Reflection::class, 'inspectReflectionClass'], + 'ReflectionClassConstant' => [Inspect\Reflection::class, 'inspectReflectionClassConstant'], + 'ReflectionZendExtension' => [Inspect\Reflection::class, 'inspectReflectionZendExtension'], + 'ReflectionExtension' => [Inspect\Reflection::class, 'inspectReflectionExtension'], + 'ReflectionFunction' => [Inspect\Reflection::class, 'inspectReflectionFunction'], + 'ReflectionFunctionAbstract' => [Inspect\Reflection::class, 'inspectReflectionFunction'], + 'ReflectionMethod' => [Inspect\Reflection::class, 'inspectReflectionMethod'], + 'ReflectionParameter' => [Inspect\Reflection::class, 'inspectReflectionParameter'], + 'ReflectionProperty' => [Inspect\Reflection::class, 'inspectReflectionProperty'], + 'ReflectionType' => [Inspect\Reflection::class, 'inspectReflectionType'], + 'ReflectionGenerator' => [Inspect\Reflection::class, 'inspectReflectionGenerator'], + ]; + + const RESOURCES = [ + // Bzip + 'bzip2' => null, + + // Cubrid + 'cubrid connection' => null, + 'persistent cubrid connection' => null, + 'cubrid request' => null, + 'cubrid lob' => null, + 'cubrid lob2' => null, + + // Curl + 'curl' => [Inspect\Curl::class, 'inspectCurl'], + + // Dba + 'dba' => [Inspect\Dba::class, 'inspectDba'], + 'dba persistent' => [Inspect\Dba::class, 'inspectDba'], + + // Dbase + 'dbase' => null, + + // DBX + 'dbx_link_object' => null, + 'dbx_result_object' => null, + + // Firebird + 'fbsql link' => null, + 'fbsql plink' => null, + 'fbsql result' => null, + + // FDF + 'fdf' => null, + + // FTP + 'ftp' => null, + + // GD + 'gd' => [Inspect\Gd::class, 'inspectGd'], + 'gd font' => [Inspect\Gd::class, 'inspectGdFont'], + + // Imap + 'imap' => null, + + // Ingres + 'ingres' => null, + 'ingres persistent' => null, + + // Interbase + 'interbase link' => null, + 'interbase link persistent' => null, + 'interbase query' => null, + 'interbase result' => null, + + // Ldap + 'ldap link' => null, + 'ldap result' => null, + + // mSQL + 'msql link' => null, + 'msql link persistent' => null, + 'msql query' => null, + + // msSQL + 'mssql link' => null, + 'mssql link persistent' => null, + 'mssql result' => null, + + // Oci8 + 'oci8 collection' => null, + 'oci8 connection' => null, + 'oci8 lob' => null, + 'oci8 statement' => null, + + // Odbc + 'odbc link' => null, + 'odbc link persistent' => null, + 'odbc result' => null, + + // OpenSSL + 'OpenSSL key' => null, + 'OpenSSL X.509' => null, + + // PDF + 'pdf document' => null, + 'pdf image' => null, + 'pdf object' => null, + 'pdf outline' => null, + + // PgSQL + 'pgsql large object' => null, + 'pgsql link' => null, + 'pgsql link persistent' => null, + 'pgsql result' => null, + + // Process + 'process' => [Inspect\Process::class, 'inspectProcess'], + + // Pspell + 'pspell' => null, + 'pspell config' => null, + + // Shmop + 'shmop' => null, + + // Stream + 'stream' => [Inspect\Stream::class, 'inspectStream'], + + // Socket + 'socket' => null, + + // Sybase + 'sybase-db link' => null, + 'sybase-db link persistent' => null, + 'sybase-db result' => null, + 'sybase-ct link' => null, + 'sybase-ct link persistent' => null, + 'sybase-ct result' => null, + + // Sysv + 'sysvsem' => null, + 'sysvshm' => null, + + // Wddx + 'wddx' => null, + + // Xml + 'xml' => [Inspect\Xml::class, 'inspectXmlResource'], + + // Zlib + 'zlib' => null, + 'zlib.deflate' => null, + 'zlib.inflate' => null + ]; + + protected $objectInspectors = []; + protected $resourceInspectors = []; + + protected $objectRefs = []; + protected $arrayRefs = []; + protected $arrayIds = []; + + + /** + * Construct with context to generate object inspectors + */ + public function __construct(Context $context) + { + foreach (static::OBJECTS as $class => $inspector) { + if ($inspector !== null) { + $this->objectInspectors[$class] = $inspector; + } + } + + foreach (static::RESOURCES as $type => $inspector) { + if ($inspector !== null) { + $this->resourceInspectors[$type] = $inspector; + } + } + + foreach ($context->getObjectInspectors() as $class => $inspector) { + $this->objectInspectors[$class] = $inspector; + } + + foreach ($context->getResourceInspectors() as $type => $inspector) { + $this->resourceInspectors[$type] = $inspector; + } + } + + + /** + * Inspect and report + */ + public function inspect($value, callable $entityCallback=null) + { + $output = $this->inspectValue($value); + + if ($output instanceof Entity && $entityCallback) { + $entityCallback($output, $value, $this); + } + + return $output; + } + + /** + * Invoke wrapper + */ + public function __invoke($value, callable $entityCallback=null) + { + return $this->inspect($value, $entityCallback); + } + + + /** + * Inspect single value + */ + public function inspectValue($value) + { + switch (true) { + case $value === null: + case is_bool($value): + case is_int($value): + case is_float($value): + return $value; + + case is_string($value): + return $this->inspectString($value); + + case is_resource($value): + return $this->inspectResource($value); + + case is_array($value): + return $this->inspectArray($value); + + case is_object($value): + return $this->inspectObject($value); + + default: + throw \Glitch::EUnexpectedValue( + 'Unknown entity type', + null, + $value + ); + } + } + + /** + * Inspect values list + */ + public function inspectValues(array $values): array + { + $output = []; + + foreach ($values as $key => $value) { + $output[$key] = $this->inspectValue($value); + } + + return $output; + } + + + /** + * Convert string into Entity + */ + public function inspectString(string $string) + { + // Binary string + if ($string !== '' && !preg_match('//u', $string)) { + return (new Entity('binary')) + ->setName('Binary') + ->setText(bin2hex($string)) + ->setLength(strlen($string)); + + // Class name + } elseif (class_exists($string)) { + return (new Entity('class')) + ->setClass($string); + + // Interface name + } elseif (interface_exists($string)) { + return (new Entity('interface')) + ->setClass($string); + + // Trait name + } elseif (trait_exists($string)) { + return (new Entity('trait')) + ->setClass($string); + + + // Standard string + } else { + return $string; + } + } + + /** + * Convert resource into Entity + */ + public function inspectResource($resource): Entity + { + $entity = (new Entity('resource')) + ->setName((string)$resource) + ->setClass($rType = get_resource_type($resource)); + + $typeName = str_replace(' ', '', ucwords($rType)); + $method = 'inspect'.ucfirst($typeName).'Resource'; + + if (isset($this->resourceInspectors[$rType])) { + call_user_func($this->resourceInspectors[$rType], $resource, $entity, $this); + } + + return $entity; + } + + + + /** + * Convert array into Entity + */ + public function inspectArray(array $array): ?Entity + { + $hash = $this->hashArray($array); + $isRef = $hash !== null && isset($this->arrayRefs[$hash]); + + $entity = (new Entity($isRef ? 'arrayReference' : 'array')) + ->setClass('array') + ->setLength(count($array)) + ->setHash($hash); + + if ($isRef) { + return $entity + ->setId($this->arrayRefs[$hash]) + ->setObjectId($this->arrayIds[$hash]); + } + + if ($hash !== null) { + $this->arrayRefs[$hash] = $entity->getId(); + $this->arrayIds[$hash] = $id = count($this->arrayIds) + 1; + $entity->setObjectId($id); + } + + $entity + ->setValues($this->inspectValues($array)); + + return $entity; + } + + + + /** + * Convert object into Entity + */ + public function inspectObject(object $object): ?Entity + { + $id = spl_object_id($object); + $reflection = new \ReflectionObject($object); + $className = $reflection->getName(); + $isRef = isset($this->objectRefs[$id]); + + $entity = (new Entity($isRef ? 'objectReference' : 'object')) + ->setName($this->normalizeClassName($reflection->getShortName(), $reflection)) + ->setClass($className) + ->setObjectId($id) + ->setHash(spl_object_hash($object)); + + if ($isRef) { + $entity->setId($this->objectRefs[$id]); + return $entity; + } + + $this->objectRefs[$id] = $entity->getId(); + + if (!$reflection->isInternal()) { + $entity + ->setFile($reflection->getFileName()) + ->setStartLine($reflection->getStartLine()) + ->setEndLine($reflection->getEndLine()); + } + + $parents = $this->inspectObjectParents($reflection, $entity); + + $reflections = [ + $className => $reflection + ] + $parents; + + $this->inspectObjectProperties($object, $reflections, $entity); + return $entity; + } + + /** + * Normalize virtual class name + */ + protected function normalizeClassName(string $class, \ReflectionObject $reflection): string + { + if (0 === strpos($class, "class@anonymous\x00")) { + $class = $reflection->getParentClass()->getShortName().'@anonymous'; + } + + return $class; + } + + + /** + * Inspect object parents + */ + protected function inspectObjectParents(\ReflectionObject $reflection, Entity $entity): array + { + // Parents + $reflectionBase = $reflection; + $parents = []; + + while (true) { + if (!$ref = $reflectionBase->getParentClass()) { + break; + } + + $parents[$ref->getName()] = $ref; + $reflectionBase = $ref; + } + + ksort($parents); + $interfaces = $reflection->getInterfaceNames(); + sort($interfaces); + $traits = $reflection->getTraitNames(); + sort($traits); + + $entity + ->setParentClasses(...array_keys($parents)) + ->setInterfaces(...$interfaces) + ->setTraits(...$traits); + + return $parents; + } + + /** + * Find object property provider + */ + protected function inspectObjectProperties(object $object, array $reflections, Entity $entity): void + { + $className = get_class($object); + + // Object inspector + if (isset($this->objectInspectors[$className])) { + call_user_func($this->objectInspectors[$className], $object, $entity, $this); + return; + + // Inspectable + } elseif ($object instanceof Inspectable) { + $object->glitchInspect($entity, $this); + return; + + // Debug info + } elseif (method_exists($object, '__debugInfo')) { + $entity->setValues($this->inspectValues($object->__debugInfo())); + return; + } + + + // Members + foreach (array_reverse($reflections) as $className => $reflection) { + // Parent object inspectors + if (isset($this->objectInspectors[$className])) { + call_user_func($this->objectInspectors[$className], $object, $entity, $this); + continue; + } + + // Reflection + $this->inspectClassMembers($object, $reflection, $entity); + } + } + + /** + * Inspect class members + */ + protected function inspectClassMembers(object $object, \ReflectionClass $reflection, Entity $entity): void + { + foreach ($reflection->getProperties() as $property) { + if ($property->isStatic()) { + continue; + } + + $property->setAccessible(true); + $name = $property->getName(); + $prefix = null; + $open = false; + + switch (true) { + case $property->isProtected(): + $prefix = '*'; + break; + + case $property->isPrivate(): + $prefix = '!'; + break; + + default: + $open = true; + break; + } + + $name = $prefix.$name; + + if ($entity->hasProperty($name)) { + continue; + } + + $value = $property->getValue($object); + $propValue = $this->inspectValue($value); + + if ($propValue instanceof Entity) { + $propValue->setOpen($open); + } + + $entity->setProperty($name, $propValue); + } + } + + + /** + * Convert a scalar value to a string + */ + public static function scalarToString($value): ?string + { + switch (true) { + case $value === null: + return 'null'; + + case is_bool($value): + return $value ? 'true' : 'false'; + + case is_int($value): + case is_float($value): + return (string)$value; + + case is_string($value): + return '"'.$value.'"'; + + default: + return (string)$value; + } + } + + + + /** + * Dirty way to get a hash for an array + */ + public static function hashArray(array $array): ?string + { + if (empty($array)) { + return null; + } + + $array = self::smashArray($array); + + return md5(serialize($array)); + } + + /** + * Normalize values for serialize + */ + public static function smashArray(array $array): array + { + foreach ($array as $key => $value) { + if (is_object($value)) { + $array[$key] = spl_object_id($value); + } elseif (is_array($value)) { + $array[$key] = self::smashArray($value); + } + } + + return $array; + } +} diff --git a/src/DecodeLabs/Glitch/Dumper/Renderer.php b/src/DecodeLabs/Glitch/Dumper/Renderer.php new file mode 100644 index 0000000..8cac145 --- /dev/null +++ b/src/DecodeLabs/Glitch/Dumper/Renderer.php @@ -0,0 +1,14 @@ +context = $context; + } + + + /** + * Convert Dump object to HTML string + */ + public function render(Dump $dump, bool $isFinal=false): string + { + $this->output = []; + $space = str_repeat(' ', self::SPACES); + + // Header + $this->renderHeader(); + + + // Stats + $this->output[] = '
'; + + foreach ($dump->getStats() as $key => $stat) { + $this->output[] = $space.''.$stat->render('html').''; + } + + $this->output[] = '
'; + + + // Entities + $this->output[] = '
'; + + foreach ($dump->getEntities() as $value) { + $this->output[] = ''; + + if ($value instanceof Entity) { + $this->renderEntity($value); + } else { + $this->renderScalar($value); + } + + $this->output[] = ''; + } + + + if ($traceEntity = $dump->getTraceEntity()) { + $this->output[] = ''; + $this->renderEntity($traceEntity); + $this->output[] = ''; + } + + $this->output[] = '
'; + + + // Footer + $this->output[] = ''; + $this->output[] = ''; + + $html = implode("\n", $this->output); + $this->output = []; + + + // Wrap in iframe + $id = uniqid('glitch-dump'); + + $output = []; + $output[] = ''; + $output[] = ''; + $output[] = ''; + + return implode("\n", $output); + } + + + /** + * Render scripts and styles + */ + protected function renderHeader(): void + { + $this->output[] = ''; + $this->output[] = ''; + $this->output[] = ''; + + $vendor = $this->context->getVendorPath(); + + $css = [ + //'bootstrap-reboot' => $vendor.'/components/bootstrap/css/bootstrap-reboot.min.css', + 'bootstrap' => $vendor.'/components/bootstrap/css/bootstrap.min.css', + 'glitch' => __DIR__.'/assets/dump.css' + ]; + + $js = [ + 'jQuery' => $vendor.'/components/jquery/jquery.min.js', + 'bootstrap' => $vendor.'/components/bootstrap/js/bootstrap.bundle.min.js', + 'glitch' => __DIR__.'/assets/dump.js' + ]; + + + // Meta + $this->output[] = ''; + + + // Css + foreach ($css as $name => $path) { + $this->output[] = ''; + } + + // Js + foreach ($js as $name => $path) { + $this->output[] = ''; + } + + + $output[] = ''; + $output[] = ''; + } + + + /** + * Render a scalar value + */ + protected function renderScalar($value, ?string $class=null): void + { + switch (true) { + case $value === null: + $output = $this->renderNull($class); + break; + + case is_bool($value): + $output = $this->renderBool($value, $class); + break; + + case is_int($value): + $output = $this->renderInt($value, $class); + break; + + case is_float($value): + $output = $this->renderFloat($value, $class); + break; + + case is_string($value): + $output = $this->renderString($value, $class); + break; + + default: + $output = ''; + break; + } + + $this->output[] = $output; + } + + + + /** + * Render a null scalar + */ + protected function renderNull(?string $class=null): string + { + return 'null'; + } + + /** + * Render a boolean scalar + */ + protected function renderBool(bool $value, ?string $class=null): string + { + return ''.($value ? 'true' : 'false').''; + } + + /** + * Render a integer scalar + */ + protected function renderInt(int $value, ?string $class=null): string + { + return ''.$value.''; + } + + /** + * Render a float scalar + */ + protected function renderFloat(float $value, ?string $class=null): string + { + return ''.$this->formatFloat($value).''; + } + + protected function formatFloat(float $number): string + { + $output = (string)$number; + + if (false === strpos($output, '.')) { + $output .= '.0'; + } + + return $output; + } + + + /** + * Render standard string + */ + protected function renderString(string $string, ?string $class=null): string + { + $isMultiLine = false !== strpos($string, "\n"); + + if ($class !== null) { + return ''.$this->esc($string).''; + } elseif ($isMultiLine) { + $string = str_replace("\r", '', $string); + $parts = explode("\n", $string); + $count = count($parts); + + $output = []; + $output[] = '
'.mb_strlen($string).''; + + foreach ($parts as $part) { + $output[] = '
'.$this->esc($part).'
'; + } + + $output[] = '
'; + return implode('', $output); + } else { + return ''.$this->esc($string).''.mb_strlen($string).''; + } + } + + + + /** + * Render an individual entity + */ + protected function renderEntity(Entity $entity): void + { + $id = $linkId = $entity->getId(); + $name = $this->esc($entity->getName() ?? $entity->getType()); + $showInfo = true; + $isRef = $showClass = false; + $hasText = $entity->getText() !== null; + $hasProperties = (bool)$entity->getProperties(); + $hasValues = (bool)$entity->getValues(); + $hasStack = (bool)$entity->getStackTrace(); + $open = $entity->isOpen(); + + switch ($entity->getType()) { + case 'arrayReference': + $name = 'array'; + + // no break + case 'objectReference': + $linkId = 'ref-'.$id; + $name = ''.$name.''; + $isRef = true; + break; + + case 'resource': + $showInfo = false; + break; + + case 'class': + case 'interface': + case 'trait': + $showClass = true; + $showInfo = false; + break; + } + + $this->output[] = '
'; + + // Name + if ($isRef) { + $this->output[] = ''.$name.''; + } else { + $this->output[] = ''.$name.''; + } + + // Length + if (null !== ($length = $entity->getLength())) { + $this->output[] = ''.$length.''; + } + + // Class + if ($showClass) { + $this->output[] = ':'; + $this->output[] = ''.$this->esc($entity->getClass()).''; + } + + // Info + if ($showInfo) { + $this->output[] = ''; + } + + // Meta + if ($showMeta = (bool)$entity->getAllMeta()) { + $this->output[] = ''; + } + + // Text + if ($hasText) { + $this->output[] = 't'; + } + + // Properties + if ($hasProperties) { + $this->output[] = 'p'; + } + + // Values + if ($hasValues) { + $this->output[] = 'v'; + } + + // Stack + if ($hasStack) { + $this->output[] = 's'; + } + + // Bracket + if ($hasBody = ($showInfo || $showMeta || $hasText || $hasProperties || $hasValues || $hasStack)) { + $this->output[] = '{'; + } + + // Object id + if (null !== ($objectId = $entity->getObjectId())) { + if ($isRef) { + $this->output[] = ''.$this->esc((string)$objectId).''; + } else { + $this->output[] = ''.$this->esc((string)$objectId).''; + } + } + + + + $this->output[] = '
'; + + + // Info + if ($showInfo) { + $this->renderInfoBlock($entity); + } + + // Meta + if ($showMeta) { + $this->renderMetaBlock($entity); + } + + + // Body + if ($hasText || $hasProperties || $hasValues || $hasStack) { + $this->output[] = '
'; + + // Text + if ($hasText) { + $this->renderTextBlock($entity); + } + + // Properties + if ($hasProperties) { + $this->renderPropertiesBlock($entity); + } + + // Values + if ($hasValues) { + $this->renderValuesBlock($entity); + } + + // Stack + if ($hasStack) { + $this->renderStackBlock($entity); + } + + $this->output[] = '
'; + } + + // Footer + if ($hasBody) { + $this->output[] = ''; + } + } + + /** + * Render entity info block + */ + protected function renderInfoBlock(Entity $entity): void + { + $id = $linkId = $entity->getId(); + + switch ($entity->getType()) { + case 'arrayReference': + case 'objectReference': + $linkId = 'ref-'.$id; + break; + } + + $this->output[] = '
'; + + $type = $entity->getType(); + $info = []; + + // Type + switch ($type) { + case 'object': + case 'array': + case 'class': + case 'interface': + case 'trait': + break; + + default: + $info['type'] = $type; + break; + } + + // Class + if ($type == 'object') { + $info['class'] = $entity->getClass(); + } + + // Location + if ($file = $entity->getFile()) { + $info['location'] = $this->context->normalizePath($file).' : '.$entity->getStartLine(); + } + + // Parents + if ($parents = $entity->getParentClasses()) { + $info['parentClasses'] = $parents; + } + + // Interfaces + if ($interfaces = $entity->getInterfaces()) { + $info['interfaces'] = $interfaces; + } + + // Traits + if ($traits = $entity->getTraits()) { + $info['traits'] = $traits; + } + + // Hash + if (($hash = $entity->getHash()) || $type == 'array') { + $info['hash'] = $hash; + } + + $this->renderList($info, 'info'); + + $this->output[] = '
'; + } + + /** + * Render entity meta block + */ + protected function renderMetaBlock(Entity $entity): void + { + $id = $entity->getId(); + $this->output[] = '
'; + $this->renderList($entity->getAllMeta(), 'meta'); + $this->output[] = '
'; + } + + /** + * Render entity text block + */ + protected function renderTextBlock(Entity $entity): void + { + $id = $entity->getId(); + $type = $entity->getType(); + + $this->output[] = '
'; + + if ($type === 'binary') { + $chunks = trim(chunk_split($entity->getText(), 2, "\n")); + $this->output[] = ''.str_replace("\n", '', $chunks).''; + } else { + $this->renderScalar($entity->getText()); + } + + $this->output[] = '
'; + } + + /** + * Render entity properties block + */ + protected function renderPropertiesBlock(Entity $entity): void + { + $id = $entity->getId(); + $this->output[] = '
'; + $this->renderList($entity->getProperties(), 'properties'); + $this->output[] = '
'; + } + + /** + * Render entity values block + */ + protected function renderValuesBlock(Entity $entity): void + { + $id = $entity->getId(); + $this->output[] = '
'; + $this->renderList($entity->getValues(), 'values'); + $this->output[] = '
'; + } + + /** + * Render entity stack trace block + */ + protected function renderStackBlock(Entity $entity): void + { + $id = $entity->getId(); + $this->output[] = '
'; + $this->renderStackList($entity->getStackTrace()); + $this->output[] = '
'; + } + + /** + * Render entity stack list + */ + protected function renderStackList(Trace $trace): void + { + $this->output[] = ''; + } + + + /** + * Render list + */ + protected function renderList(array $items, string $style, bool $includeKeys=true, string $class=null): void + { + $this->output[] = ''; + } + + protected function arrayIsAssoc(array $arr): bool + { + if (array() === $arr) { + return false; + } + + return array_keys($arr) !== range(0, count($arr) - 1); + } + + + /** + * Escape a value for HTML + */ + public function esc(?string $value): string + { + if ($value === null) { + return ''; + } + + return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + } +} diff --git a/src/DecodeLabs/Glitch/Dumper/Renderer/assets/dump.css b/src/DecodeLabs/Glitch/Dumper/Renderer/assets/dump.css new file mode 100644 index 0000000..7b368d7 --- /dev/null +++ b/src/DecodeLabs/Glitch/Dumper/Renderer/assets/dump.css @@ -0,0 +1,411 @@ +body { + padding: 2rem 1rem; +} + +header.stats { + margin-bottom: 2rem; +} + + + +.tooltip-inner { + background-color: #DCD9BB; + color: black; + max-width: 30rem; +} +.tooltip.bs-tooltip-right .arrow:before { + border-right-color: #DCD9BB !important; +} +.tooltip.bs-tooltip-left .arrow:before { + border-left-color: #DCD9BB !important; +} +.tooltip.bs-tooltip-bottom .arrow:before { + border-bottom-color: #DCD9BB !important; +} +.tooltip.bs-tooltip-top .arrow:before { + border-top-color: #DCD9BB !important; +} + + +samp.dump { + display: block; + font-size: 0.75rem; + margin-bottom: 1rem; + min-width: 700px; +} + +samp.dump.trace { + padding: 1rem 0; + border-top: 1px solid #EEE; +} + + +/* SCALARS */ +.null { + color: var(--pink); + font-weight: 600; +} +.bool { + color: var(--pink); + font-style: italic; + font-weight: 600; +} +.int { + color: var(--indigo); +} +.float { + color: var(--indigo); +} +.float:before { + content: 'f'; + color: var(--gray); + opacity: 0.5; + margin-right: 0.2em; +} + +/* STRING */ +.string { + color: var(--red); +} +.string.identifier { + color: var(--orange); +} +.string.m { + padding-right: 1rem; + display: inline; + white-space: pre; +} +.string.m.large:not(.show) { + cursor: s-resize; +} +.string.m.large.show { + cursor: n-resize; +} +.string.m.large:not(.show) > .line { + position: relative; +} +.string.m.large:not(.show) > .line:nth-child(1n+11) { + display: none; +} +.string.m.large:not(.show) > .line:nth-child(9) { + opacity: 0.6; +} +.string.m.large:not(.show) > .line:nth-child(10) { + opacity: 0.3; +} + +.string > .length { + color: var(--teal); + margin-left: 0.5em; + display: none; +} +.string:hover > .length { + display: inline; +} +.string.s > .line:before, .string.s > .line:after { + content: '"'; + color: var(--gray); + opacity: 0.5; +} +.string.m:before, .string.m:after { + content: '"""'; + color: var(--gray); + opacity: 0.5; +} +.string.m.large:not(.show):after { + content: '""" show more'; +} +.string.identifier:before, .string.identifier:after { + display: none; +} +.string.m > .line { + margin-left: -0.1rem; + padding-left: 0.1rem; + border-left: 1px solid #F5F5F5; +} +.string.m > .line:after { + content: '⏎'; + color: var(--gray); + opacity: 0.5; + font-size: 1.3em; + line-height: 1; + margin-left: 0.2em; +} +.string.m > .line:last-child:after { + display: none; +} + + +.pointer { + color: var(--gray); + opacity: 0.5; +} + +.g { + color: var(--gray); +} + + + +/* ENTITY */ +.entity.title, +.entity.footer { + display: inline-flex; + align-items: center; + position: relative; +} +.entity.title > * { + margin-right: 0.5em; +} +.entity.title .name { + color: var(--green) !important; + cursor: pointer; + text-decoration: none; +} +.entity.title .name.ref:before { + content: '&'; + color: var(--gray); + opacity: 0.5; + margin-right: 0.2em; +} +.entity.title .length { + cursor: pointer; + color: var(--teal); + margin-left: -0.5em; +} +.entity.title .length:before { + content: ':'; + color: var(--gray); + opacity: 0.5; +} +.popup { + position: absolute; +} +.oid { + margin-right: 0 !important; + color: var(--gray); +} +.oid:before { + content: '#'; + color: var(--gray); + opacity: 0.5; + margin-right: 0.2em; +} +a.oid:before { + content: '&'; +} + +.entity:target > .oid { + background: var(--purple); + color: white; + border-radius: 0.3em; + padding: 0 0.3em; + margin: 0 -0.3em; +} +.entity:target > .oid:before { + color: white; +} + +a.info + div { + position: relative; + left: -0.2em; +} +div.info div + h6 { + margin-top: 1rem; +} + +div.inner { + padding: 0 0 0 2rem; + display: block; +} + +div.inner > .inner { + padding: 0; +} + +div.inner.body { + border-left: 1px solid #F5F5F5; +} + +ul.list { + list-style: none; + padding: 0; + margin: 0; +} + +ul.list > li > ul.list { + margin-left: 2rem; +} +ul.list.inline { + display: inline-block; + margin: 0 0 0 2rem !important; +} +ul.list > li > .g { + margin-right: -2rem; +} + + +ul.list.inline { + display: inline-flex; + flex-wrap: wrap; +} +ul.list > li { + position: relative; +} +ul.list.inline > li:after { + content: ','; + color: var(--gray); + opacity: 0.5; + margin-right: 1em; +} +ul.list.inline > li:last-child:after { + display: none; +} + +ul.list > li > .identifier.key { + position: relative; + white-space: pre; +} +ul.list > li > .identifier.key:before { + content: 'ⓟ'; + color: var(--green); + opacity: 1; + /* + position: absolute; + display: block; + margin-left: -1.5em; + */ + + display: none; + position: relative; + margin-top: 0.3em; + margin-right: 0.6em; + font-size: 0.8em; +} + +ul.list.info > li > .string.identifier { + color: var(--cyan); +} +ul.list.info > li > .identifier.key { + color: var(--gray); + opacity: 0.75; +} +ul.list.info > li > .identifier.key:before { + content: 'ⓘ'; + color: var(--cyan); + opacity: 0.8; + display: inline; +} + + +ul.list.meta > li > .string.identifier { + color: var(--red); +} +ul.list.meta > li > .identifier.key { + color: var(--grey); + opacity: 0.75; +} +ul.list.meta > li > .identifier.key:before { + content: 'ⓜ'; + color: var(--red); + opacity: 0.8; + display: inline; +} + +ul.list.properties > li > .identifier.key { + color: var(--blue); + opacity: 0.75; +} + +ul.list.properties > li > .identifier.key:before { + content: 'ⓟ'; + color: var(--green); + display: inline; +} +ul.list.properties > li > .identifier.key.protected:before { + color: var(--yellow); +} +ul.list.properties > li > .identifier.key.private:before { + color: var(--red); + display: inline; +} +ul.list.properties > li > .identifier.key.public:before { + color: var(--green); + display: inline; +} +ul.list.properties > li > .identifier.key.virtual:before { + content: 'ⓥ'; + color: var(--purple); + display: inline; +} + + +ul.stack { + list-style: none; + padding: 0; + margin: 0; +} +ul.stack li span.number { + width: 1rem; + display: inline-block; +} +ul.stack li span.signature { + color: var(--orange); +} +ul.stack li span.signature:after { + content: "\A"; + white-space: pre; +} +ul.stack li span.file { + color: var(--green); + opacity: 0.75; + margin-left: 2rem; + filter: grayscale(0.75); +} +ul.stack li span.file:after { + content: ' :'; + color: var(--gray); + opacity: 0.5; +} +ul.stack li span.line { + color: var(--pink); +} + +.inner > div.text { + color: var(--red); +} +.inner > div.text.binary > i { + margin-right: 0.5em; + color: var(--indigo); +} + +a.badge { + cursor: help; + font-size: 0.9em; + color: white !important; +} +.badge.collapsed { + opacity: 0.3; +} + + +a.badge i { + font-style: normal; + position: relative; +} +a.badge.info i { + top: 0.05em; +} +a.badge.text i { + top: 0.05em; +} +a.badge.properties i { + top: -0.08em; +} + +a.name.collapsed ~ a.badge.body { + opacity: 0.1; +} +a.name.collapsed ~ a.badge.body.values { + opacity: 0.3; +} diff --git a/src/DecodeLabs/Glitch/Dumper/Renderer/assets/dump.js b/src/DecodeLabs/Glitch/Dumper/Renderer/assets/dump.js new file mode 100644 index 0000000..610abbf --- /dev/null +++ b/src/DecodeLabs/Glitch/Dumper/Renderer/assets/dump.js @@ -0,0 +1,97 @@ +$(function() { + $('[title]').tooltip(); + + $(document).on('click', '[data-target]', function(e) { + e.preventDefault(); + var $badge = $(this), + $entity = $badge.closest('.entity'), + isName = $badge.hasClass('name'), + $target = $($badge.attr('data-target')), + isCollapsed = !$target.hasClass('show'), + isBody = $badge.hasClass('body'), + $name = $entity.find('a.name'), + $body = $($name.attr('data-target')), + isBodyCollapsed = !$body.hasClass('show'), + otherChildren = $body.children('div.collapse.show').not($badge.attr('data-target')).length; + + + if(isBody) { + if(!isCollapsed && isBodyCollapsed) { + $body.collapse('show'); + $name.removeClass('collapsed'); + } else { + $badge.toggleClass('collapsed', !isCollapsed); + + if(!isCollapsed) { + $target.collapse('toggle'); + + // Closing + if(!otherChildren) { + $body.collapse('hide'); + $name.addClass('collapsed'); + } + } else { + // Opening + if(isBodyCollapsed) { + $target.addClass('show'); + $body.collapse('show'); + $name.removeClass('collapsed'); + } else { + $target.collapse('show'); + } + } + } + } else { + $badge.toggleClass('collapsed', !isCollapsed); + + if(isName && isCollapsed && !otherChildren) { + var $first = $entity.find('a.body:first'); + + if(!$first.length) { + $first = $entity.find('a.badge:first'); + } + + $first.click(); + } else { + $target.collapse('toggle'); + } + } + }); + + $(document).on('click', 'a.ref', function(e) { + e.preventDefault(); + + var id = $(this).attr('href'), + $target = $(id).children('.name'), + $body = $($target.attr('data-target')), + isBodyCollapsed = !$body.hasClass('show'), + $parents = $target.parents('.collapse'); + + window.location.hash = id; + + $parents.each(function() { + $('a[data-target="#'+$(this).attr('id')+'"]').removeClass('collapsed'); + }).collapse('show'); + + if(isBodyCollapsed) { + $target.click(); + } + + var elOffset = $target.offset().top, + elHeight = $target.height(), + windowHeight = $(window).height(), + offset; + + if (elHeight < windowHeight) { + offset = elOffset - ((windowHeight / 4) - (elHeight / 2)); + } else { + offset = elOffset + 50; + } + + $('html, body').animate({ scrollTop: offset}, 700); + }); + + $(document).on('click', '.string.m.large', function() { + $(this).toggleClass('show'); + }); +}); diff --git a/src/Glitch/Factory.php b/src/DecodeLabs/Glitch/Factory.php similarity index 96% rename from src/Glitch/Factory.php rename to src/DecodeLabs/Glitch/Factory.php index 349ebc4..f2316d9 100644 --- a/src/Glitch/Factory.php +++ b/src/DecodeLabs/Glitch/Factory.php @@ -4,8 +4,12 @@ * @license http://opensource.org/licenses/MIT */ declare(strict_types=1); -namespace Glitch; +namespace DecodeLabs\Glitch; +/** + * Automatically generate Exceptions on the fly based on scope and + * requested interface types + */ class Factory { const STANDARD = [ @@ -164,7 +168,7 @@ protected function __construct(?string $type, array $params=[]) protected function build(string $message, array $interfaces): \EGlitch { $this->params['rewind'] = $rewind = max((int)($this->params['rewind'] ?? 0), 0); - $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $rewind + static::REWIND); + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $rewind + static::REWIND + 2); $lastTrace = array_pop($trace); $key = $rewind + static::REWIND; @@ -189,6 +193,8 @@ protected function build(string $message, array $interfaces): \EGlitch $this->namespace = null; } + $interfaces[] = '\\DecodeLabs\\Glitch\\Inspectable'; + $this->buildDefinitions($interfaces); foreach ($this->interfaceDefs as $interface => $def) { @@ -225,7 +231,7 @@ protected function buildDefinitions(array $interfaces): void } $directType = null; - $this->traits[] = 'Glitch\\TException'; + $this->traits[] = 'DecodeLabs\\Glitch\\TException'; // Create initial interface list @@ -256,7 +262,7 @@ protected function buildDefinitions(array $interfaces): void // Sort inheritance list foreach ($this->interfaces as $interface => $info) { - if ($info !== null) { + if (!empty($info)) { $this->defineInterface($interface, $info); } } @@ -266,6 +272,7 @@ protected function buildDefinitions(array $interfaces): void $this->type = \Exception::class; } + if (empty($this->interfaces)) { $this->interfaces['\\EGlitch'] = []; } @@ -303,7 +310,7 @@ protected function listInterface(string $interface, bool $direct=false): ?string $name = array_pop($parts); $output = null; - if (empty($parts) || !preg_match('/^(E)[A-Z][a-zA-Z0-9_]+$/', $name)) { + if (!preg_match('/^(E)[A-Z][a-zA-Z0-9_]+$/', $name)) { return null; } @@ -375,6 +382,10 @@ protected function defineInterface(string $interface, array $info): void { $parent = '\\EGlitch'; + if ($interface === $parent) { + return; + } + if (isset($info['extend'])) { $parent = []; diff --git a/src/DecodeLabs/Glitch/Inspectable.php b/src/DecodeLabs/Glitch/Inspectable.php new file mode 100644 index 0000000..241e1bb --- /dev/null +++ b/src/DecodeLabs/Glitch/Inspectable.php @@ -0,0 +1,14 @@ +args = (array)$frame['args']; } + + + // Glitch specific + if ($this->className == 'Glitch' && $this->function == '__callStatic') { + $this->function = array_shift($this->args); + $this->args = $this->args[0]; + } } @@ -224,7 +231,8 @@ public static function normalizeClassName(string $class): string } elseif (preg_match('/^eval\(\)\'d/', $part)) { $name = ['eval[ '.implode(' : ', $name).' ]']; } else { - $name[] = $part; + $el = explode('\\', $part); + $name[] = array_pop($el); } } @@ -286,7 +294,7 @@ public function getArgString(): string } elseif (is_array($arg)) { $arg = '['.count($arg).']'; } elseif (is_object($arg)) { - $arg = self::normalizeClassName(get_class($arg)).' Object'; + $arg = self::normalizeClassName(get_class($arg)); } elseif (is_bool($arg)) { $arg = $arg ? 'true' : 'false'; } elseif (is_null($arg)) { @@ -303,11 +311,11 @@ public function getArgString(): string /** * Generate a full frame signature */ - public function getSignature(?bool $argString=false): string + public function getSignature(?bool $argString=false, bool $namespace=true): string { $output = ''; - if ($this->namespace !== null) { + if ($namespace && $this->namespace !== null) { $output = $this->namespace.'\\'; } @@ -319,7 +327,11 @@ public function getSignature(?bool $argString=false): string $output .= $this->getInvokeType(); } - $output .= $this->function; + if (false !== strpos($this->function, '{closure}')) { + $output .= '{closure}'; + } else { + $output .= $this->function; + } if ($argString) { $output .= $this->getArgString(); @@ -378,7 +390,7 @@ public function getCallingLine(): ?int public function toArray(): array { return [ - 'file' => PathHandler::normalizePath($this->getFile()), + 'file' => Context::getDefault()->normalizePath($this->getFile()), 'line' => $this->getLine(), 'function' => $this->function, 'class' => $this->className, diff --git a/src/Glitch/Stack/Trace.php b/src/DecodeLabs/Glitch/Stack/Trace.php similarity index 84% rename from src/Glitch/Stack/Trace.php rename to src/DecodeLabs/Glitch/Stack/Trace.php index 234d3f3..0a025ba 100644 --- a/src/Glitch/Stack/Trace.php +++ b/src/DecodeLabs/Glitch/Stack/Trace.php @@ -4,14 +4,17 @@ * @license http://opensource.org/licenses/MIT */ declare(strict_types=1); -namespace Glitch\Stack; +namespace DecodeLabs\Glitch\Stack; -use Glitch\PathHandler; +use DecodeLabs\Glitch\Context; + +use DecodeLabs\Glitch\Inspectable; +use DecodeLabs\Glitch\Dumper\Entity; /** * Represents a normalized stack trace */ -class Trace implements \IteratorAggregate +class Trace implements \IteratorAggregate, \Countable, Inspectable { protected $frames = []; @@ -84,7 +87,7 @@ public function __construct(array $frames) foreach ($frames as $frame) { if (!$frame instanceof Frame) { throw \Glitch::EUnexpectedValue([ - 'message' => 'Trace frame is not an instance of Glitch\\Frame', + 'message' => 'Trace frame is not an instance of DecodeLabs\\Glitch\\Frame', 'data' => $frame ]); } @@ -157,6 +160,14 @@ public function toArray(): array }, $this->frames); } + /** + * Count frames + */ + public function count(): int + { + return count($this->frames); + } + /** @@ -169,17 +180,20 @@ public function __debugInfo(): array $count = count($frames); foreach ($frames as $i => $frame) { - if ($i === 0) { - $output[($count + 1).': Glitch'] = [ - 'file' => PathHandler::normalizePath($frame->getFile()).' : '.$frame->getLine() - ]; - } - $output[($count - $i).': '.$frame->getSignature(true)] = [ - 'file' => PathHandler::normalizePath($frame->getCallingFile()).' : '.$frame->getCallingLine() + 'file' => Context::getDefault()->normalizePath($frame->getCallingFile()).' : '.$frame->getCallingLine() ]; } return $output; } + + + /** + * Inspect for Glitch + */ + public function glitchInspect(Entity $entity, callable $inspector): void + { + $entity->setStackTrace($this); + } } diff --git a/src/DecodeLabs/Glitch/Stat.php b/src/DecodeLabs/Glitch/Stat.php new file mode 100644 index 0000000..a40c798 --- /dev/null +++ b/src/DecodeLabs/Glitch/Stat.php @@ -0,0 +1,94 @@ +key = $key; + $this->name = $name; + $this->value = $value; + } + + + /** + * Get key + */ + public function getKey(): string + { + return $this->key; + } + + /** + * Get name + */ + public function getName(): string + { + return $this->name; + } + + + /** + * Set badge class + */ + public function setClass(string $class): Stat + { + $this->class = $class; + return $this; + } + + /** + * Get badge class + */ + public function getClass(): string + { + return $this->class; + } + + /** + * Apply class with value + */ + public function applyClass(callable $applicator): Stat + { + return $this->setClass($applicator($this->value)); + } + + + /** + * Add a named renderer + */ + public function setRenderer(string $type, callable $renderer): Stat + { + $this->renderers[$type] = $renderer; + return $this; + } + + /** + * Render to string using stack of named renderers + */ + public function render(string $type): string + { + if (isset($this->renderers[$type])) { + return $this->renderers[$type]($this->value); + } elseif ($type !== 'text' && isset($this->renderers['text'])) { + return $this->renderers['text']($this->value); + } else { + return (string)$this->value; + } + } +} diff --git a/src/Glitch/TException.php b/src/DecodeLabs/Glitch/TException.php similarity index 74% rename from src/Glitch/TException.php rename to src/DecodeLabs/Glitch/TException.php index 63a27a3..a01a241 100644 --- a/src/Glitch/TException.php +++ b/src/DecodeLabs/Glitch/TException.php @@ -4,10 +4,13 @@ * @license http://opensource.org/licenses/MIT */ declare(strict_types=1); -namespace Glitch; +namespace DecodeLabs\Glitch; -use Glitch\Stack\Frame; -use Glitch\Stack\Trace; +use DecodeLabs\Glitch\Stack\Frame; +use DecodeLabs\Glitch\Stack\Trace; + +use DecodeLabs\Glitch\Inspectable; +use DecodeLabs\Glitch\Dumper\Entity; /** * Main root exception inheritance @@ -21,6 +24,9 @@ trait TException protected $rewind; protected $stackTrace; + /** + * Override the standard Exception constructor to simplify instantiation + */ public function __construct($message, array $params=[]) { parent::__construct( @@ -88,7 +94,7 @@ public function getHttpCode(): ?int */ public function getStackFrame(): Frame { - return $this->getStackTrace()->getFirstCall(); + return $this->getStackTrace()->getFirstFrame(); } /** @@ -97,7 +103,7 @@ public function getStackFrame(): Frame public function getStackTrace(): Trace { if (!$this->stackTrace) { - $this->stackTrace = Trace::fromException($this, $this->rewind + 2); + $this->stackTrace = Trace::fromException($this, $this->rewind + 1); } return $this->stackTrace; @@ -133,11 +139,26 @@ public function __debugInfo(): array $output['types'] = array_merge($types, array_values(class_implements($this))); sort($output['types']); - $output['file'] = PathHandler::normalizePath($this->file).' : '.$this->line; + $output['file'] = Context::getDefault()->normalizePath($this->file).' : '.$this->line; // Trace $output['stackTrace'] = $this->getStackTrace(); return $output; } + + /** + * Inspect for Glitch + */ + public function glitchInspect(Entity $entity, callable $inspector): void + { + $entity + ->setText($this->message) + ->setProperty('*code', $inspector($this->code)) + ->setProperty('*http', $inspector($this->http)) + ->setValues($inspector($this->data)) + ->setFile($this->file) + ->setStartLine($this->line) + ->setStackTrace($this->getStackTrace()); + } } diff --git a/src/DecodeLabs/Glitch/Transport.php b/src/DecodeLabs/Glitch/Transport.php new file mode 100644 index 0000000..e2cab96 --- /dev/null +++ b/src/DecodeLabs/Glitch/Transport.php @@ -0,0 +1,12 @@ +incomplete($data, 1); + } + + /** + * Shortcut to normalizePath context method + */ + public static function normalizePath(string $path): string + { + return Context::getDefault()->normalizePath($path); + } + + /** + * Shortcut to logException context method + */ + public function logException(\Throwable $e): void + { + Context::getDefault()->logException($e); + } + + /** + * Private instanciation + */ private function __construct() { } diff --git a/src/Glitch/PathHandler.php b/src/Glitch/PathHandler.php deleted file mode 100644 index e799d9d..0000000 --- a/src/Glitch/PathHandler.php +++ /dev/null @@ -1,48 +0,0 @@ - $path) { - self::$aliases[$name] = $path; - } - - uasort(self::$aliases, function ($a, $b) { - return strlen($b) - strlen($a); - }); - } - - public static function getAliases(): array - { - return array_flip(self::$aliases); - } - - public static function normalizePath(string $path): string - { - foreach (self::$aliases as $name => $test) { - if (0 === strpos($path, $test)) { - return '{'.$name.'}'.substr($path, strlen($test)); - } - } - - return $path; - } -} diff --git a/src/helpers.php b/src/helpers.php index 21634c3..d3a023b 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -11,8 +11,59 @@ */ namespace { - use Glitch\Factory; - use Glitch\PathHandler; + use DecodeLabs\Glitch\Factory; + use DecodeLabs\Glitch\Context; + use DecodeLabs\Glitch\Stack\Frame; + + use Symfony\Component\VarDumper\VarDumper; + + if (!function_exists('dd')) { + /** + * Super quick global dump & die + */ + function dd($var, ...$vars): void + { + Context::getDefault()->dumpDie(func_get_args(), 1); + } + } + + if (!function_exists('dump')) { + /** + * Quick dump + */ + function dump($var, ...$vars): void + { + Context::getDefault()->dump(func_get_args(), 1); + } + } elseif (class_exists(VarDumper::class)) { + VarDumper::setHandler(function ($var) { + /** + * We have to do some silly juggling here to combine all the dump args into one + * Symfony blindly calls dump for each var, which doesn't work for us + * Instead we grab all args from the stack trace and then skip the following calls + */ + static $skip; + + if (!$skip) { + $frame = Frame::create(2); + $func = $frame->getFunctionName(); + $type = $frame->getType(); + + if (($func == 'dd' || $func == 'dump') && $type == 'globalFunction') { + $args = $frame->getArgs(); + $skip = count($args) - 1; + } else { + $args = func_get_args(); + } + + Context::getDefault()->dump($args, 2); + } else { + $skip--; + return; + } + }); + } + /** * Direct facade for generating IError based exceptions @@ -27,6 +78,4 @@ function Glitch($message, ?array $params=[], $data=null): \EGlitch $data ); } - - PathHandler::registerAlias('glitch', __DIR__); }