Select Git revision
DescribeModels.php
Forked from
Stud.IP / Stud.IP
Source project has a limited visibility.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
DescribeModels.php 10.96 KiB
<?php
namespace Studip\Cli\Commands\SORM;
use SimpleORMapCollection;
use Studip\Cli\Commands\AbstractCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar;
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 DescribeModels extends AbstractCommand
{
const METHODS_TEMPLATES = [
'findOneBy%s' => 'static static findOneBy%s(string $value, string $order = \'\')',
'findBy%s' => 'static static[] findBy%s(string $value, string $order = \'\')',
'findManyBy%s' => 'static static[] findManyBy%s(array $values, string $order = \'\')',
'findEachBy%s' => 'static static[] findEachBy%s(callable $callable, string $value, string $order = \'\')',
'findEachManyBy%s' => 'static static[] findEachManyBy%s(callable $callable, array $values, string $order = \'\')',
'countBy%s' => 'static int countBy%s(string $value)',
'deleteBy%s' => 'static int deleteBy%s(string $value)',
];
protected static $defaultName = 'sorm:describe';
private $progress;
protected function configure(): void
{
$this->setDescription('Describe models');
$this->setHelp('This command will add neccessary @property annotations to SimpleORMap classes in a folder');
$this->addArgument(
'folder',
InputArgument::OPTIONAL,
'Folder to scan (will default to lib/models)',
'lib/models'
);
$this->addOption(
'recursive',
'r',
InputOption::VALUE_NEGATABLE,
'Scan into subfolders recursively'
);
$this->addOption(
'bootstrap',
'b',
InputOption::VALUE_OPTIONAL,
'Execute bootstrap file before scanning folder'
);
}
public function execute(InputInterface $input, OutputInterface $output)
{
$bootstrap = $input->getOption('bootstrap');
if ($bootstrap) {
if (!file_exists($bootstrap)) {
throw new \Exception("Invalid bootstrap file {$bootstrap} provided");
}
require_once $bootstrap;
}
$recursive = $input->getOption('recursive') ?? false;
$folder = $input->getArgument('folder');
$iterator = $this->getFolderIterator($folder, $recursive, ['php']);
$this->progress = new ProgressBar($output, iterator_count($iterator));
$this->progress->start();
foreach ($iterator as $file) {
$filename = $file->getFilename();
if (!is_writable($file->getRealPath())) {
$this->outputForFile(
$output,
'<comment>Skipping not writable file ' . $this->relativeFilePath($file->getPathname()) . '</comment>'
);
continue;
}
$class_name = str_replace('.class.php', '.php', $filename);
$class_name = pathinfo($class_name, PATHINFO_FILENAME);
if (!class_exists($class_name)) {
$class_name = $this->getClassNameFromFile($file->getPathname()) ?? $class_name;
}
if (!class_exists($class_name) || !is_subclass_of($class_name, \SimpleORMap::class)) {
$this->outputForFile(
$output,
"Skipping invalid class file {$filename} (class {$class_name})",
OutputInterface::VERBOSITY_VERBOSE
);
continue;
}
try {
$reflection = new \ReflectionClass($class_name);
} catch (\Error $e) {
$this->outputForFile(
$output,
"<error>Could not get reflection for class {$class_name} ({$e->getMessage()})</error>"
);
continue;
}
if ($reflection->isAbstract()) {
$this->outputForFile(
$output,
"Skipping abstract class {$class_name}",
OutputInterface::VERBOSITY_VERBOSE
);
continue;
}
$model = $reflection->newInstance();
$meta = $model->getTableMetaData();
$properties = [];
$methods = [];
foreach ($meta['fields'] as $field => $info) {
$name = mb_strtolower($field);
$type = $this->getPHPType($info);
$properties[$name] = [
'type' => $type,
'description' => 'database column',
];
$methods = array_merge($methods, $this->makeMethods($reflection, $field));
if ($alias = array_search($name, $meta['alias_fields'])) {
$properties[$alias] = [
'type' => $type,
'description' => "alias column for {$name}",
];
$methods = array_merge($methods, $this->makeMethods($reflection, $alias));
}
}
foreach ($meta['relations'] as $relation) {
$options = $model->getRelationOptions($relation);
$related_class_name = $options['class_name'];
if (in_array($options['type'], ['has_many', 'has_and_belongs_to_many'])) {
$related_class_name = SimpleORMapCollection::class;
}
if ($reflection->inNamespace()) {
$related_class_name = "\\{$related_class_name}";
if (mb_strpos($related_class_name, "\\{$reflection->getNamespaceName()}") === 0) {
$related_class_name = substr($related_class_name, strlen($reflection->getNamespaceName()) + 2);
}
}
$properties[$relation] = [
'type' => $related_class_name,
'description' => "{$options['type']} {$options['class_name']}",
];
}
if ($this->updateDocBlockOfClass($reflection, $properties, $methods)) {
$this->outputForFile(
$output,
'<info>Updated ' . $this->relativeFilePath($file->getPathname()) . '</info>'
);
} else {
$this->outputForFile(
$output,
'No changes in ' . $this->relativeFilePath($file->getPathname()),
OutputInterface::VERBOSITY_VERBOSE
);
}
}
$this->progress->clear();
return Command::SUCCESS;
}
private function outputForFile($output, ...$args)
{
$this->progress->advance();
$this->progress->clear();
$output->writeln(...$args);
$this->progress->display();
}
private function getPHPType($info)
{
if (preg_match('/^(?:tiny|small|medium|big)?int(?:eger)?/iS', $info['type'])) {
return 'int';
}
if (preg_match('/^(?:decimal|double|float|numeric)/iS', $info['type'])) {
return 'float';
}
if (preg_match('/^bool(?:ean)?/iS', $info['type'])) {
return 'bool';
}
return 'string';
}
private function updateDocBlockOfClass(\ReflectionClass $reflection, array $properties, array $methods): bool
{
$has_docblock = (bool) $reflection->getDocComment();
$docblock = $reflection->getDocComment() ?: $this->getDefaultDocblock();
$docblock_lines = array_map('rtrim', explode("\n", $docblock));
$properties_started = false;
$docblock_lines = array_filter($docblock_lines, function ($line) use (&$properties_started) {
$line = ltrim($line, '* ');
if ($properties_started) {
return $line === '/';
}
$properties_started = strpos($line, '@property ') === 0;
return !$properties_started;
});
$docblock_lines = array_reverse($docblock_lines);
while ($docblock_lines[1] === ' *') {
array_splice($docblock_lines, 1, 1);
}
$docblock_lines = array_reverse($docblock_lines);
$properties = array_map(function ($variable, $property) {
return " * @property {$property['type']} \${$variable} {$property['description']}";
}, array_keys($properties), array_values($properties));
array_unshift($properties, ' *');
array_splice($docblock_lines, -1, 0, $properties);
$methods = array_map(function ($method) {
return " * @method {$method}";
}, $methods);
array_unshift($methods, ' *');
array_splice($docblock_lines, -1, 0, $methods);
$new_docblock = implode("\n", $docblock_lines);
if ($docblock === $new_docblock) {
return false;
}
$contents = file_get_contents($reflection->getFileName());
if ($has_docblock) {
$contents = str_replace($docblock, $new_docblock, $contents);
} else {
$contents = preg_replace(
'/^class/m',
$new_docblock . "\nclass",
$contents,
1
);
}
file_put_contents($reflection->getFileName(), $contents);
return true;
}
private function getDefaultDocBlock(): string
{
return implode("\n", [
'/**',
' * @license GPL2 or any later version',
' */',
]);
}
/**
* @see https://stackoverflow.com/a/14250011
*/
private function getClassNameFromFile(string $filename): ?string
{
$code = file_get_contents($filename);
$tokens = token_get_all($code);
$namespace = '';
for ($i = 0, $l = count($tokens); $i < $l; $i += 1) {
if ($tokens[$i][0] === T_NAMESPACE) {
for ($j= $i + 1; $j < $l; $j += 1) {
if ($tokens[$j][0] === T_STRING) {
$namespace .= "\\" . $tokens[$j][1];
} elseif ($tokens[$j] === '{' || $tokens[$j] === ';') {
break;
}
}
}
if ($tokens[$i][0] === T_CLASS) {
for ($j = $i + 1; $j < $l; $j += 1) {
if ($tokens[$j] === '{') {
return ltrim($namespace . "\\" . $tokens[$i + 2][1], "\\");
}
}
}
}
return null;
}
private function makeMethods(\ReflectionClass $reflection, string $field): array
{
$result = [];
foreach (self::METHODS_TEMPLATES as $name => $template) {
$method = sprintf($name, $field);
if (!$reflection->hasMethod($method)) {
$result[] = sprintf($template, $field);
}
}
return $result;
}
}