Skip to content

Commit 9c40752

Browse files
authored
Feat/rewrote control registering (#29)
1 parent ffa2ccf commit 9c40752

File tree

12 files changed

+316
-121
lines changed

12 files changed

+316
-121
lines changed

src/Access.php

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<?php
2+
3+
namespace Lomkit\Access;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use Illuminate\Support\Collection;
7+
use Illuminate\Support\Str;
8+
use Lomkit\Access\Controls\Control;
9+
use ReflectionClass;
10+
use ReflectionException;
11+
use SplFileInfo;
12+
use Symfony\Component\Finder\Finder;
13+
14+
class Access
15+
{
16+
/**
17+
* The default path where control reside.
18+
*
19+
* @var array
20+
*/
21+
public static array $controlDiscoveryPaths;
22+
23+
/**
24+
* The registered controls.
25+
*
26+
* @var Control[]
27+
*/
28+
protected static array $controls;
29+
30+
/**
31+
* Add multiple control to access.
32+
*
33+
* @param Control[] $controls
34+
*
35+
* @return Access
36+
*/
37+
public function addControls(array $controls): static
38+
{
39+
foreach ($controls as $control) {
40+
$this->addControl($control);
41+
}
42+
43+
return $this;
44+
}
45+
46+
/**
47+
* Add a control to access.
48+
*
49+
* @param Control $control
50+
*
51+
* @return Access
52+
*/
53+
public function addControl(Control $control): self
54+
{
55+
static::$controls[class_basename($control)] = $control;
56+
57+
return $this;
58+
}
59+
60+
/**
61+
* Get the control instance for the given model.
62+
*
63+
* @param Model|class-string<Model> $model
64+
*
65+
* @return Control|null
66+
*/
67+
public static function controlForModel(Model|string $model): ?Control
68+
{
69+
if (!is_string($model)) {
70+
$model = $model::class;
71+
}
72+
73+
foreach (static::$controls as $control) {
74+
if ($control->isModel($model)) {
75+
return $control;
76+
}
77+
}
78+
79+
return null;
80+
}
81+
82+
/**
83+
* Discover controls for a given path.
84+
*
85+
* @var string[]
86+
*/
87+
public function discoverControls(array $paths): self
88+
{
89+
(new Collection($paths))
90+
->flatMap(function ($directory) {
91+
return glob($directory, GLOB_ONLYDIR);
92+
})
93+
->reject(function ($directory) {
94+
return !is_dir($directory);
95+
})
96+
->each(function ($directory) {
97+
$controls = Finder::create()->files()->in($directory);
98+
99+
foreach ($controls as $control) {
100+
try {
101+
$control = new ReflectionClass(
102+
static::classFromFile($control, base_path())
103+
);
104+
} catch (ReflectionException) {
105+
continue;
106+
}
107+
108+
if (!$control->isInstantiable()) {
109+
continue;
110+
}
111+
112+
$this->addControl($control->newInstance());
113+
}
114+
});
115+
116+
return $this;
117+
}
118+
119+
/**
120+
* Get the control directories that should be used to discover controls.
121+
*
122+
* @return array
123+
*/
124+
public function discoverControlsWithin(): array
125+
{
126+
return static::$controlDiscoveryPaths ?? [
127+
app()->path('Access/Controls'),
128+
];
129+
}
130+
131+
/**
132+
* Extract the class name from the given file path.
133+
*
134+
* @param SplFileInfo $file
135+
* @param string $basePath
136+
*
137+
* @return string
138+
*/
139+
protected function classFromFile(SplFileInfo $file, string $basePath)
140+
{
141+
$class = trim(Str::replaceFirst($basePath, '', $file->getRealPath()), DIRECTORY_SEPARATOR);
142+
143+
return ucfirst(Str::camel(str_replace(
144+
[DIRECTORY_SEPARATOR, ucfirst(basename(app()->path())).'\\'],
145+
['\\', app()->getNamespace()],
146+
ucfirst(Str::replaceLast('.php', '', $class))
147+
)));
148+
}
149+
}

src/AccessServiceProvider.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public function register()
3030
__DIR__.'/../config/access-control.php',
3131
'access-control'
3232
);
33+
34+
$this->registerAccessControls();
3335
}
3436

3537
/**
@@ -92,6 +94,18 @@ protected function registerStubs()
9294
});
9395
}
9496

97+
/**
98+
* Register the default paths controls.
99+
*
100+
* @return void
101+
*/
102+
private function registerAccessControls(): void
103+
{
104+
$access = new Access();
105+
106+
$access->discoverControls($access->discoverControlsWithin());
107+
}
108+
95109
/**
96110
* Register the package's publishable resources.
97111
*

src/Console/ControlMakeCommand.php

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44

55
use Illuminate\Console\GeneratorCommand;
66
use Illuminate\Support\Collection;
7+
use InvalidArgumentException;
78
use Symfony\Component\Console\Attribute\AsCommand;
89
use Symfony\Component\Console\Input\InputInterface;
910
use Symfony\Component\Console\Input\InputOption;
1011
use Symfony\Component\Console\Output\OutputInterface;
1112
use Symfony\Component\Finder\Finder;
1213

1314
use function Laravel\Prompts\multiselect;
15+
use function Laravel\Prompts\select;
1416

1517
#[AsCommand(name: 'make:control')]
1618
class ControlMakeCommand extends GeneratorCommand
@@ -41,7 +43,7 @@ class ControlMakeCommand extends GeneratorCommand
4143
*
4244
* @return string
4345
*/
44-
protected function getStub()
46+
protected function getStub(): string
4547
{
4648
return $this->resolveStubPath('/stubs/control.stub');
4749
}
@@ -53,7 +55,7 @@ protected function getStub()
5355
*
5456
* @return string
5557
*/
56-
protected function resolveStubPath($stub)
58+
protected function resolveStubPath($stub): string
5759
{
5860
return file_exists($customPath = $this->laravel->basePath(trim($stub, '/')))
5961
? $customPath
@@ -67,7 +69,7 @@ protected function resolveStubPath($stub)
6769
*
6870
* @return string
6971
*/
70-
protected function getDefaultNamespace($rootNamespace)
72+
protected function getDefaultNamespace($rootNamespace): string
7173
{
7274
return $rootNamespace.'\Access\Controls';
7375
}
@@ -79,7 +81,7 @@ protected function getDefaultNamespace($rootNamespace)
7981
*
8082
* @return string
8183
*/
82-
protected function buildClass($name)
84+
protected function buildClass($name): string
8385
{
8486
$rootNamespace = $this->rootNamespace();
8587
$controlNamespace = $this->getNamespace($name);
@@ -90,6 +92,10 @@ protected function buildClass($name)
9092

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

95+
if ($this->option('model')) {
96+
$replace = $this->buildModelReplacements($replace);
97+
}
98+
9399
if ($baseControlExists) {
94100
$replace['use Lomkit\Access\Controls\Control;'] = '';
95101
} else {
@@ -103,6 +109,30 @@ protected function buildClass($name)
103109
);
104110
}
105111

112+
/**
113+
* Build the model replacement values.
114+
*
115+
* @param array $replace
116+
*
117+
* @return array
118+
*/
119+
protected function buildModelReplacements(array $replace): array
120+
{
121+
$modelClass = $this->parseModel($this->option('model'));
122+
123+
return array_merge($replace, [
124+
'DummyFullModelClass' => $modelClass,
125+
'{{ namespacedModel }}' => $modelClass,
126+
'{{namespacedModel}}' => $modelClass,
127+
'DummyModelClass' => class_basename($modelClass),
128+
'{{ model }}' => class_basename($modelClass),
129+
'{{model}}' => class_basename($modelClass),
130+
'DummyModelVariable' => lcfirst(class_basename($modelClass)),
131+
'{{ modelVariable }}' => lcfirst(class_basename($modelClass)),
132+
'{{modelVariable}}' => lcfirst(class_basename($modelClass)),
133+
]);
134+
}
135+
106136
/**
107137
* Build the model replacement values.
108138
*
@@ -148,6 +178,7 @@ protected function getOptions()
148178
{
149179
return [
150180
['perimeters', 'p', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'The perimeters that the control relies on'],
181+
['model', 'm', InputOption::VALUE_REQUIRED, 'The model the control relies on'],
151182
];
152183
}
153184

@@ -164,13 +195,27 @@ protected function afterPromptingForMissingArguments(InputInterface $input, Outp
164195
if ($this->didReceiveOptions($input)) {
165196
return;
166197
}
167-
$perimeters = multiselect(
168-
'What perimeters should this control apply to? (Optional)',
169-
$this->possiblePerimeters(),
170-
);
171198

172-
if ($perimeters) {
173-
$input->setOption('perimeters', $perimeters);
199+
if (!empty($this->possiblePerimeters())) {
200+
$perimeters = multiselect(
201+
'What perimeters should this control apply to? (Optional)',
202+
$this->possiblePerimeters(),
203+
);
204+
205+
if ($perimeters) {
206+
$input->setOption('perimeters', $perimeters);
207+
}
208+
}
209+
210+
if (!empty($this->possibleModels())) {
211+
$model = select(
212+
'What model should this control apply to? (Optional)',
213+
$this->possibleModels(),
214+
);
215+
216+
if ($model) {
217+
$input->setOption('model', $model);
218+
}
174219
}
175220
}
176221

@@ -189,4 +234,22 @@ protected function possiblePerimeters()
189234
->values()
190235
->all();
191236
}
237+
238+
/**
239+
* Get the fully-qualified model class name.
240+
*
241+
* @param string $model
242+
*
243+
* @throws \InvalidArgumentException
244+
*
245+
* @return string
246+
*/
247+
protected function parseModel(string $model): string
248+
{
249+
if (preg_match('([^A-Za-z0-9_/\\\\])', $model)) {
250+
throw new InvalidArgumentException('Model name contains invalid characters.');
251+
}
252+
253+
return $this->qualifyModel($model);
254+
}
192255
}

src/Console/stubs/control.stub

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ use Illuminate\Database\Eloquent\Builder;
99

1010
class {{ class }} extends Control
1111
{
12+
/**
13+
* The model the control refers to.
14+
* @var class-string<Model>
15+
*/
16+
protected string $model = {{ namespacedModel }}::class;
17+
1218
/**
1319
* Retrieve the list of perimeter definitions for the current control.
1420
*

0 commit comments

Comments
 (0)