Skip to content

Instantly share code, notes, and snippets.

@Maksclub
Last active October 5, 2021 22:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Maksclub/16cd480348fa750332d12ce9fe195925 to your computer and use it in GitHub Desktop.
Save Maksclub/16cd480348fa750332d12ce9fe195925 to your computer and use it in GitHub Desktop.
<?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