Skip to content

Commit

Permalink
Add support for generating policies (#614)
Browse files Browse the repository at this point in the history
  • Loading branch information
faustbrian authored May 6, 2023
1 parent e6f9cb1 commit 1a62e81
Show file tree
Hide file tree
Showing 31 changed files with 993 additions and 6 deletions.
3 changes: 3 additions & 0 deletions config/blueprint.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@

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

'policy_namespace' => 'Policies',

/*
|--------------------------------------------------------------------------
| Application Path
Expand Down Expand Up @@ -154,6 +156,7 @@
'notification' => \Blueprint\Generators\Statements\NotificationGenerator::class,
'resource' => \Blueprint\Generators\Statements\ResourceGenerator::class,
'view' => \Blueprint\Generators\Statements\ViewGenerator::class,
'policy' => \Blueprint\Generators\PolicyGenerator::class,
],

];
44 changes: 40 additions & 4 deletions src/Generators/ControllerGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Blueprint\Concerns\HandlesTraits;
use Blueprint\Contracts\Generator;
use Blueprint\Models\Controller;
use Blueprint\Models\Policy;
use Blueprint\Models\Statements\DispatchStatement;
use Blueprint\Models\Statements\EloquentStatement;
use Blueprint\Models\Statements\FireStatement;
Expand Down Expand Up @@ -62,22 +63,57 @@ protected function buildMethods(Controller $controller)

$methods = '';

$controllerModelName = Str::singular($controller->prefix());

if ($controller->policy()?->authorizeResource()) {
$methods .= str_replace(
[
'{{ modelClass }}',
'{{ modelVariable }}',
],
[
Str::studly($controllerModelName),
Str::camel($controllerModelName),
],
$this->filesystem->stub('controller.authorize-resource.stub')
);
}

foreach ($controller->methods() as $name => $statements) {
$method = str_replace('{{ method }}', $name, $template);

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

$search = '(Request $request';
$method = str_replace($search, $search . ', ' . $context . ' ' . $variable, $method);
$method = str_replace($search, $search . ', ' . $controllerModelName . ' ' . $variable, $method);
$this->addImport($controller, $reference);
}

$body = '';
$using_validation = false;

if ($controller->policy() && !$controller->policy()->authorizeResource()) {
if (in_array(Policy::$resourceAbilityMap[$name], $controller->policy()->methods())) {
$body .= self::INDENT . str_replace(
[
'{{ method }}',
'{{ modelClass }}',
'{{ modelVariable }}',
],
[
$name,
Str::studly($controllerModelName),
'$' . Str::camel($controllerModelName),
],
in_array($name, ['index', 'create', 'store'])
? "\$this->authorize('{{ method }}', {{ modelClass }}::class);"
: "\$this->authorize('{{ method }}', {{ modelVariable }});"
) . PHP_EOL . PHP_EOL;
}
}

foreach ($statements as $statement) {
if ($statement instanceof SendStatement) {
$body .= self::INDENT . $statement->output() . PHP_EOL;
Expand Down
65 changes: 65 additions & 0 deletions src/Generators/PolicyGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace Blueprint\Generators;

use Blueprint\Concerns\HandlesImports;
use Blueprint\Contracts\Generator;
use Blueprint\Models\Policy;
use Blueprint\Tree;
use Illuminate\Support\Str;

class PolicyGenerator extends AbstractClassGenerator implements Generator
{
use HandlesImports;

protected $types = ['policies'];

public function output(Tree $tree): array
{
$this->tree = $tree;

$stub = $this->filesystem->stub('policy.class.stub');

/** @var \Blueprint\Models\Policy $policy */
foreach ($tree->policies() as $policy) {
$this->addImport($policy, $policy->fullyQualifiedModelClassName());

$path = $this->getPath($policy);

$this->create($path, $this->populateStub($stub, $policy));
}

return $this->output;
}

protected function populateStub(string $stub, Policy $policy)
{
$stub = str_replace('{{ namespace }}', $policy->fullyQualifiedNamespace(), $stub);
$stub = str_replace('{{ class }}', $policy->className(), $stub);
$stub = str_replace('{{ methods }}', $this->buildMethods($policy), $stub);
$stub = str_replace('{{ imports }}', $this->buildImports($policy), $stub);

return $stub;
}

protected function buildMethods(Policy $policy)
{
$methods = '';

foreach ($policy->methods() as $name) {
$methods .= str_replace(
[
'{{ modelClass }}',
'{{ modelVariable }}',
],
[
Str::studly($policy->name()),
Str::camel($policy->name()),
],
$this->filesystem->stub('policy.method.' . $name . '.stub'),
) . PHP_EOL;
}

return trim($methods);
}
}
32 changes: 31 additions & 1 deletion src/Lexers/ControllerLexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use Blueprint\Contracts\Lexer;
use Blueprint\Models\Controller;
use Blueprint\Models\Policy;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;

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

public function analyze(array $tokens): array
{
$registry = ['controllers' => []];
$registry = [
'controllers' => [],
'policies' => [],
];

if (empty($tokens['controllers'])) {
return $registry;
Expand Down Expand Up @@ -50,6 +55,31 @@ public function analyze(array $tokens): array
unset($definition['invokable']);
}

if (isset($definition['meta'])) {
if (isset($definition['meta']['policies'])) {
$authorizeResource = Arr::get($definition, 'meta.policies', true);

$policy = new Policy(
$controller->prefix(),
$authorizeResource === true
? Policy::$supportedMethods
: array_unique(
array_map(
fn (string $method): string => Policy::$resourceAbilityMap[$method],
preg_split('/,([ \t]+)?/', $definition['meta']['policies'])
)
),
$authorizeResource === true,
);

$controller->policy($policy);

$registry['policies'][] = $policy;
}

unset($definition['meta']);
}

foreach ($definition as $method => $body) {
$controller->addMethod($method, $this->statementLexer->analyze($body));
}
Expand Down
14 changes: 14 additions & 0 deletions src/Models/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ class Controller implements BlueprintModel
*/
private $methods = [];

/**
* @var Policy
*/
private $policy;

/**
* @var bool
*/
Expand Down Expand Up @@ -91,6 +96,15 @@ public function addMethod(string $name, array $statements)
$this->methods[$name] = $statements;
}

public function policy(?Policy $policy = null): ?Policy
{
if ($policy) {
$this->policy = $policy;
}

return $this->policy;
}

public function prefix()
{
if (Str::endsWith($this->name(), 'Controller')) {
Expand Down
126 changes: 126 additions & 0 deletions src/Models/Policy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

namespace Blueprint\Models;

use Blueprint\Contracts\Model as BlueprintModel;
use Illuminate\Support\Str;

class Policy implements BlueprintModel
{
/** @var array */
public static $supportedMethods = [
'viewAny',
'view',
'create',
'update',
'delete',
'restore',
'forceDelete',
];

/** @var array */
public static $resourceAbilityMap = [
'index' => 'viewAny',
'show' => 'view',
'create' => 'create',
'store' => 'create',
'edit' => 'update',
'update' => 'update',
'destroy' => 'delete',
];

/**
* @var string
*/
private $name;

/**
* @var string
*/
private $namespace;

/**
* @var array<int, string>
*/
private $methods;

/**
* @var bool
*/
private $authorizeResource;

/**
* Controller constructor.
*/
public function __construct(string $name, array $methods, bool $authorizeResource)
{
$this->name = class_basename($name);
$this->namespace = trim(implode('\\', array_slice(explode('\\', str_replace('/', '\\', $name)), 0, -1)), '\\');
$this->methods = $methods;
$this->authorizeResource = $authorizeResource;
}

public function name(): string
{
return $this->name;
}

public function className(): string
{
return $this->name() . (Str::endsWith($this->name(), 'Policy') ? '' : 'Policy');
}

public function namespace()
{
if (empty($this->namespace)) {
return '';
}

return $this->namespace;
}

public function fullyQualifiedNamespace()
{
$fqn = config('blueprint.namespace');

if (config('blueprint.policy_namespace')) {
$fqn .= '\\' . config('blueprint.policy_namespace');
}

if ($this->namespace) {
$fqn .= '\\' . $this->namespace;
}

return $fqn;
}

public function fullyQualifiedClassName()
{
return $this->fullyQualifiedNamespace() . '\\' . $this->className();
}

public function methods(): array
{
return $this->methods;
}

public function authorizeResource(): bool
{
return $this->authorizeResource;
}

public function fullyQualifiedModelClassName()
{
$fqn = config('blueprint.namespace');

if (config('blueprint.models_namespace')) {
$fqn .= '\\' . config('blueprint.models_namespace');
}

if ($this->namespace) {
$fqn .= '\\' . $this->namespace;
}

return $fqn . '\\' . $this->name;
}
}
5 changes: 5 additions & 0 deletions src/Tree.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public function models()
return $this->tree['models'];
}

public function policies()
{
return $this->tree['policies'];
}

public function seeders()
{
return $this->tree['seeders'];
Expand Down
4 changes: 4 additions & 0 deletions stubs/controller.authorize-resource.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
public function __construct()
{
$this->authorizeResource({{ modelClass }}::class, '{{ modelVariable }}');
}
12 changes: 12 additions & 0 deletions stubs/policy.class.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace {{ namespace }};

use App\Models\User;
{{ imports }}
use Illuminate\Auth\Access\Response;

class {{ class }}
{
{{ methods }}
}
7 changes: 7 additions & 0 deletions stubs/policy.method.create.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
//
}
Loading

0 comments on commit 1a62e81

Please sign in to comment.