Skip to content

Commit

Permalink
Better error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
inxilpro committed Sep 29, 2021
1 parent c4022c9 commit 5d03db0
Show file tree
Hide file tree
Showing 16 changed files with 371 additions and 69 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ format. This project adheres to [Semantic Versioning](https://semver.org/spec/v2

## [Unreleased]

## Added

- Added `Gretel` facade
- Added options for handling missing or mis-configured breadcrumbs (see README)
- Added additional exceptions for more granular handling

## Fixed

- Calling `Collection` methods on `RequestBreadcrumbs` now automatically populates the collection first

## [1.1.0]

### Changed
Expand Down
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Gretel is a Laravel package for adding route-based breadcrumbs to your applicati
- [Using Gretel With Your CSS Framework of Choice](#supported-frameworks)
- [Using a Custom Template](#custom-breadcrumb-view) (while maintaining accessibility)
- [Caching Breadcrumbs](#caching-breadcrumbs) (required if using `route:cache`)
- [Handling Errors](#handling-errors)

## Installation

Expand Down Expand Up @@ -144,7 +145,6 @@ accepts a few optional attributes:

| Attribute | |
|--------------------|-------------------------------------------------------------------------------------|
| `throw-if-missing` | Renders breadcrumbs, but throws an exception if none are set for the current route. |
| `framework` | Render to match a UI framework (`"tailwind"` by default) |
| `view` | Render a custom view (supersedes the `framework` attribute) |
| `jsonld` | Render as a JSON-LD `<script>` tag |
Expand Down Expand Up @@ -255,3 +255,22 @@ php artisan breadcrumbs:clear
```

Please note that you must cache your breadcrumbs **before you cache your routes**.

### Handling Errors

Sometimes you'll mis-configure a breadcrumb or forget to define one. You can register handlers
on the `Gretel` facade to handle these cases:

```php
// Log or report a missing breadcrumb
Gretel::handleMissingBreadcrumbs(fn(MissingBreadcrumbException $exception) => Log::warning($exception->getMessage()));

// Throw an exception locally if there's a missing breadcrumb
Gretel::throwOnMissingBreadcrumbs(! App::environment('production'));

// Log or report a mis-configured breadcrumb (i.e. a parent route that doesn't exist)
Gretel::handleMisconfiguredBreadcrumbs(fn() => Log::warning('Missing breadcrumb.'));

// Throw an exception locally if there's a mis-configured breadcrumb
Gretel::throwOnMisconfiguredBreadcrumbs(! App::environment('production'));
```
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@
"providers": [
"Glhd\\Gretel\\Support\\GretelServiceProvider"
],
"aliases": {}
"aliases": {
"Gretel": "Glhd\\Gretel\\Support\\Facades\\Gretel"
}
}
}
}
4 changes: 3 additions & 1 deletion src/Exceptions/MissingBreadcrumbException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

class MissingBreadcrumbException extends RuntimeException
{
public function __construct(string $name)
public function __construct(?string $name)
{
$name ??= '[unnamed route]';

parent::__construct("There is no breadcrumb registered for '{$name}'.");
}
}
13 changes: 0 additions & 13 deletions src/Exceptions/ParentParametersCannotBeInferredException.php

This file was deleted.

26 changes: 26 additions & 0 deletions src/Exceptions/UnresolvableParentException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Glhd\Gretel\Exceptions;

use InvalidArgumentException;

class UnresolvableParentException extends InvalidArgumentException
{
public function __construct($value, string $route)
{
$message = is_string($value)
? "Unable to find parent '{$value}' for route '{$route}'"
: $this->invalidTypeMessage($value, $route);

parent::__construct($message);
}

protected function invalidTypeMessage($value, string $route): string
{
$got = is_object($value)
? get_class($value)
: gettype($value);

return "Unable to resolve parent for route '{$route}'. Expected a route name or a RouteBreadcrumb object, but got '{$got}' instead.";
}
}
20 changes: 11 additions & 9 deletions src/Macros.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,17 @@ public static function breadcrumb(
$parent = null,
Closure $relation = null
): Route {
if (!$name = $route->getName()) {
throw new UnnamedRouteException();
}

$title = TitleResolver::make($title);
$parent = ParentResolver::make($parent, $name, $relation);
$url = UrlResolver::make($name, $route->parameterNames());

$registry->register(new RouteBreadcrumb($name, $title, $parent, $url));
$registry->withExceptionHandling(function() use ($registry, $route, $title, $parent, $relation) {
if (!$name = $route->getName()) {
throw new UnnamedRouteException();
}

$title = TitleResolver::make($title);
$parent = ParentResolver::make($parent, $name, $relation);
$url = UrlResolver::make($name, $route->parameterNames());

$registry->register(new RouteBreadcrumb($name, $title, $parent, $url));
});

return $route;
}
Expand Down
76 changes: 76 additions & 0 deletions src/Registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

namespace Glhd\Gretel;

use Closure;
use Glhd\Gretel\Exceptions\MissingBreadcrumbException;
use Glhd\Gretel\Routing\RouteBreadcrumb;
use Illuminate\Routing\Route;
use Illuminate\Support\Collection;
use Illuminate\Support\Traits\ForwardsCalls;
use Throwable;

/**
* @mixin Collection
Expand All @@ -15,11 +17,74 @@ class Registry
{
use ForwardsCalls;

protected const HANDLER_MISSING = 'missing';

protected const HANDLER_MISCONFIGURED = 'misconfigured';

protected Collection $breadcrumbs;

protected Collection $exception_handlers;

public function __construct()
{
$this->breadcrumbs = new Collection();
$this->exception_handlers = new Collection();

// Default to not throwing on missing breadcrumbs, only mis-configured ones
$this->throwOnMissingBreadcrumbs(false);
}

public function withExceptionHandling(Closure $callback)
{
try {
return $callback();
} catch (MissingBreadcrumbException $exception) {
$this->callHandler(static::HANDLER_MISSING, $exception);
} catch (Throwable $exception) {
$this->callHandler(static::HANDLER_MISCONFIGURED, $exception);
}

return null;
}

public function handleMissingBreadcrumbs(Closure $callback): self
{
$this->exception_handlers->put(static::HANDLER_MISSING, $callback);

return $this;
}

public function throwOnMissingBreadcrumbs(bool $throw = true): self
{
if (!$throw) {
return $this->handleMissingBreadcrumbs(static function() {
// Ignore exception
});
}

return $this->handleMissingBreadcrumbs(static function(Throwable $throwable) {
throw $throwable;
});
}

public function handleMisconfiguredBreadcrumbs(Closure $callback): self
{
$this->exception_handlers->put(static::HANDLER_MISCONFIGURED, $callback);

return $this;
}

public function throwOnMisconfiguredBreadcrumbs(bool $throw = true): self
{
if (!$throw) {
return $this->handleMisconfiguredBreadcrumbs(static function() {
// Ignore exception
});
}

return $this->handleMisconfiguredBreadcrumbs(static function(Throwable $throwable) {
throw $throwable;
});
}

public function clear(): Registry
Expand Down Expand Up @@ -81,4 +146,15 @@ protected function forwardDecoratedCallTo($object, $method, $parameters)

return $result;
}

protected function callHandler(string $kind, Throwable $exception): void
{
$handler = $this->exception_handlers->get($kind);

if (!$handler instanceof Closure) {
throw $exception;
}

$handler($exception);
}
}
8 changes: 6 additions & 2 deletions src/Resolvers/ParentResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
use Arr;
use Closure;
use Glhd\Gretel\Exceptions\UnmatchedRouteException;
use Glhd\Gretel\Exceptions\UnresolvableParentException;
use Glhd\Gretel\Registry;
use Glhd\Gretel\Routing\RouteBreadcrumb;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use RuntimeException;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

Expand Down Expand Up @@ -58,7 +58,7 @@ public function resolve(array $parameters, Registry $registry)
}

if (!($result instanceof RouteBreadcrumb)) {
throw new RuntimeException('Unable to resolve parent breadcrumb.');
throw new UnresolvableParentException($result, $name);
}

if (!empty($parameters)) {
Expand All @@ -69,6 +69,10 @@ public function resolve(array $parameters, Registry $registry)
return $result;
}

/**
* Please note that this behavior is intentionally undocumented and may be
* removed at any time. Use at your own risk.
*/
protected function findParentByUrl(string $url, Registry $registry): RouteBreadcrumb
{
$router = app('router');
Expand Down
10 changes: 9 additions & 1 deletion src/Routing/RequestBreadcrumbs.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Glhd\Gretel\Routing;

use Glhd\Gretel\Exceptions\MissingBreadcrumbException;
use Glhd\Gretel\Registry;
use Glhd\Gretel\Resolvers\Resolver;
use Glhd\Gretel\View\Breadcrumb;
Expand Down Expand Up @@ -43,6 +44,13 @@ public function toCollection(): BreadcrumbCollection
return $this->breadcrumbs;
}

public function throwIfMissing(): void
{
if ($this->toCollection()->isEmpty()) {
throw new MissingBreadcrumbException($this->route->getName());
}
}

public function toArray(): array
{
return $this->toCollection()->toArray();
Expand All @@ -55,7 +63,7 @@ public function toJson($options = JSON_THROW_ON_ERROR)

public function __call($name, $arguments)
{
return $this->forwardDecoratedCallTo($this->breadcrumbs, $name, $arguments);
return $this->forwardDecoratedCallTo($this->toCollection(), $name, $arguments);
}

protected function walk($value, $depth = 0): ?Breadcrumb
Expand Down
25 changes: 25 additions & 0 deletions src/Support/Facades/Gretel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Glhd\Gretel\Support\Facades;

use Closure;
use Glhd\Gretel\Registry;
use Glhd\Gretel\Routing\RouteBreadcrumb;
use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Facade;

/**
* @method static Registry handleMissingBreadcrumbs(Closure $callback)
* @method static Registry throwOnMissingBreadcrumbs(bool $throw = true)
* @method static Registry handleMisconfiguredBreadcrumbs(Closure $callback)
* @method static Registry throwOnMisconfiguredBreadcrumbs(bool $throw = true)
* @method static RouteBreadcrumb|null get(Route|string $route)
* @method static RouteBreadcrumb getOrFail(Route|string $route)
*/
class Gretel extends Facade
{
protected static function getFacadeAccessor(): string
{
return Registry::class;
}
}
Loading

0 comments on commit 5d03db0

Please sign in to comment.