Skip to content

Commit 1a62e81

Browse files
author
Brian Faust
authored
Add support for generating policies (#614)
1 parent e6f9cb1 commit 1a62e81

31 files changed

+993
-6
lines changed

config/blueprint.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030

3131
'controllers_namespace' => 'Http\\Controllers',
3232

33+
'policy_namespace' => 'Policies',
34+
3335
/*
3436
|--------------------------------------------------------------------------
3537
| Application Path
@@ -154,6 +156,7 @@
154156
'notification' => \Blueprint\Generators\Statements\NotificationGenerator::class,
155157
'resource' => \Blueprint\Generators\Statements\ResourceGenerator::class,
156158
'view' => \Blueprint\Generators\Statements\ViewGenerator::class,
159+
'policy' => \Blueprint\Generators\PolicyGenerator::class,
157160
],
158161

159162
];

src/Generators/ControllerGenerator.php

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Blueprint\Concerns\HandlesTraits;
77
use Blueprint\Contracts\Generator;
88
use Blueprint\Models\Controller;
9+
use Blueprint\Models\Policy;
910
use Blueprint\Models\Statements\DispatchStatement;
1011
use Blueprint\Models\Statements\EloquentStatement;
1112
use Blueprint\Models\Statements\FireStatement;
@@ -62,22 +63,57 @@ protected function buildMethods(Controller $controller)
6263

6364
$methods = '';
6465

66+
$controllerModelName = Str::singular($controller->prefix());
67+
68+
if ($controller->policy()?->authorizeResource()) {
69+
$methods .= str_replace(
70+
[
71+
'{{ modelClass }}',
72+
'{{ modelVariable }}',
73+
],
74+
[
75+
Str::studly($controllerModelName),
76+
Str::camel($controllerModelName),
77+
],
78+
$this->filesystem->stub('controller.authorize-resource.stub')
79+
);
80+
}
81+
6582
foreach ($controller->methods() as $name => $statements) {
6683
$method = str_replace('{{ method }}', $name, $template);
6784

6885
if (in_array($name, ['edit', 'update', 'show', 'destroy'])) {
69-
$context = Str::singular($controller->prefix());
70-
$reference = $this->fullyQualifyModelReference($controller->namespace(), Str::camel($context));
71-
$variable = '$' . Str::camel($context);
86+
$reference = $this->fullyQualifyModelReference($controller->namespace(), $controllerModelName);
87+
$variable = '$' . Str::camel($controllerModelName);
7288

7389
$search = '(Request $request';
74-
$method = str_replace($search, $search . ', ' . $context . ' ' . $variable, $method);
90+
$method = str_replace($search, $search . ', ' . $controllerModelName . ' ' . $variable, $method);
7591
$this->addImport($controller, $reference);
7692
}
7793

7894
$body = '';
7995
$using_validation = false;
8096

97+
if ($controller->policy() && !$controller->policy()->authorizeResource()) {
98+
if (in_array(Policy::$resourceAbilityMap[$name], $controller->policy()->methods())) {
99+
$body .= self::INDENT . str_replace(
100+
[
101+
'{{ method }}',
102+
'{{ modelClass }}',
103+
'{{ modelVariable }}',
104+
],
105+
[
106+
$name,
107+
Str::studly($controllerModelName),
108+
'$' . Str::camel($controllerModelName),
109+
],
110+
in_array($name, ['index', 'create', 'store'])
111+
? "\$this->authorize('{{ method }}', {{ modelClass }}::class);"
112+
: "\$this->authorize('{{ method }}', {{ modelVariable }});"
113+
) . PHP_EOL . PHP_EOL;
114+
}
115+
}
116+
81117
foreach ($statements as $statement) {
82118
if ($statement instanceof SendStatement) {
83119
$body .= self::INDENT . $statement->output() . PHP_EOL;

src/Generators/PolicyGenerator.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
namespace Blueprint\Generators;
4+
5+
use Blueprint\Concerns\HandlesImports;
6+
use Blueprint\Contracts\Generator;
7+
use Blueprint\Models\Policy;
8+
use Blueprint\Tree;
9+
use Illuminate\Support\Str;
10+
11+
class PolicyGenerator extends AbstractClassGenerator implements Generator
12+
{
13+
use HandlesImports;
14+
15+
protected $types = ['policies'];
16+
17+
public function output(Tree $tree): array
18+
{
19+
$this->tree = $tree;
20+
21+
$stub = $this->filesystem->stub('policy.class.stub');
22+
23+
/** @var \Blueprint\Models\Policy $policy */
24+
foreach ($tree->policies() as $policy) {
25+
$this->addImport($policy, $policy->fullyQualifiedModelClassName());
26+
27+
$path = $this->getPath($policy);
28+
29+
$this->create($path, $this->populateStub($stub, $policy));
30+
}
31+
32+
return $this->output;
33+
}
34+
35+
protected function populateStub(string $stub, Policy $policy)
36+
{
37+
$stub = str_replace('{{ namespace }}', $policy->fullyQualifiedNamespace(), $stub);
38+
$stub = str_replace('{{ class }}', $policy->className(), $stub);
39+
$stub = str_replace('{{ methods }}', $this->buildMethods($policy), $stub);
40+
$stub = str_replace('{{ imports }}', $this->buildImports($policy), $stub);
41+
42+
return $stub;
43+
}
44+
45+
protected function buildMethods(Policy $policy)
46+
{
47+
$methods = '';
48+
49+
foreach ($policy->methods() as $name) {
50+
$methods .= str_replace(
51+
[
52+
'{{ modelClass }}',
53+
'{{ modelVariable }}',
54+
],
55+
[
56+
Str::studly($policy->name()),
57+
Str::camel($policy->name()),
58+
],
59+
$this->filesystem->stub('policy.method.' . $name . '.stub'),
60+
) . PHP_EOL;
61+
}
62+
63+
return trim($methods);
64+
}
65+
}

src/Lexers/ControllerLexer.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
use Blueprint\Contracts\Lexer;
66
use Blueprint\Models\Controller;
7+
use Blueprint\Models\Policy;
8+
use Illuminate\Support\Arr;
79
use Illuminate\Support\Str;
810

911
class ControllerLexer implements Lexer
@@ -20,7 +22,10 @@ public function __construct(StatementLexer $statementLexer)
2022

2123
public function analyze(array $tokens): array
2224
{
23-
$registry = ['controllers' => []];
25+
$registry = [
26+
'controllers' => [],
27+
'policies' => [],
28+
];
2429

2530
if (empty($tokens['controllers'])) {
2631
return $registry;
@@ -50,6 +55,31 @@ public function analyze(array $tokens): array
5055
unset($definition['invokable']);
5156
}
5257

58+
if (isset($definition['meta'])) {
59+
if (isset($definition['meta']['policies'])) {
60+
$authorizeResource = Arr::get($definition, 'meta.policies', true);
61+
62+
$policy = new Policy(
63+
$controller->prefix(),
64+
$authorizeResource === true
65+
? Policy::$supportedMethods
66+
: array_unique(
67+
array_map(
68+
fn (string $method): string => Policy::$resourceAbilityMap[$method],
69+
preg_split('/,([ \t]+)?/', $definition['meta']['policies'])
70+
)
71+
),
72+
$authorizeResource === true,
73+
);
74+
75+
$controller->policy($policy);
76+
77+
$registry['policies'][] = $policy;
78+
}
79+
80+
unset($definition['meta']);
81+
}
82+
5383
foreach ($definition as $method => $body) {
5484
$controller->addMethod($method, $this->statementLexer->analyze($body));
5585
}

src/Models/Controller.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ class Controller implements BlueprintModel
2828
*/
2929
private $methods = [];
3030

31+
/**
32+
* @var Policy
33+
*/
34+
private $policy;
35+
3136
/**
3237
* @var bool
3338
*/
@@ -91,6 +96,15 @@ public function addMethod(string $name, array $statements)
9196
$this->methods[$name] = $statements;
9297
}
9398

99+
public function policy(?Policy $policy = null): ?Policy
100+
{
101+
if ($policy) {
102+
$this->policy = $policy;
103+
}
104+
105+
return $this->policy;
106+
}
107+
94108
public function prefix()
95109
{
96110
if (Str::endsWith($this->name(), 'Controller')) {

src/Models/Policy.php

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<?php
2+
3+
namespace Blueprint\Models;
4+
5+
use Blueprint\Contracts\Model as BlueprintModel;
6+
use Illuminate\Support\Str;
7+
8+
class Policy implements BlueprintModel
9+
{
10+
/** @var array */
11+
public static $supportedMethods = [
12+
'viewAny',
13+
'view',
14+
'create',
15+
'update',
16+
'delete',
17+
'restore',
18+
'forceDelete',
19+
];
20+
21+
/** @var array */
22+
public static $resourceAbilityMap = [
23+
'index' => 'viewAny',
24+
'show' => 'view',
25+
'create' => 'create',
26+
'store' => 'create',
27+
'edit' => 'update',
28+
'update' => 'update',
29+
'destroy' => 'delete',
30+
];
31+
32+
/**
33+
* @var string
34+
*/
35+
private $name;
36+
37+
/**
38+
* @var string
39+
*/
40+
private $namespace;
41+
42+
/**
43+
* @var array<int, string>
44+
*/
45+
private $methods;
46+
47+
/**
48+
* @var bool
49+
*/
50+
private $authorizeResource;
51+
52+
/**
53+
* Controller constructor.
54+
*/
55+
public function __construct(string $name, array $methods, bool $authorizeResource)
56+
{
57+
$this->name = class_basename($name);
58+
$this->namespace = trim(implode('\\', array_slice(explode('\\', str_replace('/', '\\', $name)), 0, -1)), '\\');
59+
$this->methods = $methods;
60+
$this->authorizeResource = $authorizeResource;
61+
}
62+
63+
public function name(): string
64+
{
65+
return $this->name;
66+
}
67+
68+
public function className(): string
69+
{
70+
return $this->name() . (Str::endsWith($this->name(), 'Policy') ? '' : 'Policy');
71+
}
72+
73+
public function namespace()
74+
{
75+
if (empty($this->namespace)) {
76+
return '';
77+
}
78+
79+
return $this->namespace;
80+
}
81+
82+
public function fullyQualifiedNamespace()
83+
{
84+
$fqn = config('blueprint.namespace');
85+
86+
if (config('blueprint.policy_namespace')) {
87+
$fqn .= '\\' . config('blueprint.policy_namespace');
88+
}
89+
90+
if ($this->namespace) {
91+
$fqn .= '\\' . $this->namespace;
92+
}
93+
94+
return $fqn;
95+
}
96+
97+
public function fullyQualifiedClassName()
98+
{
99+
return $this->fullyQualifiedNamespace() . '\\' . $this->className();
100+
}
101+
102+
public function methods(): array
103+
{
104+
return $this->methods;
105+
}
106+
107+
public function authorizeResource(): bool
108+
{
109+
return $this->authorizeResource;
110+
}
111+
112+
public function fullyQualifiedModelClassName()
113+
{
114+
$fqn = config('blueprint.namespace');
115+
116+
if (config('blueprint.models_namespace')) {
117+
$fqn .= '\\' . config('blueprint.models_namespace');
118+
}
119+
120+
if ($this->namespace) {
121+
$fqn .= '\\' . $this->namespace;
122+
}
123+
124+
return $fqn . '\\' . $this->name;
125+
}
126+
}

src/Tree.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ public function models()
3030
return $this->tree['models'];
3131
}
3232

33+
public function policies()
34+
{
35+
return $this->tree['policies'];
36+
}
37+
3338
public function seeders()
3439
{
3540
return $this->tree['seeders'];
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
public function __construct()
2+
{
3+
$this->authorizeResource({{ modelClass }}::class, '{{ modelVariable }}');
4+
}

stubs/policy.class.stub

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace {{ namespace }};
4+
5+
use App\Models\User;
6+
{{ imports }}
7+
use Illuminate\Auth\Access\Response;
8+
9+
class {{ class }}
10+
{
11+
{{ methods }}
12+
}

stubs/policy.method.create.stub

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* Determine whether the user can create models.
3+
*/
4+
public function create(User $user): bool
5+
{
6+
//
7+
}

0 commit comments

Comments
 (0)