diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 9acc8e70cb8f3c2432a833234a9c0a120ee05585..7e73a33983a777d1fce769c2fad008b26e91d5ab 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -66,6 +66,8 @@ - Die Evaluationen wurden ausgebaut. Stattdessen sollte man nun die neuen Fragebögen verwenden ([Issue #3787]https://gitlab.studip.de/studip/studip/-/issues/3787) - 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`. ## Security related issues diff --git a/cli/Commands/Make/Migration.php b/cli/Commands/Make/Migration.php new file mode 100644 index 0000000000000000000000000000000000000000..d9ae1d161728791818f6b168478c590ec785f6de --- /dev/null +++ b/cli/Commands/Make/Migration.php @@ -0,0 +1,108 @@ +<?php + +namespace Studip\Cli\Commands\Make; + +use Nette\PhpGenerator\PhpFile; +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; + +final class Migration extends Command +{ + private const DEFAULT_BRANCH = '0'; + + protected static $defaultName = 'make:migration'; + + protected function configure(): void + { + $this->setDescription('Create a new migration file'); + $this->addArgument('name', InputArgument::REQUIRED, 'The name of the migration'); + $this->addOption( + 'branch', + 'b', + InputOption::VALUE_OPTIONAL, + 'The branch of the migration file', + self::DEFAULT_BRANCH + ); + $this->addOption('domain', 'd', InputOption::VALUE_OPTIONAL, 'The domain of the migration file', 'studip'); + + $defaultPath = $GLOBALS['STUDIP_BASE_PATH'] . '/db/migrations'; + $this->addOption( + 'path', + 'p', + InputOption::VALUE_OPTIONAL, + 'The location where the migration file should be created', + $defaultPath + ); + $this->addOption( + 'description', + 'D', + InputOption::VALUE_OPTIONAL, + 'The description for the migration', + '' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $branch = $input->getOption('branch'); + $domain = $input->getOption('domain'); + $name = $input->getArgument('name'); + $path = $input->getOption('path'); + $description = $input->getOption('description'); + $verbose = $input->getOption('verbose'); + + $version = $this->getNextMigrationVersion($branch, $domain, $path, $verbose); + $filename = $this->createMigrationFile($path, $version, $name, $description); + + if ($verbose) { + $output->writeln('Migration file ' . $filename . ' created.'); + } + + return Command::SUCCESS; + } + + private function getNextMigrationVersion(string $branch, string $domain, string $path, bool $verbose): string + { + $version = new \DBSchemaVersion($domain, $branch); + $migrator = new \Migrator($path, $version, $verbose); + $topVersions = $migrator->topVersion(true); + + if ($branch === self::DEFAULT_BRANCH) { + $branches = array_keys($topVersions); + usort($branches, 'version_compare'); + $branch = array_pop($branches); + } + + return sprintf('%s.%s', $branch, isset($topVersions[$branch]) ? $topVersions[$branch] + 1 : 1); + } + + private function createMigrationFile(string $path, string $version, string $name, string $description): string + { + if ($description === '') { + $description = '// Add content'; + } else { + $description = "return '$description';"; + } + $file = new PhpFile(); + $class = $file->addClass(str_replace(' ', '', ucwords($name))); + $class + ->setFinal() + ->setExtends(\Migration::class) + ->addComment("Description of class.\nSecond line\n"); + $class->addMethod('description')->addBody($description); + $class->addMethod('up')->addBody('// Add content'); + $class->addMethod('down')->addBody('// Add content'); + + $printer = new StudipClassPrinter(); + $result = $printer->printFile($file); + $migrationName = $version . '_' . str_replace(' ', '_', lcfirst($name)); + $filename = $path . '/' . $migrationName . '.php'; + + file_put_contents($filename, $result); + + return $filename; + } +} diff --git a/cli/Commands/Make/Model.php b/cli/Commands/Make/Model.php new file mode 100644 index 0000000000000000000000000000000000000000..57b15af743215e93f409663f91ca2416ac2251b0 --- /dev/null +++ b/cli/Commands/Make/Model.php @@ -0,0 +1,125 @@ +<?php + +namespace Studip\Cli\Commands\Make; + +use Nette\PhpGenerator\PhpFile; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ChoiceQuestion; + + +final class Model extends Command +{ + protected static $defaultName = 'make:model'; + + protected function configure(): void + { + $this->setDescription('Create a sorm-model file'); + $this->addArgument('name', InputArgument::REQUIRED, 'The name of the sorm-model'); + $this->addArgument('db-table', InputArgument::OPTIONAL, 'The name of the related db-table'); + $this->addOption('namespace', 's', InputOption::VALUE_OPTIONAL, 'Namespace', ''); + $defaultPath = $GLOBALS['STUDIP_BASE_PATH'] . '/lib/models'; + $this->addOption( + 'path', + 'p', + InputOption::VALUE_OPTIONAL, + 'The location where the model file should be created', + $defaultPath + ); + } + + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $namespace = $input->getOption('namespace'); + $name = $input->getArgument('name'); + $dbTable = $input->getArgument('db-table'); + $path = $input->getOption('path'); + $verbose = $input->getOption('verbose'); + + $filename = $this->createModelFile( + $path, + $name, + $input, + $output, + $dbTable, + $namespace + ); + + if ($verbose) { + $output->writeln('Model file ' . $filename . ' created.'); + } + + return Command::SUCCESS; + } + + private function createModelFile( + string $path, + string $name, + InputInterface $input, + OutputInterface $output, + string $dbTable = null, + string $namespace = null, + ): string + { + if (!$dbTable) { + $dbTable = strtosnakecase($name); + } + + $file = new PhpFile(); + $className = str_replace(' ', '', ucwords($name)); + + if ($namespace) { + $className = ucfirst($namespace) . '\\' . $className; + } + $class = $file->addClass($className); + $class->setExtends(\SimpleORMap::class); + $class->addComment(ucfirst($name) . '.php'); + $class->addComment('model class for table ' . $dbTable); + $method = $class->addMethod('configure') + ->setStatic() + ->setProtected(); + + $method->addBody(sprintf('$config[\'db_table\'] = \'%s\';', $dbTable)); + $method->addBody('parent::configure($config);'); + $method->addParameter('config', []); + + + $printer = new StudipClassPrinter(); + $result = $printer->printFile($file); + + $modelName = str_replace(' ', '_', ucfirst($name)); + $filename = $path . '/' . $modelName . '.php'; + + file_put_contents($filename, $result); + + $helper = $this->getHelper('question'); + + $tableExists = \DBManager::get()->execute('SHOW TABLES LIKE ?', [$dbTable]); + + $describeModel = false; + if ($tableExists) { + $question = new ChoiceQuestion( + "\nDescribe model:\n", + $modelName + ); + $describeModel = $helper->ask($input, $output, $question); + } + + if ($describeModel) { + $greetInput = new ArrayInput([ + 'command' => 'sorm:describe', + 'name' => 'Fabien', + '--yell' => true, + ]); + + $returnCode = $this->getApplication()->doRun($greetInput, $output); + } + + return $filename; + } +} diff --git a/cli/Commands/Make/StudipClassPrinter.php b/cli/Commands/Make/StudipClassPrinter.php new file mode 100644 index 0000000000000000000000000000000000000000..5e860850ef6bb909099529487725c7a9f4e15287 --- /dev/null +++ b/cli/Commands/Make/StudipClassPrinter.php @@ -0,0 +1,18 @@ +<?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 858dc3d2e3fdbd32009dfcda385259b0cd7cd590..4d19e2b1149550418f3b4f315cd542e2c131f5f5 100755 --- a/cli/studip +++ b/cli/studip @@ -32,6 +32,8 @@ $commands = [ Commands\Cronjobs\CronjobWorker::class, Commands\DB\Dump::class, Commands\DB\MoveMatrikelnummer::class, + Commands\Make\Migration::class, + Commands\Make\Model::class, Commands\DI\Reset::class, Commands\Files\Dump::class, Commands\Fix\Biest7789::class, diff --git a/composer.json b/composer.json index e1288181c1c53a2f060cfd6b40d1f5ff5831307a..efafcdbd86bf5b52b551486c34f22a885c2fa9dd 100644 --- a/composer.json +++ b/composer.json @@ -128,7 +128,8 @@ "symfony/polyfill-php84": "1.30.0", "nyholm/psr7": "1.8.1", "nyholm/psr7-server": "1.1.0", - "league/oauth2-client": "2.7.0" + "league/oauth2-client": "2.7.0", + "nette/php-generator": "4.1.5" }, "replace": { "symfony/polyfill-php73": "*", diff --git a/composer.lock b/composer.lock index 0cc31d19a4aea87bb0e1198220a1fe09873d5b65..14101efc5d97e18885a1b348d5c60d9884b8ffbc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "cad2a823e38968efd43dc8b63fdd8812", + "content-hash": "5ec4df54d92509cb7d4ff4f9dc95803b", "packages": [ { "name": "algo26-matthias/idna-convert", @@ -2026,6 +2026,161 @@ }, "time": "2022-11-28T03:29:06+00:00" }, + { + "name": "nette/php-generator", + "version": "v4.1.5", + "source": { + "type": "git", + "url": "https://github.com/nette/php-generator.git", + "reference": "690b00d81d42d5633e4457c43ef9754573b6f9d6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/php-generator/zipball/690b00d81d42d5633e4457c43ef9754573b6f9d6", + "reference": "690b00d81d42d5633e4457c43ef9754573b6f9d6", + "shasum": "" + }, + "require": { + "nette/utils": "^3.2.9 || ^4.0", + "php": "8.0 - 8.3" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "^2.4", + "nikic/php-parser": "^4.18 || ^5.0", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.8" + }, + "suggest": { + "nikic/php-parser": "to use ClassType::from(withBodies: true) & ClassType::fromCode()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "😠Nette PHP Generator: generates neat PHP code for you. Supports new PHP 8.3 features.", + "homepage": "https://nette.org", + "keywords": [ + "code", + "nette", + "php", + "scaffolding" + ], + "support": { + "issues": "https://github.com/nette/php-generator/issues", + "source": "https://github.com/nette/php-generator/tree/v4.1.5" + }, + "time": "2024-05-12T17:31:02+00:00" + }, + { + "name": "nette/utils", + "version": "v4.0.5", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", + "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", + "shasum": "" + }, + "require": { + "php": "8.0 - 8.4" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "^2.5", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.0.5" + }, + "time": "2024-08-07T15:39:19+00:00" + }, { "name": "nikic/fast-route", "version": "v1.3.0",