Skip to content

Commit 0f01500

Browse files
committed
Some refactoring to allow catching exceptions in shortcode class code, added validation helper function to shortcode class
1 parent 4d10e7f commit 0f01500

File tree

9 files changed

+218
-34
lines changed

9 files changed

+218
-34
lines changed

readme.md

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ common data types. The $casts property should be an array where the key is the n
155155
being cast and the value is the type you wish to cast the column to. The supported cast types are:
156156
`int`, `integer`, `real`, `float`, `double`, `boolean`, `array` (comma separated values) and `date`.
157157

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

175175

176-
### Option to not throw exceptions from views
176+
### Attribute validation
177+
178+
There is a simple way to validate attributes.
179+
Error messages will be rendered on the shortcode place.
180+
For convenients it will return attributes.
181+
182+
```php
183+
class YourShortcode extends Shortcode
184+
{
185+
/**
186+
* Render shortcode
187+
*
188+
* @param string $content
189+
* @return string
190+
*/
191+
public function render($content)
192+
{
193+
$atts = $this->validate([
194+
'post_id' => 'required|numeric|exists:posts,id',
195+
]);
196+
197+
//
198+
}
199+
}
200+
```
201+
202+
### Option to not throw exceptions from shortcodes
177203

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

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

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

212237
## TODO
213238

214-
1. Integrate Laravel Telescope
215-
1. Attributes validation
216239
1. Add custom widget for debugbar integration
217240
1. Create performance profile tests, optimize performance
218241

src/Manager.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,20 @@ class Manager
1313
use Macroable;
1414

1515
/**
16-
* @var array
16+
* @var array Configuration
1717
*/
1818
public $config;
19+
1920
/**
2021
* @var array Shared attributes
2122
*/
2223
public $shared = [];
24+
2325
/**
2426
* @var Application
2527
*/
2628
protected $app;
29+
2730
/**
2831
* @var Renderer
2932
*/

src/Renderer.php

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44

55
use Exception;
66
use Illuminate\Contracts\Foundation\Application;
7+
use Illuminate\Support\Arr;
8+
use Illuminate\Support\Facades\Log;
79
use Illuminate\Support\Traits\Macroable;
10+
use Illuminate\Validation\ValidationException;
11+
use Throwable;
812

913
class Renderer
1014
{
@@ -121,7 +125,7 @@ private function doShortcodeTag($m)
121125
if (! $instance instanceof Shortcode) {
122126
$content = "Class {$shortcode} is not an instance of " . Shortcode::class;
123127
} else {
124-
$content = $m[1] . $instance->render(isset($m[5]) ? $m[5] : null) . $m[6];
128+
$content = $m[1] . $this->renderShortcode($instance, isset($m[5]) ? $m[5] : null) . $m[6];
125129
}
126130
} else {
127131
$content = "Class {$shortcode} doesn't exists";
@@ -132,6 +136,36 @@ private function doShortcodeTag($m)
132136
return $content;
133137
}
134138

139+
/**
140+
* Render shortcode from the class instance
141+
*
142+
* @param Shortcode $shortcode
143+
* @param string|null $content
144+
* @return string
145+
*/
146+
private function renderShortcode(Shortcode $shortcode, $content)
147+
{
148+
try {
149+
return $shortcode->render($content);
150+
} catch (ValidationException $e) {
151+
return 'Validation error: <br>' . implode('<br>', Arr::flatten($e->errors()));
152+
} catch (Throwable $e) {
153+
if ($this->manager->config['throw_exceptions']) {
154+
throw $e;
155+
}
156+
157+
Log::error($e);
158+
// Report to sentry if it's intergated
159+
if (class_exists('Sentry')) {
160+
if (app()->environment('production')) {
161+
\Sentry::captureException($e);
162+
}
163+
}
164+
165+
return "[$shortcode->tag] " . get_class($e) . ' ' . $e->getMessage();
166+
}
167+
}
168+
135169
/**
136170
* Record rendered shortcode info.
137171
*

src/Shortcode.php

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
use Illuminate\Contracts\Foundation\Application;
77
use Illuminate\Support\Arr;
88
use Illuminate\Support\Collection;
9-
use Illuminate\Support\Facades\Log;
109
use Illuminate\Support\Traits\Macroable;
11-
use Throwable;
1210

1311
abstract class Shortcode implements ShortcodeContract
1412
{
@@ -29,6 +27,11 @@ abstract class Shortcode implements ShortcodeContract
2927
*/
3028
public $attributes = [];
3129

30+
/**
31+
* @var string Rendered tag name
32+
*/
33+
public $tag;
34+
3235
/**
3336
* @var Manager
3437
*/
@@ -46,11 +49,6 @@ abstract class Shortcode implements ShortcodeContract
4649
*/
4750
protected $casts = [];
4851

49-
/**
50-
* @var string Rendered tag name
51-
*/
52-
protected $tag;
53-
5452
/**
5553
* AbstractShortcode constructor.
5654
*
@@ -77,6 +75,21 @@ public function atts(): array
7775
return $this->applyDefaultAtts($this->attributes(), $this->atts);
7876
}
7977

78+
/**
79+
* Validate and return attributes
80+
*
81+
* @param array $rules
82+
* @return array
83+
*/
84+
public function validate(array $rules)
85+
{
86+
$atts = $this->atts();
87+
88+
$this->app->make('validator')->validate($this->atts(), $rules);
89+
90+
return $atts;
91+
}
92+
8093
/**
8194
* Combine user attributes with known attributes and fill in defaults when needed.
8295
*
@@ -134,24 +147,7 @@ public function shared($key = null, $defatul = null)
134147
*/
135148
protected function view($name, $data = [])
136149
{
137-
if ($this->manager->config['throw_exceptions']) {
138-
return $this->app['view']->make($name, $data)->render();
139-
}
140-
141-
// Render view without throwing exceptions
142-
try {
143-
return $this->app['view']->make($name, $data)->renderSimple();
144-
} catch (Throwable $e) {
145-
Log::error($e);
146-
// Report to sentry if it's intergated
147-
if (class_exists('Sentry')) {
148-
if (app()->environment('production')) {
149-
\Sentry::captureException($e);
150-
}
151-
}
152-
153-
return "[$this->tag] ".get_class($e).' '.$e->getMessage();
154-
}
150+
return $this->app['view']->make($name, $data)->render();
155151
}
156152

157153
/**

tests/Resources/ExceptionShortcode.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,6 @@ class ExceptionShortcode extends Shortcode
3030
*/
3131
public function render($content)
3232
{
33-
return $this->view('shortcode-exception');
33+
return $someUnknownVar;
3434
}
3535
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace Vedmant\LaravelShortcodes\Tests\Resources;
4+
5+
use Vedmant\LaravelShortcodes\Shortcode;
6+
7+
class ExceptionViewShortcode extends Shortcode
8+
{
9+
/**
10+
* @var string Shortcode description
11+
*/
12+
public $description = 'Exception shortcode for test';
13+
14+
/**
15+
* @var array Shortcode attributes with default values
16+
*/
17+
public $attributes = [
18+
'class' => [
19+
'default' => '',
20+
'description' => 'Class name',
21+
'sample' => 'some-class',
22+
],
23+
];
24+
25+
/**
26+
* Render shortcode.
27+
*
28+
* @param string $content
29+
* @return string
30+
*/
31+
public function render($content)
32+
{
33+
return $this->view('shortcode-exception');
34+
}
35+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace Vedmant\LaravelShortcodes\Tests\Resources;
4+
5+
use Vedmant\LaravelShortcodes\Shortcode;
6+
7+
class ValidationShortcode extends Shortcode
8+
{
9+
/**
10+
* @var string Shortcode description
11+
*/
12+
public $description = 'Test shortcode with validation';
13+
14+
/**
15+
* @var array Shortcode attributes with default values
16+
*/
17+
public $attributes = [
18+
'required' => [
19+
'default' => '',
20+
],
21+
'string' => [
22+
'default' => '',
23+
],
24+
'numeric' => [
25+
'default' => '',
26+
],
27+
];
28+
29+
/**
30+
* Render shortcode.
31+
*
32+
* @param string $content
33+
* @return string
34+
*/
35+
public function render($content)
36+
{
37+
$this->validate([
38+
'required' => 'required',
39+
'string' => 'required|string',
40+
'numeric' => 'required|numeric',
41+
]);
42+
43+
return 'Success';
44+
}
45+
}

tests/Unit/ShortcodeTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
namespace Vedmant\LaravelShortcodes\Tests\Unit;
44

55
use Carbon\Carbon;
6+
use Illuminate\Validation\ValidationException;
67
use Vedmant\LaravelShortcodes\Tests\Resources\CastsShortcode;
8+
use Vedmant\LaravelShortcodes\Tests\Resources\ValidationShortcode;
79
use Vedmant\LaravelShortcodes\Tests\TestCase;
810

911
class ShortcodeTest extends TestCase
@@ -38,4 +40,16 @@ public function testCasts()
3840
$this->assertIsArray($atts['json']);
3941
$this->assertInstanceOf(Carbon::class, $atts['date']);
4042
}
43+
44+
public function testValidation()
45+
{
46+
$shortcode = new ValidationShortcode($this->app, $this->manager, [], 'validation');
47+
48+
try {
49+
$shortcode->render(null);
50+
$this->fail('Expected ValidationException not thrown');
51+
} catch(ValidationException $e) {
52+
$this->assertCount(3, $e->errors());
53+
}
54+
}
4155
}

tests/Unit/ViewTest.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace Vedmant\LaravelShortcodes\Tests\Unit;
44

55
use Vedmant\LaravelShortcodes\Tests\Resources\ExceptionShortcode;
6+
use Vedmant\LaravelShortcodes\Tests\Resources\ExceptionViewShortcode;
7+
use Vedmant\LaravelShortcodes\Tests\Resources\ValidationShortcode;
68
use Vedmant\LaravelShortcodes\Tests\TestCase;
79

810
class ViewTest extends TestCase
@@ -73,6 +75,17 @@ public function testRenderWithoutThrowing()
7375

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

78+
$this->assertStringStartsWith('[exception] ErrorException Undefined variable: someUnknownVar', (string) $rendered);
79+
}
80+
81+
public function testRenderWithoutThrowingInView()
82+
{
83+
$this->manager->config['throw_exceptions'] = false;
84+
$this->addViewsPath();
85+
$this->manager->add('exception', ExceptionViewShortcode::class);
86+
87+
$rendered = $this->app['view']->make('exception')->render();
88+
7689
$this->assertStringStartsWith('[exception] ErrorException Undefined variable: notExisting ', (string) $rendered);
7790
}
7891

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

98+
$this->expectExceptionMessage('Undefined variable: someUnknownVar');
99+
100+
$rendered = $this->app['view']->make('exception')->render();
101+
}
102+
103+
public function testRenderWithThrowingInView()
104+
{
105+
$this->manager->config['throw_exceptions'] = true;
106+
$this->addViewsPath();
107+
$this->manager->add('exception', ExceptionViewShortcode::class);
108+
85109
$this->expectExceptionMessage('Undefined variable: notExisting');
86110

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

114+
public function testRenderWithValidation()
115+
{
116+
$this->addViewsPath();
117+
118+
$this->manager->add('validation', ValidationShortcode::class);
119+
120+
$rendered = $this->manager->render('[validation]');
121+
$this->assertEquals('Validation error: <br>The required field is required.<br>The string field is required.<br>The numeric field is required.', (string) $rendered);
122+
}
123+
90124
private function addViewsPath()
91125
{
92126
app('view')->addLocation(__DIR__.'/../views');

0 commit comments

Comments
 (0)