Skip to content

Commit

Permalink
Some refactoring to allow catching exceptions in shortcode class code…
Browse files Browse the repository at this point in the history
…, added validation helper function to shortcode class
  • Loading branch information
vedmant committed Feb 17, 2020
1 parent 4d10e7f commit 0f01500
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 34 deletions.
35 changes: 29 additions & 6 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ common data types. The $casts property should be an array where the key is the n
being cast and the value is the type you wish to cast the column to. The supported cast types are:
`int`, `integer`, `real`, `float`, `double`, `boolean`, `array` (comma separated values) and `date`.

```blade
```php
class YourShortcode extends Shortcode
{
/**
Expand All @@ -173,12 +173,37 @@ Now the `show_ids` attribute will always be cast to an array when you access it.
(array attributes are casted from comma separated string, eg. "1,2,3").


### Option to not throw exceptions from views
### Attribute validation

There is a simple way to validate attributes.
Error messages will be rendered on the shortcode place.
For convenients it will return attributes.

```php
class YourShortcode extends Shortcode
{
/**
* Render shortcode
*
* @param string $content
* @return string
*/
public function render($content)
{
$atts = $this->validate([
'post_id' => 'required|numeric|exists:posts,id',
]);

//
}
}
```

### Option to not throw exceptions from shortcodes

There is a useful option to aviod server (500) error for whole page when one of shortocode views has thrown an exception.
There is a useful option to aviod server (500) error for whole page when one of shortocode has thrown an exception.

To enable it set `'throw_exceptions' => false,` in the `shortcodes.php` config file.
It works only when `$this->view('some-view');` method is used in the shortcode class.

This will render exception details in the place of a shortcode and will not crash whole page request with 500 error.
It will still log exception to a log file and report to [Sentry](https://sentry.io/) if it's integrated.
Expand Down Expand Up @@ -211,8 +236,6 @@ $ vendor/bin/phpunit

## TODO

1. Integrate Laravel Telescope
1. Attributes validation
1. Add custom widget for debugbar integration
1. Create performance profile tests, optimize performance

Expand Down
5 changes: 4 additions & 1 deletion src/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,20 @@ class Manager
use Macroable;

/**
* @var array
* @var array Configuration
*/
public $config;

/**
* @var array Shared attributes
*/
public $shared = [];

/**
* @var Application
*/
protected $app;

/**
* @var Renderer
*/
Expand Down
36 changes: 35 additions & 1 deletion src/Renderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@

use Exception;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Validation\ValidationException;
use Throwable;

class Renderer
{
Expand Down Expand Up @@ -121,7 +125,7 @@ private function doShortcodeTag($m)
if (! $instance instanceof Shortcode) {
$content = "Class {$shortcode} is not an instance of " . Shortcode::class;
} else {
$content = $m[1] . $instance->render(isset($m[5]) ? $m[5] : null) . $m[6];
$content = $m[1] . $this->renderShortcode($instance, isset($m[5]) ? $m[5] : null) . $m[6];
}
} else {
$content = "Class {$shortcode} doesn't exists";
Expand All @@ -132,6 +136,36 @@ private function doShortcodeTag($m)
return $content;
}

/**
* Render shortcode from the class instance
*
* @param Shortcode $shortcode
* @param string|null $content
* @return string
*/
private function renderShortcode(Shortcode $shortcode, $content)
{
try {
return $shortcode->render($content);
} catch (ValidationException $e) {
return 'Validation error: <br>' . implode('<br>', Arr::flatten($e->errors()));
} catch (Throwable $e) {
if ($this->manager->config['throw_exceptions']) {
throw $e;
}

Log::error($e);
// Report to sentry if it's intergated
if (class_exists('Sentry')) {
if (app()->environment('production')) {
\Sentry::captureException($e);
}
}

return "[$shortcode->tag] " . get_class($e) . ' ' . $e->getMessage();
}
}

/**
* Record rendered shortcode info.
*
Expand Down
46 changes: 21 additions & 25 deletions src/Shortcode.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Traits\Macroable;
use Throwable;

abstract class Shortcode implements ShortcodeContract
{
Expand All @@ -29,6 +27,11 @@ abstract class Shortcode implements ShortcodeContract
*/
public $attributes = [];

/**
* @var string Rendered tag name
*/
public $tag;

/**
* @var Manager
*/
Expand All @@ -46,11 +49,6 @@ abstract class Shortcode implements ShortcodeContract
*/
protected $casts = [];

/**
* @var string Rendered tag name
*/
protected $tag;

/**
* AbstractShortcode constructor.
*
Expand All @@ -77,6 +75,21 @@ public function atts(): array
return $this->applyDefaultAtts($this->attributes(), $this->atts);
}

/**
* Validate and return attributes
*
* @param array $rules
* @return array
*/
public function validate(array $rules)
{
$atts = $this->atts();

$this->app->make('validator')->validate($this->atts(), $rules);

return $atts;
}

/**
* Combine user attributes with known attributes and fill in defaults when needed.
*
Expand Down Expand Up @@ -134,24 +147,7 @@ public function shared($key = null, $defatul = null)
*/
protected function view($name, $data = [])
{
if ($this->manager->config['throw_exceptions']) {
return $this->app['view']->make($name, $data)->render();
}

// Render view without throwing exceptions
try {
return $this->app['view']->make($name, $data)->renderSimple();
} catch (Throwable $e) {
Log::error($e);
// Report to sentry if it's intergated
if (class_exists('Sentry')) {
if (app()->environment('production')) {
\Sentry::captureException($e);
}
}

return "[$this->tag] ".get_class($e).' '.$e->getMessage();
}
return $this->app['view']->make($name, $data)->render();
}

/**
Expand Down
2 changes: 1 addition & 1 deletion tests/Resources/ExceptionShortcode.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ class ExceptionShortcode extends Shortcode
*/
public function render($content)
{
return $this->view('shortcode-exception');
return $someUnknownVar;
}
}
35 changes: 35 additions & 0 deletions tests/Resources/ExceptionViewShortcode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace Vedmant\LaravelShortcodes\Tests\Resources;

use Vedmant\LaravelShortcodes\Shortcode;

class ExceptionViewShortcode extends Shortcode
{
/**
* @var string Shortcode description
*/
public $description = 'Exception shortcode for test';

/**
* @var array Shortcode attributes with default values
*/
public $attributes = [
'class' => [
'default' => '',
'description' => 'Class name',
'sample' => 'some-class',
],
];

/**
* Render shortcode.
*
* @param string $content
* @return string
*/
public function render($content)
{
return $this->view('shortcode-exception');
}
}
45 changes: 45 additions & 0 deletions tests/Resources/ValidationShortcode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace Vedmant\LaravelShortcodes\Tests\Resources;

use Vedmant\LaravelShortcodes\Shortcode;

class ValidationShortcode extends Shortcode
{
/**
* @var string Shortcode description
*/
public $description = 'Test shortcode with validation';

/**
* @var array Shortcode attributes with default values
*/
public $attributes = [
'required' => [
'default' => '',
],
'string' => [
'default' => '',
],
'numeric' => [
'default' => '',
],
];

/**
* Render shortcode.
*
* @param string $content
* @return string
*/
public function render($content)
{
$this->validate([
'required' => 'required',
'string' => 'required|string',
'numeric' => 'required|numeric',
]);

return 'Success';
}
}
14 changes: 14 additions & 0 deletions tests/Unit/ShortcodeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
namespace Vedmant\LaravelShortcodes\Tests\Unit;

use Carbon\Carbon;
use Illuminate\Validation\ValidationException;
use Vedmant\LaravelShortcodes\Tests\Resources\CastsShortcode;
use Vedmant\LaravelShortcodes\Tests\Resources\ValidationShortcode;
use Vedmant\LaravelShortcodes\Tests\TestCase;

class ShortcodeTest extends TestCase
Expand Down Expand Up @@ -38,4 +40,16 @@ public function testCasts()
$this->assertIsArray($atts['json']);
$this->assertInstanceOf(Carbon::class, $atts['date']);
}

public function testValidation()
{
$shortcode = new ValidationShortcode($this->app, $this->manager, [], 'validation');

try {
$shortcode->render(null);
$this->fail('Expected ValidationException not thrown');
} catch(ValidationException $e) {
$this->assertCount(3, $e->errors());
}
}
}
34 changes: 34 additions & 0 deletions tests/Unit/ViewTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace Vedmant\LaravelShortcodes\Tests\Unit;

use Vedmant\LaravelShortcodes\Tests\Resources\ExceptionShortcode;
use Vedmant\LaravelShortcodes\Tests\Resources\ExceptionViewShortcode;
use Vedmant\LaravelShortcodes\Tests\Resources\ValidationShortcode;
use Vedmant\LaravelShortcodes\Tests\TestCase;

class ViewTest extends TestCase
Expand Down Expand Up @@ -73,6 +75,17 @@ public function testRenderWithoutThrowing()

$rendered = $this->app['view']->make('exception')->render();

$this->assertStringStartsWith('[exception] ErrorException Undefined variable: someUnknownVar', (string) $rendered);
}

public function testRenderWithoutThrowingInView()
{
$this->manager->config['throw_exceptions'] = false;
$this->addViewsPath();
$this->manager->add('exception', ExceptionViewShortcode::class);

$rendered = $this->app['view']->make('exception')->render();

$this->assertStringStartsWith('[exception] ErrorException Undefined variable: notExisting ', (string) $rendered);
}

Expand All @@ -82,11 +95,32 @@ public function testRenderWithThrowing()
$this->addViewsPath();
$this->manager->add('exception', ExceptionShortcode::class);

$this->expectExceptionMessage('Undefined variable: someUnknownVar');

$rendered = $this->app['view']->make('exception')->render();
}

public function testRenderWithThrowingInView()
{
$this->manager->config['throw_exceptions'] = true;
$this->addViewsPath();
$this->manager->add('exception', ExceptionViewShortcode::class);

$this->expectExceptionMessage('Undefined variable: notExisting');

$rendered = $this->app['view']->make('exception')->render();
}

public function testRenderWithValidation()
{
$this->addViewsPath();

$this->manager->add('validation', ValidationShortcode::class);

$rendered = $this->manager->render('[validation]');
$this->assertEquals('Validation error: <br>The required field is required.<br>The string field is required.<br>The numeric field is required.', (string) $rendered);
}

private function addViewsPath()
{
app('view')->addLocation(__DIR__.'/../views');
Expand Down

0 comments on commit 0f01500

Please sign in to comment.