Skip to content

Commit

Permalink
Merge branch 'release/v0.15.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
betterthanclay committed Oct 14, 2019
2 parents 56a21ba + 78c1303 commit e8113d0
Show file tree
Hide file tree
Showing 12 changed files with 397 additions and 41 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## v0.15.0 (2019-10-14)
* Added production mode error message page
* Added ability to gracefully handle parse errors
* Automatically detect text/plain header and switch renderer
* Bypass segfault with EventBase class
* Added contain() helper to Context
* Fully wrap ErrorException in error_handler()
* Updated docs

## v0.14.8 (2019-10-04)
* Fixed base renderDump in production mode
* Updated text icon rendering
Expand Down
87 changes: 77 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Glitch is a standalone PHP package designed to improve end-to-end error generati

The project aims to provide a radically enhanced Exception framework that decouples the _meaning_ of an Exception from the underlying _implementation_ functionality, alongside deep data inspection tools and an Exception handling interface.

![v0.13.0 interface](docs/ui-v0.13.0.png)
![v0.15.0 interface](docs/ui-v0.15.0.png)

## Installation
Glitch should be installed via composer
Expand All @@ -15,11 +15,29 @@ composer require decodelabs/glitch
```


### Importing

Glitch uses a [Veneer Facade](https://github.com/decodelabs/veneer) so you don't _need_ to add any <code>use</code> declarations to your code, the class will be aliased into whatever namespace you are working in.

However, if you want to avoid filling your namespace with class aliases, you can import the Facade with:

```php
use DecodeLabs\Glitch;
```

### Setup
Glitch will work out of the box with minimal setup. It uses a [Veneer Facade](https://github.com/decodelabs/veneer) so you can use it in any namespace context without having to import anything.

Otherwise, Glitch works out of the box without any special setup.
There are however some optional steps you can take to customise operation.


Register as the default error handler:

```php
Glitch::registerAsErrorHandler();
```


Register base path aliases for easier reading of file names in dumps:

```php
Expand Down Expand Up @@ -48,13 +66,6 @@ Glitch::setRunMode('development');
```


Register as the default error handler:

```php
Glitch::registerAsErrorHandler();
```


## Dumps
Dump anything and everything easily, using simple global functions.
The functions mirror those used in Symfony/VarDumper, maintaining compatibility by using Symfony's VarDumper interface if it is already loaded.
Expand Down Expand Up @@ -180,9 +191,65 @@ class Thing {
```


## Wrapping exceptions

If you want to present a unified interface in your own libraries, you ideally need to ensure that the only exceptions you throw from your library are from your libraries namespace - exceptions thrown by third party libraries called by _your_ library may need to be wrapped to maintain namespace isolation.

The old, long way:

```php
namespace My\Library;

use Someone\Elses\Thing;

class MyClass {
public function doSomething() {
$thing = new Thing();

try {
return $thing->doIt();
} catch(Someone\Elses\ThingException $e) {
throw new My\Library\ThingException($e->getMessage(), $e->getCode(), $e);
}
}
}
```

Glitch offers a <code>contain()</code> method that will wrap your third party code and automatically convert any throw exceptions to Glitch exceptions from the primary stack frame and namespace of your library:

```php
namespace My\Library;

use Someone\Elses\Thing;

class MyClass {
public function doSomething() {
$thing = new Thing();

/*
* If doIt() throws an exception from Someone\Elses namespace, Glitch will
* wrap it and throw a Glitch exception from My\Library.
*/
return Glitch::contain(function() use($thing) {
return $thing->doIt();
}, function($e) {
/*
* Optionally, you can send a second function to contain() inspect the source
* exception and return the required types for the output Glitch
*/
if($e instanceof Someone\Elses\ThingException) {
return 'EThingGoneWrong';
} else {
return 'ERuntime';
}
});
}
}
```

## Other information
[Rationale for Glitch Exceptions](docs/Rationale.md)
- [Rationale for Glitch Exceptions](docs/Rationale.md)
- [An explanation of how the Glitch interface works](docs/HowItWorks.md)


## Licensing
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"symfony/polyfill-mbstring": "^1.7",

"decodelabs/veneer": "~0.4",
"decodelabs/enlighten": "~0.6.1",
"decodelabs/enlighten": "~0.7",
"psr/log": "^1.0",

"components/jquery": "~3.3"
Expand Down
164 changes: 164 additions & 0 deletions docs/HowItWorks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# How the Glitch interface works

The Glitch Exception structure's main aim is to generate dynamic Exceptions based on a set of criteria in any particular context, with minimum boilerplate code.

## Calling the exception generator

Glitch combines a number of techniques to create a predictable and easy to use interface to the Exception generator mechanism.
It is made of three essential parts:

- A [Veneer Facade](https://github.com/decodelabs/veneer) that acts as a static reference to the active Context instance
- A <code>\__call()</code> method in the main Context class
- A global function in the root namespace for calls to Glitch without a type

### Veneer
More information about how Veneer works can be found in the [documentation for that project](https://github.com/decodelabs/veneer), however the main principle is for the library to generate a private static class that can act as a proxy to an instance of an object, and then automatically alias that private static class into the namespace you are working in, using <code>class_alias()</code>.

This allows you as a developer to reference the functionality in the target object instance by calling the equivalent _static_ methods on the alias, without even having to define a <code>use</code> statement in the header of your file.

Glitch registers <code>DecodeLabs\Glitch\Context</code> as the target object to be referenced under the Facade named "Glitch"; calling a static method on the <code>Glitch</code> class in _any_ namespace passes the call through to the Context object.

```php
namespace Any\Old\Namespace;

Glitch::setRunMode('production');

// is directly equivalent to

$glitchContext = $psr11Container->get('glitch');
$glitchContext->setRunMode('production');
```

Having this Facade available effectively allows you as a developer to reference Glitch in the most straightforward way possible.


### Using dynamic method names as parameters
Glitch then utilizes an aspect of PHP that allows method names to be used as arbitrary parameters: <code>\__call()</code> and <code>\__callStatic()</code>.

The main Context class contains a <code>\__call()</code> magic method that captures all method calls on the object for which a function has not been defined. Due to the dynamic nature of PHP, this can include arbitrary strings:

```php
$object->{'abitrary method name'}();
```

Glitch uses this feature as a means of passing through the projected _type_ of exception to be generated, and parses that method name out to expand commas into an array:

```php
throw $glitchContext->{'ERuntime,ENotFound'}('message');

// internally:
// $types = ['ERuntime', 'ENotFound'];
```

The Veneer Facade then acts as a go-between allowing _static_ invocation of this function using <code>\__callStatic()</code> in exactly the same way:

```php
throw Glitch::{'ERuntime,ENotFound'}('message');
```

#### Global function

The global Glitch function in the root namespace acts as a fallback for when you may want to generate a generic EGlitch exception without any other types defined:

```php
throw Glitch('message');
```


## Calling the factory
Once the projected exception types have been captured by one of the <code>\__call()</code> magic methods, they are then passed with the message and exception parameters to an Exception Factory.

It is the sole responsibility of the Factory to actually generate an instance of an Exception for the calling code to throw.

It uses a combination of <code>eval()</code> and anonymous classes to build a custom class specific to the current context containing a mix of interfaces and traits, to define type, message and functionality.

### Stack frame
The Exception Factory uses <code>debug_backtrace()</code> to work out the namespace from which Glitch was called and uses this to decide which interfaces need to be generated and what needs to be rolled into the final Exception class.

It's aim is to have an interface named with each of the types defined in the original call to the Factory (eg <code>ERuntime</code>, <code>ENotFound</code>) defined _within the namespace of the originating call_ so that <code>catch</code> blocks can reference the type directly.

```php
namespace Any\Old\Namespace;

try {
throw Glitch::ERuntime('message');
} catch(ERuntime $e) {
// do something
}
```

Secondary to that, if the requested types are listed as primary exception types by the Factory then there will also be an interface to represent it in the root namespace (note the backslash in the catch block):

```php
namespace Any\Old\Namespace;

try {
throw Glitch::ERuntime('message');
} catch(\ERuntime $e) {
// do something
}
```

On top of that, the Factory will ensure there is an interface named <code>EGlitch</code> at _every_ namespace level up the tree to the target namespace so that developers can choose the granularity of catch blocks, ad hoc:

```php
namespace Any\Old\Namespace;

use MyLibrary\InnerNamespace\SomeClass;

$myLibrary = new SomeClass();

try {
// This method will throw a Glitch
$myLibrary->doAThing();
} catch(MyLibrary\InnerNamespace\EGlitch $e | MyLibrary\EGlitch $e | \EGlitch $e) {
// All of the above tests will match
}
```

To increase compatibility with SPL exceptions, any E* types that have a corresponding SPL Exception class type will extend from _that_ type, rather than the root Exception class:

```php
namespace Any\Old\Namespace;

try {
throw Glitch::ERuntime('message');
} catch(\RuntimeException $e) {
// do something
}
```


And then for _any_ interface that is added to the final type definition, the equivalent <code>\<InterfaceName>Trait</code> trait will be added too, if it exists. This allows the inclusion of context specific functionality within a specific category of Exceptions without having to tie the functionality to a particular meaning.


As an example, given the fallowing Glitch call:

```php
namespace MyVendor\MyLibrary\SubFunctions;

trait ERuntimeTrait {
public function extraFunction() {
echo 'hello world';
}
}

throw Glitch::ERuntime('message');
```

The resulting anonymous class will include:

- <code>MyVendor\MyLibrary\SubFunctions\ERuntime</code> interface
- <code>MyVendor\MyLibrary\SubFunctions\ERuntimeTrait</code> trait, with <code>extraFunction()</code>
- <code>\ERuntime</code> interface
- <code>MyVendor\MyLibrary\SubFunctions\EGlitch</code> interface
- <code>MyVendor\MyLibrary\EGlitch</code> interface
- <code>MyVendor\EGlitch</code> interface
- <code>\EGlitch</code> interface
- <code>\EGlitchTrait</code> trait (which is predefined)
- <code>\RuntimeException</code> base class


#### Repeated execution

Once the Factory has generated an Exception for a particular subgroup of requested types within a specific namespace, it is hashed and cached so that repeated calls to the Factory within the same context can just return a new instance of the anonymous class. The resulting performance overhead of general usage of Glitch Exceptions then tends to be trivial, while the _development_ overhead is **massively** reduced as there is no need to define individual Exception classes for every type of error in all of your libraries.
Binary file removed docs/ui-v0.13.0.png
Binary file not shown.
Binary file added docs/ui-v0.15.0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 18 additions & 5 deletions src/EGlitchTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,20 @@ public function __construct($message, array $params=[])
$message = 'blah';
}

parent::__construct(
$args = [
$message,
$params['code'] ?? 0,
$params['previous'] ?? null
);
(int)($params['code'] ?? 0)
];

if ($this instanceof \ErrorException) {
$args[] = (int)($params['severity'] ?? 0);
$args[] = (string)($params['file'] ?? '');
$args[] = (int)($params['line'] ?? 0);
}

$args[] = $params['previous'] ?? null;

parent::__construct(...$args);

if (isset($params['file'])) {
$this->file = $params['file'];
Expand All @@ -65,7 +74,11 @@ public function __construct($message, array $params=[])
$this->type = $params['type'] ?? null;
$this->interfaces = (array)($params['interfaces'] ?? []);

unset($params['data'], $params['rewind'], $params['http'], $params['type'], $params['interfaces']);
if (isset($params['stackTrace']) && $params['stackTrace'] instanceof Trace) {
$this->stackTrace = $params['stackTrace'];
}

unset($params['data'], $params['rewind'], $params['http'], $params['type'], $params['interfaces'], $params['stackTrace']);
$this->params = $params;
}

Expand Down
Loading

0 comments on commit e8113d0

Please sign in to comment.