Skip to content
Snippets Groups Projects
Commit c25db6aa authored by David Siegfried's avatar David Siegfried Committed by Jan-Hendrik Willms
Browse files

add make plugin, fixes #4563

Closes #4563

Merge request studip/studip!3374
parent 5c08983d
No related branches found
No related tags found
No related merge requests found
......@@ -67,8 +67,9 @@
- Die Klassen `DbView`, `DbSnapshot` und die zugehörigen Dateien in `lib/dbviews` wurden ausgebaut. ([Issue #4390](https://gitlab.studip.de/studip/studip/-/issues/4390))
- Als Ersatz dienen Datenbankabfragen mittels der `DBManager`-Klasse oder mittels `SimpleORMap`-Modellen.
- Es wurden zwei neue CLI-Kommandos hinzugefügt, womit man Klassenrümpfe für SORM-Models und Migrationen erstellen kann. Bei den Migrationen wird die Versionsnummer für die jeweilige `domain` automatisch ermittelt.
- `cli/studip make:model` und `cli/studip make:migration`.
- `cli/studip make:model` und `cli/studip make:migration`.
- Es wurde ein neues CLI-Kommando hinzugefügt, womit man auf einfache Weise ein Plugin-Grundgerüst erstellen kann.
- `cli/studip make:plugin`
## Security related issues
-
......
......@@ -3,6 +3,7 @@
namespace Studip\Cli\Commands\Make;
use Nette\PhpGenerator\PhpFile;
use Nette\PhpGenerator\PsrPrinter;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
......@@ -96,7 +97,7 @@ final class Migration extends Command
$class->addMethod('up')->addBody('// Add content');
$class->addMethod('down')->addBody('// Add content');
$printer = new StudipClassPrinter();
$printer = new PsrPrinter();
$result = $printer->printFile($file);
$migrationName = $version . '_' . str_replace(' ', '_', lcfirst($name));
$filename = $path . '/' . $migrationName . '.php';
......
......@@ -3,6 +3,7 @@
namespace Studip\Cli\Commands\Make;
use Nette\PhpGenerator\PhpFile;
use Nette\PhpGenerator\PsrPrinter;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\ArrayInput;
......@@ -89,7 +90,7 @@ final class Model extends Command
$method->addParameter('config', []);
$printer = new StudipClassPrinter();
$printer = new PsrPrinter();
$result = $printer->printFile($file);
$modelName = str_replace(' ', '_', ucfirst($name));
......
<?php
namespace Studip\Cli\Commands\Make;
use Nette\PhpGenerator\PhpFile;
use Nette\PhpGenerator\PsrPrinter;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
final class Plugin extends Command
{
private const VALID_PLUGIN_INTERFACES = [
\SystemPlugin::class,
\StandardPlugin::class,
\AdminCourseAction::class,
\AdminCourseContents::class,
\AdminCourseWidgetPlugin::class,
\AdministrationPlugin::class,
\DetailspagePlugin::class,
\ExternPagePlugin::class,
\FileSystemPlugin::class,
\FileUploadHook::class,
\ForumModule::class,
\HomepagePlugin::class,
\LibraryPlugin::class,
\MetricsPlugin::class,
\PortalPlugin::class,
\PrivacyPlugin::class,
\ScorePlugin::class,
\QuestionnaireAssignmentPlugin::class,
];
protected static $defaultName = 'make:plugin';
protected function configure(): void
{
$this->addArgument('name', InputArgument::OPTIONAL, 'Name of the plugin');
$this->addOption('origin', 'o', InputOption::VALUE_OPTIONAL, 'Origin of the plugin');
$this->addOption('description', 'd', InputOption::VALUE_OPTIONAL, 'Description of the plugin');
$this->addOption('plugin-version', 'pv', InputOption::VALUE_OPTIONAL, 'Version of the plugin');
$this->addOption('min-version', 'min', InputOption::VALUE_OPTIONAL, 'Minimum version of Stud.IP the plugin supports');
$this->addOption('max-version', 'max', InputOption::VALUE_OPTIONAL, 'Maximum version of Stud.IP the plugin supports');
$this->addOption('plugin-interfaces', 'I', InputOption::VALUE_OPTIONAL, 'Comma separated list of plugin interfaces');
$this->addOption('with-controller', 'c', InputOption::VALUE_OPTIONAL, 'Create default controller');
$this->addOption('force', 'F', InputOption::VALUE_NEGATABLE, 'Force creation of the plugin (even if a plugin with that name and origin already exists)', false);
$this->setDescription('Create a new plain plugin frame');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$helper = $this->getHelper('question');
// Get name of the plugin (if not already passed via command line)
$name = $input->getArgument('name');
if ($name === null) {
$question = new Question('Please enter the name of the plugin: ');
$question->setMaxAttempts(3);
$question->setValidator(function ($name): string {
if (!$name) {
throw new \RuntimeException('The name of the plugin is required');
}
return $name;
});
$question->setTrimmable(true);
$name = $helper->ask($input, $output, $question);
}
// Get origin of the plugin (if not already passed via command line)
$origin = $input->getOption('origin');
if ($origin === null) {
$question = new Question('Please enter the origin of the plugin: ');
$question->setAutocompleterValues($this->getKnownOrigins());
$question->setMaxAttempts(3);
$question->setValidator(function ($origin): string {
if (!$origin) {
throw new \RuntimeException('The origin of the plugin is required');
}
return $origin;
});
$question->setTrimmable(true);
$origin = $helper->ask($input, $output, $question);
}
$interfaces = null;
if ($input->hasOption('plugin-interfaces')) {
$interfaces = explode(',', $input->getOption('plugin-interfaces'));
$interfaces = array_filter($interfaces);
$interfaces = array_intersect($interfaces, self::VALID_PLUGIN_INTERFACES);
$interfaces = $interfaces ?: null;
}
$controllers = $input->getOption('with-controller');
if (!$input->getOption('no-interaction')) {
$version = $input->getOption('plugin-version');
if ($version === null) {
$question = new Question('Please enter the version of the plugin: ', '1.0');
$version = $helper->ask($input, $output, $question);
}
$minVersion = $input->getOption('min-version');
if ($minVersion === null) {
$question = new Question('Please enter the studipMinVersion of the plugin: ', '');
$minVersion = $helper->ask($input, $output, $question);
}
$maxVersion = $input->getOption('max-version');
if ($maxVersion === null) {
$question = new Question('Please enter the studipMaxVersion of the plugin: ', '');
$maxVersion = $helper->ask($input, $output, $question);
}
$description = $input->getOption('description');
if ($description === null) {
$question = new Question('Please enter the description of the plugin: ');
$description = $helper->ask($input, $output, $question);
}
if ($interfaces === null) {
$question = new ChoiceQuestion(
'Please enter the interfaces of the plugin: ',
self::VALID_PLUGIN_INTERFACES,
0
);
$question->setMultiselect(true);
$interfaces = $helper->ask($input, $output, $question);
}
if ($controllers === null) {
$question = new ConfirmationQuestion(
'Should controller classes be created? (y/n) ',
false,
'/^(y|j)/i'
);
$controllers = $helper->ask($input, $output, $question);
if ($controllers) {
$question = new ConfirmationQuestion(
'Do you want to define the controllers and actions interactively? (y/n) ',
false,
'/^(y|j)/i'
);
if ($helper->ask($input, $output, $question)) {
$controllers = [];
do {
$question = new Question('- Please enter the name of a controller: ');
$question->setMaxAttempts(3);
$question->setValidator(function ($controller): string {
if ($controller && preg_match('/[^a-z_]/', $controller)) {
throw new \RuntimeException('The name of the controller may only contain letters and the underscore character.');
}
return strtolower($controller);
});
$question->setTrimmable(true);
$controller = $helper->ask($input, $output, $question);
if ($controller) {
$controllers[$controller] = [];
do {
$question = new Question('- Please enter the name of an action (use special name !crud for the action "index", "edit", "store", "delete"): ');
$question->setMaxAttempts(3);
$question->setValidator(function ($action): string {
if ($action === '!crud') {
return $action;
}
if ($action && preg_match('/[^a-z_]/', $action)) {
throw new \RuntimeException('The name of the action may only contain letters and the underscore character.');
}
return strtolower($action);
});
$question->setTrimmable(true);
$action = $helper->ask($input, $output, $question);
if ($action === '!crud') {
$controllers[$controller][] = 'index';
$controllers[$controller][] = 'edit';
$controllers[$controller][] = 'store';
$controllers[$controller][] = 'delete';
} elseif ($action) {
$controllers[$controller][] = $action;
}
} while ($action !== '');
}
} while ($controller !== '');
}
}
}
}
// Cleanup
$className = strtopascalcase($name);
$interfaces = $interfaces ?? [\SystemPlugin::class];
$pluginPath = $GLOBALS['STUDIP_BASE_PATH'] . "/public/plugins_packages/$origin/$className";
if (
file_exists($pluginPath)
&& !$input->getOption('force')
) {
$question = new ConfirmationQuestion(
'There is already a plugin with that origin and name. Overwrite? (y/n) ',
false,
'/^(y|j)/i'
);
if (!$helper->ask($input, $output, $question)) {
$output->writeln('<error>Aborted');
exit;
}
}
mkdir($pluginPath, 0755, true);
mkdir("$pluginPath/controllers", 0755, true);
mkdir("$pluginPath/views", 0755, true);
mkdir("$pluginPath/lib/classes/", 0755, true);
mkdir("$pluginPath/lib/models", 0755, true);
mkdir("$pluginPath/migrations", 0755, true);
file_put_contents(
"$pluginPath/plugin.manifest",
$this->generatePluginManifest(
$name,
$className,
$origin,
$version ?? '',
$minVersion ?? '',
$maxVersion ?? '',
$description ?? ''
)
);
// Generate Plugin-Class
$file = new PhpFile();
$class = $file->addClass($className);
$class->setExtends(\StudIPPlugin::class);
foreach ($interfaces as $interface) {
$class->addImplement($interface);
}
$method = $class->addMethod('__construct');
$method->addBody('parent::__construct();');
$method = $class->addMethod('perform');
$method->addParameter('unconsumed_path');
$method->addBody("//Import here styles or scripts for example");
$method->addBody('parent::perform($unconsumed_path);');
foreach ($interfaces as $interface) {
foreach (get_class_methods($interface) as $method) {
$class->inheritMethod($method);
};
}
$printer = new PsrPrinter();
$result = $printer->printFile($file);
// Include requiring of bootstrap
$result = str_replace(
'<?php',
'<?php' . PHP_EOL . 'require __DIR__ . \'/bootstrap.php\';' . PHP_EOL,
$result
);
$filename = "$pluginPath/$className.php";
file_put_contents($filename, $result);
// Create bootstrap
$bootstrap = implode(PHP_EOL, [
'<?php',
'StudipAutoloader::addAutoloadPath(__DIR__ . \'/lib/classes\');',
'StudipAutoloader::addAutoloadPath(__DIR__ . \'/lib/models\');',
]);
file_put_contents(
"$pluginPath/bootstrap.php",
$bootstrap
);
if ($controllers !== null) {
$controllers = $this->createControllersAndView($controllers);
// Create controllers and views
foreach ($controllers as $controller_name => $actions) {
$file = new PhpFile();
$class = $file->addClass(strtopascalcase($controller_name . ' Controller'));
$class->addProperty('_autobind', true);
$class->setExtends(\PluginController::class);
foreach ($actions as $action) {
$method = $class->addMethod("{$action}_action");
$method->addBody('//add your code here');
}
$printer = new PsrPrinter();
$result = $printer->printFile($file);
$filename = "{$pluginPath}/controllers/{$controller_name}.php";
file_put_contents($filename, $result);
$viewPath = "$pluginPath/views/$controller_name";
mkdir($viewPath, 0755, true);
foreach ($actions as $action) {
file_put_contents("{$viewPath}/{$action}.php", '');
}
}
}
$output->writeln('<info>Your plugin has been created!</info>');
return Command::SUCCESS;
}
private function generatePluginManifest(
string $name,
string $class_name,
string $origin,
string $version,
string $minVersion,
string $maxVersion,
string $description,
): string {
if ($version === '') {
$version = '1.0';
}
$manifest = "pluginname=$name\n";
$manifest .= "pluginclassname=$class_name\n";
$manifest .= "origin=$origin\n";
$manifest .= "version=$version\n";
if ($description) {
$manifest .= "description=$description\n";
}
if ($minVersion) {
$manifest .= "studipMinVersion=$minVersion\n";
}
if ($maxVersion) {
$manifest .= "studipMaxVersion=$maxVersion\n";
}
return $manifest;
}
private function getKnownOrigins(): array
{
$origins = glob($GLOBALS['STUDIP_BASE_PATH'] . '/public/plugins_packages/*', GLOB_ONLYDIR);
$origins = array_map('basename', $origins);
natcasesort($origins);
return $origins;
}
private function createControllersAndView(mixed $controllers): array
{
if ($controllers === true) {
return ['show' => ['index']];
}
if (is_string($controllers)) {
return [$controllers => ['index']];
}
return $controllers;
}
}
<?php
namespace Studip\Cli\Commands\Make;
use Nette\PhpGenerator\Printer;
final class StudipClassPrinter extends Printer
{
/** length of the line after which the line will break */
public int $wrapLength = 120;
/** indentation character, can be replaced with a sequence of spaces */
public string $indentation = ' ';
/** number of blank lines between properties */
public int $linesBetweenProperties = 0;
/** number of blank lines between methods */
public int $linesBetweenMethods = 1;
public string $returnTypeColon = ': ';
}
......@@ -34,6 +34,7 @@ $commands = [
Commands\DB\MoveMatrikelnummer::class,
Commands\Make\Migration::class,
Commands\Make\Model::class,
Commands\Make\Plugin::class,
Commands\DI\Reset::class,
Commands\Files\Dump::class,
Commands\Fix\Biest7789::class,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment