aboutsummaryrefslogtreecommitdiff
path: root/cli
diff options
context:
space:
mode:
authorDavid Siegfried <david.siegfried@uni-vechta.de>2024-09-06 13:17:34 +0000
committerJan-Hendrik Willms <tleilax+studip@gmail.com>2024-09-06 13:17:34 +0000
commitc25db6aaac0a7dc553513f8cae7336ed1931ac85 (patch)
tree80a14ae163fae5116b65af5e0b3c2a4132dece63 /cli
parent5c08983d892929041dfc0b9d3b10439bae9ce1b5 (diff)
add make plugin, fixes #4563
Closes #4563 Merge request studip/studip!3374
Diffstat (limited to 'cli')
-rw-r--r--cli/Commands/Make/Migration.php3
-rw-r--r--cli/Commands/Make/Model.php3
-rw-r--r--cli/Commands/Make/Plugin.php374
-rw-r--r--cli/Commands/Make/StudipClassPrinter.php18
-rwxr-xr-xcli/studip1
5 files changed, 379 insertions, 20 deletions
diff --git a/cli/Commands/Make/Migration.php b/cli/Commands/Make/Migration.php
index d9ae1d1..992b02e 100644
--- a/cli/Commands/Make/Migration.php
+++ b/cli/Commands/Make/Migration.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\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';
diff --git a/cli/Commands/Make/Model.php b/cli/Commands/Make/Model.php
index 57b15af..82e683b 100644
--- a/cli/Commands/Make/Model.php
+++ b/cli/Commands/Make/Model.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));
diff --git a/cli/Commands/Make/Plugin.php b/cli/Commands/Make/Plugin.php
new file mode 100644
index 0000000..f361f34
--- /dev/null
+++ b/cli/Commands/Make/Plugin.php
@@ -0,0 +1,374 @@
+<?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;
+ }
+}
diff --git a/cli/Commands/Make/StudipClassPrinter.php b/cli/Commands/Make/StudipClassPrinter.php
deleted file mode 100644
index 5e86085..0000000
--- a/cli/Commands/Make/StudipClassPrinter.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?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 = ': ';
-}
diff --git a/cli/studip b/cli/studip
index 4d19e2b..8effcae 100755
--- a/cli/studip
+++ b/cli/studip
@@ -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,