Skip to content

Commit 64303f7

Browse files
authored
Merge pull request #10 from Lomkit/feature/commands
✨ commands
2 parents f98d76e + 83f14ba commit 64303f7

File tree

13 files changed

+581
-3
lines changed

13 files changed

+581
-3
lines changed

src/AccessServiceProvider.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,28 @@
22

33
namespace Lomkit\Access;
44

5+
use Illuminate\Foundation\Events\PublishingStubs;
6+
use Illuminate\Support\Facades\Event;
57
use Illuminate\Support\ServiceProvider;
8+
use Lomkit\Access\Console\ControlMakeCommand;
9+
use Lomkit\Access\Console\PerimeterMakeCommand;
610

711
class AccessServiceProvider extends ServiceProvider
812
{
13+
protected array $devCommands = [
14+
'ControlMake' => ControlMakeCommand::class,
15+
'PerimeterMake' => PerimeterMakeCommand::class,
16+
];
17+
918
/**
1019
* Registers the service provider.
1120
*
1221
* @return void
1322
*/
1423
public function register()
1524
{
25+
$this->registerCommands($this->devCommands);
26+
1627
$this->mergeConfigFrom(
1728
__DIR__.'/../config/access-control.php',
1829
'access-control'
@@ -27,6 +38,42 @@ public function register()
2738
public function boot()
2839
{
2940
$this->registerPublishing();
41+
42+
$this->registerStubs();
43+
}
44+
45+
/**
46+
* Register the given commands.
47+
*
48+
* @param array $commands
49+
*
50+
* @return void
51+
*/
52+
protected function registerCommands(array $commands)
53+
{
54+
foreach ($commands as $commandName => $command) {
55+
$method = "register{$commandName}Command";
56+
57+
if (method_exists($this, $method)) {
58+
$this->{$method}();
59+
} else {
60+
$this->app->singleton($command);
61+
}
62+
}
63+
64+
$this->commands(array_values($commands));
65+
}
66+
67+
/**
68+
* Register the stubs on the default laravel stub publish command.
69+
*/
70+
protected function registerStubs()
71+
{
72+
Event::listen(function (PublishingStubs $event) {
73+
$event->add(realpath(__DIR__.'/Console/stubs/control.stub'), 'controller.stub');
74+
$event->add(realpath(__DIR__.'/Console/stubs/perimeter.plain.stub'), 'perimeter.plain.stub');
75+
$event->add(realpath(__DIR__.'/Console/stubs/perimeter.overlay.stub'), 'perimeter.overlay.stub');
76+
});
3077
}
3178

3279
/**
@@ -42,4 +89,14 @@ private function registerPublishing()
4289
], 'access-control-config');
4390
}
4491
}
92+
93+
/**
94+
* Get the services provided by the provider.
95+
*
96+
* @return array
97+
*/
98+
public function provides()
99+
{
100+
return array_values($this->devCommands);
101+
}
45102
}

src/Console/ControlMakeCommand.php

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
<?php
2+
3+
namespace Lomkit\Access\Console;
4+
5+
use Illuminate\Console\GeneratorCommand;
6+
use Illuminate\Support\Collection;
7+
use Symfony\Component\Console\Attribute\AsCommand;
8+
use Symfony\Component\Console\Input\InputInterface;
9+
use Symfony\Component\Console\Input\InputOption;
10+
use Symfony\Component\Console\Output\OutputInterface;
11+
use Symfony\Component\Finder\Finder;
12+
13+
use function Laravel\Prompts\multiselect;
14+
15+
#[AsCommand(name: 'make:control')]
16+
class ControlMakeCommand extends GeneratorCommand
17+
{
18+
/**
19+
* The console command name.
20+
*
21+
* @var string
22+
*/
23+
protected $name = 'make:control';
24+
25+
/**
26+
* The console command description.
27+
*
28+
* @var string
29+
*/
30+
protected $description = 'Create a new control class';
31+
32+
/**
33+
* The type of class being generated.
34+
*
35+
* @var string
36+
*/
37+
protected $type = 'Control';
38+
39+
/**
40+
* Get the stub file for the generator.
41+
*
42+
* @return string
43+
*/
44+
protected function getStub()
45+
{
46+
return $this->resolveStubPath('/stubs/control.stub');
47+
}
48+
49+
/**
50+
* Resolve the fully-qualified path to the stub.
51+
*
52+
* @param string $stub
53+
*
54+
* @return string
55+
*/
56+
protected function resolveStubPath($stub)
57+
{
58+
return file_exists($customPath = $this->laravel->basePath(trim($stub, '/')))
59+
? $customPath
60+
: __DIR__.$stub;
61+
}
62+
63+
/**
64+
* Get the default namespace for the class.
65+
*
66+
* @param string $rootNamespace
67+
*
68+
* @return string
69+
*/
70+
protected function getDefaultNamespace($rootNamespace)
71+
{
72+
return $rootNamespace.'\Access\Controls';
73+
}
74+
75+
/**
76+
* Build the class with the given name.
77+
*
78+
* Remove the base controller import if we are already in the base namespace.
79+
*
80+
* @param string $name
81+
*
82+
* @return string
83+
*/
84+
protected function buildClass($name)
85+
{
86+
$rootNamespace = $this->rootNamespace();
87+
$controlNamespace = $this->getNamespace($name);
88+
89+
$replace = [];
90+
91+
$baseControlExists = file_exists($this->getPath("{$rootNamespace}Access\Controls\Control"));
92+
93+
$replace = $this->buildPerimetersReplacements($replace, $this->option('perimeters'));
94+
95+
if ($baseControlExists) {
96+
$replace["use {$controlNamespace}\Control;\n"] = '';
97+
} else {
98+
$replace[' extends Control'] = '';
99+
$replace["use {$rootNamespace}Access\Controls\Control;\n"] = '';
100+
}
101+
102+
return str_replace(
103+
array_keys($replace),
104+
array_values($replace),
105+
parent::buildClass($name)
106+
);
107+
}
108+
109+
/**
110+
* Build the model replacement values.
111+
*
112+
* @param array $replace
113+
* @param array $perimeters
114+
*
115+
* @return array
116+
*/
117+
protected function buildPerimetersReplacements(array $replace, array $perimeters)
118+
{
119+
$perimetersImplementation = '';
120+
121+
foreach ($perimeters as $perimeter) {
122+
$perimeterClass = $this->rootNamespace().'Access\\Perimeters\\'.$perimeter;
123+
124+
$perimetersImplementation .= <<<PERIMETER
125+
\\n
126+
$perimeterClass::new()
127+
->should(function (Model \$user, string \$method, Model \$model) {
128+
return true;
129+
})
130+
->allowed(function (Model \$user) {
131+
return true;
132+
})
133+
->query(function (Builder \$query, Model \$user) {
134+
return \$query;
135+
}),\\n
136+
PERIMETER;
137+
}
138+
139+
return array_merge($replace, [
140+
'{{ perimeters }}' => $perimetersImplementation,
141+
'{{perimeters}}' => $perimetersImplementation,
142+
]);
143+
}
144+
145+
/**
146+
* Get the console command options.
147+
*
148+
* @return array
149+
*/
150+
protected function getOptions()
151+
{
152+
return [
153+
['perimeters', 'p', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'The perimeters that the control relies on'],
154+
];
155+
}
156+
157+
/**
158+
* Interact further with the user if they were prompted for missing arguments.
159+
*
160+
* @param \Symfony\Component\Console\Input\InputInterface $input
161+
* @param \Symfony\Component\Console\Output\OutputInterface $output
162+
*
163+
* @return void
164+
*/
165+
protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output)
166+
{
167+
if ($this->didReceiveOptions($input)) {
168+
return;
169+
}
170+
$perimeters = multiselect(
171+
'What perimeters should this control apply to? (Optional)',
172+
$this->possiblePerimeters(),
173+
);
174+
175+
if ($perimeters) {
176+
$input->setOption('perimeters', $perimeters);
177+
}
178+
}
179+
180+
/**
181+
* Get a list of possible model names.
182+
*
183+
* @return array<int, string>
184+
*/
185+
protected function possiblePerimeters()
186+
{
187+
$perimetersPath = is_dir(app_path('Access/Perimeters')) ? app_path('Access/Perimeters') : app_path();
188+
189+
return (new Collection(Finder::create()->files()->depth(0)->in($perimetersPath)))
190+
->map(fn ($file) => $file->getBasename('.php'))
191+
->sort()
192+
->values()
193+
->all();
194+
}
195+
}

0 commit comments

Comments
 (0)