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[] = '