Your IP : 52.14.158.115


Current Path : /var/www/www-root/data/www/monolith-realty.ru/bitrix/modules/main/lib/cli/
Upload File :
Current File : /var/www/www-root/data/www/monolith-realty.ru/bitrix/modules/main/lib/cli/ormannotatecommand.php

<?php
/**
 * Bitrix Framework
 * @package    bitrix
 * @subpackage main
 * @copyright  2001-2018 Bitrix
 */

namespace Bitrix\Main\Cli;

use Bitrix\Main\Application;
use Bitrix\Main\Loader;
use Bitrix\Main\ORM\Annotations\AnnotationInterface;
use Bitrix\Main\ORM\Annotations\AnnotationTrait;
use Bitrix\Main\ORM\Fields\ArrayField;
use Bitrix\Main\ORM\Fields\BooleanField;
use Bitrix\Main\ORM\Data\DataManager;
use Bitrix\Main\ORM\Entity;
use Bitrix\Main\ORM\Fields\DateField;
use Bitrix\Main\ORM\Fields\DatetimeField;
use Bitrix\Main\ORM\Fields\FloatField;
use Bitrix\Main\ORM\Fields\IntegerField;
use Bitrix\Main\Type\Date;
use Bitrix\Main\Type\DateTime;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
 * @package    bitrix
 * @subpackage main
 */
class OrmAnnotateCommand extends Command implements AnnotationInterface
{
	use AnnotationTrait;

	protected $debug = 0;

	protected $modulesScanned = [];

	protected $filesIncluded = 0;

	/** @var array Filled by handleClasses() */
	protected $entitiesFound = [];

	protected $excludedFiles = [
		'main/lib/text/string.php',
		'main/lib/composite/compatibility/aliases.php',
		'sale/lib/delivery/extra_services/string.php',
	];

	protected function configure()
	{
		$inBitrixDir = realpath(Application::getDocumentRoot().Application::getPersonalRoot()) === realpath(getcwd());

		$this
			// the name of the command (the part after "bin/console")
			->setName('orm:annotate')

			// the short description shown while running "php bin/console list"
			->setDescription('Scans project for ORM Entities.')

			// the full command description shown when running the command with
			// the "--help" option
			->setHelp('This system command optimizes Entity Relation Map building.')

			->setDefinition(
				new InputDefinition(array(
					new InputArgument(
						'output', InputArgument::OPTIONAL, 'File for annotations to be saved to',
						$inBitrixDir
							? 'modules/orm_annotations.php'
							: Application::getDocumentRoot().Application::getPersonalRoot().'/modules/orm_annotations.php'
					),
					new InputOption(
						'modules', 'm', InputOption::VALUE_OPTIONAL,
						'Modules to be scanned, separated by comma.', 'main'
					),
					new InputOption(
						'clean', 'c', InputOption::VALUE_NONE,
						'Clean current entity map.'
					),
				))
			)
		;

		// disable Loader::requireModule exception
		Loader::setRequireThrowException(false);
	}

	protected function execute(InputInterface $input, OutputInterface $output)
	{
		$output->writeln([
			'Entity Scanner',
			'==============',
			'',
		]);

		$time = microtime(true);
		$memoryBefore = memory_get_usage();

		/** @var \Exception[] $exceptions deferred errors */
		$exceptions = [];

		// handle already known classes (but we don't know their modules)
		// as long as there are no any Table by default, we can ignore it
		//$this->handleClasses($this->getDeclaredClassesDiff(), $input, $output);

		// skip already defined classes
		$this->getDeclaredClassesDiff();

		// scan dirs
		$inputModules = [];
		$inputModulesRaw = $input->getOption('modules');

		if (!empty($inputModulesRaw) && $inputModulesRaw != 'all')
		{
			$inputModules = explode(',', $inputModulesRaw);
		}

		$dirs = $this->getDirsToScan($inputModules, $input, $output);

		foreach ($dirs as $dir)
		{
			$this->scanDir($dir, $input, $output);
		}

		// scan for bitrix entities
		$this->scanBitrixEntities($inputModules, $input, $output);

		// get classes from outside regular filesystem (e.g. iblock, hlblock)
		try
		{
			$this->handleVirtualClasses($inputModules, $input, $output);
		}
		catch (\Exception $e)
		{
			$exceptions[] = $e;
		}

		// output file path
		$filePath = $input->getArgument('output');
		$filePath = ($filePath[0] == '/')
			? $filePath // absolute
			: getcwd().'/'.$filePath; // relative

		// handle entities
		$annotations = [];

		// get current annotations
		if (!$input->getOption('clean') && file_exists($filePath) && is_readable($filePath))
		{
			$rawAnnotations = explode('/* '.static::ANNOTATION_MARKER, file_get_contents($filePath));

			foreach ($rawAnnotations as $rawAnnotation)
			{
				if ($rawAnnotation[0] === ':')
				{
					$endPos = mb_strpos($rawAnnotation, ' */');
					$entityClass = mb_substr($rawAnnotation, 1, $endPos - 1);
					//$annotation = substr($rawAnnotation, $endPos + 3 + strlen(PHP_EOL));

					$annotations[$entityClass] = '/* '.static::ANNOTATION_MARKER.rtrim($rawAnnotation);
				}
			}
		}

		// add/rewrite new entities
		foreach ($this->entitiesFound as $entityMeta)
		{
			try
			{
				$entityClass = $entityMeta['class'];
				$annotateUfOnly = $entityMeta['ufOnly'];

				$entity = Entity::getInstance($entityClass);
				$entityAnnotation = static::annotateEntity($entity, $annotateUfOnly);

				if (!empty($entityAnnotation))
				{
					$annotations[$entityClass] = "/* ".static::ANNOTATION_MARKER.":{$entityClass} */".PHP_EOL;
					$annotations[$entityClass] .= $entityAnnotation;
				}
			}
			catch (\Exception $e)
			{
				$exceptions[] = $e;
			}
		}

		// write to file
		$fileContent = '<?php'.PHP_EOL.PHP_EOL.join(PHP_EOL, $annotations);
		file_put_contents($filePath, $fileContent);

		$output->writeln('Map has been saved to: '.$filePath);

		// summary stats
		$time = round(microtime(true) - $time, 2);
		$memoryAfter = memory_get_usage();
		$memoryDiff = $memoryAfter - $memoryBefore;

		$output->writeln('Scanned modules: '.join(', ', $this->modulesScanned));
		$output->writeln('Scanned files: '.$this->filesIncluded);
		$output->writeln('Found entities: '.count($this->entitiesFound));
		$output->writeln('Time: '.$time.' sec');
		$output->writeln('Memory usage: '.(round($memoryAfter/1024/1024, 1)).'M (+'.(round($memoryDiff/1024/1024, 1)).'M)');
		$output->writeln('Memory peak usage: '.(round(memory_get_peak_usage()/1024/1024, 1)).'M');

		if (!empty($exceptions))
		{
			$io = new SymfonyStyle($input, $output);

			foreach ($exceptions as $e)
			{
				$io->warning('Exception: '.$e->getMessage().PHP_EOL.$e->getTraceAsString());
			}
		}

		return 0;
	}

	protected function getDirsToScan($inputModules, InputInterface $input, OutputInterface $output)
	{
		$basePaths = [
			//Application::getDocumentRoot().Application::getPersonalRoot().'/modules/',
			Application::getDocumentRoot().'/local/modules/'
		];

		$dirs = [];

		foreach ($basePaths as $basePath)
		{
			if (!file_exists($basePath))
			{
				continue;
			}

			$moduleList = [];

			foreach (new \DirectoryIterator($basePath) as $item)
			{
				if($item->isDir() && !$item->isDot())
				{
					$moduleList[] = $item->getFilename();
				}
			}

			// filter for input modules
			if (!empty($inputModules))
			{
				$moduleList = array_intersect($moduleList, $inputModules);
			}

			foreach ($moduleList as $moduleName)
			{
				// filter for installed modules
				if (!Loader::includeModule($moduleName))
				{
					continue;
				}

				$libDir = $basePath.$moduleName.'/lib';
				if (is_dir($libDir) && is_readable($libDir))
				{
					$dirs[] = $libDir;
				}

				$libDir = $basePath.$moduleName.'/dev/lib';
				if (is_dir($libDir) && is_readable($libDir))
				{
					$dirs[] = $libDir;
				}

				$this->modulesScanned[] = $moduleName;
			}
		}

		return $dirs;
	}

	protected function scanBitrixEntities($inputModules, InputInterface $input, OutputInterface $output)
	{
		$basePath = Application::getDocumentRoot().Application::getPersonalRoot().'/modules/';

		// get all available modules
		$moduleList = [];

		foreach (new \DirectoryIterator($basePath) as $item)
		{
			if($item->isDir() && !$item->isDot())
			{
				$moduleList[] = $item->getFilename();
			}
		}

		// filter for input modules
		if (!empty($inputModules))
		{
			$moduleList = array_intersect($moduleList, $inputModules);
		}

		// collect classes
		foreach ($moduleList as $moduleName)
		{
			$ufPath = $basePath.$moduleName.'/meta/'.static::ANNOTATION_UF_FILENAME;

			if (file_exists($ufPath))
			{
				$classes = include $ufPath;

				foreach ($classes as $class)
				{
					if (class_exists($class))
					{
						$this->entitiesFound[] = [
							'class' => $class,
							'ufOnly' => true,
						];
					}
				}
			}
		}

		// clear diff buffer
		$this->getDeclaredClassesDiff();
	}

	protected function registerFallbackAutoload()
	{
		spl_autoload_register(function($className) {
			list($vendor, $module) = explode('\\', $className);

			if (!empty($module))
			{
				Loader::includeModule($module);
			}

			return Loader::autoLoad($className);
		});
	}

	protected function scanDir($dir, InputInterface $input, OutputInterface $output)
	{
		$this->debug($output,'scan dir: '.$dir);

		$this->registerFallbackAutoload();

		foreach (
			$iterator = new \RecursiveIteratorIterator(
				new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS | \RecursiveDirectoryIterator::FOLLOW_SYMLINKS),
				\RecursiveIteratorIterator::SELF_FIRST) as $item
		)
		{
			// check for stop list
			foreach ($this->excludedFiles as $excludedFile)
			{
				$currentPath = str_replace('\\', '/', $item->getPathname());
				if (mb_substr($currentPath, -mb_strlen($excludedFile)) === $excludedFile)
				{
					continue 2;
				}
			}

			/** @var $iterator \RecursiveDirectoryIterator */
			/** @var $item \SplFileInfo */
			if ($item->isFile() && $item->isReadable() && mb_substr($item->getFilename(), -4) == '.php')
			{
				$this->debug($output,'handle file: '.$item->getPathname());

				try
				{
					// get classes from file
					include_once $item->getPathname();
					$this->filesIncluded++;

					$classes = $this->getDeclaredClassesDiff();

					// check classes
					$this->handleClasses($classes, $input, $output);
				}
				catch (\Throwable $e) // php7
				{
					$this->debug($output, $e->getMessage());
				}
				catch (\Exception $e) // php5
				{
					$this->debug($output, $e->getMessage());
				}
			}
		}
	}

	protected function handleClasses($classes, InputInterface $input, OutputInterface $output)
	{
		foreach ($classes as $class)
		{
			$debugMsg = $class;

			if (is_subclass_of($class, DataManager::class) && mb_substr($class, -5) == 'Table')
			{
				if ((new \ReflectionClass($class))->isAbstract())
				{
					continue;
				}

				$debugMsg .= ' found!';
				$this->entitiesFound[] = [
					'class' => $class,
					'ufOnly' => false,
				];
			}

			$this->debug($output, $debugMsg);
		}
	}

	protected function getDeclaredClassesDiff()
	{
		static $lastDeclaredClasses = [];

		$currentDeclaredClasses = get_declared_classes();
		$diff = array_diff($currentDeclaredClasses, $lastDeclaredClasses);
		$lastDeclaredClasses = $currentDeclaredClasses;

		return $diff;
	}

	/**
	 * Builds annotation for classes outside regular filesystem (e.g. iblock, hlblock)
	 *
	 * @param array           $inputModules
	 * @param InputInterface  $input
	 * @param OutputInterface $output
	 */
	protected function handleVirtualClasses($inputModules, InputInterface $input, OutputInterface $output)
	{
		// init new classes by event
		$event = new \Bitrix\Main\Event("main", "onVirtualClassBuildList", [], $inputModules);
		$event->send();

		// no need to handle event result, get classes from the memory
		$classes = $this->getDeclaredClassesDiff();

		$this->handleClasses($classes, $input, $output);
	}

	/**
	 * @deprecated
	 *
	 * @param $field
	 *
	 * @return string
	 */
	public static function scalarFieldToTypeHint($field)
	{
		if (is_string($field))
		{
			$fieldClass = $field;
		}
		else
		{
			$fieldClass = get_class($field);
		}

		switch ($fieldClass)
		{
			case DateField::class:
				return '\\'.Date::class;
			case DatetimeField::class:
				return '\\'.DateTime::class;
			case IntegerField::class:
				return '\\int';
			case BooleanField::class:
				return '\\boolean';
			case FloatField::class:
				return '\\float';
			case ArrayField::class:
				return 'array';
			default:
				return '\\string';
		}
	}

	protected function debug(OutputInterface $output, $message)
	{
		if ($this->debug)
		{
			$output->writeln($message);
		}
	}
}