Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions src/Access.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

namespace Lomkit\Access;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Lomkit\Access\Controls\Control;
use ReflectionClass;
use ReflectionException;
use SplFileInfo;
use Symfony\Component\Finder\Finder;

class Access
{
/**
* The default path where control reside.
*
* @var array
*/
public static array $controlDiscoveryPaths;

/**
* The registered controls.
*
* @var Control[]
*/
protected static array $controls;

/**
* Add multiple control to access.
*
* @param Control[] $controls
*
* @return Access
*/
public function addControls(array $controls): static
{
foreach ($controls as $control) {
$this->addControl($control);
}

return $this;
}

/**
* Add a control to access.
*
* @param Control $control
*
* @return Access
*/
public function addControl(Control $control): self
{
static::$controls[class_basename($control)] = $control;

return $this;
}

/**
* Get the control instance for the given model.
*
* @param Model|class-string<Model> $model
*
* @return Control|null
*/
public static function controlForModel(Model|string $model): ?Control
{
if (!is_string($model)) {
$model = $model::class;
}

foreach (static::$controls as $control) {
if ($control->isModel($model)) {
return $control;
}
}

return null;
}

/**
* Discover controls for a given path.
*
* @var string[]
*/
public function discoverControls(array $paths): self
{
(new Collection($paths))
->flatMap(function ($directory) {
return glob($directory, GLOB_ONLYDIR);
})
->reject(function ($directory) {
return !is_dir($directory);
})
->each(function ($directory) {
$controls = Finder::create()->files()->in($directory);

foreach ($controls as $control) {
try {
$control = new ReflectionClass(
static::classFromFile($control, base_path())
);
} catch (ReflectionException) {
continue;
}

if (!$control->isInstantiable()) {
continue;
}

$this->addControl($control->newInstance());
}
});

return $this;
}

/**
* Get the control directories that should be used to discover controls.
*
* @return array
*/
public function discoverControlsWithin(): array
{
return static::$controlDiscoveryPaths ?? [
app()->path('Access/Controls'),
];
}

/**
* Extract the class name from the given file path.
*
* @param SplFileInfo $file
* @param string $basePath
*
* @return string
*/
protected function classFromFile(SplFileInfo $file, string $basePath)
{
$class = trim(Str::replaceFirst($basePath, '', $file->getRealPath()), DIRECTORY_SEPARATOR);

return ucfirst(Str::camel(str_replace(
[DIRECTORY_SEPARATOR, ucfirst(basename(app()->path())).'\\'],
['\\', app()->getNamespace()],
ucfirst(Str::replaceLast('.php', '', $class))
)));
}
}
14 changes: 14 additions & 0 deletions src/AccessServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public function register()
__DIR__.'/../config/access-control.php',
'access-control'
);

$this->registerAccessControls();
}

/**
Expand Down Expand Up @@ -92,6 +94,18 @@ protected function registerStubs()
});
}

/**
* Register the default paths controls.
*
* @return void
*/
private function registerAccessControls(): void
{
$access = new Access();

$access->discoverControls($access->discoverControlsWithin());
}

/**
* Register the package's publishable resources.
*
Expand Down
83 changes: 73 additions & 10 deletions src/Console/ControlMakeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Collection;
use InvalidArgumentException;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Finder\Finder;

use function Laravel\Prompts\multiselect;
use function Laravel\Prompts\select;

#[AsCommand(name: 'make:control')]
class ControlMakeCommand extends GeneratorCommand
Expand Down Expand Up @@ -41,7 +43,7 @@ class ControlMakeCommand extends GeneratorCommand
*
* @return string
*/
protected function getStub()
protected function getStub(): string
{
return $this->resolveStubPath('/stubs/control.stub');
}
Expand All @@ -53,7 +55,7 @@ protected function getStub()
*
* @return string
*/
protected function resolveStubPath($stub)
protected function resolveStubPath($stub): string
{
return file_exists($customPath = $this->laravel->basePath(trim($stub, '/')))
? $customPath
Expand All @@ -67,7 +69,7 @@ protected function resolveStubPath($stub)
*
* @return string
*/
protected function getDefaultNamespace($rootNamespace)
protected function getDefaultNamespace($rootNamespace): string
{
return $rootNamespace.'\Access\Controls';
}
Expand All @@ -79,7 +81,7 @@ protected function getDefaultNamespace($rootNamespace)
*
* @return string
*/
protected function buildClass($name)
protected function buildClass($name): string
{
$rootNamespace = $this->rootNamespace();
$controlNamespace = $this->getNamespace($name);
Expand All @@ -90,6 +92,10 @@ protected function buildClass($name)

$replace = $this->buildPerimetersReplacements($replace, $this->option('perimeters'));

if ($this->option('model')) {
$replace = $this->buildModelReplacements($replace);
}

if ($baseControlExists) {
$replace['use Lomkit\Access\Controls\Control;'] = '';
} else {
Expand All @@ -103,6 +109,30 @@ protected function buildClass($name)
);
}

/**
* Build the model replacement values.
*
* @param array $replace
*
* @return array
*/
protected function buildModelReplacements(array $replace): array
{
$modelClass = $this->parseModel($this->option('model'));

return array_merge($replace, [
'DummyFullModelClass' => $modelClass,
'{{ namespacedModel }}' => $modelClass,
'{{namespacedModel}}' => $modelClass,
'DummyModelClass' => class_basename($modelClass),
'{{ model }}' => class_basename($modelClass),
'{{model}}' => class_basename($modelClass),
'DummyModelVariable' => lcfirst(class_basename($modelClass)),
'{{ modelVariable }}' => lcfirst(class_basename($modelClass)),
'{{modelVariable}}' => lcfirst(class_basename($modelClass)),
]);
}

/**
* Build the model replacement values.
*
Expand Down Expand Up @@ -148,6 +178,7 @@ protected function getOptions()
{
return [
['perimeters', 'p', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'The perimeters that the control relies on'],
['model', 'm', InputOption::VALUE_REQUIRED, 'The model the control relies on'],
];
}

Expand All @@ -164,13 +195,27 @@ protected function afterPromptingForMissingArguments(InputInterface $input, Outp
if ($this->didReceiveOptions($input)) {
return;
}
$perimeters = multiselect(
'What perimeters should this control apply to? (Optional)',
$this->possiblePerimeters(),
);

if ($perimeters) {
$input->setOption('perimeters', $perimeters);
if (!empty($this->possiblePerimeters())) {
$perimeters = multiselect(
'What perimeters should this control apply to? (Optional)',
$this->possiblePerimeters(),
);

if ($perimeters) {
$input->setOption('perimeters', $perimeters);
}
}

if (!empty($this->possibleModels())) {
$model = select(
'What model should this control apply to? (Optional)',
$this->possibleModels(),
);

if ($model) {
$input->setOption('model', $model);
}
}
}

Expand All @@ -189,4 +234,22 @@ protected function possiblePerimeters()
->values()
->all();
}

/**
* Get the fully-qualified model class name.
*
* @param string $model
*
* @throws \InvalidArgumentException
*
* @return string
*/
protected function parseModel(string $model): string
{
if (preg_match('([^A-Za-z0-9_/\\\\])', $model)) {
throw new InvalidArgumentException('Model name contains invalid characters.');
}

return $this->qualifyModel($model);
}
}
6 changes: 6 additions & 0 deletions src/Console/stubs/control.stub
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ use Illuminate\Database\Eloquent\Builder;

class {{ class }} extends Control
{
/**
* The model the control refers to.
* @var class-string<Model>
*/
protected string $model = {{ namespacedModel }}::class;

/**
* Retrieve the list of perimeter definitions for the current control.
*
Expand Down
Loading