14use Bitrix\Main\ORM\Annotations\AnnotationTrait;
25use Symfony\Component\Console\Command\Command;
26use Symfony\Component\Console\Input\InputArgument;
27use Symfony\Component\Console\Input\InputDefinition;
28use Symfony\Component\Console\Input\InputInterface;
29use Symfony\Component\Console\Input\InputOption;
30use Symfony\Component\Console\Output\OutputInterface;
31use Symfony\Component\Console\Style\SymfonyStyle;
43 protected $modulesScanned = [];
45 protected $filesIncluded = 0;
48 protected $entitiesFound = [];
50 protected $excludedFiles = [
51 'main/lib/text/string.php',
52 'main/lib/composite/compatibility/aliases.php',
53 'sale/lib/delivery/extra_services/string.php',
56 protected function configure()
62 ->setName(
'orm:annotate')
65 ->setDescription(
'Scans project for ORM Entities.')
69 ->setHelp(
'This system command optimizes Entity Relation Map building.')
72 new InputDefinition(array(
74 'output', InputArgument::OPTIONAL,
'File for annotations to be saved to',
76 ?
'modules/orm_annotations.php'
80 'modules',
'm', InputOption::VALUE_OPTIONAL,
81 'Modules to be scanned, separated by comma.',
'main'
84 'clean',
'c', InputOption::VALUE_NONE,
85 'Clean current entity map.'
92 Loader::setRequireThrowException(
false);
95 protected function execute(InputInterface $input, OutputInterface $output)
103 $time = microtime(
true);
104 $memoryBefore = memory_get_usage();
114 $this->getDeclaredClassesDiff();
118 $inputModulesRaw = $input->getOption(
'modules');
120 if (!empty($inputModulesRaw) && $inputModulesRaw !=
'all')
122 $inputModules = explode(
',', $inputModulesRaw);
125 $dirs = $this->getDirsToScan($inputModules, $input, $output);
127 foreach ($dirs as $dir)
129 $this->scanDir($dir, $input, $output);
133 $this->scanBitrixEntities($inputModules, $input, $output);
138 $this->handleVirtualClasses($inputModules, $input, $output);
140 catch (\Exception $e)
146 $filePath = $input->getArgument(
'output');
147 $filePath = ($filePath[0] ==
'/')
149 : getcwd().
'/'.$filePath;
155 if (!$input->getOption(
'clean') && file_exists($filePath) && is_readable($filePath))
157 $rawAnnotations = explode(
'/* '.static::ANNOTATION_MARKER, file_get_contents($filePath));
159 foreach ($rawAnnotations as $rawAnnotation)
161 if ($rawAnnotation[0] ===
':')
163 $endPos = mb_strpos($rawAnnotation,
' */');
164 $entityClass = mb_substr($rawAnnotation, 1, $endPos - 1);
167 $annotations[$entityClass] =
'/* '.static::ANNOTATION_MARKER.rtrim($rawAnnotation);
173 foreach ($this->entitiesFound as $entityMeta)
177 $entityClass = $entityMeta[
'class'];
178 $annotateUfOnly = $entityMeta[
'ufOnly'];
180 $entity = Entity::getInstance($entityClass);
181 $entityAnnotation = static::annotateEntity($entity, $annotateUfOnly);
183 if (!empty($entityAnnotation))
185 $annotations[$entityClass] =
"/* ".static::ANNOTATION_MARKER.
":{$entityClass} */".PHP_EOL;
186 $annotations[$entityClass] .= $entityAnnotation;
189 catch (\Exception $e)
196 $fileContent =
'<?php'.PHP_EOL.PHP_EOL.join(PHP_EOL, $annotations);
197 file_put_contents($filePath, $fileContent);
199 $output->writeln(
'Map has been saved to: '.$filePath);
202 $time = round(microtime(
true) - $time, 2);
203 $memoryAfter = memory_get_usage();
204 $memoryDiff = $memoryAfter - $memoryBefore;
206 $output->writeln(
'Scanned modules: '.join(
', ', $this->modulesScanned));
207 $output->writeln(
'Scanned files: '.$this->filesIncluded);
208 $output->writeln(
'Found entities: '.count($this->entitiesFound));
209 $output->writeln(
'Time: '.$time.
' sec');
210 $output->writeln(
'Memory usage: '.(round($memoryAfter/1024/1024, 1)).
'M (+'.(round($memoryDiff/1024/1024, 1)).
'M)');
211 $output->writeln(
'Memory peak usage: '.(round(memory_get_peak_usage()/1024/1024, 1)).
'M');
213 if (!empty($exceptions))
215 $io =
new SymfonyStyle($input, $output);
217 foreach ($exceptions as $e)
219 $io->warning(
'Exception: '.$e->getMessage().PHP_EOL.$e->getTraceAsString());
226 protected function getDirsToScan($inputModules, InputInterface $input, OutputInterface $output)
235 foreach ($basePaths as $basePath)
237 if (!file_exists($basePath))
244 foreach (
new \DirectoryIterator($basePath) as $item)
246 if($item->isDir() && !$item->isDot())
248 $moduleList[] = $item->getFilename();
253 if (!empty($inputModules))
255 $moduleList = array_intersect($moduleList, $inputModules);
258 foreach ($moduleList as $moduleName)
261 if (!Loader::includeModule($moduleName))
266 $libDir = $basePath.$moduleName.
'/lib';
267 if (is_dir($libDir) && is_readable($libDir))
272 $libDir = $basePath.$moduleName.
'/dev/lib';
273 if (is_dir($libDir) && is_readable($libDir))
278 $this->modulesScanned[] = $moduleName;
285 protected function scanBitrixEntities($inputModules, InputInterface $input, OutputInterface $output)
292 foreach (
new \DirectoryIterator($basePath) as $item)
294 if($item->isDir() && !$item->isDot())
296 $moduleList[] = $item->getFilename();
301 if (!empty($inputModules))
303 $moduleList = array_intersect($moduleList, $inputModules);
307 foreach ($moduleList as $moduleName)
309 $ufPath = $basePath.$moduleName.
'/meta/'.static::ANNOTATION_UF_FILENAME;
311 if (file_exists($ufPath))
313 $classes = include $ufPath;
315 foreach ($classes as $class)
317 if (class_exists($class))
319 $this->entitiesFound[] = [
329 $this->getDeclaredClassesDiff();
332 protected function registerFallbackAutoload()
334 spl_autoload_register(
function($className) {
335 list($vendor, $module) = explode(
'\\', $className);
339 Loader::includeModule($module);
342 return Loader::autoLoad($className);
346 protected function scanDir($dir, InputInterface $input, OutputInterface $output)
348 $this->debug($output,
'scan dir: '.$dir);
350 $this->registerFallbackAutoload();
353 $iterator =
new \RecursiveIteratorIterator(
354 new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS | \RecursiveDirectoryIterator::FOLLOW_SYMLINKS),
355 \RecursiveIteratorIterator::SELF_FIRST) as $item
359 foreach ($this->excludedFiles as $excludedFile)
361 $currentPath = str_replace(
'\\',
'/', $item->getPathname());
362 if (mb_substr($currentPath, -mb_strlen($excludedFile)) === $excludedFile)
370 if ($item->isFile() && $item->isReadable() && mb_substr($item->getFilename(), -4) ==
'.php')
372 $this->debug($output,
'handle file: '.$item->getPathname());
377 include_once $item->getPathname();
378 $this->filesIncluded++;
380 $classes = $this->getDeclaredClassesDiff();
383 $this->handleClasses($classes, $input, $output);
385 catch (\Throwable $e)
387 $this->debug($output, $e->getMessage());
389 catch (\Exception $e)
391 $this->debug($output, $e->getMessage());
397 protected function handleClasses($classes, InputInterface $input, OutputInterface $output)
399 foreach ($classes as $class)
403 if (is_subclass_of($class, DataManager::class) && mb_substr($class, -5) ==
'Table')
405 if ((
new \ReflectionClass($class))->isAbstract())
410 $debugMsg .=
' found!';
411 $this->entitiesFound[] = [
417 $this->debug($output, $debugMsg);
421 protected function getDeclaredClassesDiff()
423 static $lastDeclaredClasses = [];
425 $currentDeclaredClasses = get_declared_classes();
426 $diff = array_diff($currentDeclaredClasses, $lastDeclaredClasses);
427 $lastDeclaredClasses = $currentDeclaredClasses;
439 protected function handleVirtualClasses($inputModules, InputInterface $input, OutputInterface $output)
442 $event = new \Bitrix\Main\Event(
"main",
"onVirtualClassBuildList", [], $inputModules);
446 $classes = $this->getDeclaredClassesDiff();
448 $this->handleClasses($classes, $input, $output);
458 public static function scalarFieldToTypeHint($field)
460 if (is_string($field))
462 $fieldClass = $field;
466 $fieldClass = get_class($field);
471 case DateField::class:
472 return '\\'.Date::class;
473 case DatetimeField::class:
474 return '\\'.DateTime::class;
475 case IntegerField::class:
477 case BooleanField::class:
479 case FloatField::class:
481 case ArrayField::class:
488 protected function debug(OutputInterface $output, $message)
492 $output->writeln($message);