aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/Commands/SORM/DescribeModels.php105
-rw-r--r--composer.json3
-rw-r--r--composer.lock51
-rw-r--r--lib/classes/StudipArrayObject.class.php5
-rw-r--r--lib/models/SimpleCollection.class.php1
-rw-r--r--lib/models/SimpleORMapCollection.class.php1
6 files changed, 146 insertions, 20 deletions
diff --git a/cli/Commands/SORM/DescribeModels.php b/cli/Commands/SORM/DescribeModels.php
index b1084d6..43ce21f 100644
--- a/cli/Commands/SORM/DescribeModels.php
+++ b/cli/Commands/SORM/DescribeModels.php
@@ -1,6 +1,19 @@
<?php
namespace Studip\Cli\Commands\SORM;
+use Error;
+use Exception;
+use I18NString;
+use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
+use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
+use PHPStan\PhpDocParser\Lexer\Lexer;
+use PHPStan\PhpDocParser\Parser\ConstExprParser;
+use PHPStan\PhpDocParser\Parser\PhpDocParser;
+use PHPStan\PhpDocParser\Parser\TokenIterator;
+use PHPStan\PhpDocParser\Parser\TypeParser;
+use PHPStan\PhpDocParser\ParserConfig;
+use ReflectionClass;
+use SimpleORMap;
use SimpleORMapCollection;
use Studip\Cli\Commands\AbstractCommand;
use Symfony\Component\Console\Command\Command;
@@ -14,8 +27,8 @@ final class DescribeModels extends AbstractCommand
{
protected static $defaultName = 'sorm:describe';
- private $progress;
- private $reflection;
+ private ProgressBar $progress;
+ private ReflectionClass $reflection;
protected function configure(): void
{
@@ -48,7 +61,7 @@ final class DescribeModels extends AbstractCommand
$bootstrap = $input->getOption('bootstrap');
if ($bootstrap) {
if (!file_exists($bootstrap)) {
- throw new \Exception("Invalid bootstrap file {$bootstrap} provided");
+ throw new Exception("Invalid bootstrap file {$bootstrap} provided");
}
require_once $bootstrap;
}
@@ -77,7 +90,7 @@ final class DescribeModels extends AbstractCommand
$class_name = $this->getClassNameFromFile($file->getPathname()) ?? $class_name;
}
- if (!class_exists($class_name) || !is_subclass_of($class_name, \SimpleORMap::class)) {
+ if (!class_exists($class_name) || !is_subclass_of($class_name, SimpleORMap::class)) {
$this->outputForFile(
$output,
"Skipping invalid class file {$filename} (class {$class_name})",
@@ -87,8 +100,8 @@ final class DescribeModels extends AbstractCommand
}
try {
- $this->reflection = new \ReflectionClass($class_name);
- } catch (\Error $e) {
+ $this->reflection = new ReflectionClass($class_name);
+ } catch (Error $e) {
$this->outputForFile(
$output,
"<error>Could not get reflection for class {$class_name} ({$e->getMessage()})</error>"
@@ -108,6 +121,10 @@ final class DescribeModels extends AbstractCommand
$model = $this->reflection->newInstance();
+ // Get current properties
+ $current_properties = $this->getPropertiesFromDocBlock();
+
+
// Get configuration for class
$config_property = $this->reflection->getProperty('config');
$config_property->setAccessible(true);
@@ -121,23 +138,26 @@ final class DescribeModels extends AbstractCommand
if (!isset($meta['fields']['id']) && count($meta['pk']) > 0) {
$properties['id'] = [
'type' => count($meta['pk']) > 1 ? 'array' : $this->getPHPType($meta['pk'][0], $meta['fields'][$meta['pk'][0]]),
- 'description' => 'alias for pk',
+ 'description' => $current_properties['id']['description'] ?? 'alias for pk',
];
}
foreach ($meta['fields'] as $field => $info) {
$name = mb_strtolower($field);
$type = $this->getPHPType($field, $info, $model_config);
+ if ($type === 'int' && isset($current_properties[$field]) && $current_properties[$field]['type'] === 'bool') {
+ $type = 'bool';
+ }
$properties[$name] = [
'type' => $type,
- 'description' => 'database column',
+ 'description' => $current_properties[$field]['description'] ?? 'database column',
];
$alias = array_search($name, $meta['alias_fields']);
if ($alias) {
$properties[$alias] = [
'type' => $type,
- 'description' => "alias column for {$name}",
+ 'description' => $current_properties[$field]['description'] ?? "alias column for {$name}",
];
}
}
@@ -147,10 +167,11 @@ final class DescribeModels extends AbstractCommand
$related_class_name = $options['class_name'];
if (in_array($options['type'], ['has_many', 'has_and_belongs_to_many'])) {
- $related_type = implode('|', [
+ $related_type = sprintf(
+ '%s<%s>',
$this->adjustNamespaceForClass(SimpleORMapCollection::class),
- $this->adjustNameSpaceForClass($related_class_name) . '[]',
- ]);
+ $this->adjustNameSpaceForClass($related_class_name),
+ );
} else {
$related_type = $this->adjustNamespaceForClass($related_class_name);
@@ -161,12 +182,14 @@ final class DescribeModels extends AbstractCommand
&& $meta['fields'][$options['foreign_key']]['null'] === 'YES'
) {
$related_type .= '|null';
+ } elseif (preg_match('/^find(One?)By/', $options['assoc_func'])) {
+ $related_type .= '|null';
}
}
$properties[$relation] = [
'type' => $related_type,
- 'description' => "{$options['type']} " . $this->adjustNamespaceForClass($related_class_name),
+ 'description' => $current_properties[$relation]['description'] ?? ("{$options['type']} " . $this->adjustNamespaceForClass($related_class_name)),
];
}
@@ -184,8 +207,8 @@ final class DescribeModels extends AbstractCommand
$properties[$field] = [
'property_type' => $property_type,
- 'type' => $this->getAdditionFieldType($definition),
- 'description' => 'additional field',
+ 'type' => $current_properties[$field]['type'] ?? $this->getAdditionFieldType($definition),
+ 'description' => $current_properties[$field]['description'] ?? 'additional field',
];
}
@@ -226,7 +249,7 @@ final class DescribeModels extends AbstractCommand
if (isset($config['serialized_fields'][$field])) {
$type[] = $this->adjustNamespaceForClass($config['serialized_fields'][$field]);
} elseif (isset($config['i18n_fields'][$field])) {
- $type[] = $this->adjustNamespaceForClass(\I18NString::class);
+ $type[] = $this->adjustNamespaceForClass(I18NString::class);
} elseif (preg_match('/^(?:tiny|small|medium|big)?int(?:eger)?/i', $info['type'])) {
$type[] = 'int';
} elseif (preg_match('/^(?:decimal|double|float|numeric)/i', $info['type'])) {
@@ -256,7 +279,7 @@ final class DescribeModels extends AbstractCommand
return $line === '/';
}
- $properties_started = strpos($line, '@property ') === 0;
+ $properties_started = str_starts_with($line, '@property ');
return !$properties_started;
});
@@ -363,4 +386,52 @@ final class DescribeModels extends AbstractCommand
return 'mixed';
}
+
+ private function getPropertiesFromDocBlock(): array
+ {
+ $docblock = $this->reflection->getDocComment();
+
+ if (!trim($docblock)) {
+ return [];
+ }
+
+ $config = new ParserConfig([]);
+ $lexer = new Lexer($config);
+ $constExprParser = new ConstExprParser($config);
+ $typeParser = new TypeParser($config, $constExprParser);
+ $phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser);
+
+ $tokens = new TokenIterator($lexer->tokenize($docblock));
+ $parsed = $phpDocParser->parse($tokens);
+
+ $properties = [
+ ...$parsed->getPropertyTagValues(),
+ ...$parsed->getPropertyReadTagValues(),
+ ...$parsed->getPropertyWriteTagValues(),
+ ];
+
+ $result = [];
+ foreach ($properties as $property) {
+ $key = substr($property->propertyName, 1);
+ $result[$key] = [
+ 'type' => (string) $property->type,
+ 'description' => $this->cleanupDescription($property->description),
+ ];
+ }
+
+ return $result;
+ }
+
+ private function cleanupDescription(string $description): ?string
+ {
+ if (
+ in_array($description, ['database column', 'additional field', 'alias for pk'])
+ || preg_match('/^(has_one|has_many|has_and_belongs_to_many|belongs_to) \\S+/', $description)
+ || preg_match('/^alias column for \\S+/', $description)
+ ) {
+ return null;
+ }
+
+ return $description;
+ }
}
diff --git a/composer.json b/composer.json
index 01ef302..caf8724 100644
--- a/composer.json
+++ b/composer.json
@@ -14,7 +14,8 @@
"codeception/module-asserts": "^1.3",
"overtrue/phplint": "^3.0",
"phpstan/phpstan": "^1.8",
- "symfony/var-dumper": "^5.4"
+ "symfony/var-dumper": "^5.4",
+ "phpstan/phpdoc-parser": "^2.1"
},
"require": {
"php": "^7.2",
diff --git a/composer.lock b/composer.lock
index 3ea7d02..44b3f38 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": "a43332898f0d0a0e8d09c1144bf0da2a",
+ "content-hash": "054756aa03f56925515ee49819ddffb3",
"packages": [
{
"name": "algo26-matthias/idna-convert",
@@ -5913,6 +5913,53 @@
"time": "2020-07-07T09:29:14+00:00"
},
{
+ "name": "phpstan/phpdoc-parser",
+ "version": "2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpdoc-parser.git",
+ "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68",
+ "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/annotations": "^2.0",
+ "nikic/php-parser": "^5.3.0",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/extension-installer": "^1.0",
+ "phpstan/phpstan": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^9.6",
+ "symfony/process": "^5.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\PhpDocParser\\": [
+ "src/"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPDoc parser with support for nullable, intersection and generic types",
+ "support": {
+ "issues": "https://github.com/phpstan/phpdoc-parser/issues",
+ "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0"
+ },
+ "time": "2025-02-19T13:28:12+00:00"
+ },
+ {
"name": "phpstan/phpstan",
"version": "1.10.8",
"source": {
@@ -7664,7 +7711,7 @@
"ext-mbstring": "*",
"ext-dom": "*"
},
- "platform-dev": [],
+ "platform-dev": {},
"platform-overrides": {
"php": "7.4"
},
diff --git a/lib/classes/StudipArrayObject.class.php b/lib/classes/StudipArrayObject.class.php
index 47f116b..b0434ec 100644
--- a/lib/classes/StudipArrayObject.class.php
+++ b/lib/classes/StudipArrayObject.class.php
@@ -13,6 +13,11 @@
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
+ *
+ * @template TKey
+ * @template TValue
+ * @implements IteratorAggregate<TKey, TValue>
+ * @implements ArrayAccess<TKey, TValue>
*/
class StudipArrayObject implements IteratorAggregate, ArrayAccess, Countable
{
diff --git a/lib/models/SimpleCollection.class.php b/lib/models/SimpleCollection.class.php
index bbce972..aceb99e 100644
--- a/lib/models/SimpleCollection.class.php
+++ b/lib/models/SimpleCollection.class.php
@@ -14,6 +14,7 @@
* @category Stud.IP
*
* @template T
+ * @extends StudipArrayObject<T>
*/
class SimpleCollection extends StudipArrayObject
{
diff --git a/lib/models/SimpleORMapCollection.class.php b/lib/models/SimpleORMapCollection.class.php
index 8575847..8be6226 100644
--- a/lib/models/SimpleORMapCollection.class.php
+++ b/lib/models/SimpleORMapCollection.class.php
@@ -16,6 +16,7 @@
* @extends SimpleCollection<SimpleORMap>
*
* @template T of SimpleORMap
+ * @extends SimpleCollection<T>
*/
class SimpleORMapCollection extends SimpleCollection
{