Skip to content

Commit 8efe1bc

Browse files
committed
TDD generation of Form Requests
1 parent 675b028 commit 8efe1bc

File tree

9 files changed

+454
-0
lines changed

9 files changed

+454
-0
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php
2+
3+
namespace Blueprint\Generators\Statements;
4+
5+
use Blueprint\Column;
6+
use Blueprint\Contracts\Generator;
7+
use Blueprint\Model;
8+
use Blueprint\Models\Statements\ValidateStatement;
9+
use Blueprint\Translators\Rules;
10+
use Illuminate\Support\Str;
11+
12+
class FormRequestGenerator implements Generator
13+
{
14+
private const INDENT = ' ';
15+
16+
/**
17+
* @var \Illuminate\Contracts\Filesystem\Filesystem
18+
*/
19+
private $files;
20+
21+
private $models = [];
22+
23+
public function __construct($files)
24+
{
25+
$this->files = $files;
26+
}
27+
28+
public function output(array $tree): array
29+
{
30+
$output = [];
31+
32+
$stub = $this->files->get(STUBS_PATH . '/form-request.stub');
33+
34+
$this->registerModels($tree['models']);
35+
36+
/** @var \Blueprint\Controller $controller */
37+
foreach ($tree['controllers'] as $controller) {
38+
foreach ($controller->methods() as $method => $statements) {
39+
foreach ($statements as $statement) {
40+
if (!$statement instanceof ValidateStatement) {
41+
continue;
42+
}
43+
44+
$context = $this->getContextFromController($controller->name());
45+
$name = $this->getName($context, $method);
46+
$path = $this->getPath($name);
47+
48+
if ($this->files->exists($path)) {
49+
continue;
50+
}
51+
52+
$this->files->put(
53+
$path,
54+
$this->populateStub($stub, $name, $context, $statement)
55+
);
56+
57+
$output['created'][] = $path;
58+
}
59+
}
60+
}
61+
62+
return $output;
63+
}
64+
65+
protected function getPath(string $name)
66+
{
67+
return 'app/Http/Requests/' . $name . '.php';
68+
}
69+
70+
protected function populateStub(string $stub, string $name, $context, ValidateStatement $validateStatement)
71+
{
72+
$stub = str_replace('DummyNamespace', 'App\\Http\\Requests', $stub);
73+
$stub = str_replace('DummyClass', $name, $stub);
74+
$stub = str_replace('// rules...', $this->buildRules($context, $validateStatement), $stub);
75+
76+
return $stub;
77+
}
78+
79+
private function buildRules(string $context, ValidateStatement $validateStatement)
80+
{
81+
return trim(array_reduce($validateStatement->data(), function ($output, $field) use ($context) {
82+
[$qualifier, $column] = $this->splitField($field);
83+
84+
if (is_null($qualifier)) {
85+
$qualifier = $context;
86+
}
87+
88+
$rules = $this->validationRules($qualifier, $column);
89+
90+
$output .= self::INDENT . "'{$column}' => '{$rules}'," . PHP_EOL;
91+
return $output;
92+
}, ''));
93+
}
94+
95+
private function getContextFromController(string $name)
96+
{
97+
$context = $name;
98+
99+
if (Str::endsWith($name, 'Controller')) {
100+
$context = Str::substr($name, 0, -10);
101+
}
102+
103+
return Str::singular($context);
104+
}
105+
106+
private function modelForContext(string $context)
107+
{
108+
return $this->models[Str::studly($context)] ?? $this->models[Str::lower($context)];
109+
}
110+
111+
private function getName(string $context, string $method)
112+
{
113+
return $context . Str::studly($method) . 'Request';
114+
}
115+
116+
private function splitField($field)
117+
{
118+
if (Str::contains($field, '.')) {
119+
return explode('.', $field, 2);
120+
}
121+
122+
return [null, $field];
123+
}
124+
125+
private function validationRules(string $qualifier, string $column)
126+
{
127+
/** @var Model $model */
128+
$model = $this->modelForContext($qualifier);
129+
130+
if (!is_null($model) && $model->hasColumn($column)) {
131+
$column = $model->column($column);
132+
133+
return implode('|', Rules::fromColumn($column));
134+
}
135+
136+
return 'required';
137+
}
138+
139+
private function registerModels(?array $models)
140+
{
141+
$this->models = $models ?? [];
142+
}
143+
144+
145+
}

src/Model.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,14 @@ public function enableSoftDeletes()
6363
{
6464
$this->softDeletes = true;
6565
}
66+
67+
public function hasColumn(string $name)
68+
{
69+
return isset($this->columns[$name]);
70+
}
71+
72+
public function column(string $name)
73+
{
74+
return $this->columns[$name];
75+
}
6676
}

src/Translators/Rules.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Blueprint\Translators;
4+
5+
use Blueprint\Column;
6+
7+
class Rules
8+
{
9+
public static function fromColumn(Column $column)
10+
{
11+
// TODO: what about nullable?
12+
$rules = ['required'];
13+
14+
// TODO: handle translation for...
15+
// common names (email)
16+
// relationship (user_id = exists:users,id)
17+
// dataType (integer,digit,date,etc)
18+
// attributes (lengths,precisions,enums|set)
19+
// modifiers (unsigned, nullable, unique)
20+
21+
// hack for tests...
22+
if (in_array($column->dataType(), ['string', 'longText'])) {
23+
$rules = array_merge($rules, ['string']);
24+
}
25+
26+
if ($column->attributes()) {
27+
if (in_array($column->dataType(), ['string', 'char'])) {
28+
$rules = array_merge($rules, ['max:' . implode($column->attributes())]);
29+
}
30+
}
31+
32+
return $rules;
33+
}
34+
}

stubs/form-request.stub

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace DummyNamespace;
4+
5+
use Illuminate\Foundation\Http\FormRequest;
6+
7+
class DummyClass extends FormRequest
8+
{
9+
/**
10+
* Determine if the user is authorized to make this request.
11+
*
12+
* @return bool
13+
*/
14+
public function authorize()
15+
{
16+
return true;
17+
}
18+
19+
/**
20+
* Get the validation rules that apply to the request.
21+
*
22+
* @return array
23+
*/
24+
public function rules()
25+
{
26+
return [
27+
// rules...
28+
];
29+
}
30+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<?php
2+
3+
namespace Tests\Feature\Generator\Statements;
4+
5+
use Blueprint\Blueprint;
6+
use Blueprint\Generators\Statements\FormRequestGenerator;
7+
use Blueprint\Lexers\StatementLexer;
8+
use Tests\TestCase;
9+
10+
/**
11+
* @see FormRequestGenerator
12+
*/
13+
class FormRequestGeneratorTest extends TestCase
14+
{
15+
private $blueprint;
16+
17+
private $files;
18+
19+
/** @var FormRequestGenerator */
20+
private $subject;
21+
22+
protected function setUp(): void
23+
{
24+
parent::setUp();
25+
26+
$this->files = \Mockery::mock();
27+
$this->subject = new FormRequestGenerator($this->files);
28+
29+
$this->blueprint = new Blueprint();
30+
$this->blueprint->registerLexer(new \Blueprint\Lexers\ModelLexer());
31+
$this->blueprint->registerLexer(new \Blueprint\Lexers\ControllerLexer(new StatementLexer()));
32+
$this->blueprint->registerGenerator($this->subject);
33+
}
34+
35+
/**
36+
* @test
37+
*/
38+
public function output_writes_nothing_for_empty_tree()
39+
{
40+
$this->files->expects('get')
41+
->with('stubs/form-request.stub')
42+
->andReturn(file_get_contents('stubs/form-request.stub'));
43+
44+
$this->files->shouldNotHaveReceived('put');
45+
46+
$this->assertEquals([], $this->subject->output(['controllers' => []]));
47+
}
48+
49+
/**
50+
* @test
51+
*/
52+
public function output_writes_nothing_without_validate_statements()
53+
{
54+
$this->files->expects('get')
55+
->with('stubs/form-request.stub')
56+
->andReturn(file_get_contents('stubs/form-request.stub'));
57+
58+
$this->files->shouldNotHaveReceived('put');
59+
60+
$tokens = $this->blueprint->parse($this->fixture('definitions/controllers-only.bp'));
61+
$tree = $this->blueprint->analyze($tokens);
62+
63+
$this->assertEquals([], $this->subject->output($tree));
64+
}
65+
66+
/**
67+
* @test
68+
*/
69+
public function output_writes_form_requests()
70+
{
71+
$this->files->expects('get')
72+
->with('stubs/form-request.stub')
73+
->andReturn(file_get_contents('stubs/form-request.stub'));
74+
75+
$this->files->expects('exists')
76+
->with('app/Http/Requests/PostIndexRequest.php')
77+
->andReturnFalse();
78+
$this->files->expects('put')
79+
->with('app/Http/Requests/PostIndexRequest.php', $this->fixture('form-requests/post-index.php'));
80+
81+
$this->files->expects('exists')
82+
->with('app/Http/Requests/PostStoreRequest.php')
83+
->andReturnFalse();
84+
$this->files->expects('put')
85+
->with('app/Http/Requests/PostStoreRequest.php', $this->fixture('form-requests/post-store.php'));
86+
87+
$this->files->expects('exists')
88+
->with('app/Http/Requests/OtherStoreRequest.php')
89+
->andReturnFalse();
90+
$this->files->expects('put')
91+
->with('app/Http/Requests/OtherStoreRequest.php', $this->fixture('form-requests/other-store.php'));
92+
93+
$tokens = $this->blueprint->parse($this->fixture('definitions/validate-statements.bp'));
94+
$tree = $this->blueprint->analyze($tokens);
95+
96+
$this->assertEquals(['created' => ['app/Http/Requests/PostIndexRequest.php', 'app/Http/Requests/PostStoreRequest.php', 'app/Http/Requests/OtherStoreRequest.php']], $this->subject->output($tree));
97+
}
98+
99+
/**
100+
* @test
101+
*/
102+
public function it_only_outputs_new_form_requests()
103+
{
104+
$this->files->expects('get')
105+
->with('stubs/form-request.stub')
106+
->andReturn(file_get_contents('stubs/form-request.stub'));
107+
108+
$this->files->expects('exists')
109+
->with('app/Http/Requests/PostIndexRequest.php')
110+
->andReturnTrue();
111+
$this->files->expects('exists')
112+
->with('app/Http/Requests/PostStoreRequest.php')
113+
->andReturnTrue();
114+
$this->files->expects('exists')
115+
->with('app/Http/Requests/OtherStoreRequest.php')
116+
->andReturnTrue();
117+
118+
$tokens = $this->blueprint->parse($this->fixture('definitions/validate-statements.bp'));
119+
$tree = $this->blueprint->analyze($tokens);
120+
121+
$this->assertEquals([], $this->subject->output($tree));
122+
}
123+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
models:
2+
Post:
3+
title: string:400
4+
content: longtext
5+
published_at: nullable timestamp
6+
7+
controllers:
8+
Post:
9+
index:
10+
validate: post.title
11+
render: post.index with:posts
12+
13+
store:
14+
validate: title, content
15+
redirect: post.index
16+
17+
Other:
18+
store:
19+
validate: other.field, other.text
20+
redirect: post.index

0 commit comments

Comments
 (0)