Last active
October 5, 2021 22:03
-
-
Save Maksclub/16cd480348fa750332d12ce9fe195925 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Hunter\Application\InnerClass { | |
use Psr\Log\LoggerInterface; | |
use Symfony\Component\Console\Command\Command; | |
use Symfony\Component\Console\Input\InputInterface; | |
use Symfony\Component\Console\Output\OutputInterface; | |
use Symfony\Component\Finder\Finder; | |
class ProhibitInnerClassCommand extends Command | |
{ | |
private const PATH = __DIR__ . '/../../'; | |
protected static $defaultName = 'app:prohibit_inner_class_uses'; | |
private Finder $finder; | |
public function __construct(private LoggerInterface $logger) | |
{ | |
parent::__construct(); | |
} | |
protected function execute(InputInterface $input, OutputInterface $output): int | |
{ | |
$this->createFinder(); | |
$innerClasses = $this->findInnerClasses(); | |
$this->createFinder(); | |
$violations= []; | |
foreach ($this->findUses($innerClasses) as $found) { | |
$found !== null && $violations[] = $this->handleViolations($found, $innerClasses); | |
} | |
$violations = array_merge(...$violations); | |
foreach ($violations as $violation => $cred) { | |
$this->logger->error('', [$violation, $cred]); | |
// $output->write('Error! ' . $violation); | |
// $output->write('Cred! ' . $cred); | |
} | |
return 0; | |
} | |
private function handleViolations(ClassWithInnerUse $classWithInnerUse, array $innerClasses): array | |
{ | |
$viols = []; | |
foreach ($classWithInnerUse->useImports as $usedInner) { | |
if (!isset($innerClasses[$usedInner])) { | |
$this->logger->warning('Странно попал класс сюда'); | |
continue; | |
} | |
foreach ($innerClasses[$usedInner] as $availableExpr) { | |
if ($availableExpr === $classWithInnerUse->class) { | |
return []; | |
} | |
if (!class_exists($availableExpr)) { | |
$this->logger->warning('Должен быть неймспейсом, если класс — обратить внимание!', [$availableExpr]); | |
if (str_contains($classWithInnerUse->class, $availableExpr)) { | |
return []; | |
} | |
} | |
$viols[$classWithInnerUse->class][] = 'Импортирует класс ' . $usedInner . '. Хоть и не должен!'; | |
} | |
} | |
return $viols; | |
} | |
private function findInnerClasses(): array | |
{ | |
$innerClasses = []; | |
foreach ($this->finder as $file) { | |
$inner = $this->grabInnerClasses($file->getContents()); | |
if (!$inner) { | |
continue; | |
} | |
foreach ($inner->classNames as $className) { | |
$availableExprs = []; | |
$innerClass = $inner->namespace . '\\' . $className; | |
if (!class_exists($innerClass)) { | |
$this->logger->warning('Не понятная ошибка с существованием класса', [$innerClass]); | |
continue; | |
} | |
$rfInner = new \ReflectionClass($innerClass); | |
foreach($rfInner->getAttributes(InnerClass::class) as $attr) { | |
/** @var InnerClass $innerAttr */ | |
$innerAttr = $attr->newInstance(); | |
$availableExprs[] = $innerAttr->of ?: true; | |
} | |
$innerClasses[$innerClass] = $availableExprs; | |
} | |
} | |
return $innerClasses; | |
} | |
/** | |
* @return \Generator|ClassWithInnerUse[] | |
*/ | |
private function findUses(array $innerClasses): \Generator | |
{ | |
// Ищем классы, которые используют InnerClasses | |
foreach ($this->finder as $file) { | |
$useInnerArr = $this->grabUsedClasses($file->getContents(), $innerClasses); | |
foreach ($useInnerArr as $usedInner) { | |
yield $usedInner; | |
} | |
} | |
} | |
/** | |
* @param string $phpCode | |
* | |
* @return InnerClasses|null | |
*/ | |
private function grabInnerClasses(string $phpCode): ?InnerClasses | |
{ | |
$result = new InnerClasses(); | |
$expectAttribute = $expectInnerClassToken = $expectInnerClass = $expectNamespace = false; | |
// TODO:: поискать по неймспейсу и если нет в них, то смысла нет искать по всему классу | |
foreach (\PhpToken::tokenize($phpCode) as $token) { | |
$tokenName = token_name($token->id); | |
if ($expectInnerClassToken && $tokenName === 'T_CLASS') { | |
$expectInnerClassToken = false; | |
$expectInnerClass = true; | |
continue; | |
} | |
if ($expectInnerClass && $tokenName === 'T_STRING') { | |
if (!$result->namespace) { | |
$this->logger->warning('Namespace должен быть найден раньше атрибута и InnerClass'); | |
} | |
$result->classNames[] = $token->text; | |
$expectInnerClass = false; | |
} | |
if ($tokenName === 'T_NAMESPACE') { | |
$expectNamespace = true; | |
continue; | |
} | |
if ($expectNamespace && $tokenName === 'T_NAME_QUALIFIED') { | |
$result->namespace = $token->text; | |
$expectNamespace = false; | |
continue; | |
} | |
if ($tokenName === 'T_ATTRIBUTE') { | |
$expectAttribute = true; | |
continue; | |
} | |
if ($expectAttribute && $tokenName === 'T_STRING') { | |
$expectAttribute = false; | |
if ($token->text === 'InnerClass') { | |
$expectInnerClassToken = true; | |
continue; | |
} | |
} | |
} | |
return count($result->classNames) > 0 ? $result : null; | |
} | |
/** | |
* @param string $phpCode | |
* @param array<string, true> $existInnerClasses | |
* | |
* @return ClassWithInnerUse[] | |
*/ | |
private function grabUsedClasses(string $phpCode, array $existInnerClasses): array | |
{ | |
$result = []; | |
$expectNamespace = $expectedUseClass = $expectedOwnerClass = false; | |
$namespace = $class = null; | |
$useImports = []; | |
foreach (\PhpToken::tokenize($phpCode) as $token) { | |
$tokenName = token_name($token->id); | |
if ($tokenName === 'T_CLASS') { | |
// Если это второй класс в неймспейсе, а первый уже обработан | |
if ($namespace !== null && $class !== null) { | |
if (count($useImports) > 0) { | |
$result[] = new ClassWithInnerUse($namespace, $class, $useImports); | |
} | |
$namespace = $class = null; | |
$useImports = []; | |
} | |
$expectedOwnerClass = true; | |
continue; | |
} | |
if ($expectedOwnerClass && $tokenName === 'T_STRING') { | |
$class = $token->text; | |
$expectedOwnerClass = false; | |
continue; | |
} | |
if ($tokenName === 'T_NAMESPACE') { | |
$expectNamespace = true; | |
continue; | |
} | |
if ($expectNamespace && $tokenName === 'T_NAME_QUALIFIED') { | |
$namespace = $token->text; | |
$expectNamespace = false; | |
continue; | |
} | |
if ($tokenName === 'T_USE') { | |
$expectedUseClass = true; | |
continue; | |
} | |
if ($expectedUseClass && $tokenName === 'T_NAME_QUALIFIED') { | |
if (isset($existInnerClasses[$token->text])) { | |
$useImports[] = $token->text; | |
} | |
$expectedUseClass = false; | |
continue; | |
} | |
} | |
if (count($useImports) > 0) { | |
$result[] = new ClassWithInnerUse($namespace, $class, $useImports); | |
} | |
return $result; | |
} | |
private function createFinder(): void | |
{ | |
$this->finder = new Finder(); | |
$this->finder | |
->files() | |
->in(self::PATH) | |
->name('*.php') | |
; | |
} | |
} | |
#[InnerClass] | |
class InnerClasses | |
{ | |
public function __construct( | |
public ?string $namespace = null, | |
public array $classNames = [] | |
) { | |
} | |
} | |
#[InnerClass] | |
class ClassWithInnerUse | |
{ | |
public string $class; | |
public function __construct( | |
string $namespace, | |
string $classOwnerName, | |
public array $useImports = [] | |
) { | |
$this->class = $namespace . '\\' . $classOwnerName; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment