Bitrix-D7 23.9
 
Загрузка...
Поиск...
Не найдено
ormannotatecommand.php
1<?php
10
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;
32
37class OrmAnnotateCommand extends Command implements AnnotationInterface
38{
39 use AnnotationTrait;
40
41 protected $debug = 0;
42
43 protected $modulesScanned = [];
44
45 protected $filesIncluded = 0;
46
48 protected $entitiesFound = [];
49
50 protected $excludedFiles = [
51 'main/lib/text/string.php',
52 'main/lib/composite/compatibility/aliases.php',
53 'sale/lib/delivery/extra_services/string.php',
54 ];
55
56 protected function configure()
57 {
58 $inBitrixDir = realpath(Application::getDocumentRoot().Application::getPersonalRoot()) === realpath(getcwd());
59
60 $this
61 // the name of the command (the part after "bin/console")
62 ->setName('orm:annotate')
63
64 // the short description shown while running "php bin/console list"
65 ->setDescription('Scans project for ORM Entities.')
66
67 // the full command description shown when running the command with
68 // the "--help" option
69 ->setHelp('This system command optimizes Entity Relation Map building.')
70
71 ->setDefinition(
72 new InputDefinition(array(
73 new InputArgument(
74 'output', InputArgument::OPTIONAL, 'File for annotations to be saved to',
75 $inBitrixDir
76 ? 'modules/orm_annotations.php'
77 : Application::getDocumentRoot().Application::getPersonalRoot().'/modules/orm_annotations.php'
78 ),
79 new InputOption(
80 'modules', 'm', InputOption::VALUE_OPTIONAL,
81 'Modules to be scanned, separated by comma.', 'main'
82 ),
83 new InputOption(
84 'clean', 'c', InputOption::VALUE_NONE,
85 'Clean current entity map.'
86 ),
87 ))
88 )
89 ;
90
91 // disable Loader::requireModule exception
92 Loader::setRequireThrowException(false);
93 }
94
95 protected function execute(InputInterface $input, OutputInterface $output)
96 {
97 $output->writeln([
98 'Entity Scanner',
99 '==============',
100 '',
101 ]);
102
103 $time = microtime(true);
104 $memoryBefore = memory_get_usage();
105
107 $exceptions = [];
108
109 // handle already known classes (but we don't know their modules)
110 // as long as there are no any Table by default, we can ignore it
111 //$this->handleClasses($this->getDeclaredClassesDiff(), $input, $output);
112
113 // skip already defined classes
114 $this->getDeclaredClassesDiff();
115
116 // scan dirs
117 $inputModules = [];
118 $inputModulesRaw = $input->getOption('modules');
119
120 if (!empty($inputModulesRaw) && $inputModulesRaw != 'all')
121 {
122 $inputModules = explode(',', $inputModulesRaw);
123 }
124
125 $dirs = $this->getDirsToScan($inputModules, $input, $output);
126
127 foreach ($dirs as $dir)
128 {
129 $this->scanDir($dir, $input, $output);
130 }
131
132 // scan for bitrix entities
133 $this->scanBitrixEntities($inputModules, $input, $output);
134
135 // get classes from outside regular filesystem (e.g. iblock, hlblock)
136 try
137 {
138 $this->handleVirtualClasses($inputModules, $input, $output);
139 }
140 catch (\Exception $e)
141 {
142 $exceptions[] = $e;
143 }
144
145 // output file path
146 $filePath = $input->getArgument('output');
147 $filePath = ($filePath[0] == '/')
148 ? $filePath // absolute
149 : getcwd().'/'.$filePath; // relative
150
151 // handle entities
152 $annotations = [];
153
154 // get current annotations
155 if (!$input->getOption('clean') && file_exists($filePath) && is_readable($filePath))
156 {
157 $rawAnnotations = explode('/* '.static::ANNOTATION_MARKER, file_get_contents($filePath));
158
159 foreach ($rawAnnotations as $rawAnnotation)
160 {
161 if ($rawAnnotation[0] === ':')
162 {
163 $endPos = mb_strpos($rawAnnotation, ' */');
164 $entityClass = mb_substr($rawAnnotation, 1, $endPos - 1);
165 //$annotation = substr($rawAnnotation, $endPos + 3 + strlen(PHP_EOL));
166
167 $annotations[$entityClass] = '/* '.static::ANNOTATION_MARKER.rtrim($rawAnnotation);
168 }
169 }
170 }
171
172 // add/rewrite new entities
173 foreach ($this->entitiesFound as $entityMeta)
174 {
175 try
176 {
177 $entityClass = $entityMeta['class'];
178 $annotateUfOnly = $entityMeta['ufOnly'];
179
180 $entity = Entity::getInstance($entityClass);
181 $entityAnnotation = static::annotateEntity($entity, $annotateUfOnly);
182
183 if (!empty($entityAnnotation))
184 {
185 $annotations[$entityClass] = "/* ".static::ANNOTATION_MARKER.":{$entityClass} */".PHP_EOL;
186 $annotations[$entityClass] .= $entityAnnotation;
187 }
188 }
189 catch (\Exception $e)
190 {
191 $exceptions[] = $e;
192 }
193 }
194
195 // write to file
196 $fileContent = '<?php'.PHP_EOL.PHP_EOL.join(PHP_EOL, $annotations);
197 file_put_contents($filePath, $fileContent);
198
199 $output->writeln('Map has been saved to: '.$filePath);
200
201 // summary stats
202 $time = round(microtime(true) - $time, 2);
203 $memoryAfter = memory_get_usage();
204 $memoryDiff = $memoryAfter - $memoryBefore;
205
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');
212
213 if (!empty($exceptions))
214 {
215 $io = new SymfonyStyle($input, $output);
216
217 foreach ($exceptions as $e)
218 {
219 $io->warning('Exception: '.$e->getMessage().PHP_EOL.$e->getTraceAsString());
220 }
221 }
222
223 return 0;
224 }
225
226 protected function getDirsToScan($inputModules, InputInterface $input, OutputInterface $output)
227 {
228 $basePaths = [
229 //Application::getDocumentRoot().Application::getPersonalRoot().'/modules/',
230 Application::getDocumentRoot().'/local/modules/'
231 ];
232
233 $dirs = [];
234
235 foreach ($basePaths as $basePath)
236 {
237 if (!file_exists($basePath))
238 {
239 continue;
240 }
241
242 $moduleList = [];
243
244 foreach (new \DirectoryIterator($basePath) as $item)
245 {
246 if($item->isDir() && !$item->isDot())
247 {
248 $moduleList[] = $item->getFilename();
249 }
250 }
251
252 // filter for input modules
253 if (!empty($inputModules))
254 {
255 $moduleList = array_intersect($moduleList, $inputModules);
256 }
257
258 foreach ($moduleList as $moduleName)
259 {
260 // filter for installed modules
261 if (!Loader::includeModule($moduleName))
262 {
263 continue;
264 }
265
266 $libDir = $basePath.$moduleName.'/lib';
267 if (is_dir($libDir) && is_readable($libDir))
268 {
269 $dirs[] = $libDir;
270 }
271
272 $libDir = $basePath.$moduleName.'/dev/lib';
273 if (is_dir($libDir) && is_readable($libDir))
274 {
275 $dirs[] = $libDir;
276 }
277
278 $this->modulesScanned[] = $moduleName;
279 }
280 }
281
282 return $dirs;
283 }
284
285 protected function scanBitrixEntities($inputModules, InputInterface $input, OutputInterface $output)
286 {
287 $basePath = Application::getDocumentRoot().Application::getPersonalRoot().'/modules/';
288
289 // get all available modules
290 $moduleList = [];
291
292 foreach (new \DirectoryIterator($basePath) as $item)
293 {
294 if($item->isDir() && !$item->isDot())
295 {
296 $moduleList[] = $item->getFilename();
297 }
298 }
299
300 // filter for input modules
301 if (!empty($inputModules))
302 {
303 $moduleList = array_intersect($moduleList, $inputModules);
304 }
305
306 // collect classes
307 foreach ($moduleList as $moduleName)
308 {
309 $ufPath = $basePath.$moduleName.'/meta/'.static::ANNOTATION_UF_FILENAME;
310
311 if (file_exists($ufPath))
312 {
313 $classes = include $ufPath;
314
315 foreach ($classes as $class)
316 {
317 if (class_exists($class))
318 {
319 $this->entitiesFound[] = [
320 'class' => $class,
321 'ufOnly' => true,
322 ];
323 }
324 }
325 }
326 }
327
328 // clear diff buffer
329 $this->getDeclaredClassesDiff();
330 }
331
332 protected function registerFallbackAutoload()
333 {
334 spl_autoload_register(function($className) {
335 list($vendor, $module) = explode('\\', $className);
336
337 if (!empty($module))
338 {
339 Loader::includeModule($module);
340 }
341
342 return Loader::autoLoad($className);
343 });
344 }
345
346 protected function scanDir($dir, InputInterface $input, OutputInterface $output)
347 {
348 $this->debug($output,'scan dir: '.$dir);
349
350 $this->registerFallbackAutoload();
351
352 foreach (
353 $iterator = new \RecursiveIteratorIterator(
354 new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS | \RecursiveDirectoryIterator::FOLLOW_SYMLINKS),
355 \RecursiveIteratorIterator::SELF_FIRST) as $item
356 )
357 {
358 // check for stop list
359 foreach ($this->excludedFiles as $excludedFile)
360 {
361 $currentPath = str_replace('\\', '/', $item->getPathname());
362 if (mb_substr($currentPath, -mb_strlen($excludedFile)) === $excludedFile)
363 {
364 continue 2;
365 }
366 }
367
370 if ($item->isFile() && $item->isReadable() && mb_substr($item->getFilename(), -4) == '.php')
371 {
372 $this->debug($output,'handle file: '.$item->getPathname());
373
374 try
375 {
376 // get classes from file
377 include_once $item->getPathname();
378 $this->filesIncluded++;
379
380 $classes = $this->getDeclaredClassesDiff();
381
382 // check classes
383 $this->handleClasses($classes, $input, $output);
384 }
385 catch (\Throwable $e) // php7
386 {
387 $this->debug($output, $e->getMessage());
388 }
389 catch (\Exception $e) // php5
390 {
391 $this->debug($output, $e->getMessage());
392 }
393 }
394 }
395 }
396
397 protected function handleClasses($classes, InputInterface $input, OutputInterface $output)
398 {
399 foreach ($classes as $class)
400 {
401 $debugMsg = $class;
402
403 if (is_subclass_of($class, DataManager::class) && mb_substr($class, -5) == 'Table')
404 {
405 if ((new \ReflectionClass($class))->isAbstract())
406 {
407 continue;
408 }
409
410 $debugMsg .= ' found!';
411 $this->entitiesFound[] = [
412 'class' => $class,
413 'ufOnly' => false,
414 ];
415 }
416
417 $this->debug($output, $debugMsg);
418 }
419 }
420
421 protected function getDeclaredClassesDiff()
422 {
423 static $lastDeclaredClasses = [];
424
425 $currentDeclaredClasses = get_declared_classes();
426 $diff = array_diff($currentDeclaredClasses, $lastDeclaredClasses);
427 $lastDeclaredClasses = $currentDeclaredClasses;
428
429 return $diff;
430 }
431
439 protected function handleVirtualClasses($inputModules, InputInterface $input, OutputInterface $output)
440 {
441 // init new classes by event
442 $event = new \Bitrix\Main\Event("main", "onVirtualClassBuildList", [], $inputModules);
443 $event->send();
444
445 // no need to handle event result, get classes from the memory
446 $classes = $this->getDeclaredClassesDiff();
447
448 $this->handleClasses($classes, $input, $output);
449 }
450
458 public static function scalarFieldToTypeHint($field)
459 {
460 if (is_string($field))
461 {
462 $fieldClass = $field;
463 }
464 else
465 {
466 $fieldClass = get_class($field);
467 }
468
469 switch ($fieldClass)
470 {
471 case DateField::class:
472 return '\\'.Date::class;
473 case DatetimeField::class:
474 return '\\'.DateTime::class;
475 case IntegerField::class:
476 return '\\int';
477 case BooleanField::class:
478 return '\\boolean';
479 case FloatField::class:
480 return '\\float';
481 case ArrayField::class:
482 return 'array';
483 default:
484 return '\\string';
485 }
486 }
487
488 protected function debug(OutputInterface $output, $message)
489 {
490 if ($this->debug)
491 {
492 $output->writeln($message);
493 }
494 }
495}