PHP: 사용자 지정 도구 확장을 사용한 코드 품질

32435 단어 php
PHP: Code Quality with Custom Tooling Extensions

수년 동안 PHPStan, PHP-CS-Fixer, PHP_CodeSniffer 등을 사용한 후 한 가지 조언을 드리겠습니다. 코드 품질 도구를 확장하기 위해 사용자 정의 코드를 추가하십시오.

거의 모든 프로젝트에는 제품/프로젝트의 실제 가치를 조달하는 사용자 지정 코드가 있지만 이 사용자 지정 코드 자체는 종종 PHP-CS-Fixer, PHPStan, Psalm 및 기타 도구로 실제로 개선되지 않습니다. 도구는 이 사용자 정의 코드가 어떻게 작동하는지 알지 못하므로 일부 확장을 직접 작성해야 합니다.

예: 직장에서 Active Record 클래스의 일부 속성을 사용하는 HFE(Html-Form-Element) 클래스가 있으며 그 당시 문자열을 사용하여 두 클래스를 연결했습니다. :-/

힌트: 문자열은 매우 유연하지만 미래에 프로그래밍 방식으로 사용하기에는 끔찍합니다. 가능한 한 일반 문자열을 피하는 것이 좋습니다.

1. 커스텀 PHP-CS-픽서



그래서 문자열을 일부 메타데이터로 대체할 빠른 스크립트를 작성했습니다. 가장 큰 장점은 이 사용자 정의 PHP-CS-Fixer가 향후 생성될 코드를 자동으로 수정하고 CI-pipline 또는 예를 들어 이를 적용/확인할 수 있다는 것입니다. 사전 커밋 후크 또는 PhpStorm에서 직접.

<?php

declare(strict_types=1);

use PhpCsFixer\Tokenizer\Analyzer\ArgumentsAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

final class MeerxUseMetaFromActiveRowForHFECallsFixer extends AbstractMeerxFixerHelper 
{

    /**
     * {@inheritdoc}
     */
    public function getDocumentation(): string 
    {
        return 'Use ActiveRow->m() for "HFE_"-calls, if it is possible.';
    }

    /**
     * {@inheritdoc}
     */
    public function getSampleCode(): string 
    {
        return <<<'PHP'
            <?php

            $element = UserFactory::singleton()->fetchEmpty();

            $foo = HFE_Date::Gen($element, 'created_date');
            PHP;
    }

    public function isRisky(): bool 
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function isCandidate(Tokens $tokens): bool 
    {
        return $tokens->isTokenKindFound(\T_STRING);
    }

    public function getPriority(): int {
        // must be run after NoAliasFunctionsFixer
        // must be run before MethodArgumentSpaceFixer
        return -1;
    }

    protected function applyFix(SplFileInfo $file, Tokens $tokens): void 
    {
        if (v_str_contains($file->getFilename(), 'HFE_')) {
            return;
        }

        $functionsAnalyzer = new FunctionsAnalyzer();

        // fix for "HFE_*::Gen()"
        foreach ($tokens as $index => $token) {
            $index = (int)$index;

            // only for "Gen()"-calls
            if (!$token->equals([\T_STRING, 'Gen'], false)) {
                continue;
            }

            // only for "HFE_*"-classes
            $object = (string)$tokens[$index - 2]->getContent();
            if (!v_str_starts_with($object, 'HFE_')) {
                continue;
            }

            if ($functionsAnalyzer->isGlobalFunctionCall($tokens, $index)) {
                continue;
            }

            $argumentsIndices = $this->getArgumentIndices($tokens, $index);

            if (\count($argumentsIndices) >= 2) {
                [
                    $firstArgumentIndex,
                    $secondArgumentIndex
                ] = array_keys($argumentsIndices);

                // If the second argument is not a string, we cannot make a swap.
                if (!$tokens[$secondArgumentIndex]->isGivenKind(\T_CONSTANT_ENCAPSED_STRING)) {
                    continue;
                }

                $content = trim($tokens[$secondArgumentIndex]->getContent(), '\'"');
                if (!$content) {
                    continue;
                }

                $newContent = $tokens[$firstArgumentIndex]->getContent() . '->m()->' . $content;

                $tokens[$secondArgumentIndex] = new Token([\T_CONSTANT_ENCAPSED_STRING, $newContent]);
            }
        }
    }

    /**
     * @param Token[]|Tokens $tokens <phpdoctor-ignore-this-line/>
     * @param int $functionNameIndex
     *
     * @return array<int, int> In the format: startIndex => endIndex
     */
    private function getArgumentIndices(Tokens $tokens, $functionNameIndex): array 
    {
        $argumentsAnalyzer = new ArgumentsAnalyzer();

        $openParenthesis = $tokens->getNextTokenOfKind($functionNameIndex, ['(']);
        $closeParenthesis = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParenthesis);

        // init
        $indices = [];

        foreach ($argumentsAnalyzer->getArguments($tokens, $openParenthesis, $closeParenthesis) as $startIndexCandidate => $endIndex) {
            $indices[$tokens->getNextMeaningfulToken($startIndexCandidate - 1)] = $tokens->getPrevMeaningfulToken($endIndex + 1);
        }

        return $indices;
    }
}


사용자 정의 수정 사항을 사용하려면 등록하고 활성화할 수 있습니다. https://cs.symfony.com/doc/custom_rules.html

예-결과:


$fieldGroup->addElement(HFE_Customer::Gen($element, 'customer_id'));

// <- will be replaced with ->

$fieldGroup->addElement(HFE_Customer::Gen($element, $element->m()->customer_id));


힌트: GitHub에는 PHP_CodeSniffer 및 Fixer Rules에 대한 많은 예가 있습니다. 종종 사용 사례에 50-70% 맞는 것을 선택한 다음 필요에 따라 수정할 수 있습니다.

"m()"메서드는 다음과 같으며 간단한 "ActiveRowMeta"클래스를 호출합니다. 이 클래스는 실제 값 대신 속성 이름 자체를 반환합니다.

/**
 * (M)ETA
 *
 * @return ActiveRowMeta|mixed|static
 * <p>
 * We fake the return "static" here because we want auto-completion for the current properties in the IDE.
 * <br><br>
 * But here the properties contains only the name from the property itself.
 * </p>
 *
 * @psalm-return object{string,string}
 */
final public function m() 
{
    return (new ActiveRowMeta())->create($this);
}

<?php

final class ActiveRowMeta 
{
    /**
     * @return static
     */
    public function create(ActiveRow $obj): self 
    {
        /** @var static[] $STATIC_CACHE */
        static $STATIC_CACHE = [];

        // DEBUG
        // var_dump($STATIC_CACHE);

        $cacheKey = \get_class($obj);
        if (!empty($STATIC_CACHE[$cacheKey])) {
            return $STATIC_CACHE[$cacheKey];
        }

        foreach ($obj->getObjectVars() as $propertyName => $propertyValue) {
            $this->{$propertyName} = $propertyName;
        }

        $STATIC_CACHE[$cacheKey] = $this;

        return $this;
    }

}


2. 커스텀 PHPStan 확장



다음 단계에서 정적 코드 분석이 메타데이터의 유형을 알 수 있도록 PHPStan에 대한 DynamicMethodReturnTypeExtension을 추가했습니다. + phpdocs를 통해 IDE에서 자동 완성 기능이 여전히 있습니다.

참고: 여기에서도 메타데이터를 읽기 전용으로 만들었으므로 메타데이터를 오용할 수 없습니다.

<?php

declare(strict_types=1);

namespace meerx\App\scripts\githooks\StandardMeerx\PHPStanHelper;

use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Type;

final class MeerxMetaDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension 
{

    public function getClass(): string 
    {
        return \ActiveRow::class;
    }

    public function isMethodSupported(MethodReflection $methodReflection): bool 
    {
        return $methodReflection->getName() === 'm';
    }

    /**
     * @var \PHPStan\Reflection\ReflectionProvider
     */
    private $reflectionProvider;

    public function __construct(\PHPStan\Reflection\ReflectionProvider $reflectionProvider) 
    {
        $this->reflectionProvider = $reflectionProvider;
    }

    public function getTypeFromMethodCall(
        MethodReflection $methodReflection,
        MethodCall $methodCall,
        Scope $scope
    ): Type 
    {
        $exprType = $scope->getType($methodCall->var);

        $staticClassName = $exprType->getReferencedClasses()[0];
        $classReflection = $this->reflectionProvider->getClass($staticClassName);

        return new MeerxMetaType($staticClassName, null, $classReflection);
    }
}

<?php

declare(strict_types=1);

namespace meerx\App\scripts\githooks\StandardMeerx\PHPStanHelper;

use PHPStan\Reflection\ClassMemberAccessAnswerer;
use PHPStan\Type\ObjectType;

final class MeerxMetaType extends ObjectType 
{

    public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): \PHPStan\Reflection\PropertyReflection 
    {
        return new MeerxMetaProperty($this->getClassReflection());
    }

}

<?php

declare(strict_types=1);

namespace meerx\App\scripts\githooks\StandardMeerx\PHPStanHelper;

use PHPStan\Reflection\ClassReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\NeverType;
use PHPStan\Type\StringType;

final class MeerxMetaProperty implements \PHPStan\Reflection\PropertyReflection 
{

    private ClassReflection $classReflection;

    public function __construct(ClassReflection $classReflection) 
    {
        $this->classReflection = $classReflection;
    }

    public function getReadableType(): \PHPStan\Type\Type 
    {
        return new StringType();
    }

    public function getWritableType(): \PHPStan\Type\Type 
    {
        return new NeverType();
    }

    public function isWritable(): bool 
    {
        return false;
    }

    public function getDeclaringClass(): \PHPStan\Reflection\ClassReflection 
    {
        return $this->classReflection;
    }

    public function isStatic(): bool 
    {
        return false;
    }

    public function isPrivate(): bool 
    {
        return false;
    }

    public function isPublic(): bool 
    {
        return true;
    }

    public function getDocComment(): ?string 
    {
        return null;
    }

    public function canChangeTypeAfterAssignment(): bool 
    {
        return false;
    }

    public function isReadable(): bool 
    {
        return true;
    }

    public function isDeprecated(): \PHPStan\TrinaryLogic 
    {
        return TrinaryLogic::createFromBoolean(false);
    }

    public function getDeprecatedDescription(): ?string 
    {
        return null;
    }

    public function isInternal(): \PHPStan\TrinaryLogic 
    {
        return TrinaryLogic::createFromBoolean(false);
    }
}


요약



사용자 지정 코드와 이를 개선할 수 있는 방법에 대해 생각하고 이미 사용한 도구를 사용하고 확장하여 코드를 이해할 수 있습니다. 때로는 쉽고 일부modern PHPDocs를 추가할 수 있거나 토끼 구멍으로 내려가 일부 사용자 정의 항목을 구현해야 하지만 마침내 소프트웨어, 팀 및 고객에게 도움이 될 것입니다.

좋은 웹페이지 즐겨찾기